@zq-silk/yui 0.6.8 → 0.6.10

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 (58) hide show
  1. package/README.md +10 -3
  2. package/dist/cli/commandCatalog.js +1 -1
  3. package/dist/commands/taskCommands.js +63 -28
  4. package/dist/commands/taskContextCommand.js +27 -1
  5. package/dist/commands/taskRoleRuntimeStatus.js +17 -6
  6. package/dist/controller/agentRuntimeObserver.js +6 -3
  7. package/dist/controller/controller.js +29 -47
  8. package/dist/controller/fileSchedulerStoreAdapter.js +572 -258
  9. package/dist/controller/runtime.js +8 -1
  10. package/dist/controller/runtimeEventInbox.js +16 -5
  11. package/dist/controller/runtimeHookRunFence.js +51 -5
  12. package/dist/controller/runtimeObservationHook.js +8 -2
  13. package/dist/coordination/workMailbox.js +408 -28
  14. package/dist/coordination/workMailboxQueue.js +12 -10
  15. package/dist/executor/agentExecutor.js +101 -94
  16. package/dist/executor/executorRegistry.js +47 -2
  17. package/dist/executor/fileRoleLaunchPlanner.js +4 -2
  18. package/dist/lifecycle/exactRunTerminalization.js +1 -7
  19. package/dist/repository/taskWorkspaceCoordinator.js +9 -4
  20. package/dist/runtime/agentDriver.js +83 -4
  21. package/dist/runtime/agentDriverObservation.js +25 -10
  22. package/dist/runtime/builtinAgentDrivers.js +168 -18
  23. package/dist/runtime/codexAppServerRuntime.js +355 -0
  24. package/dist/runtime/continuationManager.js +117 -0
  25. package/dist/runtime/index.js +2 -0
  26. package/dist/runtime/lifecycleReservation.js +4 -3
  27. package/dist/runtime/promptEnvelope.js +14 -3
  28. package/dist/runtime/providerContinuation.js +225 -0
  29. package/dist/runtime/providerContinuationReconciliationService.js +172 -0
  30. package/dist/runtime/providerRuntimeIdentity.js +232 -0
  31. package/dist/runtime/providerRuntimeReconciler.js +166 -0
  32. package/dist/runtime/runtimeContinuationProjection.js +34 -0
  33. package/dist/runtime/runtimeObservation.js +217 -6
  34. package/dist/runtime/runtimeProjection.js +172 -11
  35. package/dist/scheduler/activeRoleRunDelivery.js +314 -1
  36. package/dist/scheduler/leaderWakeupProcessor.js +2 -1
  37. package/dist/scheduler/operatorInputNotificationProcessor.js +3 -2
  38. package/dist/scheduler/roleRunLiveness.js +8 -7
  39. package/dist/scheduler/roleRunStall.js +4 -2
  40. package/dist/scheduler/taskExecutionProjection.js +2 -2
  41. package/dist/storage/migration/productionRegistry.js +474 -1
  42. package/dist/storage/sqliteSchema.js +102 -21
  43. package/dist/storage/sqliteStore.js +52 -110
  44. package/dist/storage/storageVersions.js +1 -1
  45. package/dist/storage/storeRpc.js +0 -1
  46. package/dist/storage/taskStore.js +40 -53
  47. package/dist/storage/upgrade/sqliteStateMigration.js +0 -21
  48. package/dist/task/nextAction.js +1 -1
  49. package/dist/web/assets/client/app.js +1 -1
  50. package/dist/web/assets/client/components.js +233 -4
  51. package/dist/web/assets/client/i18n.js +166 -2
  52. package/dist/web/assets/client/view.js +30 -13
  53. package/dist/web/assets/styles/cards.js +62 -0
  54. package/dist/web/assets/styles/widgets.js +1 -0
  55. package/dist/web/webSnapshot.js +11 -2
  56. package/package.json +1 -1
  57. package/skills/yui-leader/SKILL.md +33 -24
  58. package/skills/yui-operator/SKILL.md +7 -5
@@ -1,23 +1,29 @@
1
1
  import { createHash } from "node:crypto";
