mcp-codex-worker 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/src/execution/base-adapter.d.ts +63 -0
  2. package/dist/src/execution/base-adapter.js +74 -0
  3. package/dist/src/execution/base-adapter.js.map +1 -0
  4. package/dist/src/execution/claude-adapter.d.ts +12 -0
  5. package/dist/src/execution/claude-adapter.js +23 -0
  6. package/dist/src/execution/claude-adapter.js.map +1 -0
  7. package/dist/src/execution/codex-adapter.d.ts +21 -0
  8. package/dist/src/execution/codex-adapter.js +35 -0
  9. package/dist/src/execution/codex-adapter.js.map +1 -0
  10. package/dist/src/execution/codex-pause-flow.d.ts +23 -0
  11. package/dist/src/execution/codex-pause-flow.js +185 -0
  12. package/dist/src/execution/codex-pause-flow.js.map +1 -0
  13. package/dist/src/execution/copilot-adapter.d.ts +12 -0
  14. package/dist/src/execution/copilot-adapter.js +23 -0
  15. package/dist/src/execution/copilot-adapter.js.map +1 -0
  16. package/dist/src/execution/provider-capabilities.d.ts +35 -0
  17. package/dist/src/execution/provider-capabilities.js +58 -0
  18. package/dist/src/execution/provider-capabilities.js.map +1 -0
  19. package/dist/src/execution/provider-registry.d.ts +37 -0
  20. package/dist/src/execution/provider-registry.js +69 -0
  21. package/dist/src/execution/provider-registry.js.map +1 -0
  22. package/dist/src/mcp/resource-renderers.d.ts +25 -0
  23. package/dist/src/mcp/resource-renderers.js +190 -0
  24. package/dist/src/mcp/resource-renderers.js.map +1 -0
  25. package/dist/src/mcp/sep1686-handlers.d.ts +56 -0
  26. package/dist/src/mcp/sep1686-handlers.js +98 -0
  27. package/dist/src/mcp/sep1686-handlers.js.map +1 -0
  28. package/dist/src/mcp/tool-definitions.d.ts +157 -0
  29. package/dist/src/mcp/tool-definitions.js +242 -0
  30. package/dist/src/mcp/tool-definitions.js.map +1 -1
  31. package/dist/src/task/fsm-transitions.d.ts +17 -0
  32. package/dist/src/task/fsm-transitions.js +66 -0
  33. package/dist/src/task/fsm-transitions.js.map +1 -0
  34. package/dist/src/task/task-handle-impl.d.ts +10 -0
  35. package/dist/src/task/task-handle-impl.js +139 -0
  36. package/dist/src/task/task-handle-impl.js.map +1 -0
  37. package/dist/src/task/task-handle.d.ts +88 -0
  38. package/dist/src/task/task-handle.js +2 -0
  39. package/dist/src/task/task-handle.js.map +1 -0
  40. package/dist/src/task/task-manager.d.ts +99 -0
  41. package/dist/src/task/task-manager.js +246 -0
  42. package/dist/src/task/task-manager.js.map +1 -0
  43. package/dist/src/task/task-persistence.d.ts +18 -0
  44. package/dist/src/task/task-persistence.js +61 -0
  45. package/dist/src/task/task-persistence.js.map +1 -0
  46. package/dist/src/task/task-state.d.ts +79 -0
  47. package/dist/src/task/task-state.js +24 -0
  48. package/dist/src/task/task-state.js.map +1 -0
  49. package/dist/src/task/task-store.d.ts +46 -0
  50. package/dist/src/task/task-store.js +104 -0
  51. package/dist/src/task/task-store.js.map +1 -0
  52. package/dist/src/task/wire-state-mapper.d.ts +21 -0
  53. package/dist/src/task/wire-state-mapper.js +63 -0
  54. package/dist/src/task/wire-state-mapper.js.map +1 -0
  55. package/package.json +2 -1
  56. package/src/execution/base-adapter.ts +133 -0
  57. package/src/execution/claude-adapter.ts +40 -0
  58. package/src/execution/codex-adapter.ts +67 -0
  59. package/src/execution/codex-pause-flow.ts +225 -0
  60. package/src/execution/copilot-adapter.ts +40 -0
  61. package/src/execution/provider-capabilities.ts +100 -0
  62. package/src/execution/provider-registry.ts +81 -0
  63. package/src/mcp/resource-renderers.ts +224 -0
  64. package/src/mcp/sep1686-handlers.ts +149 -0
  65. package/src/mcp/tool-definitions.ts +255 -0
  66. package/src/task/fsm-transitions.ts +72 -0
  67. package/src/task/task-handle-impl.ts +170 -0
  68. package/src/task/task-handle.ts +135 -0
  69. package/src/task/task-manager.ts +328 -0
  70. package/src/task/task-persistence.ts +95 -0
  71. package/src/task/task-state.ts +121 -0
  72. package/src/task/task-store.ts +121 -0
  73. package/src/task/wire-state-mapper.ts +77 -0
