@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.
- package/README.md +10 -3
- package/dist/cli/commandCatalog.js +1 -1
- package/dist/commands/taskCommands.js +63 -28
- package/dist/commands/taskContextCommand.js +27 -1
- package/dist/commands/taskRoleRuntimeStatus.js +17 -6
- package/dist/controller/agentRuntimeObserver.js +6 -3
- package/dist/controller/controller.js +29 -47
- package/dist/controller/fileSchedulerStoreAdapter.js +572 -258
- package/dist/controller/runtime.js +8 -1
- package/dist/controller/runtimeEventInbox.js +16 -5
- package/dist/controller/runtimeHookRunFence.js +51 -5
- package/dist/controller/runtimeObservationHook.js +8 -2
- package/dist/coordination/workMailbox.js +408 -28
- package/dist/coordination/workMailboxQueue.js +12 -10
- package/dist/executor/agentExecutor.js +101 -94
- package/dist/executor/executorRegistry.js +47 -2
- package/dist/executor/fileRoleLaunchPlanner.js +4 -2
- package/dist/lifecycle/exactRunTerminalization.js +1 -7
- package/dist/repository/taskWorkspaceCoordinator.js +9 -4
- package/dist/runtime/agentDriver.js +83 -4
- package/dist/runtime/agentDriverObservation.js +25 -10
- package/dist/runtime/builtinAgentDrivers.js +168 -18
- package/dist/runtime/codexAppServerRuntime.js +355 -0
- package/dist/runtime/continuationManager.js +117 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/lifecycleReservation.js +4 -3
- package/dist/runtime/promptEnvelope.js +14 -3
- package/dist/runtime/providerContinuation.js +225 -0
- package/dist/runtime/providerContinuationReconciliationService.js +172 -0
- package/dist/runtime/providerRuntimeIdentity.js +232 -0
- package/dist/runtime/providerRuntimeReconciler.js +166 -0
- package/dist/runtime/runtimeContinuationProjection.js +34 -0
- package/dist/runtime/runtimeObservation.js +217 -6
- package/dist/runtime/runtimeProjection.js +172 -11
- package/dist/scheduler/activeRoleRunDelivery.js +314 -1
- package/dist/scheduler/leaderWakeupProcessor.js +2 -1
- package/dist/scheduler/operatorInputNotificationProcessor.js +3 -2
- package/dist/scheduler/roleRunLiveness.js +8 -7
- package/dist/scheduler/roleRunStall.js +4 -2
- package/dist/scheduler/taskExecutionProjection.js +2 -2
- package/dist/storage/migration/productionRegistry.js +474 -1
- package/dist/storage/sqliteSchema.js +102 -21
- package/dist/storage/sqliteStore.js +52 -110
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +0 -1
- package/dist/storage/taskStore.js +40 -53
- package/dist/storage/upgrade/sqliteStateMigration.js +0 -21
- package/dist/task/nextAction.js +1 -1
- package/dist/web/assets/client/app.js +1 -1
- package/dist/web/assets/client/components.js +233 -4
- package/dist/web/assets/client/i18n.js +166 -2
- package/dist/web/assets/client/view.js +30 -13
- package/dist/web/assets/styles/cards.js +62 -0
- package/dist/web/assets/styles/widgets.js +1 -0
- package/dist/web/webSnapshot.js +11 -2
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +33 -24
- 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
|
|
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
|
|
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 ?
|
|
18
|
-
...
|
|
19
|
-
payload: Object.freeze({ ...
|
|
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:
|
|
32
|
-
eventId
|
|
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
|
|
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:
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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({
|
|
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
|
+
}
|