2
- import { createRuntimeObservation } from "./runtimeObservation.js";
2
+ import { createRuntimeObservation, runtimeObservationSemanticKey } from "./runtimeObservation.js";
3
3
  export function mapAgentDriverHook(input) {
4
+ return mapAgentDriverHooks(input)[0];
5
+ }
6
+ export function mapAgentDriverHooks(input) {
4
7
  requireMatchingDriver(input);
5
- const mapped = input.driver.runtime.mapHook({
8
+ const raw = input.driver.runtime.mapHook({
6
9
  hookEventName: input.hookEventName,
7
10
  payload: input.payload,
8
11
  ...(input.occurrenceId === undefined ? {} : { occurrenceId: input.occurrenceId })
9
12
  });
10
- const source = mapped.kind === "turn.accepted"
13
+ const mapped = Array.isArray(raw) ? raw : [raw];
14
+ if (mapped.length === 0)
15
+ throw new Error("Agent Driver Hook must map at least one observation.");
16
+ const source = mapped.some((entry) => entry.kind === "turn.accepted")
11
17
  ? input.driver.runtime.observer?.source({
12
18
  hookEventName: input.hookEventName,
13
19
  payload: input.payload,
14
20
  ...(input.occurrenceId === undefined ? {} : { occurrenceId: input.occurrenceId })
15
21
  }) ?? null
16
22
  : null;
17
- return observation(input, source === null ? mapped : {
18
- ...mapped,
19
- payload: Object.freeze({ ...mapped.payload, observerSource: source })
20
- });
23
+ return Object.freeze(mapped.map((entry, index) => observation({ ...input, ordinal: (input.ordinal ?? 0) + index }, source === null || entry.kind !== "turn.accepted" ? entry : {
24
+ ...entry,
25
+ payload: Object.freeze({ ...entry.payload, observerSource: source })
26
+ })));
21
27
  }
22
28
  export function mapAgentDriverUsage(input, usage) {
23
29
  requireMatchingDriver(input);
@@ -27,16 +33,25 @@ export function mapAgentDriverUsage(input, usage) {
27
33
  });
28
34
  }
29
35
  function observation(input, mapped) {
36
+ const eventId = hookEventId(input, mapped);
37
+ const fence = { ...input.fence, ...mapped.fence };
30
38
  return createRuntimeObservation({
31
- schemaVersion: 1,
32
- eventId: hookEventId(input, mapped),
39
+ schemaVersion: 2,
40
+ eventId,
41
+ semanticKey: runtimeObservationSemanticKey({
42
+ eventId,
43
+ kind: mapped.kind,
44
+ fence,
45
+ ...(input.sequence === undefined ? {} : { sequence: input.sequence }),
46
+ payload: mapped.payload
47
+ }),
33
48
  kind: mapped.kind,
34
49
  authority: "provider-structured",
35
50
  receivedAt: input.receivedAt,
36
51
  ...(input.observedAt === undefined ? {} : { observedAt: input.observedAt }),
37
52
  ...(input.sequence === undefined ? {} : { sequence: input.sequence }),
38
53
  ...(input.ordinal === undefined ? {} : { ordinal: input.ordinal }),
39
- fence: input.fence,
54
+ fence,
40
55
  payload: mapped.payload
41
56
  });
42
57
  }
@@ -14,6 +14,24 @@ const STRUCTURED_CLI_CAPABILITIES = Object.freeze({
14
14
  interrupt: true,
15
15
  stop: true
16
16
  }),
17
+ conversation: Object.freeze({
18
+ persistentIdentity: "exact",
19
+ crossProcessResume: true,
20
+ readback: "partial"
21
+ }),
22
+ input: Object.freeze({
23
+ startTurn: true,
24
+ steer: "unavailable",
25
+ inject: "unavailable",
26
+ acceptance: "exact",
27
+ idempotency: "unavailable"
28
+ }),
29
+ descendants: Object.freeze({
30
+ lineage: "partial",
31
+ detachedQuery: "partial",
32
+ resultRouting: "partial"
33
+ }),
34
+ bounded: Object.freeze({ structuredTerminal: true }),
17
35
  observation: Object.freeze({
18
36
  sessionIdentity: "exact",
19
37
  sessionBootstrap: "discovered",
@@ -50,7 +68,11 @@ export const BUILTIN_AGENT_DRIVERS = Object.freeze([
50
68
  ...(hookEventName === "SessionStart" && payload.source === "startup"
51
69
  ? { startupSession: "preallocated" }
52
70
  : {}),
53
- terminal: isTerminalHook(hookEventName)
71
+ terminal: isTerminalHook(hookEventName),
72
+ ...(hookEventName !== "SubagentStop" ? {} : {
73
+ continuationId: subagentId(payload),
74
+ continuationGeneration: continuationGeneration(payload)
75
+ })
54
76
  }),
55
77
  observer: Object.freeze({
56
78
  source: (input) => transcriptObserverSource(CLAUDE_CODE_DRIVER_ID, input),
@@ -63,16 +85,40 @@ export const BUILTIN_AGENT_DRIVERS = Object.freeze([
63
85
  label: "Codex",
64
86
  protocolVersion: 1,
65
87
  adapterId: "codex",
66
- capabilities: STRUCTURED_CLI_CAPABILITIES,
88
+ capabilities: Object.freeze({
89
+ ...STRUCTURED_CLI_CAPABILITIES,
90
+ surfaces: Object.freeze(["interactive-cli", "managed-protocol"]),
91
+ conversation: Object.freeze({
92
+ persistentIdentity: "exact",
93
+ crossProcessResume: true,
94
+ readback: "exact"
95
+ }),
96
+ input: Object.freeze({
97
+ startTurn: true,
98
+ steer: "fenced",
99
+ inject: "fenced",
100
+ acceptance: "exact",
101
+ idempotency: "unavailable"
102
+ }),
103
+ descendants: Object.freeze({
104
+ lineage: "partial",
105
+ detachedQuery: "partial",
106
+ resultRouting: "partial"
107
+ })
108
+ }),
67
109
  runtime: Object.freeze({
68
110
  nativeSessionId: ({ payload }) => (optionalIdentityFrom(payload, ["session_id"])),
69
111
  nativeTurnId: ({ payload }) => (optionalIdentityFrom(payload, ["turn_id", "prompt_id"])),
70
112
  mapHook: ({ hookEventName, payload, occurrenceId }) => (mapCodexHook(hookEventName, payload, occurrenceId)),
71
- classifyHook: ({ hookEventName }) => Object.freeze({
113
+ classifyHook: ({ hookEventName, payload }) => Object.freeze({
72
114
  ...(hookEventName === "SessionStart"
73
115
  ? { startupSession: "discovered" }
74
116
  : {}),
75
- terminal: isTerminalHook(hookEventName)
117
+ terminal: isTerminalHook(hookEventName),
118
+ ...(hookEventName !== "SubagentStop" ? {} : {
119
+ continuationId: subagentId(payload),
120
+ continuationGeneration: continuationGeneration(payload)
121
+ })
76
122
  }),
77
123
  observer: Object.freeze({
78
124
  source: (input) => transcriptObserverSource(CODEX_DRIVER_ID, input),
@@ -90,9 +136,13 @@ export function builtinAgentDriverRegistry() {
90
136
  function mapClaudeHook(name, payload, occurrenceId) {
91
137
  switch (name) {
92
138
  case "SessionStart":
93
- return payload.source === "startup"
94
- ? mapped("session.ready")
95
- : mapped("session.started");
139
+ return [
140
+ payload.source === "startup"
141
+ ? mapped("session.ready")
142
+ : mapped("session.started"),
143
+ mapped("conversation.observed", { recoverability: "recoverable" }),
144
+ mapped("activation.started")
145
+ ];
96
146
  case "UserPromptSubmit":
97
147
  return mapped("turn.accepted");
98
148
  case "PreToolUse":
@@ -119,15 +169,51 @@ function mapClaudeHook(name, payload, occurrenceId) {
119
169
  });
120
170
  }
121
171
  case "SubagentStart":
122
- return operation("operation.started", "subagent", subagentId(payload));
172
+ return [
173
+ operation("operation.started", "subagent", subagentId(payload)),
174
+ continuationObservation("continuation.started", payload, {
175
+ execution: "active",
176
+ outcome: "pending",
177
+ attachment: "attached",
178
+ observationQuality: "exact",
179
+ mayWriteWorkspace: true
180
+ })
181
+ ];
123
182
  case "SubagentStop":
124
- return operation("operation.completed", "subagent", subagentId(payload));
183
+ return [
184
+ operation("operation.completed", "subagent", subagentId(payload)),
185
+ ...(optionalSummary(payload).summary === undefined ? [] : [
186
+ continuationObservation("continuation.reported", payload, {
187
+ execution: "quiescent",
188
+ outcome: "succeeded",
189
+ attachment: "attached",
190
+ observationQuality: "exact",
191
+ mayWriteWorkspace: false,
192
+ reportId: reportId(payload),
193
+ ...optionalSummary(payload)
194
+ })
195
+ ]),
196
+ continuationObservation("continuation.settled", payload, {
197
+ execution: "quiescent",
198
+ outcome: continuationOutcome(payload),
199
+ attachment: "attached",
200
+ observationQuality: "exact",
201
+ mayWriteWorkspace: false,
202
+ ...optionalSummary(payload)
203
+ })
204
+ ];
125
205
  case "Stop":
126
- return mapped("turn.completed", optionalSummary(payload));
206
+ return [
207
+ mapped("turn.completed", optionalSummary(payload)),
208
+ mapped("native-work.snapshot", {
209
+ snapshotComplete: payload.background_tasks_complete === true,
210
+ observationQuality: payload.background_tasks_complete === true ? "exact" : "partial"
211
+ })
212
+ ];
127
213
  case "StopFailure":
128
214
  return mapped("turn.failed", claudeFailure(payload));
129
215
  case "SessionEnd":
130
- return mapped("session.ended");
216
+ return [mapped("session.ended"), mapped("activation.ended")];
131
217
  default:
132
218
  throw new Error(`Claude Code Driver does not support Hook event: ${name}.`);
133
219
  }
@@ -135,7 +221,11 @@ function mapClaudeHook(name, payload, occurrenceId) {
135
221
  function mapCodexHook(name, payload, occurrenceId) {
136
222
  switch (name) {
137
223
  case "SessionStart":
138
- return mapped("session.started");
224
+ return [
225
+ mapped("session.started"),
226
+ mapped("conversation.observed", { recoverability: "recoverable" }),
227
+ mapped("activation.started")
228
+ ];
139
229
  case "UserPromptSubmit":
140
230
  return mapped("turn.accepted");
141
231
  case "PreToolUse":
@@ -151,19 +241,76 @@ function mapCodexHook(name, payload, occurrenceId) {
151
241
  ?? requireOccurrence(occurrenceId)
152
242
  });
153
243
  case "SubagentStart":
154
- return operation("operation.started", "subagent", subagentId(payload));
244
+ return [
245
+ operation("operation.started", "subagent", subagentId(payload)),
246
+ continuationObservation("continuation.started", payload, {
247
+ execution: "active",
248
+ outcome: "pending",
249
+ attachment: "attached",
250
+ observationQuality: "partial",
251
+ mayWriteWorkspace: true
252
+ })
253
+ ];
155
254
  case "SubagentStop":
156
- return operation("operation.completed", "subagent", subagentId(payload));
255
+ return [
256
+ operation("operation.completed", "subagent", subagentId(payload)),
257
+ continuationObservation("continuation.reported", payload, {
258
+ execution: "unknown",
259
+ outcome: "unknown",
260
+ attachment: "attached",
261
+ observationQuality: "partial",
262
+ mayWriteWorkspace: true,
263
+ reportId: reportId(payload),
264
+ ...optionalSummary(payload)
265
+ })
266
+ ];
157
267
  case "Stop":
158
268
  return mapped("turn.completed", optionalSummary(payload));
159
269
  case "SessionEnd":
160
- return mapped("session.ended");
270
+ return [mapped("session.ended"), mapped("activation.ended")];
161
271
  default:
162
272
  throw new Error(`Codex Driver does not support Hook event: ${name}.`);
163
273
  }
164
274
  }
165
- function mapped(kind, payload = {}) {
166
- return Object.freeze({ kind, payload: Object.freeze({ ...payload }) });
275
+ function mapped(kind, payload = {}, fence) {
276
+ return Object.freeze({
277
+ kind,
278
+ payload: Object.freeze({ ...payload }),
279
+ ...(fence === undefined ? {} : { fence: Object.freeze({ ...fence }) })
280
+ });
281
+ }
282
+ function continuationObservation(kind, native, payload) {
283
+ const continuationId = subagentId(native);
284
+ const generation = continuationGeneration(native);
285
+ return mapped(kind, payload, {
286
+ continuationId,
287
+ continuationGeneration: generation,
288
+ ...(optionalIdentityFrom(native, ["parent_agent_id", "parent_subagent_id"]) === undefined
289
+ ? {}
290
+ : {
291
+ parentContinuationId: optionalIdentityFrom(native, ["parent_agent_id", "parent_subagent_id"])
292
+ })
293
+ });
294
+ }
295
+ function continuationGeneration(native) {
296
+ return typeof native.generation === "number"
297
+ && Number.isSafeInteger(native.generation) && native.generation >= 1
298
+ ? native.generation
299
+ : 1;
300
+ }
301
+ function reportId(payload) {
302
+ return optionalIdentityFrom(payload, ["report_id", "message_id", "agent_id", "subagent_id"])
303
+ ?? subagentId(payload);
304
+ }
305
+ function continuationOutcome(payload) {
306
+ if (payload.cancelled === true || payload.status === "cancelled")
307
+ return "cancelled";
308
+ if (payload.error !== undefined || payload.status === "failed")
309
+ return "failed";
310
+ if (payload.status === undefined || payload.status === "completed" || payload.status === "succeeded") {
311
+ return "succeeded";
312
+ }
313
+ return "unknown";
167
314
  }
168
315
  function operation(kind, operationKind, operationId) {
169
316
  return mapped(kind, { operationId, operation: operationKind });
@@ -209,7 +356,10 @@ function claudeFailure(payload) {
209
356
  failure: {
210
357
  code,
211
358
  ...(details === undefined ? {} : { details }),
212
- ...(lastOutput === undefined ? {} : { lastOutput })
359
+ ...(lastOutput === undefined ? {} : { lastOutput }),
360
+ ...(payload.run_terminal === true || payload.unrecoverable === true
361
+ ? { runTerminal: true }
362
+ : {})
213
363
  },
214
364
  summary: [
215
365
  "Agent turn failed.",
@@ -0,0 +1,355 @@
1
+ export class CodexAppServerRequestError extends Error {
2
+ code;
3
+ data;
4
+ name = "CodexAppServerRequestError";
5
+ constructor(code, message, data) {
6
+ super(message);
7
+ this.code = code;
8
+ this.data = data;
9
+ }
10
+ }
11
+ /**
12
+ * Continuable Codex integration over App Server. A transport connection is
13
+ * deliberately not an Activation identity: callers supply the persisted
14
+ * Conversation/Activation fence on every state-changing request.
15
+ */
16
+ export class CodexAppServerRuntime {
17
+ transport;
18
+ constructor(transport) {
19
+ this.transport = transport;
20
+ }
21
+ async openConversation(input) {
22
+ const result = await this.transport.request("thread/start", {
23
+ cwd: text(input.cwd, "Codex thread cwd"),
24
+ ...(input.model === undefined ? {} : { model: input.model }),
25
+ ...(input.approvalPolicy === undefined ? {} : { approvalPolicy: input.approvalPolicy }),
26
+ ...(input.sandbox === undefined ? {} : { sandbox: input.sandbox }),
27
+ ...(input.developerInstructions === undefined
28
+ ? {}
29
+ : { developerInstructions: input.developerInstructions }),
30
+ ...(input.runtimeWorkspaceRoots === undefined
31
+ ? {}
32
+ : { runtimeWorkspaceRoots: [...input.runtimeWorkspaceRoots] })
33
+ });
34
+ return { conversationId: threadId(result) };
35
+ }
36
+ async resumeConversation(conversationId) {
37
+ const id = text(conversationId, "Codex thread id");
38
+ const result = await this.transport.request("thread/resume", { threadId: id });
39
+ return parseThreadSnapshot(result, id, true);
40
+ }
41
+ async readConversation(conversationId) {
42
+ const id = text(conversationId, "Codex thread id");
43
+ const result = await this.transport.request("thread/read", {
44
+ threadId: id,
45
+ includeTurns: true
46
+ });
47
+ return parseThreadSnapshot(result, id, "unknown");
48
+ }
49
+ async startTurn(input) {
50
+ const snapshot = await this.readConversation(input.conversationId);
51
+ if (input.expectedNoActiveTurn && snapshot.activeTurnId !== undefined) {
52
+ return { status: "not-accepted", reason: `active-turn:${snapshot.activeTurnId}` };
53
+ }
54
+ try {
55
+ const result = await this.transport.request("turn/start", {
56
+ threadId: snapshot.threadId,
57
+ ...(input.clientUserMessageId === undefined
58
+ ? {}
59
+ : { clientUserMessageId: text(input.clientUserMessageId, "Codex input attempt id") }),
60
+ input: [{ type: "text", text: text(input.text, "Codex Turn input") }]
61
+ });
62
+ const turnId = optionalId(result.turnId)
63
+ ?? optionalId(objectMember(result, "turn")?.id);
64
+ return turnId === undefined
65
+ ? { status: "unknown", reason: "turn/start returned no durable turn id" }
66
+ : { status: "accepted", turnId };
67
+ }
68
+ catch (error) {
69
+ return classifyMutationError(error);
70
+ }
71
+ }
72
+ async steerTurn(input) {
73
+ const threadId = text(input.conversationId, "Codex thread id");
74
+ const expectedTurnId = text(input.expectedTurnId, "Codex expected Turn id");
75
+ const snapshot = await this.readConversation(threadId);
76
+ if (snapshot.activeTurnId !== expectedTurnId) {
77
+ return {
78
+ status: "not-accepted",
79
+ reason: snapshot.activeTurnId === undefined
80
+ ? "expected Turn is no longer active"
81
+ : `active Turn changed to ${snapshot.activeTurnId}`
82
+ };
83
+ }
84
+ try {
85
+ const result = await this.transport.request("turn/steer", {
86
+ threadId,
87
+ expectedTurnId,
88
+ ...(input.clientUserMessageId === undefined
89
+ ? {}
90
+ : { clientUserMessageId: text(input.clientUserMessageId, "Codex input attempt id") }),
91
+ input: [{ type: "text", text: text(input.text, "Codex steer input") }]
92
+ });
93
+ const acceptedTurnId = optionalId(result.turnId);
94
+ if (acceptedTurnId === expectedTurnId)
95
+ return { status: "accepted", turnId: acceptedTurnId };
96
+ return acceptedTurnId === undefined
97
+ ? { status: "unknown", reason: "turn/steer returned no acceptance Turn id" }
98
+ : { status: "unknown", reason: `turn/steer returned mismatched Turn ${acceptedTurnId}` };
99
+ }
100
+ catch (error) {
101
+ return classifyMutationError(error);
102
+ }
103
+ }
104
+ async injectItems(input) {
105
+ const threadId = text(input.conversationId, "Codex thread id");
106
+ try {
107
+ await this.transport.request("thread/inject_items", {
108
+ threadId,
109
+ items: [{ type: "text", text: text(input.text, "Codex injected input") }]
110
+ });
111
+ return "accepted";
112
+ }
113
+ catch (error) {
114
+ const classified = classifyMutationError(error);
115
+ if (classified.status === "unknown")
116
+ return "unknown";
117
+ return isNotLoaded(error) ? "unavailable" : "not-accepted";
118
+ }
119
+ }
120
+ async interruptTurn(input) {
121
+ try {
122
+ await this.transport.request("turn/interrupt", {
123
+ threadId: text(input.conversationId, "Codex thread id"),
124
+ turnId: text(input.turnId, "Codex Turn id")
125
+ });
126
+ return "interrupted";
127
+ }
128
+ catch (error) {
129
+ const classified = classifyMutationError(error);
130
+ return classified.status === "not-accepted" ? "not-active" : "unknown";
131
+ }
132
+ }
133
+ async listKnownDescendants(input) {
134
+ const threadId = text(input.conversationId, "Codex thread id");
135
+ const result = await this.transport.request("thread/list", {
136
+ ancestorThreadId: threadId
137
+ });
138
+ const candidates = arrayMember(result, "threads")
139
+ .flatMap((entry) => optionalId(object(entry)?.id) === undefined
140
+ ? []
141
+ : [optionalId(object(entry)?.id)]);
142
+ // Ancestor filters are experimental: absence is never exact settlement.
143
+ return { quality: "partial", threadIds: Object.freeze([...new Set(candidates)]) };
144
+ }
145
+ /** Exact readback for child thread IDs already persisted by Yui. */
146
+ async queryKnownContinuations(input) {
147
+ if (input.providerNamespace !== "openai/codex") {
148
+ return { quality: "unavailable", continuations: [], detail: "provider mismatch" };
149
+ }
150
+ const observed = [];
151
+ try {
152
+ for (const continuation of input.continuations) {
153
+ const snapshot = await this.readConversation(continuation.continuationId);
154
+ const state = codexContinuationState(snapshot);
155
+ observed.push({
156
+ key: [
157
+ input.providerNamespace,
158
+ input.accountScope,
159
+ input.conversationId,
160
+ input.activationId,
161
+ continuation.continuationId,
162
+ continuation.generation
163
+ ].join("\u0000"),
164
+ ...state
165
+ });
166
+ }
167
+ }
168
+ catch (error) {
169
+ return {
170
+ quality: "unavailable",
171
+ continuations: [],
172
+ detail: error instanceof Error ? error.message : String(error)
173
+ };
174
+ }
175
+ return { quality: "exact", continuations: Object.freeze(observed) };
176
+ }
177
+ async route(input) {
178
+ if (input.binding.adapterId !== "codex"
179
+ || input.binding.nativeSessionId !== input.fence.conversationId
180
+ || input.binding.launchId !== input.fence.activationId)
181
+ return "unsafe";
182
+ if (input.mode === "inject") {
183
+ return this.injectItems({
184
+ conversationId: input.fence.conversationId,
185
+ text: input.text
186
+ });
187
+ }
188
+ if (input.fence.nativeTurnId === undefined)
189
+ return "unsafe";
190
+ const outcome = await this.steerTurn({
191
+ conversationId: input.fence.conversationId,
192
+ expectedTurnId: input.fence.nativeTurnId,
193
+ text: input.text,
194
+ clientUserMessageId: input.attemptId
195
+ });
196
+ return outcome.status === "accepted" ? "accepted"
197
+ : outcome.status === "not-accepted" ? "not-accepted"
198
+ : "unknown";
199
+ }
200
+ async reconcile(input) {
201
+ if (input.binding.adapterId !== "codex"
202
+ || input.binding.nativeSessionId !== input.fence.conversationId
203
+ || input.binding.launchId !== input.fence.activationId)
204
+ return "unavailable";
205
+ // inject_items has no client receipt/idempotency field in the App Server
206
+ // protocol. A lost response therefore remains unknown and is never resent.
207
+ if (input.mode === "inject")
208
+ return "unknown";
209
+ try {
210
+ const snapshot = await this.readConversation(input.fence.conversationId);
211
+ return threadContainsClientInput(snapshot.raw, input.attemptId)
212
+ ? "accepted"
213
+ : "not-accepted";
214
+ }
215
+ catch (error) {
216
+ return isNotLoaded(error) ? "unavailable" : "unknown";
217
+ }
218
+ }
219
+ }
220
+ /** thread/closed means the loaded Activation ended; the durable thread remains resumable. */
221
+ export function codexNotificationBoundary(input) {
222
+ const conversationId = optionalId(input.params.threadId)
223
+ ?? optionalId(objectMember(input.params, "thread")?.id);
224
+ const turnId = optionalId(input.params.turnId)
225
+ ?? optionalId(objectMember(input.params, "turn")?.id);
226
+ if (input.method === "thread/closed")
227
+ return { kind: "activation-ended", conversationId };
228
+ if (input.method === "turn/started")
229
+ return { kind: "turn-started", conversationId, turnId };
230
+ if (input.method === "turn/completed")
231
+ return { kind: "turn-completed", conversationId, turnId };
232
+ return { kind: "other", conversationId, turnId };
233
+ }
234
+ function parseThreadSnapshot(result, expectedThreadId, loaded) {
235
+ const thread = objectMember(result, "thread") ?? result;
236
+ const id = optionalId(thread.id) ?? expectedThreadId;
237
+ if (id !== expectedThreadId)
238
+ throw new Error("Codex App Server returned a different thread.");
239
+ const turns = arrayMember(thread, "turns").map(object).filter((entry) => (entry !== null));
240
+ const active = [...turns].reverse().find((turn) => (["inProgress", "in_progress", "running", "active"].includes(String(turn.status))));
241
+ const latestStatus = optionalTurnStatus(turns.at(-1)?.status);
242
+ return {
243
+ threadId: id,
244
+ loaded,
245
+ status: threadStatus(thread.status),
246
+ ...(optionalId(active?.id) === undefined ? {} : { activeTurnId: optionalId(active?.id) }),
247
+ ...(latestStatus === undefined ? {} : { latestTurnStatus: latestStatus }),
248
+ ...(optionalId(thread.parentThreadId) === undefined
249
+ ? {}
250
+ : { parentThreadId: optionalId(thread.parentThreadId) }),
251
+ ancestorThreadIds: Object.freeze(arrayMember(thread, "ancestorThreadIds").flatMap((entry) => (optionalId(entry) === undefined ? [] : [optionalId(entry)]))),
252
+ raw: result
253
+ };
254
+ }
255
+ function codexContinuationState(snapshot) {
256
+ if (snapshot.status === "active" || snapshot.latestTurnStatus === "inProgress") {
257
+ return {
258
+ execution: "active",
259
+ outcome: "pending",
260
+ mayWriteWorkspace: true
261
+ };
262
+ }
263
+ switch (snapshot.latestTurnStatus) {
264
+ case "completed":
265
+ return {
266
+ execution: "quiescent",
267
+ outcome: "succeeded",
268
+ resultRef: snapshot.threadId,
269
+ mayWriteWorkspace: false
270
+ };
271
+ case "interrupted":
272
+ return {
273
+ execution: "quiescent",
274
+ outcome: "cancelled",
275
+ resultRef: snapshot.threadId,
276
+ mayWriteWorkspace: false
277
+ };
278
+ case "failed":
279
+ return {
280
+ execution: "quiescent",
281
+ outcome: "failed",
282
+ resultRef: snapshot.threadId,
283
+ mayWriteWorkspace: false
284
+ };
285
+ default:
286
+ // notLoaded and thread/closed describe App Server attachment only. An
287
+ // idle thread without a terminal Turn is therefore still unknown.
288
+ return {
289
+ execution: "unknown",
290
+ outcome: "unknown",
291
+ mayWriteWorkspace: true
292
+ };
293
+ }
294
+ }
295
+ function threadStatus(value) {
296
+ const record = object(value);
297
+ const type = record?.type;
298
+ return type === "active" || type === "idle" || type === "systemError" || type === "notLoaded"
299
+ ? type
300
+ : "unknown";
301
+ }
302
+ function optionalTurnStatus(value) {
303
+ return value === "completed" || value === "interrupted"
304
+ || value === "failed" || value === "inProgress"
305
+ ? value
306
+ : undefined;
307
+ }
308
+ function threadContainsClientInput(raw, attemptId) {
309
+ const thread = objectMember(raw, "thread") ?? raw;
310
+ const turns = Array.isArray(thread.turns) ? thread.turns : [];
311
+ return turns.some((rawTurn) => {
312
+ const turn = object(rawTurn);
313
+ const items = turn === null || !Array.isArray(turn.items) ? [] : turn.items;
314
+ return items.some((rawItem) => {
315
+ const item = object(rawItem);
316
+ return item?.type === "userMessage" && item.clientId === attemptId;
317
+ });
318
+ });
319
+ }
320
+ function classifyMutationError(error) {
321
+ if (error instanceof CodexAppServerRequestError) {
322
+ if (["INVALID_PARAMS", "NOT_FOUND", "TURN_NOT_ACTIVE", -32602].includes(error.code)) {
323
+ return { status: "not-accepted", reason: error.message };
324
+ }
325
+ }
326
+ return { status: "unknown", reason: error instanceof Error ? error.message : String(error) };
327
+ }
328
+ function isNotLoaded(error) {
329
+ return error instanceof CodexAppServerRequestError
330
+ && (String(error.code).toLowerCase().includes("not_loaded")
331
+ || error.message.toLowerCase().includes("not loaded"));
332
+ }
333
+ function threadId(result) {
334
+ return text(optionalId(result.threadId) ?? optionalId(objectMember(result, "thread")?.id), "Codex thread id");
335
+ }
336
+ function object(value) {
337
+ return value !== null && typeof value === "object" && !Array.isArray(value)
338
+ ? value
339
+ : null;
340
+ }
341
+ function objectMember(value, key) {
342
+ return object(value[key]);
343
+ }
344
+ function arrayMember(value, key) {
345
+ return Array.isArray(value[key]) ? value[key] : [];
346
+ }
347
+ function optionalId(value) {
348
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
349
+ }
350
+ function text(value, label) {
351
+ if (typeof value !== "string" || value.includes("\0") || value.trim().length === 0) {
352
+ throw new Error(`${label} is invalid.`);
353
+ }
354
+ return value.trim();
355
+ }