@@ -0,0 +1,104 @@
1
+ import { isTerminalStatus } from './task-state.js';
2
+ import { isValidTransition } from './fsm-transitions.js';
3
+ /**
4
+ * In-memory task store with FSM-validated state transitions.
5
+ *
6
+ * This is the single source of truth for task state. It enforces the FSM at
7
+ * the data layer — illegal transitions are rejected before any mutation occurs.
8
+ *
9
+ * Terminal tasks can be evicted by age to prevent unbounded memory growth.
10
+ */
11
+ export class TaskStore {
12
+ tasks = new Map();
13
+ /** Store a new task. Overwrites if the ID already exists. */
14
+ create(task) {
15
+ this.tasks.set(task.id, task);
16
+ }
17
+ /** Retrieve a task by its unique ID. */
18
+ get(id) {
19
+ return this.tasks.get(id);
20
+ }
21
+ /** Linear scan for a task matching the given session ID. */
22
+ getBySessionId(sessionId) {
23
+ for (const task of this.tasks.values()) {
24
+ if (task.sessionId === sessionId)
25
+ return task;
26
+ }
27
+ return undefined;
28
+ }
29
+ /** Return all tasks as an array. */
30
+ getAll() {
31
+ return [...this.tasks.values()];
32
+ }
33
+ /**
34
+ * Transition a task to a new status.
35
+ *
36
+ * Returns `true` if the transition was valid and applied, `false` if the
37
+ * task was not found or the transition is illegal per the FSM.
38
+ */
39
+ updateStatus(id, next) {
40
+ const task = this.tasks.get(id);
41
+ if (!task)
42
+ return false;
43
+ if (!isValidTransition(task.status, next))
44
+ return false;
45
+ const now = new Date().toISOString();
46
+ task.status = next;
47
+ task.updatedAt = now;
48
+ if (isTerminalStatus(next)) {
49
+ task.completedAt = now;
50
+ }
51
+ return true;
52
+ }
53
+ /**
54
+ * Apply a partial update to a task. If the update includes a `status`
55
+ * change, the FSM is validated first — an illegal transition causes the
56
+ * entire update to be rejected (returns `undefined`).
57
+ */
58
+ update(id, updates) {
59
+ const task = this.tasks.get(id);
60
+ if (!task)
61
+ return undefined;
62
+ // Validate status transition if status is being changed
63
+ if (updates.status !== undefined && updates.status !== task.status) {
64
+ if (!isValidTransition(task.status, updates.status))
65
+ return undefined;
66
+ }
67
+ const now = new Date().toISOString();
68
+ Object.assign(task, updates, { updatedAt: now });
69
+ if (updates.status !== undefined && isTerminalStatus(updates.status)) {
70
+ task.completedAt = now;
71
+ }
72
+ return task;
73
+ }
74
+ /** Remove a task by ID. Returns `true` if it existed. */
75
+ delete(id) {
76
+ return this.tasks.delete(id);
77
+ }
78
+ /**
79
+ * Remove terminal tasks whose `updatedAt` timestamp is older than
80
+ * `maxAgeMs` milliseconds from now.
81
+ *
82
+ * Returns the number of tasks evicted.
83
+ */
84
+ evict(maxAgeMs) {
85
+ const cutoff = Date.now() - maxAgeMs;
86
+ let count = 0;
87
+ for (const [id, task] of this.tasks) {
88
+ if (isTerminalStatus(task.status) && new Date(task.updatedAt).getTime() < cutoff) {
89
+ this.tasks.delete(id);
90
+ count++;
91
+ }
92
+ }
93
+ return count;
94
+ }
95
+ /** Remove all tasks. */
96
+ clear() {
97
+ this.tasks.clear();
98
+ }
99
+ /** Return the number of stored tasks. */
100
+ size() {
101
+ return this.tasks.size;
102
+ }
103
+ }
104
+ //# sourceMappingURL=task-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-store.js","sourceRoot":"","sources":["../../../src/task/task-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { TaskStatus } from './task-state.js';
2
+ /**
3
+ * SEP-1686 wire states — the 7 status values visible to MCP clients.
4
+ * Spec reference: §4.2
5
+ */
6
+ export type WireState = 'submitted' | 'working' | 'input_required' | 'completed' | 'failed' | 'cancelled' | 'unknown';
7
+ /**
8
+ * Maps an internal 10-state TaskStatus to a SEP-1686 wire state.
9
+ *
10
+ * Uses an exhaustive switch (no default) so TypeScript raises a compile error
11
+ * if a new TaskStatus variant is added without updating this mapping.
12
+ */
13
+ export declare function mapToWireState(status: TaskStatus): WireState;
14
+ /**
15
+ * Maps an internal 10-state TaskStatus to a short bracketed display badge
16
+ * for resource renders and UI.
17
+ *
18
+ * Uses an exhaustive switch (no default) so TypeScript raises a compile error
19
+ * if a new TaskStatus variant is added without updating this mapping.
20
+ */
21
+ export declare function mapToDisplay(status: TaskStatus): string;
@@ -0,0 +1,63 @@
1
+ import { TaskStatus } from './task-state.js';
2
+ /**
3
+ * Maps an internal 10-state TaskStatus to a SEP-1686 wire state.
4
+ *
5
+ * Uses an exhaustive switch (no default) so TypeScript raises a compile error
6
+ * if a new TaskStatus variant is added without updating this mapping.
7
+ */
8
+ export function mapToWireState(status) {
9
+ switch (status) {
10
+ case TaskStatus.WAITING:
11
+ return 'submitted';
12
+ case TaskStatus.PENDING:
13
+ return 'submitted';
14
+ case TaskStatus.RUNNING:
15
+ return 'working';
16
+ case TaskStatus.RATE_LIMITED:
17
+ return 'working';
18
+ case TaskStatus.WAITING_ANSWER:
19
+ return 'input_required';
20
+ case TaskStatus.COMPLETED:
21
+ return 'completed';
22
+ case TaskStatus.FAILED:
23
+ return 'failed';
24
+ case TaskStatus.TIMED_OUT:
25
+ return 'failed';
26
+ case TaskStatus.CANCELLED:
27
+ return 'cancelled';
28
+ case TaskStatus.UNKNOWN:
29
+ return 'unknown';
30
+ }
31
+ }
32
+ /**
33
+ * Maps an internal 10-state TaskStatus to a short bracketed display badge
34
+ * for resource renders and UI.
35
+ *
36
+ * Uses an exhaustive switch (no default) so TypeScript raises a compile error
37
+ * if a new TaskStatus variant is added without updating this mapping.
38
+ */
39
+ export function mapToDisplay(status) {
40
+ switch (status) {
41
+ case TaskStatus.WAITING:
42
+ return '[wait]';
43
+ case TaskStatus.PENDING:
44
+ return '[wait]';
45
+ case TaskStatus.RUNNING:
46
+ return '[busy]';
47
+ case TaskStatus.RATE_LIMITED:
48
+ return '[rate]';
49
+ case TaskStatus.WAITING_ANSWER:
50
+ return '[input]';
51
+ case TaskStatus.COMPLETED:
52
+ return '[done]';
53
+ case TaskStatus.FAILED:
54
+ return '[fail]';
55
+ case TaskStatus.TIMED_OUT:
56
+ return '[time]';
57
+ case TaskStatus.CANCELLED:
58
+ return '[cncl]';
59
+ case TaskStatus.UNKNOWN:
60
+ return '[???]';
61
+ }
62
+ }
63
+ //# sourceMappingURL=wire-state-mapper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wire-state-mapper.js","sourceRoot":"","sources":["../../../src/task/wire-state-mapper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAe7C;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,MAAkB;IAC/C,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,UAAU,CAAC,OAAO;YACrB,OAAO,WAAW,CAAC;QACrB,KAAK,UAAU,CAAC,OAAO;YACrB,OAAO,WAAW,CAAC;QACrB,KAAK,UAAU,CAAC,OAAO;YACrB,OAAO,SAAS,CAAC;QACnB,KAAK,UAAU,CAAC,YAAY;YAC1B,OAAO,SAAS,CAAC;QACnB,KAAK,UAAU,CAAC,cAAc;YAC5B,OAAO,gBAAgB,CAAC;QAC1B,KAAK,UAAU,CAAC,SAAS;YACvB,OAAO,WAAW,CAAC;QACrB,KAAK,UAAU,CAAC,MAAM;YACpB,OAAO,QAAQ,CAAC;QAClB,KAAK,UAAU,CAAC,SAAS;YACvB,OAAO,QAAQ,CAAC;QAClB,KAAK,UAAU,CAAC,SAAS;YACvB,OAAO,WAAW,CAAC;QACrB,KAAK,UAAU,CAAC,OAAO;YACrB,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,MAAkB;IAC7C,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,UAAU,CAAC,OAAO;YACrB,OAAO,QAAQ,CAAC;QAClB,KAAK,UAAU,CAAC,OAAO;YACrB,OAAO,QAAQ,CAAC;QAClB,KAAK,UAAU,CAAC,OAAO;YACrB,OAAO,QAAQ,CAAC;QAClB,KAAK,UAAU,CAAC,YAAY;YAC1B,OAAO,QAAQ,CAAC;QAClB,KAAK,UAAU,CAAC,cAAc;YAC5B,OAAO,SAAS,CAAC;QACnB,KAAK,UAAU,CAAC,SAAS;YACvB,OAAO,QAAQ,CAAC;QAClB,KAAK,UAAU,CAAC,MAAM;YACpB,OAAO,QAAQ,CAAC;QAClB,KAAK,UAAU,CAAC,SAAS;YACvB,OAAO,QAAQ,CAAC;QAClB,KAAK,UAAU,CAAC,SAAS;YACvB,OAAO,QAAQ,CAAC;QAClB,KAAK,UAAU,CAAC,OAAO;YACrB,OAAO,OAAO,CAAC;IACnB,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-codex-worker",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "MCP server bridge for Codex app-server",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -43,6 +43,7 @@
43
43
  "dependencies": {
44
44
  "@modelcontextprotocol/sdk": "^1.0.0",
45
45
  "tsx": "^4.20.6",
46
+ "unique-names-generator": "^4.7.1",
46
47
  "zod": "^3.24.4"
47
48
  },
