@zq-silk/yui 0.6.2 → 0.6.4

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 (63) hide show
  1. package/ARCHITECTURE.md +28 -4
  2. package/README.md +60 -97
  3. package/dist/agent/argumentPolicy.js +1 -1
  4. package/dist/agent/managedRuntimeEnvironment.js +1 -0
  5. package/dist/cli/commandCatalog.js +19 -9
  6. package/dist/cli/interactionPolicy.js +4 -2
  7. package/dist/cli.js +77 -32
  8. package/dist/commands/taskCommands.js +46 -11
  9. package/dist/commands/taskContextCommand.js +1 -1
  10. package/dist/commands/taskRoleRuntimeStatus.js +170 -10
  11. package/dist/controller/agentRuntimeObserver.js +210 -0
  12. package/dist/controller/clientRuntime.js +3 -21
  13. package/dist/controller/controller.js +47 -7
  14. package/dist/controller/fileSchedulerStoreAdapter.js +522 -388
  15. package/dist/controller/runtime.js +9 -3
  16. package/dist/controller/runtimeEventInbox.js +49 -295
  17. package/dist/controller/runtimeEventProcessor.js +184 -321
  18. package/dist/controller/runtimeHookRunFence.js +226 -0
  19. package/dist/controller/runtimeLaunchCoordinator.js +91 -26
  20. package/dist/controller/runtimeObservationHook.js +112 -0
  21. package/dist/core/controllerServer.js +5 -0
  22. package/dist/executor/agentAdapter.js +18 -3
  23. package/dist/executor/fileRoleLaunchPlanner.js +64 -15
  24. package/dist/executor/managedClaudeRunner.js +121 -0
  25. package/dist/observability/executionAudit.js +6 -3
  26. package/dist/repository/taskWorkspacePreparer.js +1 -4
  27. package/dist/run/providerRetryConfig.js +8 -3
  28. package/dist/runtime/agentDriver.js +229 -0
  29. package/dist/runtime/agentDriverObservation.js +57 -0
  30. package/dist/runtime/builtinAgentDrivers.js +235 -0
  31. package/dist/runtime/builtinTranscriptObserver.js +290 -0
  32. package/dist/runtime/builtinTranscriptUsage.js +97 -0
  33. package/dist/runtime/exactControlPlane.js +2 -2
  34. package/dist/runtime/index.js +1 -1
  35. package/dist/runtime/ports.js +12 -1
  36. package/dist/runtime/runtimeObservation.js +297 -0
  37. package/dist/runtime/runtimeProjection.js +277 -0
  38. package/dist/runtime/sessionTerminationGuard.js +78 -22
  39. package/dist/runtime/tmuxAdapters.js +35 -0
  40. package/dist/scheduler/activeRoleRunDelivery.js +28 -13
  41. package/dist/scheduler/leaderWakeupProcessor.js +21 -2
  42. package/dist/scheduler/roleRunLiveness.js +2 -2
  43. package/dist/scheduler/roleRunStall.js +62 -114
  44. package/dist/storage/migration/productionRegistry.js +85 -0
  45. package/dist/storage/sqliteStore.js +3 -3
  46. package/dist/storage/storageVersions.js +1 -1
  47. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +2 -1
  48. package/dist/storage/upgrade/sqliteStateMigration.js +123 -0
  49. package/dist/telemetry/sqliteTelemetryStore.js +0 -28
  50. package/dist/telemetry/telemetryCompaction.js +1 -0
  51. package/dist/telemetry/telemetryConfig.js +4 -5
  52. package/dist/tmux/tmuxManager.js +136 -22
  53. package/dist/web/assets/client/view.js +1 -1
  54. package/dist/web/tmuxWebTerminal.js +17 -12
  55. package/dist/web/webSnapshot.js +1 -1
  56. package/dist/worktree/managedWorkspace.js +14 -0
  57. package/i18n/README.zh-CN.md +12 -7
  58. package/package.json +1 -1
  59. package/dist/controller/claudeLifecycleHook.js +0 -203
  60. package/dist/controller/codexLifecycleHook.js +0 -108
  61. package/dist/controller/providerHookRunFence.js +0 -156
  62. package/dist/lifecycle/providerLifecycleMapping.js +0 -190
  63. package/dist/telemetry/telemetryRouter.js +0 -32
