@zq-silk/yui 2.21.0 → 2.23.0

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 (145) hide show
  1. package/README.md +5 -0
  2. package/dist/cli/commandCatalog.js +87 -27
  3. package/dist/cli/dynamicCompletion.js +4 -1
  4. package/dist/cli/helpRenderer.js +12 -1
  5. package/dist/cli/interactionPolicy.js +2 -0
  6. package/dist/cli/invocationRouter.js +5 -5
  7. package/dist/cli/roleWizard.js +5 -1
  8. package/dist/cli/updateCommand.js +20 -3
  9. package/dist/cli/updateOrchestrator.js +13 -0
  10. package/dist/cli/updatePorts.js +27 -0
  11. package/dist/commands/globalRoleCommands.js +33 -8
  12. package/dist/commands/inboxCommands.js +39 -12
  13. package/dist/commands/operatorCommands.js +2 -5
  14. package/dist/commands/releaseCommands.js +16 -2
  15. package/dist/commands/roleConfiguration.js +25 -2
  16. package/dist/commands/sessionCommands.js +5 -2
  17. package/dist/commands/taskCommands.js +217 -64
  18. package/dist/commands/taskContextCommand.js +26 -4
  19. package/dist/commands/taskFactCommands.js +15 -2
  20. package/dist/commands/taskIntegrationCommands.js +3 -3
  21. package/dist/commands/taskRoleRuntimeStatus.js +19 -5
  22. package/dist/commands/textInput.js +2 -2
  23. package/dist/context/contextFamilies.js +10 -0
  24. package/dist/context/currentSkills.js +37 -4
  25. package/dist/context/runContextPack.js +44 -5
  26. package/dist/context/runInputContract.js +1 -1
  27. package/dist/context/sessionBootstrapManifest.js +21 -6
  28. package/dist/context/sessionSkillNotice.js +74 -0
  29. package/dist/context/taskContext.js +100 -16
  30. package/dist/controlPlaneCli.js +17 -7
  31. package/dist/controller/agentRuntimeObserver.js +57 -12
  32. package/dist/controller/clientRuntime.js +66 -18
  33. package/dist/controller/controller.js +19 -2
  34. package/dist/controller/diagnosticJournal.js +10 -2
  35. package/dist/controller/fileSchedulerStoreAdapter.js +74 -24
  36. package/dist/controller/hostLifecycle.js +153 -0
  37. package/dist/controller/hostLifecycleChecks.js +27 -0
  38. package/dist/controller/hostUpgrade.js +344 -0
  39. package/dist/controller/messageHealth.js +4 -3
  40. package/dist/controller/providerRetryAdmission.js +15 -1
  41. package/dist/controller/providerRetryDelivery.js +18 -2
  42. package/dist/controller/runtime.js +82 -21
  43. package/dist/controller/runtimeEventInbox.js +107 -13
  44. package/dist/controller/runtimeEventProcessor.js +3 -3
  45. package/dist/controller/sessionNotify.js +5 -46
  46. package/dist/controller/taskRunRecovery.js +153 -0
  47. package/dist/core/controllerClient.js +16 -2
  48. package/dist/core/controllerServer.js +14 -5
  49. package/dist/core/controllerTelemetry.js +51 -5
  50. package/dist/errors/cliFailure.js +14 -5
  51. package/dist/execution/codexThreadNaming.js +54 -43
  52. package/dist/executor/agentAdapter.js +32 -4
  53. package/dist/executor/agentConfigurationFields.js +2 -0
  54. package/dist/executor/fileRoleLaunchPlanner.js +18 -3
  55. package/dist/integration/gitIntegrationService.js +3 -2
  56. package/dist/integration/integrationSourceApplication.js +12 -1
  57. package/dist/interaction/notificationPresentation.js +11 -16
  58. package/dist/interaction/operatorPresentation.js +1 -1
  59. package/dist/message/messageContinuation.js +3 -1
  60. package/dist/output/boundedRead.js +36 -7
  61. package/dist/output/rolePresentation.js +3 -1
  62. package/dist/runtime/agentEndpoint.js +11 -1
  63. package/dist/runtime/agentEndpointIdentity.js +2 -0
  64. package/dist/runtime/agentHost.js +174 -40
  65. package/dist/runtime/agentHostImplementation.js +3 -1
  66. package/dist/runtime/codexAppServerRuntime.js +113 -24
  67. package/dist/runtime/codexInteractiveHost.js +102 -11
  68. package/dist/runtime/codexNativeAccess.js +1 -1
  69. package/dist/runtime/contextRecovery.js +15 -0
  70. package/dist/runtime/hostSessionLifecycle.js +83 -0
  71. package/dist/runtime/hostUpgradeGate.js +43 -0
  72. package/dist/runtime/lifecycleChecks.js +47 -0
  73. package/dist/runtime/lifecycleReservation.js +9 -1
  74. package/dist/runtime/nativeTitleReceipt.js +31 -0
  75. package/dist/runtime/nativeTitleSync.js +192 -0
  76. package/dist/runtime/providerRetry.js +9 -6
  77. package/dist/runtime/providerRuntimeIdentity.js +1 -1
  78. package/dist/runtime/structuredProviderHost.js +65 -35
  79. package/dist/scheduler/wakeupQueue.js +10 -4
  80. package/dist/storage/sqliteSchema.js +29 -0
  81. package/dist/storage/storageVersions.js +1 -1
  82. package/dist/tmux/tmuxManager.js +4 -2
  83. package/dist/web/webSettings.js +2 -0
  84. package/docs/authorization.md +18 -3
  85. package/docs/cli-information-contract.md +228 -6
  86. package/docs/cli-information-contract.zh-CN.md +95 -5
  87. package/docs/managed-turn-and-session-runtime.md +177 -21
  88. package/docs/managed-turn-and-session-runtime.zh-CN.md +126 -14
  89. package/docs/provider-retry.md +11 -0
  90. package/docs/release-workflow.md +65 -0
  91. package/docs/release-workflow.zh-CN.md +48 -0
  92. package/docs/roles-and-configuration.md +28 -0
  93. package/docs/roles-and-configuration.zh-CN.md +21 -0
  94. package/docs/storage-baseline.md +13 -1
  95. package/docs/storage-baseline.zh-CN.md +5 -1
  96. package/docs/task-delivery.md +7 -0
  97. package/docs/task-delivery.zh-CN.md +5 -0
  98. package/docs/testing/review-evaluation.md +7 -7
  99. package/docs/testing/review-evaluation.zh-CN.md +7 -7
  100. package/docs/testing/verification-levels.md +45 -2
  101. package/docs/testing/verification-levels.zh-CN.md +21 -2
  102. package/i18n/README.zh-CN.md +6 -0
  103. package/package.json +1 -1
  104. package/skills/yui-leader/SKILL.md +98 -193
  105. package/skills/yui-leader/VERSION +1 -1
  106. package/skills/yui-leader/references/artifacts.md +41 -0
  107. package/skills/yui-leader/references/assignment-execution.md +140 -0
  108. package/skills/yui-leader/references/completion.md +79 -0
  109. package/skills/yui-leader/references/delivery-scope.md +26 -0
  110. package/skills/yui-leader/references/execution.md +16 -487
  111. package/skills/yui-leader/references/implementation.md +81 -0
  112. package/skills/yui-leader/references/planning.md +7 -45
  113. package/skills/yui-leader/references/review.md +90 -0
  114. package/skills/yui-leader/references/task-progress.md +23 -0
  115. package/skills/yui-leader/references/work-allocation.md +118 -0
  116. package/skills/yui-operator/SKILL.md +77 -333
  117. package/skills/yui-operator/VERSION +1 -1
  118. package/skills/yui-operator/references/body-transport.md +62 -0
  119. package/skills/yui-operator/references/configuration.md +47 -0
  120. package/skills/yui-operator/references/lifecycle.md +41 -0
  121. package/skills/yui-operator/references/progress.md +39 -0
  122. package/skills/yui-operator/references/routing.md +57 -0
  123. package/skills/yui-operator/references/submission.md +56 -0
  124. package/skills/yui-operator/references/task-delivery.md +1 -1
  125. package/skills/yui-operator/references/web-lifecycle.md +19 -0
  126. package/skills/yui-operator/scripts/verify-submission.mjs +37 -0
  127. package/skills/yui-runtime/SKILL.md +182 -531
  128. package/skills/yui-runtime/VERSION +1 -1
  129. package/skills/yui-runtime/references/archive.md +51 -0
  130. package/skills/yui-runtime/references/command-discovery.md +41 -0
  131. package/skills/yui-runtime/references/completed-followup.md +65 -0
  132. package/skills/yui-runtime/references/context-ordering.md +3 -34
  133. package/skills/yui-runtime/references/context-reads.md +61 -0
  134. package/skills/yui-runtime/references/dispatched-run.md +61 -0
  135. package/skills/yui-runtime/references/execution-failure.md +105 -0
  136. package/skills/yui-runtime/references/global-context.md +18 -0
  137. package/skills/yui-runtime/references/global-input-controls.md +44 -0
  138. package/skills/yui-runtime/references/host-custody.md +30 -0
  139. package/skills/yui-runtime/references/inbox-recovery.md +77 -0
  140. package/skills/yui-runtime/references/incremental-context.md +40 -0
  141. package/skills/yui-runtime/references/publication.md +4 -111
  142. package/skills/yui-runtime/references/recovery.md +32 -192
  143. package/skills/yui-runtime/references/session-identity.md +18 -0
  144. package/skills/yui-runtime/references/session-replacement.md +45 -0
  145. package/skills/yui-runtime/references/uncertain-execution.md +29 -0
