diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index a158e3eda5..4ca884cb55 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -3212,70 +3212,6 @@ ], "type": "object" }, - "TimerDelivery": { - "enum": [ - "after-turn", - "steer-current-turn" - ], - "type": "string" - }, - "TimerTrigger": { - "oneOf": [ - { - "properties": { - "kind": { - "enum": [ - "delay" - ], - "type": "string" - }, - "repeat": { - "type": [ - "boolean", - "null" - ] - }, - "seconds": { - "format": "uint64", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "kind", - "seconds" - ], - "type": "object" - }, - { - "properties": { - "dtstart": { - "type": [ - "string", - "null" - ] - }, - "kind": { - "enum": [ - "schedule" - ], - "type": "string" - }, - "rrule": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "kind" - ], - "title": "KindTimerTrigger", - "type": "object" - } - ] - }, "TurnInterruptParams": { "properties": { "threadId": { diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 9f3ae7e348..07049e08cf 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -3463,81 +3463,6 @@ ], "type": "object" }, - "ThreadTimer": { - "properties": { - "createdAt": { - "format": "int64", - "type": "integer" - }, - "delivery": { - "$ref": "#/definitions/TimerDelivery" - }, - "id": { - "type": "string" - }, - "lastRunAt": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "nextRunAt": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "prompt": { - "type": "string" - }, - "trigger": { - "$ref": "#/definitions/TimerTrigger" - } - }, - "required": [ - "createdAt", - "delivery", - "id", - "prompt", - "trigger" - ], - "type": "object" - }, - "ThreadTimerFiredNotification": { - "properties": { - "threadId": { - "type": "string" - }, - "timer": { - "$ref": "#/definitions/ThreadTimer" - } - }, - "required": [ - "threadId", - "timer" - ], - "type": "object" - }, - "ThreadTimerUpdatedNotification": { - "properties": { - "threadId": { - "type": "string" - }, - "timers": { - "items": { - "$ref": "#/definitions/ThreadTimer" - }, - "type": "array" - } - }, - "required": [ - "threadId", - "timers" - ], - "type": "object" - }, "ThreadTokenUsage": { "properties": { "last": { @@ -3590,70 +3515,6 @@ ], "type": "object" }, - "TimerDelivery": { - "enum": [ - "after-turn", - "steer-current-turn" - ], - "type": "string" - }, - "TimerTrigger": { - "oneOf": [ - { - "properties": { - "kind": { - "enum": [ - "delay" - ], - "type": "string" - }, - "repeat": { - "type": [ - "boolean", - "null" - ] - }, - "seconds": { - "format": "uint64", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "kind", - "seconds" - ], - "type": "object" - }, - { - "properties": { - "dtstart": { - "type": [ - "string", - "null" - ] - }, - "kind": { - "enum": [ - "schedule" - ], - "type": "string" - }, - "rrule": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "kind" - ], - "title": "KindTimerTrigger", - "type": "object" - } - ] - }, "TokenUsageBreakdown": { "properties": { "cachedInputTokens": { @@ -4319,46 +4180,6 @@ "title": "Thread/name/updatedNotification", "type": "object" }, - { - "properties": { - "method": { - "enum": [ - "thread/timer/updated" - ], - "title": "Thread/timer/updatedNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/ThreadTimerUpdatedNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/timer/updatedNotification", - "type": "object" - }, - { - "properties": { - "method": { - "enum": [ - "thread/timer/fired" - ], - "title": "Thread/timer/firedNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/ThreadTimerFiredNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/timer/firedNotification", - "type": "object" - }, { "properties": { "method": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 4531e7b0e2..2d81f62684 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -3572,46 +3572,6 @@ "title": "Thread/name/updatedNotification", "type": "object" }, - { - "properties": { - "method": { - "enum": [ - "thread/timer/updated" - ], - "title": "Thread/timer/updatedNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/v2/ThreadTimerUpdatedNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/timer/updatedNotification", - "type": "object" - }, - { - "properties": { - "method": { - "enum": [ - "thread/timer/fired" - ], - "title": "Thread/timer/firedNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/v2/ThreadTimerFiredNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/timer/firedNotification", - "type": "object" - }, { "properties": { "method": { @@ -14379,85 +14339,6 @@ "title": "ThreadStatusChangedNotification", "type": "object" }, - "ThreadTimer": { - "properties": { - "createdAt": { - "format": "int64", - "type": "integer" - }, - "delivery": { - "$ref": "#/definitions/v2/TimerDelivery" - }, - "id": { - "type": "string" - }, - "lastRunAt": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "nextRunAt": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "prompt": { - "type": "string" - }, - "trigger": { - "$ref": "#/definitions/v2/TimerTrigger" - } - }, - "required": [ - "createdAt", - "delivery", - "id", - "prompt", - "trigger" - ], - "type": "object" - }, - "ThreadTimerFiredNotification": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "threadId": { - "type": "string" - }, - "timer": { - "$ref": "#/definitions/v2/ThreadTimer" - } - }, - "required": [ - "threadId", - "timer" - ], - "title": "ThreadTimerFiredNotification", - "type": "object" - }, - "ThreadTimerUpdatedNotification": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "threadId": { - "type": "string" - }, - "timers": { - "items": { - "$ref": "#/definitions/v2/ThreadTimer" - }, - "type": "array" - } - }, - "required": [ - "threadId", - "timers" - ], - "title": "ThreadTimerUpdatedNotification", - "type": "object" - }, "ThreadTokenUsage": { "properties": { "last": { @@ -14574,70 +14455,6 @@ ], "type": "string" }, - "TimerDelivery": { - "enum": [ - "after-turn", - "steer-current-turn" - ], - "type": "string" - }, - "TimerTrigger": { - "oneOf": [ - { - "properties": { - "kind": { - "enum": [ - "delay" - ], - "type": "string" - }, - "repeat": { - "type": [ - "boolean", - "null" - ] - }, - "seconds": { - "format": "uint64", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "kind", - "seconds" - ], - "type": "object" - }, - { - "properties": { - "dtstart": { - "type": [ - "string", - "null" - ] - }, - "kind": { - "enum": [ - "schedule" - ], - "type": "string" - }, - "rrule": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "kind" - ], - "title": "KindTimerTrigger", - "type": "object" - } - ] - }, "TokenUsageBreakdown": { "properties": { "cachedInputTokens": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index fa5fe68c12..c58326c6f8 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -8673,46 +8673,6 @@ "title": "Thread/name/updatedNotification", "type": "object" }, - { - "properties": { - "method": { - "enum": [ - "thread/timer/updated" - ], - "title": "Thread/timer/updatedNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/ThreadTimerUpdatedNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/timer/updatedNotification", - "type": "object" - }, - { - "properties": { - "method": { - "enum": [ - "thread/timer/fired" - ], - "title": "Thread/timer/firedNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/ThreadTimerFiredNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/timer/firedNotification", - "type": "object" - }, { "properties": { "method": { @@ -12234,85 +12194,6 @@ "title": "ThreadStatusChangedNotification", "type": "object" }, - "ThreadTimer": { - "properties": { - "createdAt": { - "format": "int64", - "type": "integer" - }, - "delivery": { - "$ref": "#/definitions/TimerDelivery" - }, - "id": { - "type": "string" - }, - "lastRunAt": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "nextRunAt": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "prompt": { - "type": "string" - }, - "trigger": { - "$ref": "#/definitions/TimerTrigger" - } - }, - "required": [ - "createdAt", - "delivery", - "id", - "prompt", - "trigger" - ], - "type": "object" - }, - "ThreadTimerFiredNotification": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "threadId": { - "type": "string" - }, - "timer": { - "$ref": "#/definitions/ThreadTimer" - } - }, - "required": [ - "threadId", - "timer" - ], - "title": "ThreadTimerFiredNotification", - "type": "object" - }, - "ThreadTimerUpdatedNotification": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "threadId": { - "type": "string" - }, - "timers": { - "items": { - "$ref": "#/definitions/ThreadTimer" - }, - "type": "array" - } - }, - "required": [ - "threadId", - "timers" - ], - "title": "ThreadTimerUpdatedNotification", - "type": "object" - }, "ThreadTokenUsage": { "properties": { "last": { @@ -12429,70 +12310,6 @@ ], "type": "string" }, - "TimerDelivery": { - "enum": [ - "after-turn", - "steer-current-turn" - ], - "type": "string" - }, - "TimerTrigger": { - "oneOf": [ - { - "properties": { - "kind": { - "enum": [ - "delay" - ], - "type": "string" - }, - "repeat": { - "type": [ - "boolean", - "null" - ] - }, - "seconds": { - "format": "uint64", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "kind", - "seconds" - ], - "type": "object" - }, - { - "properties": { - "dtstart": { - "type": [ - "string", - "null" - ] - }, - "kind": { - "enum": [ - "schedule" - ], - "type": "string" - }, - "rrule": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "kind" - ], - "title": "KindTimerTrigger", - "type": "object" - } - ] - }, "TokenUsageBreakdown": { "properties": { "cachedInputTokens": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadTimerFiredNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadTimerFiredNotification.json deleted file mode 100644 index 98d74d4da0..0000000000 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadTimerFiredNotification.json +++ /dev/null @@ -1,125 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "ThreadTimer": { - "properties": { - "createdAt": { - "format": "int64", - "type": "integer" - }, - "delivery": { - "$ref": "#/definitions/TimerDelivery" - }, - "id": { - "type": "string" - }, - "lastRunAt": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "nextRunAt": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "prompt": { - "type": "string" - }, - "trigger": { - "$ref": "#/definitions/TimerTrigger" - } - }, - "required": [ - "createdAt", - "delivery", - "id", - "prompt", - "trigger" - ], - "type": "object" - }, - "TimerDelivery": { - "enum": [ - "after-turn", - "steer-current-turn" - ], - "type": "string" - }, - "TimerTrigger": { - "oneOf": [ - { - "properties": { - "kind": { - "enum": [ - "delay" - ], - "type": "string" - }, - "repeat": { - "type": [ - "boolean", - "null" - ] - }, - "seconds": { - "format": "uint64", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "kind", - "seconds" - ], - "type": "object" - }, - { - "properties": { - "dtstart": { - "type": [ - "string", - "null" - ] - }, - "kind": { - "enum": [ - "schedule" - ], - "type": "string" - }, - "rrule": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "kind" - ], - "title": "KindTimerTrigger", - "type": "object" - } - ] - } - }, - "properties": { - "threadId": { - "type": "string" - }, - "timer": { - "$ref": "#/definitions/ThreadTimer" - } - }, - "required": [ - "threadId", - "timer" - ], - "title": "ThreadTimerFiredNotification", - "type": "object" -} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadTimerUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadTimerUpdatedNotification.json deleted file mode 100644 index afa5a9677a..0000000000 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadTimerUpdatedNotification.json +++ /dev/null @@ -1,128 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "ThreadTimer": { - "properties": { - "createdAt": { - "format": "int64", - "type": "integer" - }, - "delivery": { - "$ref": "#/definitions/TimerDelivery" - }, - "id": { - "type": "string" - }, - "lastRunAt": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "nextRunAt": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "prompt": { - "type": "string" - }, - "trigger": { - "$ref": "#/definitions/TimerTrigger" - } - }, - "required": [ - "createdAt", - "delivery", - "id", - "prompt", - "trigger" - ], - "type": "object" - }, - "TimerDelivery": { - "enum": [ - "after-turn", - "steer-current-turn" - ], - "type": "string" - }, - "TimerTrigger": { - "oneOf": [ - { - "properties": { - "kind": { - "enum": [ - "delay" - ], - "type": "string" - }, - "repeat": { - "type": [ - "boolean", - "null" - ] - }, - "seconds": { - "format": "uint64", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "kind", - "seconds" - ], - "type": "object" - }, - { - "properties": { - "dtstart": { - "type": [ - "string", - "null" - ] - }, - "kind": { - "enum": [ - "schedule" - ], - "type": "string" - }, - "rrule": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "kind" - ], - "title": "KindTimerTrigger", - "type": "object" - } - ] - } - }, - "properties": { - "threadId": { - "type": "string" - }, - "timers": { - "items": { - "$ref": "#/definitions/ThreadTimer" - }, - "type": "array" - } - }, - "required": [ - "threadId", - "timers" - ], - "title": "ThreadTimerUpdatedNotification", - "type": "object" -} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts index 64f370821f..a985914134 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts @@ -46,8 +46,6 @@ import type { ThreadRealtimeStartedNotification } from "./v2/ThreadRealtimeStart import type { ThreadRealtimeTranscriptUpdatedNotification } from "./v2/ThreadRealtimeTranscriptUpdatedNotification"; import type { ThreadStartedNotification } from "./v2/ThreadStartedNotification"; import type { ThreadStatusChangedNotification } from "./v2/ThreadStatusChangedNotification"; -import type { ThreadTimerFiredNotification } from "./v2/ThreadTimerFiredNotification"; -import type { ThreadTimerUpdatedNotification } from "./v2/ThreadTimerUpdatedNotification"; import type { ThreadTokenUsageUpdatedNotification } from "./v2/ThreadTokenUsageUpdatedNotification"; import type { ThreadUnarchivedNotification } from "./v2/ThreadUnarchivedNotification"; import type { TurnCompletedNotification } from "./v2/TurnCompletedNotification"; @@ -60,4 +58,4 @@ import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldW /** * Notification sent from the server to the client. */ -export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/timer/updated", "params": ThreadTimerUpdatedNotification } | { "method": "thread/timer/fired", "params": ThreadTimerFiredNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcriptUpdated", "params": ThreadRealtimeTranscriptUpdatedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }; +export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcriptUpdated", "params": ThreadRealtimeTranscriptUpdatedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadTimer.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadTimer.ts deleted file mode 100644 index f84498e513..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadTimer.ts +++ /dev/null @@ -1,7 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { TimerDelivery } from "./TimerDelivery"; -import type { TimerTrigger } from "./TimerTrigger"; - -export type ThreadTimer = { id: string, trigger: TimerTrigger, prompt: string, delivery: TimerDelivery, createdAt: number, nextRunAt: number | null, lastRunAt: number | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadTimerFiredNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadTimerFiredNotification.ts deleted file mode 100644 index 839e4e29f5..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadTimerFiredNotification.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadTimer } from "./ThreadTimer"; - -export type ThreadTimerFiredNotification = { threadId: string, timer: ThreadTimer, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadTimerUpdatedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadTimerUpdatedNotification.ts deleted file mode 100644 index cbf2ea2fc2..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadTimerUpdatedNotification.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadTimer } from "./ThreadTimer"; - -export type ThreadTimerUpdatedNotification = { threadId: string, timers: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TimerDelivery.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TimerDelivery.ts deleted file mode 100644 index fbd0aeb35e..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/TimerDelivery.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type TimerDelivery = "after-turn" | "steer-current-turn"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TimerTrigger.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TimerTrigger.ts deleted file mode 100644 index e75abec6be..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/TimerTrigger.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type TimerTrigger = { "kind": "delay", seconds: number, repeat: boolean | null, } | { "kind": "schedule", dtstart: string | null, rrule: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index eb2e06fa75..3874280e47 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -313,9 +313,6 @@ export type { ThreadStartResponse } from "./ThreadStartResponse"; export type { ThreadStartedNotification } from "./ThreadStartedNotification"; export type { ThreadStatus } from "./ThreadStatus"; export type { ThreadStatusChangedNotification } from "./ThreadStatusChangedNotification"; -export type { ThreadTimer } from "./ThreadTimer"; -export type { ThreadTimerFiredNotification } from "./ThreadTimerFiredNotification"; -export type { ThreadTimerUpdatedNotification } from "./ThreadTimerUpdatedNotification"; export type { ThreadTokenUsage } from "./ThreadTokenUsage"; export type { ThreadTokenUsageUpdatedNotification } from "./ThreadTokenUsageUpdatedNotification"; export type { ThreadUnarchiveParams } from "./ThreadUnarchiveParams"; @@ -324,8 +321,6 @@ export type { ThreadUnarchivedNotification } from "./ThreadUnarchivedNotificatio export type { ThreadUnsubscribeParams } from "./ThreadUnsubscribeParams"; export type { ThreadUnsubscribeResponse } from "./ThreadUnsubscribeResponse"; export type { ThreadUnsubscribeStatus } from "./ThreadUnsubscribeStatus"; -export type { TimerDelivery } from "./TimerDelivery"; -export type { TimerTrigger } from "./TimerTrigger"; export type { TokenUsageBreakdown } from "./TokenUsageBreakdown"; export type { ToolRequestUserInputAnswer } from "./ToolRequestUserInputAnswer"; export type { ToolRequestUserInputOption } from "./ToolRequestUserInputOption"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index ee80e1b14e..0f02f9bf7e 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -317,21 +317,6 @@ client_request_definitions! { params: v2::ThreadReadParams, response: v2::ThreadReadResponse, }, - #[experimental("thread/timer/create")] - ThreadTimerCreate => "thread/timer/create" { - params: v2::ThreadTimerCreateParams, - response: v2::ThreadTimerCreateResponse, - }, - #[experimental("thread/timer/delete")] - ThreadTimerDelete => "thread/timer/delete" { - params: v2::ThreadTimerDeleteParams, - response: v2::ThreadTimerDeleteResponse, - }, - #[experimental("thread/timer/list")] - ThreadTimerList => "thread/timer/list" { - params: v2::ThreadTimerListParams, - response: v2::ThreadTimerListResponse, - }, SkillsList => "skills/list" { params: v2::SkillsListParams, response: v2::SkillsListResponse, @@ -973,10 +958,6 @@ server_notification_definitions! { ThreadClosed => "thread/closed" (v2::ThreadClosedNotification), SkillsChanged => "skills/changed" (v2::SkillsChangedNotification), ThreadNameUpdated => "thread/name/updated" (v2::ThreadNameUpdatedNotification), - #[experimental("thread/timer/updated")] - ThreadTimerUpdated => "thread/timer/updated" (v2::ThreadTimerUpdatedNotification), - #[experimental("thread/timer/fired")] - ThreadTimerFired => "thread/timer/fired" (v2::ThreadTimerFiredNotification), ThreadTokenUsageUpdated => "thread/tokenUsage/updated" (v2::ThreadTokenUsageUpdatedNotification), TurnStarted => "turn/started" (v2::TurnStartedNotification), HookStarted => "hook/started" (v2::HookStartedNotification), diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 20db1ab828..2bf04a2655 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -3183,112 +3183,6 @@ pub struct ThreadReadResponse { pub thread: Thread, } -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "kebab-case")] -#[ts(rename_all = "kebab-case", export_to = "v2/")] -pub enum TimerDelivery { - AfterTurn, - SteerCurrentTurn, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(tag = "kind", rename_all = "camelCase")] -#[ts(tag = "kind")] -#[ts(export_to = "v2/")] -pub enum TimerTrigger { - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Delay { - #[ts(type = "number")] - seconds: u64, - repeat: Option, - }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Schedule { - dtstart: Option, - rrule: Option, - }, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadTimer { - pub id: String, - pub trigger: TimerTrigger, - pub prompt: String, - pub delivery: TimerDelivery, - #[ts(type = "number")] - pub created_at: i64, - #[ts(type = "number | null")] - pub next_run_at: Option, - #[ts(type = "number | null")] - pub last_run_at: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadTimerCreateParams { - pub thread_id: String, - pub trigger: TimerTrigger, - pub prompt: String, - pub delivery: TimerDelivery, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadTimerCreateResponse { - pub timer: ThreadTimer, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadTimerDeleteParams { - pub thread_id: String, - pub id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadTimerDeleteResponse { - pub deleted: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadTimerListParams { - pub thread_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadTimerListResponse { - pub data: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadTimerUpdatedNotification { - pub thread_id: String, - pub timers: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadTimerFiredNotification { - pub thread_id: String, - pub timer: ThreadTimer, -} - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 4bac512946..69176b0b26 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -145,11 +145,6 @@ Example with notification opt-out: - `thread/unsubscribe` — unsubscribe this connection from thread turn/item events. If this was the last subscriber, the server shuts down and unloads the thread, then emits `thread/closed`. - `thread/name/set` — set or update a thread’s user-facing name for either a loaded thread or a persisted rollout; returns `{}` on success and emits `thread/name/updated` to initialized, opted-in clients. Thread names are not required to be unique; name lookups resolve to the most recently updated thread. - `thread/unarchive` — move an archived rollout file back into the sessions directory; returns the restored `thread` on success and emits `thread/unarchived`. -- `thread/timer/create` — create a runtime timer on a loaded thread; accepts a structured `trigger`, `prompt`, and `delivery`, returns the created timer, and requires the under-development feature flag `timer_tool` to be enabled for that thread. Delay triggers use `seconds` plus optional `repeat`; `seconds: 0, repeat: true` means run whenever the thread is idle. Schedule triggers use optional `dtstart` and/or `rrule`. -- `thread/timer/delete` — delete one runtime timer from a loaded thread by id; returns `{ deleted }` and requires `timer_tool`. -- `thread/timer/list` — list runtime timers for a loaded thread; returns `{ data }` and requires `timer_tool`. -- `thread/timer/updated` — notification emitted when a loaded thread’s timer list changes, including one-shot timers deleting themselves after execution or timers restored from disk. -- `thread/timer/fired` — notification emitted when a thread timer fires, either for a synthetic follow-on turn or a same-turn steer delivery. - `thread/compact/start` — trigger conversation history compaction for a thread; returns `{}` immediately while progress streams through standard turn/item notifications. - `thread/shellCommand` — run a user-initiated `!` shell command against a thread; this runs unsandboxed with full access rather than inheriting the thread sandbox policy. Returns `{}` immediately while progress streams through standard turn/item notifications and any active turn receives the formatted output in its message stream. - `thread/backgroundTerminals/clean` — terminate all running background terminals for a thread (experimental; requires `capabilities.experimentalApi`); returns `{}` when the cleanup request is accepted. diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index ea00593570..976518465b 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -84,12 +84,8 @@ use codex_app_server_protocol::ThreadRealtimeSdpNotification; use codex_app_server_protocol::ThreadRealtimeStartedNotification; use codex_app_server_protocol::ThreadRealtimeTranscriptUpdatedNotification; use codex_app_server_protocol::ThreadRollbackResponse; -use codex_app_server_protocol::ThreadTimer; -use codex_app_server_protocol::ThreadTimerFiredNotification; -use codex_app_server_protocol::ThreadTimerUpdatedNotification; use codex_app_server_protocol::ThreadTokenUsage; use codex_app_server_protocol::ThreadTokenUsageUpdatedNotification; -use codex_app_server_protocol::TimerTrigger; use codex_app_server_protocol::ToolRequestUserInputOption; use codex_app_server_protocol::ToolRequestUserInputParams; use codex_app_server_protocol::ToolRequestUserInputQuestion; @@ -116,8 +112,6 @@ use codex_core::ThreadManager; use codex_core::find_thread_name_by_id; use codex_core::review_format::format_review_findings_block; use codex_core::review_prompts; -use codex_core::timers::TIMER_FIRED_BACKGROUND_EVENT_PREFIX; -use codex_core::timers::TIMER_UPDATED_BACKGROUND_EVENT_PREFIX; use codex_protocol::ThreadId; use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem; use codex_protocol::dynamic_tools::DynamicToolResponse as CoreDynamicToolResponse; @@ -167,35 +161,6 @@ struct CommandExecutionCompletionItem { command_actions: Vec, } -fn thread_timer_from_core(value: codex_core::timers::ThreadTimer) -> ThreadTimer { - ThreadTimer { - id: value.id, - trigger: timer_trigger_from_core(value.trigger), - prompt: value.prompt, - delivery: match value.delivery { - codex_core::timers::TimerDelivery::AfterTurn => { - codex_app_server_protocol::TimerDelivery::AfterTurn - } - codex_core::timers::TimerDelivery::SteerCurrentTurn => { - codex_app_server_protocol::TimerDelivery::SteerCurrentTurn - } - }, - created_at: value.created_at, - next_run_at: value.next_run_at, - last_run_at: value.last_run_at, - } -} - -fn timer_trigger_from_core(value: codex_core::timers::ThreadTimerTrigger) -> TimerTrigger { - match value { - codex_core::timers::ThreadTimerTrigger::Delay { seconds, repeat } => { - TimerTrigger::Delay { seconds, repeat } - } - codex_core::timers::ThreadTimerTrigger::Schedule { dtstart, rrule } => { - TimerTrigger::Schedule { dtstart, rrule } - } - } -} #[allow(clippy::too_many_arguments)] pub(crate) async fn apply_bespoke_event_handling( event: Event, @@ -1340,34 +1305,6 @@ pub(crate) async fn apply_bespoke_event_handling( .send_server_notification(ServerNotification::DeprecationNotice(notification)) .await; } - EventMsg::BackgroundEvent(event) => { - if let Some(payload) = event - .message - .strip_prefix(TIMER_UPDATED_BACKGROUND_EVENT_PREFIX) - && let Ok(timers) = - serde_json::from_str::>(payload) - { - let notification = ThreadTimerUpdatedNotification { - thread_id: conversation_id.to_string(), - timers: timers.into_iter().map(thread_timer_from_core).collect(), - }; - outgoing - .send_server_notification(ServerNotification::ThreadTimerUpdated(notification)) - .await; - } else if let Some(payload) = event - .message - .strip_prefix(TIMER_FIRED_BACKGROUND_EVENT_PREFIX) - && let Ok(timer) = serde_json::from_str::(payload) - { - let notification = ThreadTimerFiredNotification { - thread_id: conversation_id.to_string(), - timer: thread_timer_from_core(timer), - }; - outgoing - .send_server_notification(ServerNotification::ThreadTimerFired(notification)) - .await; - } - } EventMsg::ReasoningContentDelta(event) => { let notification = ReasoningSummaryTextDeltaNotification { thread_id: conversation_id.to_string(), diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index bb86058042..1e10a5696d 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -162,21 +162,12 @@ use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::ThreadStartedNotification; use codex_app_server_protocol::ThreadStatus; -use codex_app_server_protocol::ThreadTimer as ApiThreadTimer; -use codex_app_server_protocol::ThreadTimerCreateParams; -use codex_app_server_protocol::ThreadTimerCreateResponse; -use codex_app_server_protocol::ThreadTimerDeleteParams; -use codex_app_server_protocol::ThreadTimerDeleteResponse; -use codex_app_server_protocol::ThreadTimerListParams; -use codex_app_server_protocol::ThreadTimerListResponse; use codex_app_server_protocol::ThreadUnarchiveParams; use codex_app_server_protocol::ThreadUnarchiveResponse; use codex_app_server_protocol::ThreadUnarchivedNotification; use codex_app_server_protocol::ThreadUnsubscribeParams; use codex_app_server_protocol::ThreadUnsubscribeResponse; use codex_app_server_protocol::ThreadUnsubscribeStatus; -use codex_app_server_protocol::TimerDelivery as ApiTimerDelivery; -use codex_app_server_protocol::TimerTrigger as ApiTimerTrigger; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnError; use codex_app_server_protocol::TurnInterruptParams; @@ -783,18 +774,6 @@ impl CodexMessageProcessor { self.thread_read(to_connection_request_id(request_id), params) .await; } - ClientRequest::ThreadTimerCreate { request_id, params } => { - self.thread_timer_create(to_connection_request_id(request_id), params) - .await; - } - ClientRequest::ThreadTimerDelete { request_id, params } => { - self.thread_timer_delete(to_connection_request_id(request_id), params) - .await; - } - ClientRequest::ThreadTimerList { request_id, params } => { - self.thread_timer_list(to_connection_request_id(request_id), params) - .await; - } ClientRequest::ThreadShellCommand { request_id, params } => { self.thread_shell_command(to_connection_request_id(request_id), params) .await; @@ -3147,7 +3126,7 @@ impl CodexMessageProcessor { message: format!("failed to unarchive thread: {err}"), data: None, })?; - rename_rollout_with_timer_sidecar(&canonical_rollout_path, &restored_path) + tokio::fs::rename(&canonical_rollout_path, &restored_path) .await .map_err(|err| JSONRPCErrorError { code: INTERNAL_ERROR_CODE, @@ -3699,106 +3678,6 @@ impl CodexMessageProcessor { self.outgoing.send_response(request_id, response).await; } - async fn thread_timer_create( - &mut self, - request_id: ConnectionRequestId, - params: ThreadTimerCreateParams, - ) { - let ThreadTimerCreateParams { - thread_id, - trigger, - prompt, - delivery, - } = params; - let Some(thread) = self.load_timer_thread(request_id.clone(), &thread_id).await else { - return; - }; - match thread - .create_timer( - timer_trigger_to_core(trigger), - prompt, - timer_delivery_to_core(delivery), - ) - .await - { - Ok(timer) => { - self.outgoing - .send_response( - request_id, - ThreadTimerCreateResponse { - timer: api_thread_timer_from_core(timer), - }, - ) - .await; - } - Err(err) => self.send_invalid_request_error(request_id, err).await, - } - } - - async fn thread_timer_delete( - &mut self, - request_id: ConnectionRequestId, - params: ThreadTimerDeleteParams, - ) { - let ThreadTimerDeleteParams { thread_id, id } = params; - let Some(thread) = self.load_timer_thread(request_id.clone(), &thread_id).await else { - return; - }; - let deleted = match thread.delete_timer(&id).await { - Ok(deleted) => deleted, - Err(err) => { - self.send_invalid_request_error(request_id, err).await; - return; - } - }; - self.outgoing - .send_response(request_id, ThreadTimerDeleteResponse { deleted }) - .await; - } - - async fn thread_timer_list( - &mut self, - request_id: ConnectionRequestId, - params: ThreadTimerListParams, - ) { - let ThreadTimerListParams { thread_id } = params; - let Some(thread) = self.load_timer_thread(request_id.clone(), &thread_id).await else { - return; - }; - let data = thread - .list_timers() - .await - .into_iter() - .map(api_thread_timer_from_core) - .collect(); - self.outgoing - .send_response(request_id, ThreadTimerListResponse { data }) - .await; - } - - async fn load_timer_thread( - &mut self, - request_id: ConnectionRequestId, - thread_id: &str, - ) -> Option> { - let (_, thread) = match self.load_thread(thread_id).await { - Ok(value) => value, - Err(error) => { - self.outgoing.send_error(request_id, error).await; - return None; - } - }; - if !thread.enabled(Feature::TimerScheduler) { - self.send_invalid_request_error( - request_id, - format!("thread {thread_id} does not support timer scheduling"), - ) - .await; - return None; - } - Some(thread) - } - pub(crate) fn thread_created_receiver(&self) -> broadcast::Receiver { self.thread_manager.subscribe_thread_created() } @@ -5773,7 +5652,7 @@ impl CodexMessageProcessor { .join(codex_core::ARCHIVED_SESSIONS_SUBDIR); tokio::fs::create_dir_all(&archive_folder).await?; let archived_path = archive_folder.join(&file_name); - rename_rollout_with_timer_sidecar(&canonical_rollout_path, &archived_path).await?; + tokio::fs::rename(&canonical_rollout_path, &archived_path).await?; if let Some(ctx) = state_db_ctx { let _ = ctx .mark_archived(thread_id, archived_path.as_path(), Utc::now()) @@ -8031,21 +7910,6 @@ impl CodexMessageProcessor { } } -async fn rename_rollout_with_timer_sidecar(from: &Path, to: &Path) -> std::io::Result<()> { - tokio::fs::rename(from, to).await?; - - let from_sidecar = codex_core::timers::timer_sidecar_path_for_rollout(from); - let to_sidecar = codex_core::timers::timer_sidecar_path_for_rollout(to); - match tokio::fs::rename(&from_sidecar, &to_sidecar).await { - Ok(()) => Ok(()), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(err) => { - let _ = tokio::fs::rename(to, from).await; - Err(err) - } - } -} - fn normalize_thread_list_cwd_filter( cwd: Option, ) -> Result, JSONRPCErrorError> { @@ -9191,54 +9055,6 @@ fn build_thread_from_snapshot( } } -fn timer_delivery_to_core(value: ApiTimerDelivery) -> codex_core::timers::TimerDelivery { - match value { - ApiTimerDelivery::AfterTurn => codex_core::timers::TimerDelivery::AfterTurn, - ApiTimerDelivery::SteerCurrentTurn => codex_core::timers::TimerDelivery::SteerCurrentTurn, - } -} - -fn api_timer_delivery_from_core(value: codex_core::timers::TimerDelivery) -> ApiTimerDelivery { - match value { - codex_core::timers::TimerDelivery::AfterTurn => ApiTimerDelivery::AfterTurn, - codex_core::timers::TimerDelivery::SteerCurrentTurn => ApiTimerDelivery::SteerCurrentTurn, - } -} - -fn timer_trigger_to_core(value: ApiTimerTrigger) -> codex_core::timers::ThreadTimerTrigger { - match value { - ApiTimerTrigger::Delay { seconds, repeat } => { - codex_core::timers::ThreadTimerTrigger::Delay { seconds, repeat } - } - ApiTimerTrigger::Schedule { dtstart, rrule } => { - codex_core::timers::ThreadTimerTrigger::Schedule { dtstart, rrule } - } - } -} - -fn api_timer_trigger_from_core(value: codex_core::timers::ThreadTimerTrigger) -> ApiTimerTrigger { - match value { - codex_core::timers::ThreadTimerTrigger::Delay { seconds, repeat } => { - ApiTimerTrigger::Delay { seconds, repeat } - } - codex_core::timers::ThreadTimerTrigger::Schedule { dtstart, rrule } => { - ApiTimerTrigger::Schedule { dtstart, rrule } - } - } -} - -fn api_thread_timer_from_core(value: codex_core::timers::ThreadTimer) -> ApiThreadTimer { - ApiThreadTimer { - id: value.id, - trigger: api_timer_trigger_from_core(value.trigger), - prompt: value.prompt, - delivery: api_timer_delivery_from_core(value.delivery), - created_at: value.created_at, - next_run_at: value.next_run_at, - last_run_at: value.last_run_at, - } -} - pub(crate) fn summary_to_thread(summary: ConversationSummary) -> Thread { let ConversationSummary { conversation_id, diff --git a/codex-rs/app-server/tests/suite/v2/turn_interrupt.rs b/codex-rs/app-server/tests/suite/v2/turn_interrupt.rs index 8f305b1c38..2850c7b74f 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_interrupt.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_interrupt.rs @@ -22,7 +22,7 @@ use codex_app_server_protocol::UserInput as V2UserInput; use tempfile::TempDir; use tokio::time::timeout; -const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(20); +const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); #[tokio::test] async fn turn_interrupt_aborts_running_turn() -> Result<()> { diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 3d9470861f..59591dd0d1 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -67,7 +67,7 @@ use tokio::time::timeout; #[cfg(windows)] const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(25); #[cfg(not(windows))] -const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(20); +const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); const TEST_ORIGINATOR: &str = "codex_vscode"; const LOCAL_PRAGMATIC_TEMPLATE: &str = "You are a deeply pragmatic, effective software engineer."; diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 0f24156c06..bfa5b30bd6 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -48,8 +48,6 @@ use crate::render::renderable::Renderable; use crate::resume_picker::SessionSelection; #[cfg(test)] use crate::test_support::PathBufExt; -use crate::timer_scheduler::format_timer_summary; -use crate::timer_scheduler::parse_timer_spec; use crate::tui; use crate::tui::TuiEvent; use crate::update_action::UpdateAction; @@ -1985,26 +1983,6 @@ impl App { }); } - fn parse_thread_timer_spec(&mut self, thread_id: ThreadId, spec: String) { - let config = self.config.clone(); - let target = match self.remote_app_server_url.clone() { - Some(websocket_url) => crate::AppServerTarget::Remote { - websocket_url, - auth_token: self.remote_app_server_auth_token.clone(), - }, - None => crate::AppServerTarget::Embedded, - }; - let app_event_tx = self.app_event_tx.clone(); - tokio::spawn(async move { - let result = parse_timer_spec(config, target, spec.clone()).await; - app_event_tx.send(AppEvent::ThreadTimerSpecParsed { - thread_id, - spec, - result, - }); - }); - } - fn submit_feedback( &mut self, app_server: &AppServerSession, @@ -3350,20 +3328,11 @@ impl App { app_server: &mut AppServerSession, started: AppServerStartedThread, ) -> Result<()> { - let thread_id = started.session.thread_id; self.reset_thread_event_state(); let init = self.chatwidget_init_for_forked_or_resumed_thread(tui, self.config.clone()); self.replace_chat_widget(ChatWidget::new_with_app_event(init)); self.enqueue_primary_thread_session(started.session, started.turns) .await?; - if self.config.features.enabled(Feature::TimerScheduler) { - match app_server.thread_timer_list(thread_id).await { - Ok(timers) => self.chat_widget.on_thread_timers_updated(timers), - Err(err) => { - tracing::warn!(%err, "failed to load thread timers while attaching thread"); - } - } - } self.backfill_loaded_subagent_threads(app_server).await; Ok(()) } @@ -4246,87 +4215,6 @@ impl App { tui.frame_requester().schedule_frame(); } - AppEvent::OpenThreadTimers { thread_id } => { - match app_server.thread_timer_list(thread_id).await { - Ok(timers) => { - if self.chat_widget.thread_id() == Some(thread_id) { - self.chat_widget.open_thread_timers_popup(thread_id, timers); - } - } - Err(err) => { - if self.chat_widget.thread_id() == Some(thread_id) { - self.chat_widget - .add_error_message(format!("Failed to load thread timers: {err}")); - } - } - } - } - AppEvent::CreateThreadTimerFromSpec { thread_id, spec } => { - self.parse_thread_timer_spec(thread_id, spec); - } - AppEvent::ThreadTimerSpecParsed { - thread_id, - spec, - result, - } => match result { - Ok(parsed) => match app_server - .thread_timer_create( - thread_id, - parsed.trigger.clone(), - parsed.prompt.clone(), - parsed.delivery, - ) - .await - { - Ok(timer) => { - if self.chat_widget.thread_id() == Some(thread_id) { - let summary = - format_timer_summary(&timer.trigger, timer.delivery, &timer.prompt); - self.chat_widget.add_info_message( - format!("Created thread timer from `/loop {spec}`."), - Some(summary), - ); - } - } - Err(err) => { - if self.chat_widget.thread_id() == Some(thread_id) { - self.chat_widget - .add_error_message(format!("Failed to create thread timer: {err}")); - } - } - }, - Err(err) => { - if self.chat_widget.thread_id() == Some(thread_id) { - self.chat_widget - .add_error_message(format!("Failed to parse `/loop {spec}`: {err}")); - } - } - }, - AppEvent::DeleteThreadTimer { thread_id, id } => { - match app_server.thread_timer_delete(thread_id, id.clone()).await { - Ok(true) => { - if self.chat_widget.thread_id() == Some(thread_id) { - self.chat_widget.add_info_message( - format!("Deleted thread timer `{id}`."), - /*hint*/ None, - ); - } - } - Ok(false) => { - if self.chat_widget.thread_id() == Some(thread_id) { - self.chat_widget - .add_error_message(format!("No thread timer matched `{id}`.")); - } - } - Err(err) => { - if self.chat_widget.thread_id() == Some(thread_id) { - self.chat_widget.add_error_message(format!( - "Failed to delete thread timer `{id}`: {err}" - )); - } - } - } - } AppEvent::InsertHistoryCell(cell) => { let cell: Arc = cell.into(); if let Some(Overlay::Transcript(t)) = &mut self.overlay { diff --git a/codex-rs/tui/src/app/app_server_adapter.rs b/codex-rs/tui/src/app/app_server_adapter.rs index c0defe4086..0bd78f353a 100644 --- a/codex-rs/tui/src/app/app_server_adapter.rs +++ b/codex-rs/tui/src/app/app_server_adapter.rs @@ -329,10 +329,6 @@ fn server_notification_thread_target( ServerNotification::ThreadNameUpdated(notification) => { Some(notification.thread_id.as_str()) } - ServerNotification::ThreadTimerFired(notification) => Some(notification.thread_id.as_str()), - ServerNotification::ThreadTimerUpdated(notification) => { - Some(notification.thread_id.as_str()) - } ServerNotification::ThreadTokenUsageUpdated(notification) => { Some(notification.thread_id.as_str()) } diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 68fd1bab4a..d727963f0d 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -30,7 +30,6 @@ use crate::bottom_pane::ApprovalRequest; use crate::bottom_pane::StatusLineItem; use crate::bottom_pane::TerminalTitleItem; use crate::history_cell::HistoryCell; -use crate::timer_scheduler::ParsedTimerSpec; use codex_config::types::ApprovalsReviewer; use codex_features::Feature; @@ -109,30 +108,6 @@ pub(crate) enum AppEvent { /// Fork the current session into a new thread. ForkCurrentSession, - /// List timers for the specified thread and open the management UI. - OpenThreadTimers { - thread_id: ThreadId, - }, - - /// Parse a `/loop ` input into structured timer params. - CreateThreadTimerFromSpec { - thread_id: ThreadId, - spec: String, - }, - - /// Result of parsing a `/loop ` input. - ThreadTimerSpecParsed { - thread_id: ThreadId, - spec: String, - result: Result, - }, - - /// Delete one timer from the specified thread. - DeleteThreadTimer { - thread_id: ThreadId, - id: String, - }, - /// Request to exit the application. /// /// Use `ShutdownFirst` for user-initiated quits so core cleanup runs and the diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 03a726bfc0..9f3a4d83dd 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -55,17 +55,8 @@ use codex_app_server_protocol::ThreadShellCommandParams; use codex_app_server_protocol::ThreadShellCommandResponse; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; -use codex_app_server_protocol::ThreadTimer; -use codex_app_server_protocol::ThreadTimerCreateParams; -use codex_app_server_protocol::ThreadTimerCreateResponse; -use codex_app_server_protocol::ThreadTimerDeleteParams; -use codex_app_server_protocol::ThreadTimerDeleteResponse; -use codex_app_server_protocol::ThreadTimerListParams; -use codex_app_server_protocol::ThreadTimerListResponse; use codex_app_server_protocol::ThreadUnsubscribeParams; use codex_app_server_protocol::ThreadUnsubscribeResponse; -use codex_app_server_protocol::TimerDelivery; -use codex_app_server_protocol::TimerTrigger; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnInterruptParams; use codex_app_server_protocol::TurnInterruptResponse; @@ -325,28 +316,6 @@ impl AppServerSession { started_thread_from_start_response(response, config).await } - pub(crate) async fn start_ephemeral_thread_with_base_instructions( - &mut self, - config: &Config, - base_instructions: String, - ) -> Result { - let request_id = self.next_request_id(); - let mut params = thread_start_params_from_config( - config, - self.thread_params_mode(), - self.remote_cwd_override.as_deref(), - ); - params.ephemeral = Some(true); - params.persist_extended_history = false; - params.base_instructions = Some(base_instructions); - let response: ThreadStartResponse = self - .client - .request_typed(ClientRequest::ThreadStart { request_id, params }) - .await - .wrap_err("thread/start failed during timer spec parsing")?; - started_thread_from_start_response(response, config).await - } - pub(crate) async fn resume_thread( &mut self, config: Config, @@ -445,68 +414,6 @@ impl AppServerSession { Ok(response.thread) } - pub(crate) async fn thread_timer_create( - &mut self, - thread_id: ThreadId, - trigger: TimerTrigger, - prompt: String, - delivery: TimerDelivery, - ) -> Result { - let request_id = self.next_request_id(); - let response: ThreadTimerCreateResponse = self - .client - .request_typed(ClientRequest::ThreadTimerCreate { - request_id, - params: ThreadTimerCreateParams { - thread_id: thread_id.to_string(), - trigger, - prompt, - delivery, - }, - }) - .await - .wrap_err("thread/timer/create failed in TUI")?; - Ok(response.timer) - } - - pub(crate) async fn thread_timer_delete( - &mut self, - thread_id: ThreadId, - id: String, - ) -> Result { - let request_id = self.next_request_id(); - let response: ThreadTimerDeleteResponse = self - .client - .request_typed(ClientRequest::ThreadTimerDelete { - request_id, - params: ThreadTimerDeleteParams { - thread_id: thread_id.to_string(), - id, - }, - }) - .await - .wrap_err("thread/timer/delete failed in TUI")?; - Ok(response.deleted) - } - - pub(crate) async fn thread_timer_list( - &mut self, - thread_id: ThreadId, - ) -> Result> { - let request_id = self.next_request_id(); - let response: ThreadTimerListResponse = self - .client - .request_typed(ClientRequest::ThreadTimerList { - request_id, - params: ThreadTimerListParams { - thread_id: thread_id.to_string(), - }, - }) - .await - .wrap_err("thread/timer/list failed in TUI")?; - Ok(response.data) - } - #[allow(clippy::too_many_arguments)] pub(crate) async fn turn_start( &mut self, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 2b698201ce..4f92e6ca78 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -66,8 +66,8 @@ use crate::terminal_title::SetTerminalTitleResult; use crate::terminal_title::clear_terminal_title; use crate::terminal_title::set_terminal_title; use crate::text_formatting::proper_join; -use crate::timer_scheduler::format_timer_trigger; -use crate::timer_scheduler::trigger_is_recurring; +use crate::timer_scheduler::build_loop_timer_prompt; +use crate::timer_scheduler::build_timer_list_prompt; use crate::version::CODEX_CLI_VERSION; use codex_app_server_protocol::AppSummary; use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo; @@ -350,7 +350,6 @@ use crate::status_indicator_widget::STATUS_DETAILS_DEFAULT_MAX_LINES; use crate::status_indicator_widget::StatusDetailsCapitalization; use crate::text_formatting::truncate_text; use crate::tui::FrameRequester; -use codex_app_server_protocol::ThreadTimer; mod interrupts; use self::interrupts::InterruptManager; mod session_header; @@ -846,7 +845,6 @@ pub(crate) struct ChatWidget { thread_id: Option, thread_name: Option, forked_from: Option, - thread_timers: Vec, frame_requester: FrameRequester, // Whether to include the initial welcome banner on session configured show_welcome_banner: bool, @@ -2008,7 +2006,6 @@ impl ChatWidget { self.thread_id = Some(event.session_id); self.thread_name = event.thread_name.clone(); self.forked_from = event.forked_from_id; - self.thread_timers.clear(); self.current_rollout_path = event.rollout_path.clone(); self.current_cwd = Some(event.cwd.clone()); match AbsolutePathBuf::try_from(event.cwd.clone()) { @@ -2451,9 +2448,6 @@ impl ChatWidget { if self.has_queued_follow_up_messages() { return; } - if !self.thread_timers.is_empty() { - return; - } if self.active_mode_kind() != ModeKind::Plan { return; } @@ -4748,7 +4742,6 @@ impl ChatWidget { thread_id: None, thread_name: None, forked_from: None, - thread_timers: Vec::new(), queued_user_messages: VecDeque::new(), rejected_steers_queue: VecDeque::new(), pending_steers: VecDeque::new(), @@ -5126,12 +5119,7 @@ impl ChatWidget { self.open_review_popup(); } SlashCommand::Loop => { - let Some(thread_id) = self.thread_id else { - self.add_error_message("No active thread is available.".to_string()); - return; - }; - self.app_event_tx - .send(AppEvent::OpenThreadTimers { thread_id }); + self.submit_user_message(build_timer_list_prompt().into()); } SlashCommand::Rename => { self.session_telemetry @@ -5535,25 +5523,21 @@ impl ChatWidget { self.bottom_pane.drain_pending_submission_state(); } SlashCommand::Loop if !trimmed.is_empty() => { - let Some(thread_id) = self.thread_id else { - self.add_error_message("No active thread is available.".to_string()); - return; - }; let Some((prepared_args, _prepared_elements)) = self .bottom_pane .prepare_inline_args_submission(/*record_history*/ false) else { return; }; - self.add_info_message( - format!("Scheduling `/loop {prepared_args}`..."), - Some("Parsing the spec and creating the thread timer.".to_string()), - ); - self.app_event_tx.send(AppEvent::CreateThreadTimerFromSpec { - thread_id, - spec: prepared_args, - }); - self.bottom_pane.drain_pending_submission_state(); + match build_loop_timer_prompt(&prepared_args) { + Ok(prompt) => { + self.bottom_pane.drain_pending_submission_state(); + self.submit_user_message(prompt.into()); + } + Err(err) => { + self.add_error_message(err); + } + } } SlashCommand::SandboxReadRoot if !trimmed.is_empty() => { let Some((prepared_args, _prepared_elements)) = self @@ -6421,12 +6405,6 @@ impl ChatWidget { } } } - ServerNotification::ThreadTimerUpdated(notification) => { - self.on_thread_timers_updated(notification.timers); - } - ServerNotification::ThreadTimerFired(notification) => { - self.on_thread_timer_fired(notification.timer); - } ServerNotification::TurnStarted(_) => { self.last_non_retry_error = None; if !matches!(replay_kind, Some(ReplayKind::ResumeInitialMessages)) { @@ -10035,70 +10013,6 @@ impl ChatWidget { self.request_redraw(); } - pub(crate) fn on_thread_timers_updated(&mut self, timers: Vec) { - self.thread_timers = timers; - } - - pub(crate) fn on_thread_timer_fired(&mut self, timer: ThreadTimer) { - // The transcript row is rendered from the visible user - // message, so the fire notification only needs to refresh the viewport. - let _ = timer; - self.request_redraw(); - } - - pub(crate) fn open_thread_timers_popup( - &mut self, - thread_id: ThreadId, - timers: Vec, - ) { - self.thread_timers = timers.clone(); - if timers.is_empty() { - self.add_info_message( - "No thread timers are currently scheduled.".to_string(), - Some("Use `/loop ` to create one.".to_string()), - ); - return; - } - - let items = timers - .into_iter() - .map(|timer| { - let timer_id = timer.id.clone(); - let actions: Vec = vec![Box::new(move |tx| { - tx.send(AppEvent::DeleteThreadTimer { - thread_id, - id: timer_id.clone(), - }); - })]; - let trigger = format_timer_trigger(&timer.trigger); - let name = if trigger_is_recurring(&timer.trigger) { - trigger - } else { - format!("{trigger} • one-shot") - }; - let selected_description = - format!("{}\n\nPress Enter to delete this timer.", timer.prompt); - SelectionItem { - name, - description: Some(timer.prompt), - selected_description: Some(selected_description), - is_current: false, - actions, - dismiss_on_select: true, - ..Default::default() - } - }) - .collect(); - self.bottom_pane.show_selection_view(SelectionViewParams { - title: Some("Thread timers".to_string()), - subtitle: Some("Review an timer, then press Enter to delete it.".to_string()), - footer_hint: Some(standard_popup_hint_line()), - items, - ..Default::default() - }); - self.request_redraw(); - } - pub(crate) fn add_plain_history_lines(&mut self, lines: Vec>) { self.add_boxed_history(Box::new(PlainHistoryCell::new(lines))); self.request_redraw(); diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index ce823ab893..46a4129a45 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -242,7 +242,6 @@ pub(super) async fn make_chatwidget_manual( pending_status_indicator_restore: false, suppress_queue_autosend: false, thread_id: None, - thread_timers: Vec::new(), thread_name: None, forked_from: None, frame_requester: FrameRequester::test_dummy(), diff --git a/codex-rs/tui/src/chatwidget/tests/snapshots/codex_tui__chatwidget__tests__timers__thread_timers_popup_keeps_selected_timer_prompt_visible.snap b/codex-rs/tui/src/chatwidget/tests/snapshots/codex_tui__chatwidget__tests__timers__thread_timers_popup_keeps_selected_timer_prompt_visible.snap deleted file mode 100644 index 7242eabe6f..0000000000 --- a/codex-rs/tui/src/chatwidget/tests/snapshots/codex_tui__chatwidget__tests__timers__thread_timers_popup_keeps_selected_timer_prompt_visible.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: tui/src/chatwidget/tests/timers.rs -expression: popup ---- - Thread timers - Review an timer, then press Enter to delete it. - -› 1. delay 0s • one-shot Give me a random animal name. - Press Enter to delete this timer. - - Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/tests/snapshots/codex_tui__chatwidget__tests__timers__thread_timers_popup_renders_schedule_triggers_readably.snap b/codex-rs/tui/src/chatwidget/tests/snapshots/codex_tui__chatwidget__tests__timers__thread_timers_popup_renders_schedule_triggers_readably.snap deleted file mode 100644 index 41818a38ef..0000000000 --- a/codex-rs/tui/src/chatwidget/tests/snapshots/codex_tui__chatwidget__tests__timers__thread_timers_popup_renders_schedule_triggers_readably.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: tui/src/chatwidget/tests/timers.rs -expression: popup ---- - Thread timers - Review an timer, then press Enter to delete it. - -› 1. at Apr 7, 2026 10:57 AM • one-shot tell me to take a piss - Press Enter to delete this timer. - 2. weekdays at 5:00 PM wrap up for the day - - Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/tests/timers.rs b/codex-rs/tui/src/chatwidget/tests/timers.rs index 3894f36241..e51f01025a 100644 --- a/codex-rs/tui/src/chatwidget/tests/timers.rs +++ b/codex-rs/tui/src/chatwidget/tests/timers.rs @@ -1,7 +1,4 @@ use super::*; -use codex_app_server_protocol::ThreadTimer; -use codex_app_server_protocol::TimerDelivery; -use codex_app_server_protocol::TimerTrigger; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::UserMessageEvent; use insta::assert_snapshot; @@ -39,74 +36,3 @@ Do not expose scheduler internals unless they matter to the user. assert_snapshot!(rendered, @"• Give me a random animal name. Running thread timer • delay 0s • one-shot • after-turn "); } - -#[tokio::test] -async fn thread_timers_popup_keeps_selected_timer_prompt_visible() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - - chat.open_thread_timers_popup( - ThreadId::new(), - vec![ThreadTimer { - id: "timer-1".to_string(), - trigger: TimerTrigger::Delay { - seconds: 0, - repeat: None, - }, - prompt: "Give me a random animal name.".to_string(), - delivery: TimerDelivery::AfterTurn, - created_at: 0, - next_run_at: None, - last_run_at: None, - }], - ); - - let popup = render_bottom_popup(&chat, /*width*/ 80); - assert_snapshot!( - "thread_timers_popup_keeps_selected_timer_prompt_visible", - popup - ); -} - -#[tokio::test] -async fn thread_timers_popup_renders_schedule_triggers_readably() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - - chat.open_thread_timers_popup( - ThreadId::new(), - vec![ - ThreadTimer { - id: "timer-1".to_string(), - trigger: TimerTrigger::Schedule { - dtstart: Some("2026-04-07T10:57:00".to_string()), - rrule: None, - }, - prompt: "tell me to take a piss".to_string(), - delivery: TimerDelivery::AfterTurn, - created_at: 0, - next_run_at: None, - last_run_at: None, - }, - ThreadTimer { - id: "timer-2".to_string(), - trigger: TimerTrigger::Schedule { - dtstart: Some("2026-04-07T17:00:00".to_string()), - rrule: Some( - "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;BYHOUR=17;BYMINUTE=0;BYSECOND=0" - .to_string(), - ), - }, - prompt: "wrap up for the day".to_string(), - delivery: TimerDelivery::AfterTurn, - created_at: 0, - next_run_at: None, - last_run_at: None, - }, - ], - ); - - let popup = render_bottom_popup(&chat, /*width*/ 80); - assert_snapshot!( - "thread_timers_popup_renders_schedule_triggers_readably", - popup - ); -} diff --git a/codex-rs/tui/src/timer_scheduler.rs b/codex-rs/tui/src/timer_scheduler.rs index 251d2b9a07..c2f2b31033 100644 --- a/codex-rs/tui/src/timer_scheduler.rs +++ b/codex-rs/tui/src/timer_scheduler.rs @@ -1,595 +1,56 @@ -use crate::AppServerTarget; -use crate::start_app_server_for_picker; -use chrono::Datelike; -use chrono::NaiveDateTime; -use chrono::Timelike; -use codex_app_server_client::AppServerEvent; -use codex_app_server_protocol::ServerNotification; -use codex_app_server_protocol::ThreadItem; -use codex_app_server_protocol::TimerDelivery; -use codex_app_server_protocol::TimerTrigger; -use codex_app_server_protocol::TurnStatus; -use codex_core::config::Config; -use codex_protocol::models::ContentItem; -use codex_protocol::models::ResponseItem; -use codex_protocol::user_input::UserInput; -use serde::Deserialize; -use serde_json::Value; -use serde_json::json; -use std::time::Duration; - -const LOCAL_DATE_TIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S"; -const MONTH_NAMES: [&str; 12] = [ - "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", -]; - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -pub(crate) struct ParsedTimerSpec { - pub(crate) trigger: TimerTrigger, - pub(crate) prompt: String, - pub(crate) delivery: TimerDelivery, +pub(crate) fn build_timer_list_prompt() -> String { + "List the thread timers that are currently scheduled. Call the TimerList tool directly, then summarize the pending timers briefly for the user. If there are no pending timers, say that none are scheduled.".to_string() } -pub(crate) async fn parse_timer_spec( - config: Config, - target: AppServerTarget, - spec: String, -) -> std::result::Result { - if spec.trim().is_empty() { - return Err("Could not determine a prompt from /loop input.".to_string()); +pub(crate) fn build_loop_timer_prompt(spec: &str) -> Result { + let spec = spec.trim(); + if spec.is_empty() { + return Err("Use `/loop ` to create a timer.".to_string()); } - let user_prompt = build_user_prompt(&spec); - let mut app_server = start_app_server_for_picker(&config, &target) - .await - .map_err(|err| format!("failed to start timer spec parser: {err}"))?; - let started = app_server - .start_ephemeral_thread_with_base_instructions( - &config, - PARSE_TIMER_SYSTEM_PROMPT.to_string(), - ) - .await - .map_err(|err| format!("failed to start timer spec parser thread: {err}"))?; - let thread_id = started.session.thread_id; - let thread_id_string = thread_id.to_string(); - let response = app_server - .turn_start( - thread_id, - vec![UserInput::Text { - text: user_prompt, - text_elements: Vec::new(), - }], - started.session.cwd, - started.session.approval_policy, - started.session.approvals_reviewer, - started.session.sandbox_policy, - started.session.model, - started.session.reasoning_effort, - config.model_reasoning_summary, - Some(config.service_tier), - /*collaboration_mode*/ None, - config.personality, - Some(output_schema()), - ) - .await - .map_err(|err| format!("failed to parse timer spec with model: {err}"))?; - let turn_id = response.turn.id; - let result = wait_for_parser_response(&mut app_server, thread_id_string, turn_id).await?; - let parsed: ParsedTimerSpec = serde_json::from_str(&result) - .map_err(|err| format!("model returned invalid timer parse output: {err}"))?; - validate_parsed_timer_spec(parsed) -} - -pub(crate) fn format_timer_summary( - trigger: &TimerTrigger, - delivery: TimerDelivery, - prompt: &str, -) -> String { - let mode = if trigger_is_recurring(trigger) { - "recurring" - } else { - "one-shot" - }; - format!( - "{} ({mode}, {}) -> {prompt}", - format_timer_trigger(trigger), - delivery_str(delivery) - ) -} - -pub(crate) fn format_timer_trigger(trigger: &TimerTrigger) -> String { - match trigger { - TimerTrigger::Delay { seconds, repeat } => { - let suffix = if repeat.unwrap_or(false) { - ", repeat" - } else { - "" - }; - format!("delay {seconds}s{suffix}") - } - TimerTrigger::Schedule { dtstart, rrule } => match (dtstart, rrule) { - (Some(dtstart), Some(rrule)) => format_timer_rrule(rrule, Some(dtstart)) - .unwrap_or_else(|| { - format!("schedule from {}; {rrule}", format_timer_dtstart(dtstart)) - }), - (Some(dtstart), None) => format!("at {}", format_timer_dtstart(dtstart)), - (None, Some(rrule)) => { - format_timer_rrule(rrule, /*dtstart*/ None) - .unwrap_or_else(|| format!("schedule {rrule}")) - } - (None, None) => "invalid schedule".to_string(), - }, - } -} - -fn format_timer_dtstart(dtstart: &str) -> String { - let Ok(dtstart) = NaiveDateTime::parse_from_str(dtstart, LOCAL_DATE_TIME_FORMAT) else { - return dtstart.to_string(); - }; - let month_name = MONTH_NAMES - .get(dtstart.month0() as usize) - .copied() - .unwrap_or("???"); - format!( - "{} {}, {} {}", - month_name, - dtstart.day(), - dtstart.year(), - format_timer_time(dtstart) - ) -} - -fn format_timer_time(dtstart: NaiveDateTime) -> String { - let hour = dtstart.hour(); - let period = if hour < 12 { "AM" } else { "PM" }; - let hour = match hour % 12 { - 0 => 12, - hour => hour, - }; - if dtstart.second() == 0 { - format!("{hour}:{:02} {period}", dtstart.minute()) - } else { - format!( - "{hour}:{:02}:{:02} {period}", - dtstart.minute(), - dtstart.second() - ) - } -} - -fn format_timer_rrule(rrule: &str, dtstart: Option<&str>) -> Option { - let freq = rrule_part(rrule, "FREQ")?; - let interval = rrule_part(rrule, "INTERVAL") - .and_then(|value| value.parse::().ok()) - .unwrap_or(1); - let byday = rrule_part(rrule, "BYDAY"); - let byhour = rrule_part(rrule, "BYHOUR").and_then(|value| value.parse::().ok()); - let byminute = rrule_part(rrule, "BYMINUTE") - .and_then(|value| value.parse::().ok()) - .unwrap_or(0); - let bysecond = rrule_part(rrule, "BYSECOND") - .and_then(|value| value.parse::().ok()) - .unwrap_or(0); - let time = format_timer_rrule_time(byhour, byminute, bysecond, dtstart); - - match freq { - "HOURLY" => { - let cadence = if interval == 1 { - "hourly".to_string() - } else { - format!("every {interval} hours") - }; - if byminute == 0 && bysecond == 0 { - Some(format!("{cadence} on the hour")) - } else { - Some(format!("{cadence} at :{byminute:02}")) - } - } - "DAILY" => { - let cadence = if interval == 1 { - "daily".to_string() - } else { - format!("every {interval} days") - }; - Some(match time { - Some(time) => format!("{cadence} at {time}"), - None => cadence, - }) - } - "WEEKLY" => { - let cadence = byday.and_then(format_timer_rrule_days).unwrap_or_else(|| { - if interval == 1 { - "weekly".to_string() - } else { - format!("every {interval} weeks") - } - }); - Some(match time { - Some(time) => format!("{cadence} at {time}"), - None => cadence, - }) - } - _ => None, - } -} - -fn rrule_part<'a>(rrule: &'a str, key: &str) -> Option<&'a str> { - rrule.split(';').find_map(|part| { - part.split_once('=').and_then(|(part_key, value)| { - (part_key.eq_ignore_ascii_case(key) && !value.is_empty()).then_some(value) - }) - }) -} - -fn format_timer_rrule_days(byday: &str) -> Option { - if byday == "MO,TU,WE,TH,FR" { - return Some("weekdays".to_string()); - } - if byday == "SA,SU" { - return Some("weekends".to_string()); - } - let days = byday - .split(',') - .filter_map(format_timer_rrule_day) - .collect::>() - .join(", "); - (!days.is_empty()).then_some(days) -} - -fn format_timer_rrule_day(day: &str) -> Option<&'static str> { - match day { - "MO" => Some("Mondays"), - "TU" => Some("Tuesdays"), - "WE" => Some("Wednesdays"), - "TH" => Some("Thursdays"), - "FR" => Some("Fridays"), - "SA" => Some("Saturdays"), - "SU" => Some("Sundays"), - _ => None, - } -} - -fn format_timer_rrule_time( - byhour: Option, - byminute: u32, - bysecond: u32, - dtstart: Option<&str>, -) -> Option { - let dtstart = byhour.is_none().then_some(dtstart).flatten(); - let hour = match byhour { - Some(hour) => hour, - None => { - let dtstart = dtstart?; - let dtstart = NaiveDateTime::parse_from_str(dtstart, LOCAL_DATE_TIME_FORMAT).ok()?; - dtstart.hour() - } - }; - if hour > 23 || byminute > 59 || bysecond > 59 { - return None; - } - let dtstart = NaiveDateTime::parse_from_str( - &format!("2000-01-01T{hour:02}:{byminute:02}:{bysecond:02}"), - LOCAL_DATE_TIME_FORMAT, - ) - .ok()?; - Some(format_timer_time(dtstart)) -} - -pub(crate) fn trigger_is_recurring(trigger: &TimerTrigger) -> bool { - match trigger { - TimerTrigger::Delay { repeat, .. } => repeat.unwrap_or(false), - TimerTrigger::Schedule { rrule, .. } => { - rrule.as_ref().is_some_and(|rrule| !rrule.is_empty()) - } - } -} - -fn delivery_str(delivery: TimerDelivery) -> &'static str { - match delivery { - TimerDelivery::AfterTurn => "after-turn", - TimerDelivery::SteerCurrentTurn => "steer-current-turn", - } -} - -fn build_user_prompt(spec: &str) -> String { let now = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S"); let timezone = chrono::Local::now().offset().to_string(); - format!("Current local datetime: {now}\nTimezone: {timezone}\nTimer spec: {spec}") -} + Ok(format!( + r#"Create a Codex thread timer from this `/loop` request. Call the TimerCreate tool directly; do not only describe the timer. -async fn wait_for_parser_response( - app_server: &mut crate::app_server_session::AppServerSession, - thread_id: String, - turn_id: String, -) -> std::result::Result { - let mut last_agent_message = None; - loop { - let event = - tokio::time::timeout(Duration::from_secs(/*secs*/ 120), app_server.next_event()) - .await - .map_err(|_| "timed out while waiting for timer spec parser".to_string())? - .ok_or_else(|| { - "timer spec parser disconnected before returning output".to_string() - })?; - match event { - AppServerEvent::ServerNotification(ServerNotification::ItemCompleted(notification)) - if notification.thread_id == thread_id && notification.turn_id == turn_id => - { - if let Some(text) = thread_item_agent_text(¬ification.item) { - last_agent_message = Some(text); - } - } - AppServerEvent::ServerNotification(ServerNotification::RawResponseItemCompleted( - notification, - )) if notification.thread_id == thread_id && notification.turn_id == turn_id => { - if let Some(text) = response_item_agent_text(¬ification.item) { - last_agent_message = Some(text); - } - } - AppServerEvent::ServerNotification(ServerNotification::TurnCompleted(notification)) - if notification.thread_id == thread_id && notification.turn.id == turn_id => - { - if matches!(notification.turn.status, TurnStatus::Failed) - && let Some(error) = notification.turn.error - { - return Err(format!("timer spec parser failed: {}", error.message)); - } - return last_agent_message.ok_or_else(|| { - "timer spec parser did not return an agent message".to_string() - }); - } - AppServerEvent::ServerNotification(_) | AppServerEvent::Lagged { .. } => {} - AppServerEvent::ServerRequest(_) => { - return Err("timer spec parser unexpectedly requested user input".to_string()); - } - AppServerEvent::Disconnected { message } => { - return Err(format!("timer spec parser disconnected: {message}")); - } - } - } -} +Current local datetime: {now} +Local UTC offset: {timezone} -fn thread_item_agent_text(item: &ThreadItem) -> Option { - match item { - ThreadItem::AgentMessage { text, .. } if !text.trim().is_empty() => Some(text.clone()), - ThreadItem::AgentMessage { .. } - | ThreadItem::UserMessage { .. } - | ThreadItem::Reasoning { .. } - | ThreadItem::Plan { .. } - | ThreadItem::McpToolCall { .. } - | ThreadItem::WebSearch { .. } - | ThreadItem::DynamicToolCall { .. } - | ThreadItem::CommandExecution { .. } - | ThreadItem::FileChange { .. } - | ThreadItem::ImageView { .. } - | ThreadItem::ImageGeneration { .. } - | ThreadItem::HookPrompt { .. } - | ThreadItem::CollabAgentToolCall { .. } - | ThreadItem::EnteredReviewMode { .. } - | ThreadItem::ExitedReviewMode { .. } - | ThreadItem::ContextCompaction { .. } => None, - } -} +/loop request: +{spec} -fn response_item_agent_text(item: &ResponseItem) -> Option { - match item { - ResponseItem::Message { role, content, .. } if role == "assistant" => { - let text = content - .iter() - .filter_map(|content| match content { - ContentItem::OutputText { text } => Some(text.as_str()), - ContentItem::InputText { .. } | ContentItem::InputImage { .. } => None, - }) - .collect::(); - (!text.trim().is_empty()).then_some(text) - } - ResponseItem::Message { .. } - | ResponseItem::Reasoning { .. } - | ResponseItem::LocalShellCall { .. } - | ResponseItem::FunctionCall { .. } - | ResponseItem::ToolSearchCall { .. } - | ResponseItem::FunctionCallOutput { .. } - | ResponseItem::CustomToolCall { .. } - | ResponseItem::CustomToolCallOutput { .. } - | ResponseItem::WebSearchCall { .. } - | ResponseItem::ImageGenerationCall { .. } - | ResponseItem::ToolSearchOutput { .. } - | ResponseItem::GhostSnapshot { .. } - | ResponseItem::Compaction { .. } - | ResponseItem::Other => None, - } -} - -fn validate_parsed_timer_spec( - parsed: ParsedTimerSpec, -) -> std::result::Result { - if parsed.prompt.trim().is_empty() { - return Err("model did not return an timer prompt".to_string()); - } - Ok(ParsedTimerSpec { - prompt: parsed.prompt.trim().to_string(), - ..parsed - }) -} - -fn output_schema() -> Value { - json!({ - "type": "object", - "properties": { - "trigger": { - "type": "object", - "properties": { - "kind": { "type": "string", "enum": ["delay", "schedule"] }, - "seconds": { "type": ["integer", "null"], "minimum": 0 }, - "repeat": { "type": ["boolean", "null"] }, - "dtstart": { "type": ["string", "null"] }, - "rrule": { "type": ["string", "null"] } - }, - "required": ["kind", "seconds", "repeat", "dtstart", "rrule"], - "additionalProperties": false - }, - "prompt": { "type": "string" }, - "delivery": { "type": "string", "enum": ["after-turn", "steer-current-turn"] } - }, - "required": ["trigger", "prompt", "delivery"], - "additionalProperties": false - }) -} - -const PARSE_TIMER_SYSTEM_PROMPT: &str = r#"Parse Codex `/loop` timer specs into a structured timer definition. - -Return only the JSON object requested by the response schema. - -Rules: -- Extract the timer prompt by removing the scheduling phrase but preserving the user's requested task. +Interpretation rules: +- Extract the timer prompt by removing the scheduling phrase while preserving the task the user wants to run later. - Use delivery "after-turn" unless the user clearly asks for same-turn/current-turn steering; then use "steer-current-turn". - Treat `/loop` as recurring by default when there is no explicit one-time timing. A bare absolute date/time is a single run; do not infer recurrence solely from the `/loop` command name. -- For "now", "immediately", or specs with no explicit timing, use { "kind": "delay", "seconds": 0, "repeat": true } unless the user clearly asked for one-shot behavior. This means the timer fires whenever the thread is idle. -- For delay triggers, set dtstart and rrule to null. -- For schedule triggers, set seconds and repeat to null. -- For relative timing like "in 30 seconds", use a delay trigger with seconds set to the relative delay and repeat true unless the user clearly asked for one-shot behavior. -- For interval timing like "every 5 minutes", use a delay trigger with seconds set to the interval and repeat true. -- For absolute wall-clock timing like "at 9pm", "tomorrow at 8am", or "at 10:57", use a one-shot schedule trigger with dtstart set to the next matching local datetime in YYYY-MM-DDTHH:MM:SS and rrule null unless the user explicitly asks for recurrence with words like "every", "daily", "weekly", "hourly", "each", "repeat", or "recurring". -- For ambiguous wall-clock times without AM/PM, choose the soonest future local occurrence. Example: if the current local datetime is 2026-04-06T23:28:00 and the spec says "at 11:30", return 2026-04-06T23:30:00, not 2026-04-07T11:30:00. -- For recurring calendar timing, use a schedule trigger with rrule set to an RFC 5545 RRULE string and dtstart set when the user supplies a start datetime; otherwise null. +- For "now", "immediately", or specs with no explicit timing, use a delay trigger with seconds 0 and repeat true. This makes the timer run whenever the thread is idle. +- For relative timing like "in 30 seconds", use a delay trigger with the relative seconds and repeat true unless the user clearly asked for one-shot behavior. +- For interval timing like "every 5 minutes", use a delay trigger with the interval seconds and repeat true. +- For absolute wall-clock timing like "at 9pm", "tomorrow at 8am", or "at 10:57", use a one-shot schedule trigger with dtstart set to the next matching local datetime in YYYY-MM-DDTHH:MM:SS and no rrule unless the user explicitly asks for recurrence with words like "every", "daily", "weekly", "hourly", "each", "repeat", or "recurring". +- For ambiguous wall-clock times without AM/PM, choose the soonest future local occurrence. +- For recurring calendar timing, use a schedule trigger with rrule set to an RFC 5545 RRULE string and dtstart set when the user supplies a start datetime; otherwise omit dtstart. - For schedule triggers, use floating local wall-clock datetimes without timezone suffixes. -"#; +- After TimerCreate succeeds, briefly confirm the schedule and the timer prompt."# + )) +} #[cfg(test)] mod tests { use super::*; - use pretty_assertions::assert_eq; #[test] - fn format_timer_summary_renders_delay() { - assert_eq!( - format_timer_summary( - &TimerTrigger::Delay { - seconds: 30, - repeat: Some(false), - }, - TimerDelivery::AfterTurn, - "remind me to take a break", - ), - "delay 30s (one-shot, after-turn) -> remind me to take a break" - ); + fn build_loop_timer_prompt_asks_model_to_call_timer_create() { + let prompt = build_loop_timer_prompt("every 5 minutes run tests") + .expect("valid /loop prompt should build"); + + assert!(prompt.contains("Call the TimerCreate tool directly")); + assert!(prompt.contains("every 5 minutes run tests")); + assert!(prompt.contains("Current local datetime:")); } #[test] - fn format_timer_trigger_renders_one_shot_schedule_as_local_time() { - assert_eq!( - format_timer_trigger(&TimerTrigger::Schedule { - dtstart: Some("2026-04-07T21:00:00".to_string()), - rrule: None, - }), - "at Apr 7, 2026 9:00 PM" - ); - } + fn build_timer_list_prompt_asks_model_to_call_timer_list() { + let prompt = build_timer_list_prompt(); - #[test] - fn format_timer_trigger_preserves_invalid_schedule_dtstart() { - assert_eq!( - format_timer_trigger(&TimerTrigger::Schedule { - dtstart: Some("not-a-date".to_string()), - rrule: None, - }), - "at not-a-date" - ); - } - - #[test] - fn format_timer_trigger_renders_daily_rrule_as_local_time() { - assert_eq!( - format_timer_trigger(&TimerTrigger::Schedule { - dtstart: Some("2026-04-07T21:00:00".to_string()), - rrule: Some("FREQ=DAILY;BYHOUR=21;BYMINUTE=0;BYSECOND=0".to_string()), - }), - "daily at 9:00 PM" - ); - } - - #[test] - fn format_timer_trigger_renders_weekday_rrule_as_local_time() { - assert_eq!( - format_timer_trigger(&TimerTrigger::Schedule { - dtstart: Some("2026-04-07T17:00:00".to_string()), - rrule: Some( - "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;BYHOUR=17;BYMINUTE=0;BYSECOND=0".to_string(), - ), - }), - "weekdays at 5:00 PM" - ); - } - - #[test] - fn format_timer_trigger_renders_hourly_rrule_as_text() { - assert_eq!( - format_timer_trigger(&TimerTrigger::Schedule { - dtstart: None, - rrule: Some("FREQ=HOURLY;BYMINUTE=0;BYSECOND=0".to_string()), - }), - "hourly on the hour" - ); - } - - #[test] - fn format_timer_trigger_preserves_unrecognized_rrule() { - assert_eq!( - format_timer_trigger(&TimerTrigger::Schedule { - dtstart: Some("2026-04-07T21:00:00".to_string()), - rrule: Some("FREQ=YEARLY;BYMONTH=4".to_string()), - }), - "schedule from Apr 7, 2026 9:00 PM; FREQ=YEARLY;BYMONTH=4" - ); - } - - #[test] - fn parser_output_schema_avoids_unsupported_union_keywords() { - let schema = output_schema(); - assert_eq!(schema.pointer("/properties/trigger/oneOf"), None); - assert_eq!(schema.pointer("/properties/trigger/anyOf"), None); - assert_eq!( - schema.pointer("/properties/trigger/properties/kind/enum"), - Some(&json!(["delay", "schedule"])) - ); - } - - #[test] - fn parser_prompt_defaults_ambiguous_loop_to_idle_recurring() { - assert!( - PARSE_TIMER_SYSTEM_PROMPT - .contains(r#"{ "kind": "delay", "seconds": 0, "repeat": true }"#) - ); - assert!(PARSE_TIMER_SYSTEM_PROMPT.contains("A bare absolute date/time is a single run")); - assert!(PARSE_TIMER_SYSTEM_PROMPT.contains("For absolute wall-clock timing like")); - assert!(PARSE_TIMER_SYSTEM_PROMPT.contains("choose the soonest future local occurrence")); - assert!(PARSE_TIMER_SYSTEM_PROMPT.contains("2026-04-06T23:30:00")); - } - - #[test] - fn parsed_timer_spec_accepts_permissive_delay_trigger_shape() { - let parsed: ParsedTimerSpec = serde_json::from_value(json!({ - "trigger": { - "kind": "delay", - "seconds": 10, - "repeat": false, - "dtstart": null, - "rrule": null - }, - "prompt": "tell me a joke", - "delivery": "after-turn" - })) - .expect("permissive parser schema output should deserialize"); - - assert_eq!( - parsed, - ParsedTimerSpec { - trigger: TimerTrigger::Delay { - seconds: 10, - repeat: Some(false), - }, - prompt: "tell me a joke".to_string(), - delivery: TimerDelivery::AfterTurn, - } - ); + assert!(prompt.contains("Call the TimerList tool directly")); } }