48
49
  "devDependencies": {
@@ -0,0 +1,133 @@
1
+ // ---------------------------------------------------------------------------
2
+ // BaseProviderAdapter — Template Method pattern for provider lifecycle
3
+ // Spec reference: §3.4
4
+ // ---------------------------------------------------------------------------
5
+
6
+ import type { TaskHandle } from '../task/task-handle.js';
7
+ import type { Provider } from '../task/task-state.js';
8
+ import type { ProviderCapabilities } from './provider-capabilities.js';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Supporting types
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /**
15
+ * Options passed to `BaseProviderAdapter.spawn()` to start a provider session.
16
+ */
17
+ export interface ProviderSpawnOptions {
18
+ taskId: string;
19
+ prompt: string;
20
+ cwd: string;
21
+ timeout: number;
22
+ model?: string;
23
+ }
24
+
25
+ /**
26
+ * Result of an availability check on a provider adapter.
27
+ */
28
+ export type AvailabilityResult =
29
+ | { available: true }
30
+ | { available: false; reason: string };
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Abstract class
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /**
37
+ * Base class for all provider adapters.
38
+ *
39
+ * Implements the Template Method pattern: `spawn()` provides the common
40
+ * lifecycle skeleton (abort controller, timeout, error handling) and
41
+ * delegates the actual provider interaction to `executeSession()`.
42
+ */
43
+ export abstract class BaseProviderAdapter {
44
+ /** Unique provider identifier. */
45
+ abstract readonly id: Provider;
46
+
47
+ /** Human-readable display name. */
48
+ abstract readonly displayName: string;
49
+
50
+ // -- Abstract methods (subclasses must implement) --------------------------
51
+
52
+ /** Check whether the provider is currently available. */
53
+ abstract checkAvailability(): AvailabilityResult;
54
+
55
+ /** Return the capability matrix for this provider. */
56
+ abstract getCapabilities(): ProviderCapabilities;
57
+
58
+ /** Return runtime statistics for observability. */
59
+ abstract getStats(): Record<string, unknown>;
60
+
61
+ /**
62
+ * Template Method hook — the actual provider session logic.
63
+ * Subclasses implement this to drive the provider (spawn process, call API, etc.).
64
+ */
65
+ protected abstract executeSession(
66
+ handle: TaskHandle,
67
+ prompt: string,
68
+ signal: AbortSignal,
69
+ options: ProviderSpawnOptions,
70
+ ): Promise<void>;
71
+
72
+ // -- Public lifecycle methods ---------------------------------------------
73
+
74
+ /**
75
+ * Spawn a provider session for the given task.
76
+ *
77
+ * Template method: checks terminal state, sets up AbortController + timeout,
78
+ * calls `executeSession`, and handles errors.
79
+ */
80
+ async spawn(options: ProviderSpawnOptions, handle: TaskHandle): Promise<void> {
81
+ // Guard: do nothing if already in a terminal state
82
+ if (handle.isTerminal()) {
83
+ return;
84
+ }
85
+
86
+ const controller = new AbortController();
87
+ handle.registerAbort(controller);
88
+
89
+ let timer: ReturnType<typeof setTimeout> | undefined;
90
+
91
+ if (options.timeout > 0) {
92
+ timer = setTimeout(() => {
93
+ controller.abort();
94
+ }, options.timeout);
95
+ // Allow the Node.js process to exit even if this timer is pending
96
+ if (timer && typeof timer === 'object' && 'unref' in timer) {
97
+ timer.unref();
98
+ }
99
+ }
100
+
101
+ try {
102
+ await this.executeSession(handle, options.prompt, controller.signal, options);
103
+ } catch (err: unknown) {
104
+ if (controller.signal.aborted) {
105
+ handle.markCancelled('Session timed out');
106
+ } else {
107
+ const message = err instanceof Error ? err.message : String(err);
108
+ handle.markFailed(message);
109
+ }
110
+ } finally {
111
+ if (timer !== undefined) {
112
+ clearTimeout(timer);
113
+ }
114
+ handle.unregisterAbort();
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Attempt to abort a running task by ID.
120
+ * Default implementation returns false (not supported).
121
+ */
122
+ async abort(_taskId: string): Promise<boolean> {
123
+ return false;
124
+ }
125
+
126
+ /**
127
+ * Graceful shutdown hook. Called when the server is stopping.
128
+ * Default implementation is a no-op.
129
+ */
130
+ async shutdown(): Promise<void> {
131
+ // no-op by default
132
+ }
133
+ }
@@ -0,0 +1,40 @@
1
+ // ---------------------------------------------------------------------------
2
+ // ClaudeAdapter — Phase 2 stub
3
+ // Spec reference: §3.4
4
+ // ---------------------------------------------------------------------------
5
+
6
+ import type { Provider } from '../task/task-state.js';
7
+ import type { TaskHandle } from '../task/task-handle.js';
8
+ import type { ProviderCapabilities } from './provider-capabilities.js';
9
+ import { CLAUDE_CAPABILITIES } from './provider-capabilities.js';
10
+ import {
11
+ BaseProviderAdapter,
12
+ type ProviderSpawnOptions,
13
+ type AvailabilityResult,
14
+ } from './base-adapter.js';
15
+
16
+ export class ClaudeAdapter extends BaseProviderAdapter {
17
+ readonly id: Provider = 'claude-cli';
18
+ readonly displayName = 'Claude';
19
+
20
+ checkAvailability(): AvailabilityResult {
21
+ return { available: false, reason: 'Phase 2 — not yet implemented' };
22
+ }
23
+
24
+ getCapabilities(): ProviderCapabilities {
25
+ return CLAUDE_CAPABILITIES;
26
+ }
27
+
28
+ getStats(): Record<string, unknown> {
29
+ return {};
30
+ }
31
+
32
+ protected async executeSession(
33
+ _handle: TaskHandle,
34
+ _prompt: string,
35
+ _signal: AbortSignal,
36
+ _options: ProviderSpawnOptions,
37
+ ): Promise<void> {
38
+ throw new Error('Not implemented — Phase 2');
39
+ }
40
+ }
@@ -0,0 +1,67 @@
1
+ // ---------------------------------------------------------------------------
2
+ // CodexAdapter — wraps CodexRuntime behind the BaseProviderAdapter contract
3
+ // Spec reference: §3.4
4
+ // ---------------------------------------------------------------------------
5
+
6
+ import type { Provider } from '../task/task-state.js';
7
+ import type { TaskHandle } from '../task/task-handle.js';
8
+ import type { ProviderCapabilities } from './provider-capabilities.js';
9
+ import { CODEX_CAPABILITIES } from './provider-capabilities.js';
10
+ import {
11
+ BaseProviderAdapter,
12
+ type ProviderSpawnOptions,
13
+ type AvailabilityResult,
14
+ } from './base-adapter.js';
15
+ import type { CodexRuntime } from '../services/codex-runtime.js';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Options
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export interface CodexAdapterOptions {
22
+ command?: string;
23
+ args?: string[];
24
+ env?: NodeJS.ProcessEnv;
25
+ }
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Adapter
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export class CodexAdapter extends BaseProviderAdapter {
32
+ readonly id: Provider = 'codex';
33
+ readonly displayName = 'Codex';
34
+
35
+ private readonly options: CodexAdapterOptions;
36
+ private runtime?: CodexRuntime;
37
+
38
+ constructor(options: CodexAdapterOptions) {
39
+ super();
40
+ this.options = options;
41
+ }
42
+
43
+ checkAvailability(): AvailabilityResult {
44
+ return { available: true };
45
+ }
46
+
47
+ getCapabilities(): ProviderCapabilities {
48
+ return CODEX_CAPABILITIES;
49
+ }
50
+
51
+ getStats(): Record<string, unknown> {
52
+ return {};
53
+ }
54
+
55
+ protected async executeSession(
56
+ _handle: TaskHandle,
57
+ _prompt: string,
58
+ _signal: AbortSignal,
59
+ _options: ProviderSpawnOptions,
60
+ ): Promise<void> {
61
+ throw new Error('CodexAdapter.executeSession is not yet implemented');
62
+ }
63
+
64
+ async shutdown(): Promise<void> {
65
+ await this.runtime?.shutdown();
66
+ }
67
+ }
@@ -0,0 +1,225 @@
1
+ import type { AppServerClient } from '../services/app-server-client.js';
2
+ import type { TaskHandle } from '../task/task-handle.js';
3
+ import type { PendingQuestion } from '../task/task-state.js';
4
+ import type { JsonLineRequest, PendingServerRequest } from '../types/codex.js';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+
10
+ function asObject(value: unknown): Record<string, unknown> | undefined {
11
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
12
+ return undefined;
13
+ }
14
+ return value as Record<string, unknown>;
15
+ }
16
+
17
+ function asArray(value: unknown): unknown[] | undefined {
18
+ return Array.isArray(value) ? value : undefined;
19
+ }
20
+
21
+ function asString(value: unknown): string | undefined {
22
+ return typeof value === 'string' ? value : undefined;
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Translation: Codex server-request → PendingQuestion
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /**
30
+ * Translate a raw Codex JSON-RPC server request into a provider-blind
31
+ * PendingQuestion variant.
32
+ *
33
+ * Returns `null` for:
34
+ * - Guardian Subagent auto-approval events (method contains `autoApprovalReview`)
35
+ * - Unrecognised / unmapped methods
36
+ *
37
+ * Spec reference: §6.1, §6.2
38
+ */
39
+ export function translateCodexRequestToPendingQuestion(
40
+ req: JsonLineRequest,
41
+ ): PendingQuestion | null {
42
+ // Guardian Subagent bypass (PR #13860) — never surface as a pause
43
+ if (req.method.includes('autoApprovalReview')) {
44
+ return null;
45
+ }
46
+
47
+ const params = asObject(req.params);
48
+ const requestId = String(req.id);
49
+
50
+ switch (req.method) {
51
+ // -- user_input --------------------------------------------------------
52
+ case 'tool/requestUserInput':
53
+ case 'item/tool/requestUserInput': {
54
+ const rawQuestions = asArray(params?.questions) ?? [];
55
+ const questions = rawQuestions.map((raw) => {
56
+ const q = asObject(raw) ?? {};
57
+ return {
58
+ id: asString(q.id) ?? '',
59
+ text: asString(q.text) ?? asString(q.question) ?? '',
60
+ ...(asArray(q.options)
61
+ ? { options: (q.options as unknown[]).map(String) }
62
+ : {}),
63
+ ...(q.isOther !== undefined || q.allowEmpty !== undefined
64
+ ? { allowFreeform: Boolean(q.isOther ?? q.allowEmpty) }
65
+ : {}),
66
+ };
67
+ });
68
+ return { type: 'user_input', requestId, questions };
69
+ }
70
+
71
+ // -- command_approval --------------------------------------------------
72
+ case 'item/commandExecution/requestApproval':
73
+ case 'execCommandApproval': {
74
+ const command = asString(params?.command) ?? '';
75
+ const sandboxPolicy = asString(params?.sandbox_policy) ?? asString(params?.sandboxPolicy);
76
+ return {
77
+ type: 'command_approval',
78
+ requestId,
79
+ command,
80
+ ...(sandboxPolicy !== undefined ? { sandboxPolicy } : {}),
81
+ };
82
+ }
83
+
84
+ // -- file_approval -----------------------------------------------------
85
+ case 'item/fileChange/requestApproval':
86
+ case 'applyPatchApproval': {
87
+ const rawChanges = asArray(params?.file_changes) ?? asArray(params?.fileChanges) ?? [];
88
+ const fileChanges = rawChanges.map((raw) => {
89
+ const fc = asObject(raw) ?? {};
90
+ return {
91
+ path: asString(fc.path) ?? '',
92
+ patch: asString(fc.patch) ?? '',
93
+ };
94
+ });
95
+ return { type: 'file_approval', requestId, fileChanges };
96
+ }
97
+
98
+ // -- elicitation -------------------------------------------------------
99
+ case 'mcpServer/elicitation/request': {
100
+ const serverName = asString(params?.server_name) ?? asString(params?.serverName);
101
+ const message = asString(params?.message) ?? '';
102
+ const schema = params?.schema;
103
+ return {
104
+ type: 'elicitation',
105
+ requestId,
106
+ ...(serverName !== undefined ? { serverName } : {}),
107
+ message,
108
+ ...(schema !== undefined ? { schema } : {}),
109
+ };
110
+ }
111
+
112
+ // -- dynamic_tool ------------------------------------------------------
113
+ case 'item/tool/call': {
114
+ const toolName = asString(params?.tool_name) ?? asString(params?.toolName) ?? '';
115
+ const rawArgs = params?.arguments;
116
+ const args =
117
+ typeof rawArgs === 'string'
118
+ ? rawArgs
119
+ : rawArgs !== undefined
120
+ ? JSON.stringify(rawArgs)
121
+ : '{}';
122
+ return { type: 'dynamic_tool', requestId, toolName, arguments: args };
123
+ }
124
+
125
+ default:
126
+ return null;
127
+ }
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Elicitation auto-decline (Issue #11816 workaround)
132
+ // ---------------------------------------------------------------------------
133
+
134
+ const ELICITATION_AUTO_DECLINE_MS = 30_000;
135
+
136
+ function scheduleElicitationAutoDecline(
137
+ client: AppServerClient,
138
+ handle: TaskHandle,
139
+ question: Extract<PendingQuestion, { type: 'elicitation' }>,
140
+ timeoutMs: number = ELICITATION_AUTO_DECLINE_MS,
141
+ ): void {
142
+ const timer = setTimeout(() => {
143
+ const still = handle
144
+ .getPendingQuestions()
145
+ .find((q) => q.requestId === question.requestId);
146
+ if (!still) return;
147
+
148
+ client
149
+ .respondToServerRequest(question.requestId, {
150
+ action: 'decline',
151
+ content: {},
152
+ })
153
+ .catch(() => {});
154
+
155
+ const head = handle.getPendingQuestions()[0];
156
+ if (head?.requestId === question.requestId) {
157
+ handle.dequeuePendingQuestion();
158
+ }
159
+
160
+ handle.writeOutput(
161
+ `[elicitation] auto-declined after ${timeoutMs}ms (issue #11816 workaround)`,
162
+ );
163
+ }, timeoutMs);
164
+ timer.unref();
165
+ }
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // Attachment: wire pause-flow onto an AppServerClient + TaskHandle
169
+ // ---------------------------------------------------------------------------
170
+
171
+ function extractThreadIdFromParams(params: unknown): string | undefined {
172
+ const obj = asObject(params);
173
+ const threadId = obj?.threadId;
174
+ if (typeof threadId === 'string') return threadId;
175
+ const thread = asObject(obj?.thread);
176
+ if (typeof thread?.id === 'string') return thread.id;
177
+ return undefined;
178
+ }
179
+
180
+ /**
181
+ * Listen for `server-request` events on the AppServerClient, translate each
182
+ * to a PendingQuestion, queue it on the handle, and transition to
183
+ * WAITING_ANSWER. Returns an unsubscribe function.
184
+ *
185
+ * Spec reference: §6.2, §6.3
186
+ */
187
+ export function attachPauseFlow(
188
+ client: AppServerClient,
189
+ handle: TaskHandle,
190
+ threadId: string,
191
+ ): () => void {
192
+ const onServerRequest = (pending: PendingServerRequest) => {
193
+ const reqThreadId = extractThreadIdFromParams(pending.params);
194
+ if (reqThreadId !== threadId) return;
195
+
196
+ // Guardian Subagent bypass — log but don't surface
197
+ if (pending.method.includes('autoApprovalReview')) {
198
+ handle.writeOutputFileOnly(`[guardian] ${pending.method}`);
199
+ return;
200
+ }
201
+
202
+ const req: JsonLineRequest = {
203
+ method: pending.method,
204
+ id: pending.id,
205
+ params: pending.params,
206
+ };
207
+
208
+ const question = translateCodexRequestToPendingQuestion(req);
209
+ if (!question) return;
210
+
211
+ handle.queuePendingQuestion(question);
212
+ if (!handle.isTerminal()) {
213
+ handle.markInputRequired();
214
+ }
215
+
216
+ if (question.type === 'elicitation') {
217
+ scheduleElicitationAutoDecline(client, handle, question);
218
+ }
219
+ };
220
+
221
+ client.on('server-request', onServerRequest);
222
+ return () => {
223
+ client.off('server-request', onServerRequest);
224
+ };
225
+ }