@@ -49,9 +49,10 @@ export class CodexAppServerRuntime {
49
49
  const result = await this.transport.request("thread/resume", {
50
50
  threadId: id,
51
51
  ...(options.cwd === undefined ? {} : { cwd: text(options.cwd, "Codex thread cwd") }),
52
- ...threadOptions(options)
52
+ ...threadOptions(options),
53
+ excludeTurns: true
53
54
  });
54
- return parseThreadSnapshot(result, id, true);
55
+ return this.withLatestTurn(parseThreadSnapshot(result, id, true));
55
56
  }
56
57
  async setConversationName(input) {
57
58
  await this.transport.request("thread/name/set", {
@@ -63,9 +64,81 @@ export class CodexAppServerRuntime {
63
64
  const id = text(conversationId, "Codex thread id");
64
65
  const result = await this.transport.request("thread/read", {
65
66
  threadId: id,
66
- includeTurns: options.includeTurns ?? true
67
+ includeTurns: false
67
68
  });
68
- return parseThreadSnapshot(result, id, "unknown");
69
+ const snapshot = parseThreadSnapshot(result, id, "unknown");
70
+ return options.includeTurns === false ? snapshot : this.withLatestTurn(snapshot);
71
+ }
72
+ /** Current execution metadata, never an unbounded transcript hydration. */
73
+ async withLatestTurn(snapshot) {
74
+ const page = await this.transport.request("thread/turns/list", {
75
+ threadId: snapshot.threadId, limit: 1, sortDirection: "desc", itemsView: "notLoaded"
76
+ });
77
+ const records = codexPage(page, new Set()).data;
78
+ if (records.length > 1)
79
+ throw new Error("Codex latest Turn page exceeded its requested bound.");
80
+ const turns = records.map(parseTurnSnapshot);
81
+ const latest = turns[0];
82
+ return {
83
+ ...snapshot,
84
+ // The metadata response may contain no Turns. Do not carry an old active
85
+ // identity over a separately observed latest Turn.
86
+ activeTurnId: latest?.status === "inProgress" ? latest.turnId : undefined,
87
+ latestTurnStatus: latest?.status,
88
+ turns
89
+ };
90
+ }
91
+ /** Recover only the persisted owned Turn. Scan metadata, then its items,
92
+ * newest first, stopping at the last nonempty assistant message. A malformed
93
+ * page or oversized individual item remains an error, never guessed success. */
94
+ async readTurn(conversationId, turnId) {
95
+ const threadId = text(conversationId, "Codex thread id");
96
+ const expectedTurnId = text(turnId, "Codex owned Turn id");
97
+ const seen = new Set();
98
+ let cursor;
99
+ for (;;) {
100
+ const page = codexPage(await this.transport.request("thread/turns/list", {
101
+ threadId, limit: 50, sortDirection: "desc", itemsView: "notLoaded",
102
+ ...(cursor === undefined ? {} : { cursor })
103
+ }), seen);
104
+ if (page.data.length > 50)
105
+ throw new Error("Codex Turn page exceeded its requested bound.");
106
+ const found = page.data.map(parseTurnSnapshot).find(turn => turn.turnId === expectedTurnId);
107
+ if (found !== undefined) {
108
+ if (found.status === "inProgress")
109
+ return found;
110
+ const output = await this.readTurnOutput(threadId, expectedTurnId);
111
+ return { ...found, ...(output === undefined ? {} : { output }) };
112
+ }
113
+ if (page.nextCursor === null)
114
+ return undefined;
115
+ cursor = page.nextCursor;
116
+ }
117
+ }
118
+ async readTurnOutput(threadId, turnId) {
119
+ const seen = new Set();
120
+ let cursor;
121
+ for (;;) {
122
+ const page = codexPage(await this.transport.request("thread/items/list", {
123
+ threadId, turnId, limit: 1, sortDirection: "desc",
124
+ ...(cursor === undefined ? {} : { cursor })
125
+ }), seen);
126
+ if (page.data.length > 1)
127
+ throw new Error("Codex item page exceeded its requested bound.");
128
+ for (const value of page.data) {
129
+ const entry = object(value);
130
+ const item = object(entry?.item);
131
+ if (entry?.turnId !== turnId || item === null) {
132
+ throw new Error("Codex item page does not belong to the owned Turn.");
133
+ }
134
+ if (item.type === "agentMessage" && typeof item.text === "string" && item.text.trim().length > 0) {
135
+ return item.text;
136
+ }
137
+ }
138
+ if (page.nextCursor === null)
139
+ return undefined;
140
+ cursor = page.nextCursor;
141
+ }
69
142
  }
70
143
  async readGoal(conversationId) {
71
144
  const id = text(conversationId, "Codex thread id");
@@ -98,6 +171,7 @@ export class CodexAppServerRuntime {
98
171
  return this.startTurn({
99
172
  conversationId: input.conversationId,
100
173
  text: input.text,
174
+ ...(input.additionalContext === undefined ? {} : { additionalContext: input.additionalContext }),
101
175
  expectedNoActiveTurn: input.expectedNoActiveTurn,
102
176
  ...(input.requireQuiescent ? { requireQuiescent: true } : {}),
103
177
  ...(input.expectedFailedNativeTurnId === undefined ? {} : {
@@ -157,6 +231,7 @@ export class CodexAppServerRuntime {
157
231
  try {
158
232
  const result = await this.transport.request("turn/start", {
159
233
  threadId,
234
+ ...(input.additionalContext === undefined ? {} : { additionalContext: input.additionalContext }),
160
235
  ...(input.clientUserMessageId === undefined
161
236
  ? {}
162
237
  : { clientUserMessageId: text(input.clientUserMessageId, "Codex input attempt id") }),
@@ -192,6 +267,7 @@ export class CodexAppServerRuntime {
192
267
  try {
193
268
  const result = await this.transport.request("turn/steer", {
194
269
  threadId,
270
+ ...(input.additionalContext === undefined ? {} : { additionalContext: input.additionalContext }),
195
271
  expectedTurnId,
196
272
  ...(input.clientUserMessageId === undefined
197
273
  ? {}
@@ -330,26 +406,7 @@ function parseThreadSnapshot(result, expectedThreadId, loaded) {
330
406
  throw new Error("Codex thread status is missing or unsupported.");
331
407
  if (!Array.isArray(thread.turns))
332
408
  throw new Error("Codex thread turns are missing or invalid.");
333
- const turnSnapshots = thread.turns.map((value) => {
334
- const turn = object(value);
335
- if (turn === null)
336
- throw new Error("Codex thread contains an invalid Turn.");
337
- const turnId = text(optionalId(turn.id), "Codex response Turn id");
338
- const status = optionalTurnStatus(turn.status);
339
- if (status === undefined)
340
- throw new Error("Codex Turn status is missing or unsupported.");
341
- const output = codexTurnOutput(turn);
342
- const error = providerError(turn.error);
343
- return {
344
- turnId,
345
- status,
346
- ...(output === undefined ? {} : { output }),
347
- ...(error === undefined ? {} : { error }),
348
- ...(turn.error === undefined || turn.error === null ? {} : {
349
- rawError: serializeAgentErrorRaw(turn.error)
350
- })
351
- };
352
- });
409
+ const turnSnapshots = thread.turns.map(parseTurnSnapshot);
353
410
  const active = [...turnSnapshots].reverse().find((turn) => turn.status === "inProgress");
354
411
  const latestStatus = turnSnapshots.at(-1)?.status;
355
412
  return {
@@ -365,6 +422,38 @@ function parseThreadSnapshot(result, expectedThreadId, loaded) {
365
422
  raw: result
366
423
  };
367
424
  }
425
+ function parseTurnSnapshot(value) {
426
+ const turn = object(value);
427
+ if (turn === null)
428
+ throw new Error("Codex thread contains an invalid Turn.");
429
+ const turnId = text(optionalId(turn.id), "Codex response Turn id");
430
+ const status = optionalTurnStatus(turn.status);
431
+ if (status === undefined)
432
+ throw new Error("Codex Turn status is missing or unsupported.");
433
+ const output = codexTurnOutput(turn);
434
+ const error = providerError(turn.error);
435
+ return {
436
+ turnId,
437
+ status,
438
+ ...(output === undefined ? {} : { output }),
439
+ ...(error === undefined ? {} : { error }),
440
+ ...(turn.error === undefined || turn.error === null ? {} : {
441
+ rawError: serializeAgentErrorRaw(turn.error)
442
+ })
443
+ };
444
+ }
445
+ function codexPage(page, seen) {
446
+ if (!Array.isArray(page.data)
447
+ || !(page.nextCursor === null || typeof page.nextCursor === "string" && page.nextCursor.length > 0)) {
448
+ throw new Error("Codex history page is missing data or its continuation boundary.");
449
+ }
450
+ if (typeof page.nextCursor === "string") {
451
+ if (seen.has(page.nextCursor))
452
+ throw new Error("Codex history cursor repeated.");
453
+ seen.add(page.nextCursor);
454
+ }
455
+ return { data: page.data, nextCursor: page.nextCursor };
456
+ }
368
457
  /** Extract the last assistant message from one native Codex Turn without rewriting it. */
369
458
  export function codexTurnOutput(turn) {
370
459
  for (const value of [...arrayMember(turn, "items")].reverse()) {
@@ -4,6 +4,11 @@ import { once } from "node:events";
4
4
  import WebSocket, { WebSocketServer } from "ws";
5
5
  import { AGENT_HOST_CONTROL_PROTOCOL } from "./agentHostProtocol.js";
6
6
  import { openCodexInteractiveConnection } from "./structuredProviderHost.js";
7
+ import { createSessionSkillNotice, createSessionSkillLifecycleCheck } from "../context/sessionSkillNotice.js";
8
+ import { isNativeContextRecovery } from "./contextRecovery.js";
9
+ import { createHostSessionLifecycle } from "./hostSessionLifecycle.js";
10
+ import { createNonBlockingChecks } from "./lifecycleChecks.js";
11
+ import { createNativeTitleSync } from "./nativeTitleSync.js";
7
12
  /**
8
13
  * Keep the native TUI and its transparent App Server attachment in one pane.
9
14
  * The existing Host acknowledgement carries the ID from that TUI's exact
@@ -15,6 +20,8 @@ import { openCodexInteractiveConnection } from "./structuredProviderHost.js";
15
20
  */
16
21
  export async function runCodexInteractiveHost(home, payload, openAgentHostControl) {
17
22
  const environment = payload.environment;
23
+ const skillNotice = createSessionSkillNotice(home);
24
+ const pendingNotices = new Map();
18
25
  if (environment.YUI_SESSION_SCOPE !== "global" || environment.YUI_ADAPTER_ID !== "codex"
19
26
  || payload.providerControl !== undefined) {
20
27
  throw new Error("Interactive Codex Host requires a global native TUI launch.");
@@ -28,8 +35,19 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
28
35
  throw new Error("Codex proxy base arguments are invalid.");
29
36
  }
30
37
  const expectedId = environment.YUI_NATIVE_SESSION_ID;
38
+ const lifecycleChecks = createNonBlockingChecks([createSessionSkillLifecycleCheck(home, () => environment)], error => process.stderr.write(`Session lifecycle check failed: ${String(error)}\n`));
39
+ const lifecycle = createHostSessionLifecycle({ after: lifecycleChecks });
40
+ lifecycle.send({ type: "launch" });
31
41
  let snapshot = {
32
- schemaVersion: 1, state: "starting", adapterId: "codex", updatedAt: new Date().toISOString()
42
+ schemaVersion: 1, state: lifecycle.state, adapterId: "codex", updatedAt: new Date().toISOString()
43
+ };
44
+ const transition = (event, fields = {}) => {
45
+ const result = lifecycle.send(event);
46
+ if (result.status === "ignored")
47
+ return;
48
+ if (result.status === "invalid")
49
+ throw new Error(result.reason);
50
+ snapshot = { ...snapshot, ...fields, state: result.state, updatedAt: new Date().toISOString() };
33
51
  };
34
52
  const control = await openAgentHostControl(home, payload, () => snapshot, async (request) => {
35
53
  if (request.type !== "status") {
@@ -41,6 +59,9 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
41
59
  let client;
42
60
  let closing = false;
43
61
  let startupRequestId;
62
+ let initializeRequestId;
63
+ let nativeAccountHome;
64
+ let titleSync;
44
65
  let connection;
45
66
  let relay;
46
67
  const signals = ["SIGTERM", "SIGHUP", "SIGINT"];
@@ -48,11 +69,9 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
48
69
  const fail = (error) => {
49
70
  if (closing || snapshot.state === "failed")
50
71
  return;
51
- snapshot = {
52
- ...snapshot, state: "failed",
53
- detail: error instanceof Error ? error.message : String(error),
54
- updatedAt: new Date().toISOString()
55
- };
72
+ transition({ type: "failure", outcome: "failed" }, {
73
+ detail: error instanceof Error ? error.message : String(error)
74
+ });
56
75
  process.stderr.write(`Yui Codex attachment failed: ${snapshot.detail}\n`);
57
76
  client?.terminate();
58
77
  stop();
@@ -91,6 +110,11 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
91
110
  if (binary)
92
111
  throw new Error("Codex TUI sent a binary App Server request.");
93
112
  const message = jsonObject(JSON.parse(data.toString()));
113
+ if (message.method === "thread/name/set")
114
+ titleSync?.close();
115
+ if (message.method === "initialize"
116
+ && (typeof message.id === "string" || typeof message.id === "number"))
117
+ initializeRequestId = message.id;
94
118
  if (snapshot.nativeSessionId === undefined
95
119
  && (message.method === "thread/start" || message.method === "thread/resume")) {
96
120
  if (startupRequestId !== undefined)
@@ -106,6 +130,23 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
106
130
  startupRequestId = message.id;
107
131
  message.params = codexInteractiveStartupParameters(payload, params);
108
132
  }
133
+ if (message.method === "turn/start" || message.method === "turn/steer") {
134
+ if (snapshot.nativeSessionId === undefined)
135
+ throw new Error("Skill check requires the reserved native Thread.");
136
+ if (typeof message.id !== "string" && typeof message.id !== "number"
137
+ || pendingNotices.has(message.id))
138
+ throw new Error("Turn input requires a unique correlation id.");
139
+ const params = jsonObject(message.params);
140
+ if (params.threadId !== snapshot.nativeSessionId)
141
+ throw new Error("Turn input targets another Thread.");
142
+ const prepared = codexInteractiveSkillInput(skillNotice, environment, snapshot.nativeSessionId, params);
143
+ message.params = prepared.params;
144
+ pendingNotices.set(message.id, {
145
+ commit: prepared.commit,
146
+ ...(message.method === "turn/steer" && typeof params.expectedTurnId === "string"
147
+ ? { expectedTurnId: params.expectedTurnId } : {})
148
+ });
149
+ }
109
150
  void connection.send(message).catch(fail);
110
151
  }
111
152
  catch (error) {
@@ -115,6 +156,30 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
115
156
  });
116
157
  connection.onMessage((message) => {
117
158
  try {
159
+ if (snapshot.nativeSessionId !== undefined
160
+ && isNativeContextRecovery("codex", snapshot.nativeSessionId, message))
161
+ skillNotice.invalidate();
162
+ if (message.method === "thread/name/updated"
163
+ && message.params?.threadId === snapshot.nativeSessionId)
164
+ titleSync?.close();
165
+ if (message.method === undefined && message.id === initializeRequestId) {
166
+ const initialized = message.result;
167
+ nativeAccountHome = typeof initialized?.codexHome === "string" ? initialized.codexHome : undefined;
168
+ initializeRequestId = undefined;
169
+ }
170
+ if (message.method === undefined && (typeof message.id === "string" || typeof message.id === "number")) {
171
+ const pending = pendingNotices.get(message.id);
172
+ if (pending !== undefined) {
173
+ pendingNotices.delete(message.id);
174
+ const result = message.result;
175
+ const turnId = result?.turn?.id ?? result?.turnId;
176
+ if (message.error === undefined && typeof turnId === "string" && turnId.length > 0
177
+ && (pending.expectedTurnId === undefined || pending.expectedTurnId === turnId)) {
178
+ pending.commit();
179
+ titleSync?.notify();
180
+ }
181
+ }
182
+ }
118
183
  let nativeSessionId;
119
184
  if (startupRequestId !== undefined && message.id === startupRequestId
120
185
  && message.method === undefined) {
@@ -135,13 +200,17 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
135
200
  fail(error);
136
201
  return;
137
202
  }
138
- if (nativeSessionId === undefined || snapshot.state === "failed")
203
+ if (nativeSessionId === undefined || closing || snapshot.state === "failed")
139
204
  return;
140
205
  startupRequestId = undefined;
141
- snapshot = {
142
- ...snapshot, state: "ready", nativeSessionId, conversationId: nativeSessionId,
143
- updatedAt: new Date().toISOString()
144
- };
206
+ // Ready here means the native TUI attachment is ready, not a managed
207
+ // AgentRun acceptance or proof that the native Thread is idle.
208
+ transition({ type: "interactive-connected", nativeSessionId }, {
209
+ nativeSessionId, conversationId: nativeSessionId
210
+ });
211
+ titleSync = createNativeTitleSync(payload, nativeSessionId, nativeAccountHome, result => process.stderr.write(`Native title: ${JSON.stringify(result)}\n`));
212
+ if (expectedId !== undefined)
213
+ titleSync.notify();
145
214
  });
146
215
  }
147
216
  catch (error) {
@@ -165,6 +234,7 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
165
234
  for (const signal of signals)
166
235
  process.on(signal, stop);
167
236
  const [code] = await once(child, "exit");
237
+ transition({ type: "exit" });
168
238
  return typeof code === "number" ? code : 1;
169
239
  }
170
240
  catch (error) {
@@ -172,7 +242,9 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
172
242
  throw error;
173
243
  }
174
244
  finally {
245
+ lifecycleChecks.close();
175
246
  closing = true;
247
+ titleSync?.close();
176
248
  for (const signal of signals)
177
249
  process.removeListener(signal, stop);
178
250
  stop();
@@ -183,6 +255,25 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
183
255
  await control.close();
184
256
  }
185
257
  }
258
+ /** Keep application metadata out of native user input, including on steer.
259
+ * Formal titles are synchronized separately; no automatic-title heuristic is assumed. */
260
+ export function codexInteractiveSkillInput(notice, environment, nativeSessionId, params) {
261
+ if (!Array.isArray(params.input))
262
+ throw new Error("Turn input must be an array.");
263
+ const prepared = notice.prepare("", environment, nativeSessionId);
264
+ return {
265
+ params: {
266
+ ...params,
267
+ ...(prepared.context === undefined ? {} : {
268
+ additionalContext: {
269
+ ...(params.additionalContext == null ? {} : jsonObject(params.additionalContext)),
270
+ "yui.session-skills": { kind: "application", value: prepared.context }
271
+ }
272
+ })
273
+ },
274
+ commit: prepared.commit
275
+ };
276
+ }
186
277
  /** The remote TUI can omit local launch configuration. Apply the reserved
187
278
  * workspace and managed identity on its actual startup, never by starting a
188
279
  * second Thread or changing daemon-wide configuration. */
@@ -5,7 +5,7 @@ const READS = new Set([
5
5
  "config/read", "configRequirements/read", "model/list", "collaborationMode/list",
6
6
  "experimentalFeature/list", "skills/list", "hooks/list", "mcpServerStatus/list", "account/read",
7
7
  "account/rateLimits/read", "modelProvider/capabilities/read", "permissionProfile/list",
8
- "thread/read", "thread/turns/list", "thread/items/list", "thread/goal/get"
8
+ "thread/read", "thread/turns/list", "thread/items/list", "thread/goal/get", "thread/backgroundTerminals/list"
9
9
  ]);
10
10
  const REQUESTS = new Set([
11
11
  "item/commandExecution/requestApproval", "item/fileChange/requestApproval",
@@ -0,0 +1,15 @@
1
+ /** Only explicit, identity-bound native lifecycle events invalidate context.
2
+ * Absence of an event never proves that instructions are still in memory. */
3
+ export function isNativeContextRecovery(adapter, nativeSessionId, message) {
4
+ if (adapter === "claude") {
5
+ return message.type === "system" && message.subtype === "compact_boundary"
6
+ && message.session_id === nativeSessionId;
7
+ }
8
+ const params = message.params;
9
+ if (!params || params.threadId !== nativeSessionId)
10
+ return false;
11
+ if (message.method === "thread/compacted")
12
+ return true;
13
+ const item = params.item;
14
+ return message.method === "item/completed" && item?.type === "contextCompaction";
15
+ }
@@ -0,0 +1,83 @@
1
+ const live = [
2
+ "idle", "starting", "ready", "busy", "settling", "delivery-unknown", "rejected", "failed"
3
+ ];
4
+ const available = ["idle", "busy", "rejected", "failed"];
5
+ const occupancy = (value) => (event) => "occupancy" in event && event.occupancy === value;
6
+ /** Lifecycle only: no version checks, upgrade phases, I/O or business policy.
7
+ * Each event selects one guarded edge; callers cannot assign a target state.
8
+ * Existing authority/admission checks must succeed before sending commands. */
9
+ export const HOST_SESSION_LIFECYCLE_TRANSITIONS = {
10
+ launch: [{ from: [...available, "exited"], to: "starting" }],
11
+ submit: [{ from: available, to: "starting" }],
12
+ connected: [
13
+ { from: live, guard: occupancy("owned"), to: "ready" },
14
+ { from: live, guard: occupancy("unknown"), to: "delivery-unknown" },
15
+ { from: live, guard: occupancy("busy"), to: "busy" },
16
+ { from: live, guard: occupancy("idle"), to: "idle" }
17
+ ],
18
+ accepted: [{ from: ["starting", "ready", "delivery-unknown"], to: "ready" }],
19
+ "interactive-connected": [{ from: ["starting", "ready"], to: "ready" }],
20
+ "native-started": [{ from: live, to: "busy" }],
21
+ terminal: [{ from: ["starting", "ready", "busy", "delivery-unknown", "settling", "failed"], to: "settling" }],
22
+ settled: [
23
+ { from: live, guard: occupancy("idle"), to: "idle" },
24
+ { from: live, guard: occupancy("busy"), to: "busy" }
25
+ ],
26
+ disconnected: [{ from: live, to: "starting" }],
27
+ "authority-changed": [{ from: available, to: "idle" }],
28
+ failure: ["busy", "delivery-unknown", "rejected", "failed"].map(outcome => ({
29
+ from: live, guard: event => event.type === "failure" && event.outcome === outcome, to: outcome
30
+ })),
31
+ exit: [{ from: live, to: "exited" }]
32
+ };
33
+ export function createHostSessionLifecycle(hooks = {}) {
34
+ let state = "idle";
35
+ let revision = 0;
36
+ let identity = {};
37
+ const edgeFor = (event) => HOST_SESSION_LIFECYCLE_TRANSITIONS[event.type].find(rule => rule.from.includes(state) && (!rule.guard || rule.guard(event)));
38
+ return {
39
+ get state() { return state; },
40
+ get revision() { return revision; },
41
+ /** State eligibility only; caller still owns exact writer/resource guards. */
42
+ can(event) { return edgeFor(event) !== undefined; },
43
+ send(input) {
44
+ const event = Object.freeze({ ...input });
45
+ if (state === "exited" && event.type !== "launch")
46
+ return { status: "ignored", state, reason: "exited" };
47
+ if (!["launch", "connected"].includes(event.type)) {
48
+ for (const key of ["processInstanceId", "nativeSessionId"]) {
49
+ if (identity[key] && event[key] && identity[key] !== event[key]) {
50
+ return { status: "ignored", state, reason: `stale-${key}` };
51
+ }
52
+ }
53
+ if (["accepted", "terminal"].includes(event.type)
54
+ && identity.attemptId && event.attemptId && identity.attemptId !== event.attemptId) {
55
+ return { status: "ignored", state, reason: "stale-attempt" };
56
+ }
57
+ if (event.type === "terminal" && identity.nativeTurnId && event.nativeTurnId
58
+ && identity.nativeTurnId !== event.nativeTurnId) {
59
+ return { status: "ignored", state, reason: "stale-turn" };
60
+ }
61
+ }
62
+ const edge = edgeFor(event);
63
+ if (!edge)
64
+ return { status: "invalid", state, reason: `No ${event.type} edge from ${state}` };
65
+ const transition = Object.freeze({ from: state, to: edge.to, revision: revision + 1, event });
66
+ hooks.before?.notify(transition);
67
+ state = edge.to;
68
+ revision += 1;
69
+ // A new authorized attachment must not inherit the departed proxy's
70
+ // identity fence. A launch reusing a live connection supplies its IDs.
71
+ if (event.type === "launch")
72
+ identity = {};
73
+ identity = {
74
+ processInstanceId: event.processInstanceId ?? identity.processInstanceId,
75
+ nativeSessionId: event.nativeSessionId ?? identity.nativeSessionId,
76
+ attemptId: event.attemptId,
77
+ nativeTurnId: event.nativeTurnId
78
+ };
79
+ hooks.after?.notify(transition);
80
+ return { status: "applied", state };
81
+ }
82
+ };
83
+ }
@@ -0,0 +1,43 @@
1
+ /** Host-local input admission fence. The existing lifecycle mailbox owns the
2
+ * durable request; this gate owns only the actual loaded process's drain. */
3
+ export function createHostUpgradeGate() {
4
+ let current;
5
+ let draining;
6
+ return {
7
+ get observation() { return current; },
8
+ get fenced() { return current !== undefined; },
9
+ request(fence, identity) {
10
+ if (fence.nativeSessionId !== identity.nativeSessionId
11
+ || fence.processInstanceId !== identity.processInstanceId
12
+ || fence.hostGeneration !== identity.hostGeneration)
13
+ throw new Error("Host upgrade identity changed.");
14
+ if (current && (fence.requestId !== current.requestId || fence.nativeSessionId !== current.nativeSessionId
15
+ || fence.processInstanceId !== current.processInstanceId || fence.hostGeneration !== current.hostGeneration)) {
16
+ throw new Error("Host already holds a different upgrade request.");
17
+ }
18
+ current ??= { requestId: fence.requestId, nativeSessionId: fence.nativeSessionId,
19
+ processInstanceId: fence.processInstanceId, hostGeneration: fence.hostGeneration, state: "waiting" };
20
+ },
21
+ async drain(input) {
22
+ if (!current || current.state !== "waiting")
23
+ return;
24
+ if (input.busy || input.events.pending !== 0 || input.events.pendingTerminals !== 0
25
+ || input.events.unpersisted !== 0 || input.events.failure !== undefined)
26
+ return;
27
+ draining ??= (async () => {
28
+ try {
29
+ if (!await input.checkQuiescent())
30
+ return;
31
+ await input.release();
32
+ current = { ...current, state: "drained" };
33
+ }
34
+ catch (error) {
35
+ current = { ...current, state: "failed",
36
+ detail: error instanceof Error ? error.message : String(error) };
37
+ }
38
+ })();
39
+ await draining;
40
+ draining = undefined;
41
+ }
42
+ };
43
+ }
@@ -0,0 +1,47 @@
1
+ export function createNonBlockingChecks(checks, onError = () => { }) {
2
+ if (new Set(checks.map(check => check.id)).size !== checks.length)
3
+ throw new Error("Duplicate lifecycle check id.");
4
+ const abort = new AbortController();
5
+ const lanes = checks.map(check => ({ check, running: false, pending: undefined }));
6
+ const launch = (lane) => {
7
+ if (lane.running || abort.signal.aborted)
8
+ return;
9
+ lane.running = true;
10
+ // Defer even synchronous callbacks until the caller has committed its state.
11
+ void Promise.resolve().then(async () => {
12
+ while (lane.pending && !abort.signal.aborted) {
13
+ const { value } = lane.pending;
14
+ lane.pending = undefined;
15
+ try {
16
+ await lane.check.run(value, abort.signal);
17
+ }
18
+ catch (error) {
19
+ try {
20
+ onError(error);
21
+ }
22
+ catch { /* Diagnostics cannot gate lifecycle. */ }
23
+ }
24
+ }
25
+ }).finally(() => {
26
+ lane.running = false;
27
+ if (lane.pending)
28
+ launch(lane);
29
+ });
30
+ };
31
+ return {
32
+ notify(observation) {
33
+ if (abort.signal.aborted)
34
+ return;
35
+ for (const lane of lanes) {
36
+ lane.pending = { value: observation };
37
+ launch(lane);
38
+ }
39
+ },
40
+ get pendingCount() { return lanes.filter(lane => lane.pending !== undefined).length; },
41
+ close() {
42
+ abort.abort();
43
+ for (const lane of lanes)
44
+ lane.pending = undefined;
45
+ }
46
+ };
47
+ }
@@ -3,6 +3,9 @@ export const RUNTIME_LIFECYCLE_OWNER = "runtime-lifecycle";
3
3
  export const RUNTIME_CLEANUP_REQUIRED_REASON = "runtime-cleanup-required";
4
4
  export const RUNTIME_HOST_DETACH_REQUIRED_REASON = "runtime-host-detach-required";
5
5
  export const RUNTIME_SESSION_REPLACE_REQUIRED_REASON = "runtime-session-replace-required";
6
+ export const RUNTIME_HOST_UPGRADE_REASON = "runtime-host-upgrade-required";
7
+ export const RUNTIME_HOST_UPGRADE_DRAINED = "runtime-host-upgrade-drained";
8
+ export const RUNTIME_HOST_UPGRADE_FAILED = "runtime-host-upgrade-failed";
6
9
  /**
7
10
  * A Role runtime lifecycle lane already holds an explicit cleanup obligation.
8
11
  * This is scheduler
@@ -48,12 +51,17 @@ export function runtimeCleanupDisposition(mailbox) {
48
51
  return "end-session";
49
52
  if (reasons.includes(RUNTIME_HOST_DETACH_REQUIRED_REASON))
50
53
  return "detach-host";
54
+ if (reasons.includes(RUNTIME_HOST_UPGRADE_REASON))
55
+ return "upgrade-host";
51
56
  return null;
52
57
  }
53
58
  export function isRuntimeCleanupReason(reason) {
54
59
  return reason === RUNTIME_CLEANUP_REQUIRED_REASON
55
60
  || reason === RUNTIME_SESSION_REPLACE_REQUIRED_REASON
56
- || reason === RUNTIME_HOST_DETACH_REQUIRED_REASON;
61
+ || reason === RUNTIME_HOST_DETACH_REQUIRED_REASON
62
+ || reason === RUNTIME_HOST_UPGRADE_REASON
63
+ || reason === RUNTIME_HOST_UPGRADE_DRAINED
64
+ || reason === RUNTIME_HOST_UPGRADE_FAILED;
57
65
  }
58
66
  export function hasRuntimeLifecycleWork(mailbox) {
59
67
  return mailbox !== null && mailboxHasWork(mailbox);
@@ -0,0 +1,31 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ export function nativeTitleDirectory(home, identity) {
5
+ const key = createHash("sha256").update(JSON.stringify([
6
+ identity.scope, identity.taskId ?? null, identity.roleName, identity.agentId, identity.threadId
7
+ ])).digest("hex");
8
+ return join(home, "runtime", "native-titles", key);
9
+ }
10
+ export function parseNativeTitleReceipt(text, identity) {
11
+ const value = JSON.parse(text);
12
+ if (value.schemaVersion !== 1 || JSON.stringify(value.identity) !== JSON.stringify(identity)
13
+ || !value.accountHome || !value.attemptId || !value.requestedTitle
14
+ || !(value.expectedTitle === null || typeof value.expectedTitle === "string")
15
+ || !["submitted", "confirmed", "unknown", "preserved"].includes(value.status)
16
+ || !Array.isArray(value.generatedTitles) || value.generatedTitles.some(title => typeof title !== "string")) {
17
+ throw new Error("Invalid native title receipt; preserve it for diagnosis.");
18
+ }
19
+ return value;
20
+ }
21
+ /** Read-only diagnostics can reuse confirmation, never a submitted naming intent.
22
+ * Unavailable/uncertain evidence supplies no ownership of an existing title. */
23
+ export function confirmedNativeTitles(home, identity) {
24
+ try {
25
+ const receipt = parseNativeTitleReceipt(readFileSync(join(nativeTitleDirectory(home, identity), "receipt.json"), "utf8"), identity);
26
+ return receipt.status === "confirmed" ? receipt.generatedTitles : [];
27
+ }
28
+ catch {
29
+ return [];
30
+ }
31
+ }