@@ -1,203 +0,0 @@
1
- import { callController } from "../core/controllerClient.js";
2
- import { runtimeLifecycleSignalKey } from "../runtime/lifecycleReservation.js";
3
- import { FileRuntimeEventInbox, MAX_RUNTIME_EVENT_FILE_BYTES } from "./runtimeEventInbox.js";
4
- import { resolveProviderHookRunFence } from "./providerHookRunFence.js";
5
- /**
6
- * Hidden CLI entrypoint used by the managed Claude lifecycle plugin. It parses
7
- * by hook_event_name and writes one immutable runtime-inbox event per fact:
8
- * SessionStart → native-session-lifecycle (carrying the source variant),
9
- * UserPromptSubmit → native-prompt-accepted (the exact provider-accepted fence),
10
- * PostToolUse → native-turn-progress (the exact provider/tool progress fence),
11
- * StopFailure → claude-stop-failure. The durable write is authoritative; the
12
- * socket call is only a wake-up hint.
13
- */
14
- export async function runClaudeLifecycleHookCommand(stdinJson, environment = process.env, call = callController) {
15
- const home = requireIdentity(environment.YUI_HOME, "YUI_HOME");
16
- const inbox = new FileRuntimeEventInbox(home);
17
- const envelope = parseClaudeHookEnvelope(stdinJson, environment);
18
- if (envelope.kind === "session-start") {
19
- inbox.enqueueSessionLifecycle({
20
- scope: "task",
21
- taskId: envelope.taskId,
22
- roleName: envelope.roleName,
23
- agentId: envelope.agentId,
24
- adapterId: "claude",
25
- launchId: envelope.launchId,
26
- nativeSessionId: envelope.nativeSessionId,
27
- runId: envelope.runId,
28
- sessionSource: envelope.sessionSource
29
- });
30
- }
31
- else if (envelope.kind === "prompt-submit") {
32
- inbox.enqueuePromptAccepted({
33
- scope: "task",
34
- taskId: envelope.taskId,
35
- roleName: envelope.roleName,
36
- agentId: envelope.agentId,
37
- adapterId: "claude",
38
- launchId: envelope.launchId,
39
- nativeSessionId: envelope.nativeSessionId,
40
- runId: envelope.runId,
41
- receiptId: envelope.receiptId
42
- });
43
- }
44
- else if (envelope.kind === "turn-progress") {
45
- inbox.enqueueProviderProgress({
46
- scope: "task",
47
- taskId: envelope.taskId,
48
- roleName: envelope.roleName,
49
- agentId: envelope.agentId,
50
- adapterId: "claude",
51
- launchId: envelope.launchId,
52
- nativeSessionId: envelope.nativeSessionId,
53
- runId: envelope.runId,
54
- progressId: envelope.progressId
55
- });
56
- }
57
- else {
58
- inbox.enqueueClaudeStopFailure({
59
- scope: "task",
60
- taskId: envelope.taskId,
61
- roleName: envelope.roleName,
62
- agentId: envelope.agentId,
63
- adapterId: "claude",
64
- launchId: envelope.launchId,
65
- nativeSessionId: envelope.nativeSessionId,
66
- runId: envelope.runId,
67
- error: envelope.error,
68
- ...(envelope.errorDetails === undefined
69
- ? {}
70
- : { errorDetails: envelope.errorDetails }),
71
- ...(envelope.lastAssistantMessage === undefined
72
- ? {}
73
- : { lastAssistantMessage: envelope.lastAssistantMessage })
74
- });
75
- }
76
- // The immutable event is authoritative; the socket call is only a hint.
77
- await call(home, "scheduler.signal", {
78
- key: runtimeLifecycleSignalKey({
79
- scope: "task",
80
- taskId: envelope.taskId,
81
- roleName: envelope.roleName
82
- })
83
- }, { timeoutMs: 100 }).catch(() => { });
84
- }
85
- /**
86
- * Parses a Claude hook payload by hook_event_name into the exact fenced
87
- * envelope. Every event requires the managed launch envelope (task/role/agent/
88
- * launch/run) and the payload session id must match YUI_NATIVE_SESSION_ID, so a
89
- * mismatched generation fails closed before any inbox write.
90
- */
91
- export function parseClaudeHookEnvelope(stdinJson, environment) {
92
- const payload = parseObject(stdinJson);
93
- if (payload.hook_event_name !== "SessionStart"
94
- && payload.hook_event_name !== "UserPromptSubmit"
95
- && payload.hook_event_name !== "PostToolUse"
96
- && payload.hook_event_name !== "StopFailure") {
97
- throw new Error("Managed Claude lifecycle ingestion received an unsupported hook event.");
98
- }
99
- const sessionSource = payload.hook_event_name === "SessionStart"
100
- ? requireIdentity(payload.source, "Claude SessionStart source")
101
- : undefined;
102
- const base = parseClaudeEnvelope(payload, environment, {
103
- allowPreallocatedClaudeStartup: sessionSource === "startup"
104
- });
105
- switch (payload.hook_event_name) {
106
- case "SessionStart":
107
- return {
108
- ...base,
109
- kind: "session-start",
110
- sessionSource: sessionSource
111
- };
112
- case "UserPromptSubmit":
113
- return {
114
- ...base,
115
- kind: "prompt-submit",
116
- receiptId: requireIdentity(base.receiptId, "Claude transport receipt id")
117
- };
118
- case "PostToolUse":
119
- return {
120
- ...base,
121
- kind: "turn-progress",
122
- progressId: requireIdentity(payload.tool_use_id, "Claude PostToolUse id")
123
- };
124
- case "StopFailure":
125
- return {
126
- ...base,
127
- kind: "stop-failure",
128
- error: requireResult(payload.error, "Claude StopFailure error"),
129
- ...(payload.error_details === undefined
130
- ? {}
131
- : { errorDetails: requireResult(payload.error_details, "Claude StopFailure error_details") }),
132
- ...(payload.last_assistant_message === undefined
133
- ? {}
134
- : {
135
- lastAssistantMessage: requireResult(payload.last_assistant_message, "Claude StopFailure last_assistant_message")
136
- })
137
- };
138
- default:
139
- throw new Error("Managed Claude lifecycle ingestion received an unsupported hook event.");
140
- }
141
- }
142
- function parseClaudeEnvelope(payload, environment, options) {
143
- const nativeSessionId = requireIdentity(payload.session_id, "Claude session id");
144
- return {
145
- ...resolveProviderHookRunFence(environment, "claude", nativeSessionId, options),
146
- adapterId: "claude"
147
- };
148
- }
149
- export function parseClaudeStopFailureHookNotification(stdinJson, environment) {
150
- const envelope = parseClaudeHookEnvelope(stdinJson, environment);
151
- if (envelope.kind !== "stop-failure") {
152
- throw new Error("Managed Claude lifecycle ingestion accepts only StopFailure.");
153
- }
154
- return {
155
- taskId: envelope.taskId,
156
- roleName: envelope.roleName,
157
- agentId: envelope.agentId,
158
- adapterId: "claude",
159
- launchId: envelope.launchId,
160
- runId: envelope.runId,
161
- nativeSessionId: envelope.nativeSessionId,
162
- type: "StopFailure",
163
- error: envelope.error,
164
- ...(envelope.errorDetails === undefined ? {} : { errorDetails: envelope.errorDetails }),
165
- ...(envelope.lastAssistantMessage === undefined
166
- ? {}
167
- : { lastAssistantMessage: envelope.lastAssistantMessage })
168
- };
169
- }
170
- function parseObject(value) {
171
- if (value === undefined
172
- || Buffer.byteLength(value, "utf8") > MAX_RUNTIME_EVENT_FILE_BYTES) {
173
- throw new Error("Claude lifecycle hook stdin JSON is invalid.");
174
- }
175
- try {
176
- const parsed = JSON.parse(value);
177
- if (!isObject(parsed))
178
- throw new Error("shape");
179
- return parsed;
180
- }
181
- catch {
182
- throw new Error("Claude lifecycle hook stdin JSON is invalid.");
183
- }
184
- }
185
- function requireIdentity(value, label) {
186
- if (typeof value !== "string" || value.includes("\0")) {
187
- throw new Error(`${label} is required.`);
188
- }
189
- const normalized = value.trim();
190
- if (normalized.length === 0 || normalized.length > 1_024) {
191
- throw new Error(`${label} is invalid.`);
192
- }
193
- return normalized;
194
- }
195
- function requireResult(value, label) {
196
- if (typeof value !== "string" || value.includes("\0") || value.trim().length === 0) {
197
- throw new Error(`${label} is required.`);
198
- }
199
- return value;
200
- }
201
- function isObject(value) {
202
- return typeof value === "object" && value !== null && !Array.isArray(value);
203
- }
@@ -1,108 +0,0 @@
1
- import { callController } from "../core/controllerClient.js";
2
- import { runtimeLifecycleSignalKey } from "../runtime/lifecycleReservation.js";
3
- import { FileRuntimeEventInbox, MAX_RUNTIME_EVENT_FILE_BYTES } from "./runtimeEventInbox.js";
4
- import { resolveProviderHookRunFence } from "./providerHookRunFence.js";
5
- /**
6
- * Hidden CLI entrypoint used by the managed Codex lifecycle hooks. Codex 0.145
7
- * fires SessionStart and UserPromptSubmit inside run_turn(input); this parses
8
- * by hook_event_name and writes one immutable runtime-inbox event per fact.
9
- * SessionStart maps (via the adapter) to provider-session-started only — never
10
- * pre-input readiness; UserPromptSubmit is the exact provider-accepted fence.
11
- * The durable write is authoritative; the socket call is only a wake-up hint.
12
- */
13
- export async function runCodexLifecycleHookCommand(stdinJson, environment = process.env, call = callController) {
14
- const home = requireIdentity(environment.YUI_HOME, "YUI_HOME");
15
- const inbox = new FileRuntimeEventInbox(home);
16
- const envelope = parseCodexHookEnvelope(stdinJson, environment);
17
- if (envelope.kind === "session-start") {
18
- inbox.enqueueSessionLifecycle({
19
- scope: "task",
20
- taskId: envelope.taskId,
21
- roleName: envelope.roleName,
22
- agentId: envelope.agentId,
23
- adapterId: "codex",
24
- launchId: envelope.launchId,
25
- nativeSessionId: envelope.nativeSessionId,
26
- runId: envelope.runId
27
- // Codex SessionStart arrives within the first turn, not before input.
28
- });
29
- }
30
- else {
31
- inbox.enqueuePromptAccepted({
32
- scope: "task",
33
- taskId: envelope.taskId,
34
- roleName: envelope.roleName,
35
- agentId: envelope.agentId,
36
- adapterId: "codex",
37
- launchId: envelope.launchId,
38
- nativeSessionId: envelope.nativeSessionId,
39
- runId: envelope.runId,
40
- receiptId: envelope.receiptId
41
- });
42
- }
43
- await call(home, "scheduler.signal", {
44
- key: runtimeLifecycleSignalKey({
45
- scope: "task",
46
- taskId: envelope.taskId,
47
- roleName: envelope.roleName
48
- })
49
- }, { timeoutMs: 100 }).catch(() => { });
50
- }
51
- /**
52
- * Parses a Codex hook payload by hook_event_name into the exact fenced envelope.
53
- * Every event requires the managed launch envelope, and the payload session id
54
- * must match YUI_NATIVE_SESSION_ID, so a mismatched generation fails closed
55
- * before any inbox write.
56
- */
57
- export function parseCodexHookEnvelope(stdinJson, environment) {
58
- const payload = parseObject(stdinJson);
59
- if (payload.hook_event_name !== "SessionStart"
60
- && payload.hook_event_name !== "UserPromptSubmit") {
61
- throw new Error("Managed Codex lifecycle ingestion received an unsupported hook event.");
62
- }
63
- const base = parseCodexEnvelope(payload, environment);
64
- switch (payload.hook_event_name) {
65
- case "SessionStart":
66
- return { ...base, kind: "session-start" };
67
- case "UserPromptSubmit":
68
- return {
69
- ...base,
70
- kind: "prompt-submit",
71
- receiptId: requireIdentity(base.receiptId, "Codex transport receipt id")
72
- };
73
- default:
74
- throw new Error("Managed Codex lifecycle ingestion received an unsupported hook event.");
75
- }
76
- }
77
- function parseCodexEnvelope(payload, environment) {
78
- const nativeSessionId = requireIdentity(payload.session_id, "Codex session id");
79
- return resolveProviderHookRunFence(environment, "codex", nativeSessionId);
80
- }
81
- function parseObject(value) {
82
- if (value === undefined
83
- || Buffer.byteLength(value, "utf8") > MAX_RUNTIME_EVENT_FILE_BYTES) {
84
- throw new Error("Codex lifecycle hook stdin JSON is invalid.");
85
- }
86
- try {
87
- const parsed = JSON.parse(value);
88
- if (!isObject(parsed))
89
- throw new Error("shape");
90
- return parsed;
91
- }
92
- catch {
93
- throw new Error("Codex lifecycle hook stdin JSON is invalid.");
94
- }
95
- }
96
- function requireIdentity(value, label) {
97
- if (typeof value !== "string" || value.includes("\0")) {
98
- throw new Error(`${label} is required.`);
99
- }
100
- const normalized = value.trim();
101
- if (normalized.length === 0 || normalized.length > 1_024) {
102
- throw new Error(`${label} is invalid.`);
103
- }
104
- return normalized;
105
- }
106
- function isObject(value) {
107
- return typeof value === "object" && value !== null && !Array.isArray(value);
108
- }
@@ -1,156 +0,0 @@
1
- import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
2
- import { hasRuntimeCleanupObligation, isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
3
- import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
4
- import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactTaskRuntimeEnvironment, exactControlPlaneDigest, parseExactControlPlaneDescriptor, refreshReusedTaskRuntimeDescriptorSource } from "../runtime/exactControlPlane.js";
5
- import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
6
- /**
7
- * Resolves turn identity from the current durable in-flight fence. The one
8
- * exception is Claude SessionStart(startup), which can arrive synchronously
9
- * before Session projection and is fenced by the exact Run-bound launch
10
- * reservation plus deterministic native identity. The immutable hook event is
11
- * revalidated by the normal inbox fold before changing any state.
12
- */
13
- export function resolveProviderHookRunFence(environment, adapterId, payloadNativeSessionId, options = {}) {
14
- if (environment.YUI_SESSION_SCOPE !== "task") {
15
- throw new Error("Provider lifecycle hook requires a Task session scope.");
16
- }
17
- if (environment.YUI_ADAPTER_ID !== adapterId) {
18
- throw new Error(`Provider lifecycle hook requires the ${adapterId} adapter.`);
19
- }
20
- const home = requireIdentity(environment.YUI_HOME, "YUI_HOME");
21
- const taskId = requireIdentity(environment.YUI_TASK_ID, "Task id");
22
- const roleName = requireIdentity(environment.YUI_ROLE, "Role name");
23
- const agentId = requireIdentity(environment.YUI_AGENT_ID, "Agent id");
24
- const workspace = requireIdentity(environment.YUI_WORKSPACE, "YUI workspace");
25
- const runtimeSource = environment[YUI_TASK_RUNTIME_DESCRIPTOR];
26
- const runtime = runtimeSource === undefined
27
- ? undefined
28
- : assertExactTaskRuntimeEnvironment(runtimeSource, environment, exactControlPlaneDigest(parseExactControlPlaneDescriptor(requireIdentity(environment[YUI_CONTROL_PLANE_DESCRIPTOR], "Exact control-plane descriptor"))), home);
29
- const launchId = requireIdentity(runtime?.launchId ?? environment.YUI_LAUNCH_ID, "Launch id");
30
- const nativeSessionId = requireIdentity(payloadNativeSessionId, "Provider session id");
31
- const expectedNativeSessionId = runtime?.nativeSessionId ?? environment.YUI_NATIVE_SESSION_ID;
32
- if (expectedNativeSessionId !== undefined
33
- && nativeSessionId !== requireIdentity(expectedNativeSessionId, "YUI native session id")) {
34
- throw new Error("Provider lifecycle hook native session does not match its launch envelope.");
35
- }
36
- const store = openCompatibleFileTaskStore(home);
37
- const task = store.getTask(taskId);
38
- if (task === null || task.status !== "active") {
39
- throw new Error("Provider lifecycle hook Task is not current and active.");
40
- }
41
- const role = store.getRole(taskId, roleName);
42
- if (role === null || role.activeAgentId !== agentId) {
43
- throw new Error("Provider lifecycle hook Role or Agent is not current.");
44
- }
45
- const sessions = store.getTaskRoleSessionSet(taskId, roleName);
46
- if (sessions !== null && sessions.activeAgentId !== agentId) {
47
- throw new Error("Provider lifecycle hook Session Agent is not current.");
48
- }
49
- const inFlight = sessions?.inFlight;
50
- const session = sessions?.sessions[agentId];
51
- const mailbox = store.getWorkMailbox(runtimeLifecycleTarget({
52
- scope: "task",
53
- taskId,
54
- roleName
55
- }));
56
- const exactReservation = isRuntimeLaunchReservation(mailbox?.processing, launchId)
57
- && !hasRuntimeCleanupObligation(mailbox);
58
- const executionRef = mailbox?.processing?.executionRef;
59
- const startupRunId = options.allowPreallocatedClaudeStartup === true
60
- ? requireIdentity(runtime?.runId ?? environment.YUI_RUN_ID, "Run id")
61
- : undefined;
62
- const deterministicClaudeStartup = adapterId === "claude"
63
- && options.allowPreallocatedClaudeStartup === true
64
- && expectedNativeSessionId !== undefined
65
- && session === undefined
66
- && exactReservation
67
- && executionRef?.type === "run"
68
- && executionRef.taskId === taskId
69
- && executionRef.id === startupRunId
70
- && nativeSessionId === nativeSessionIdForLaunch(home, launchId, agentId, adapterId);
71
- if ((inFlight === null || inFlight === undefined) && !deterministicClaudeStartup) {
72
- throw new Error("Provider lifecycle hook has no matching durable in-flight Run.");
73
- }
74
- if (inFlight !== null && inFlight !== undefined && inFlight.agentId !== agentId) {
75
- throw new Error("Provider lifecycle hook has no matching durable in-flight Run.");
76
- }
77
- if (deterministicClaudeStartup
78
- && inFlight !== null
79
- && inFlight !== undefined
80
- && (inFlight.runId !== startupRunId
81
- || inFlight.receiptId !== formatAgentRunReceiptId(taskId, startupRunId))) {
82
- throw new Error("Provider lifecycle hook has no matching durable in-flight Run.");
83
- }
84
- const runId = inFlight?.runId ?? startupRunId;
85
- let effectiveRuntime = runtime;
86
- let effectiveLaunchId = launchId;
87
- const sessionLaunchId = session?.launchId;
88
- if (runtime !== undefined
89
- && session !== undefined
90
- && sessionLaunchId !== undefined
91
- && typeof runtimeSource === "string"
92
- && !runtimeSource.trimStart().startsWith("{")
93
- && (runtime.runId !== runId
94
- || runtime.launchId !== sessionLaunchId
95
- || runtime.nativeSessionId !== session.nativeSessionId)) {
96
- // A reused native pane keeps its original descriptor source. Advance only
97
- // that Hook-owned source to the current durable generation before the
98
- // volatile fence; the Controller no longer scans history to keep it fresh.
99
- effectiveRuntime = refreshReusedTaskRuntimeDescriptorSource(runtimeSource, home, store, {
100
- runId,
101
- launchId: sessionLaunchId,
102
- nativeSessionId: session.nativeSessionId
103
- });
104
- effectiveLaunchId = effectiveRuntime.launchId;
105
- }
106
- if (effectiveRuntime?.runId !== undefined && effectiveRuntime.runId !== runId) {
107
- throw new Error("Provider lifecycle hook Run does not match its current descriptor.");
108
- }
109
- const run = store.getActiveAgentRun(taskId, roleName);
110
- if (run === null
111
- || run.id !== runId
112
- || run.status !== "active"
113
- || run.effective.agentId !== agentId
114
- || run.effective.adapterId !== adapterId) {
115
- throw new Error("Provider lifecycle hook Run does not match durable active state.");
116
- }
117
- if (run.effective.workspace.root !== workspace) {
118
- throw new Error("Provider lifecycle hook workspace does not match the durable Run snapshot.");
119
- }
120
- if (session !== undefined) {
121
- if (session.adapterId !== adapterId
122
- || session.launchId !== effectiveLaunchId
123
- || session.nativeSessionId !== nativeSessionId
124
- || session.effective.workspace.root !== workspace) {
125
- throw new Error("Provider lifecycle hook Session does not match its durable generation.");
126
- }
127
- }
128
- else {
129
- const runtimeDiscoveredCodex = adapterId === "codex"
130
- && expectedNativeSessionId === undefined
131
- && exactReservation;
132
- if (!runtimeDiscoveredCodex && !deterministicClaudeStartup) {
133
- throw new Error("Provider lifecycle hook launch is not durably reserved.");
134
- }
135
- }
136
- return {
137
- taskId,
138
- roleName,
139
- agentId,
140
- launchId: effectiveLaunchId,
141
- runId,
142
- ...(inFlight?.receiptId === undefined ? {} : { receiptId: inFlight.receiptId }),
143
- nativeSessionId,
144
- workspace
145
- };
146
- }
147
- function requireIdentity(value, label) {
148
- if (typeof value !== "string" || value.includes("\0")) {
149
- throw new Error(`${label} is required.`);
150
- }
151
- const normalized = value.trim();
152
- if (normalized.length === 0 || normalized.length > 1_024) {
153
- throw new Error(`${label} is invalid.`);
154
- }
155
- return normalized;
156
- }
@@ -1,190 +0,0 @@
1
- import { createCanonicalLifecycleEvent, CanonicalLifecycleError } from "./canonicalLifecycleEvent.js";
2
- /**
3
- * Claude, mapped from its observed SessionStart / UserPromptSubmit / StopFailure
4
- * contract (claude 2.1.x). SessionStart with source=startup fires at process
5
- * startup, *before* the first prompt, so only that exact variant proves pre-input
6
- * readiness. A resume/clear/compact SessionStart occurs within an existing
7
- * session and is downgraded to provider-session-started (never pre-input-ready).
8
- * A single prompt push precedes the only acceptance fence, an identity-matched
9
- * UserPromptSubmit.
10
- */
11
- const CLAUDE_PRE_INPUT_SESSION_SOURCE = "startup";
12
- const CLAUDE_LIFECYCLE_MAPPING = {
13
- adapterId: "claude",
14
- preInputReadiness: {
15
- status: "supported",
16
- nativeEvent: "SessionStart(source=startup)",
17
- note: "Claude fires SessionStart at process startup before the first prompt, so "
18
- + "readiness is proven pre-input by a native durable event. Only the "
19
- + "startup variant qualifies; resume/clear/compact are session-started only."
20
- },
21
- supportedSignals: [
22
- "native-session-start",
23
- "native-prompt-submit",
24
- "native-turn-progress",
25
- "native-turn-complete",
26
- "native-stop-failure"
27
- ],
28
- map(signal) {
29
- switch (signal.kind) {
30
- case "native-session-start": {
31
- // The SessionStart source discriminator is required for Claude: readiness
32
- // safety depends on distinguishing startup from later in-session variants.
33
- if (signal.sessionSource === undefined) {
34
- throw new CanonicalLifecycleError("Claude native-session-start requires the SessionStart source variant.");
35
- }
36
- if (signal.sessionSource !== CLAUDE_PRE_INPUT_SESSION_SOURCE) {
37
- // A non-startup SessionStart (resume/clear/compact) fires inside an
38
- // existing session and cannot prove pre-input readiness; downgrade it.
39
- return createCanonicalLifecycleEvent({
40
- phase: "provider-session-started",
41
- source: "provider-native",
42
- evidence: "provider-native-durable",
43
- fence: signal.fence
44
- });
45
- }
46
- // SessionStart(startup) proves readiness before any input for Claude.
47
- return createCanonicalLifecycleEvent({
48
- phase: "provider-ready",
49
- source: "provider-native",
50
- evidence: "provider-native-durable",
51
- preInputReady: true,
52
- readinessVariant: `SessionStart(source=${signal.sessionSource})`,
53
- fence: signal.fence
54
- });
55
- }
56
- case "native-prompt-submit":
57
- return createCanonicalLifecycleEvent({
58
- phase: "provider-accepted",
59
- source: "provider-native",
60
- evidence: "provider-native-durable",
61
- fence: signal.fence
62
- });
63
- case "native-turn-progress":
64
- return createCanonicalLifecycleEvent({
65
- phase: "turn-progress",
66
- source: "provider-native",
67
- evidence: "provider-native-durable",
68
- ...(signal.sequence === undefined ? {} : { sequence: signal.sequence }),
69
- fence: signal.fence
70
- });
71
- case "native-turn-complete":
72
- return createCanonicalLifecycleEvent({
73
- phase: "turn-terminal",
74
- source: "provider-native",
75
- evidence: "provider-native-durable",
76
- summary: signal.summary,
77
- fence: signal.fence
78
- });
79
- case "native-stop-failure":
80
- return createCanonicalLifecycleEvent({
81
- phase: "turn-terminal",
82
- source: "provider-native",
83
- evidence: "provider-native-durable",
84
- summary: signal.summary,
85
- fence: signal.fence
86
- });
87
- default:
88
- throw unsupportedSignal("claude", signal);
89
- }
90
- }
91
- };
92
- /**
93
- * Codex 0.145, mapped from its observed run_turn(input) -> SessionStart ->
94
- * UserPromptSubmit ordering. SessionStart fires *inside* run_turn — after the
95
- * first input — so it can only prove that the session/thread now exists, never
96
- * pre-input readiness. UserPromptSubmit is the acceptance fence; the notify
97
- * agent-turn-complete is the terminal fact. Codex emits no StopFailure hook.
98
- */
99
- const CODEX_LIFECYCLE_MAPPING = {
100
- adapterId: "codex",
101
- preInputReadiness: {
102
- status: "unsupported",
103
- reason: "not-available",
104
- note: "Codex 0.145 SessionStart fires within run_turn(input), i.e. after the "
105
- + "first input; no native event precedes the first prompt, so pre-input "
106
- + "readiness is not available and fails closed."
107
- },
108
- supportedSignals: [
109
- "native-session-start",
110
- "native-prompt-submit",
111
- "native-turn-progress",
112
- "native-turn-complete"
113
- ],
114
- map(signal) {
115
- switch (signal.kind) {
116
- case "native-session-start":
117
- // SessionStart proves the thread exists but arrives after the first
118
- // input, so it maps to session-started only — never ready, never pre-input.
119
- return createCanonicalLifecycleEvent({
120
- phase: "provider-session-started",
121
- source: "provider-native",
122
- evidence: "provider-native-durable",
123
- fence: signal.fence
124
- });
125
- case "native-prompt-submit":
126
- return createCanonicalLifecycleEvent({
127
- phase: "provider-accepted",
128
- source: "provider-native",
129
- evidence: "provider-native-durable",
130
- fence: signal.fence
131
- });
132
- case "native-turn-progress":
133
- return createCanonicalLifecycleEvent({
134
- phase: "turn-progress",
135
- source: "provider-native",
136
- evidence: "provider-native-durable",
137
- ...(signal.sequence === undefined ? {} : { sequence: signal.sequence }),
138
- fence: signal.fence
139
- });
140
- case "native-turn-complete":
141
- return createCanonicalLifecycleEvent({
142
- phase: "turn-terminal",
143
- source: "provider-native",
144
- evidence: "provider-native-durable",
145
- summary: signal.summary,
146
- fence: signal.fence
147
- });
148
- case "native-stop-failure":
149
- // Codex 0.145 has no StopFailure hook; refuse rather than invent one.
150
- throw unsupportedSignal("codex", signal);
151
- default:
152
- throw unsupportedSignal("codex", signal);
153
- }
154
- }
155
- };
156
- const PROVIDER_LIFECYCLE_MAPPINGS = Object.freeze({
157
- claude: CLAUDE_LIFECYCLE_MAPPING,
158
- codex: CODEX_LIFECYCLE_MAPPING
159
- });
160
- /**
161
- * Registry lookup: returns the mapping for an adapter without the caller
162
- * branching on the provider name. Unknown adapters fail closed.
163
- */
164
- export function providerLifecycleMapping(adapterId) {
165
- const mapping = findProviderLifecycleMapping(adapterId);
166
- if (mapping === null) {
167
- throw new CanonicalLifecycleError(`No lifecycle mapping for adapter: ${adapterId}.`);
168
- }
169
- return mapping;
170
- }
171
- export function findProviderLifecycleMapping(adapterId) {
172
- return adapterId === "codex" || adapterId === "claude"
173
- ? PROVIDER_LIFECYCLE_MAPPINGS[adapterId]
174
- : null;
175
- }
176
- /** Neutral capability lookup for consumers that need only the readiness fact. */
177
- export function preInputReadinessCapability(adapterId) {
178
- return providerLifecycleMapping(adapterId).preInputReadiness;
179
- }
180
- /**
181
- * Maps a native signal through the adapter selected by its fence. The fence's
182
- * adapterId chooses the mapping, so a Codex signal can never be mapped by
183
- * Claude's rules or vice versa.
184
- */
185
- export function mapNativeLifecycleSignal(signal) {
186
- return providerLifecycleMapping(signal.fence.adapterId).map(signal);
187
- }
188
- function unsupportedSignal(adapterId, signal) {
189
- return new CanonicalLifecycleError(`Adapter ${adapterId} does not emit native signal: ${signal.kind}.`);
190
- }