@zq-silk/yui 2.22.0 → 2.25.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 (89) hide show
  1. package/README.md +9 -4
  2. package/dist/artifacts/artifactCapability.js +6 -6
  3. package/dist/artifacts/gitArtifactRef.js +10 -10
  4. package/dist/artifacts/jobArtifact.js +5 -5
  5. package/dist/artifacts/managedGit.js +15 -12
  6. package/dist/artifacts/taskArtifactRepository.js +35 -33
  7. package/dist/cli/commandCatalog.js +2 -2
  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/cli.js +9 -2
  12. package/dist/commands/configCommands.js +64 -1
  13. package/dist/commands/projectCommands.js +9 -9
  14. package/dist/commands/taskCompletionGate.js +1 -1
  15. package/dist/commands/taskIntegrationCommands.js +1 -1
  16. package/dist/commands/taskPublicationAdoptCommand.js +1 -1
  17. package/dist/commands/taskRoleRuntimeStatus.js +4 -0
  18. package/dist/commands/taskUpstreamCommands.js +1 -1
  19. package/dist/config/configCatalog.js +3 -0
  20. package/dist/config/gitConfig.js +20 -0
  21. package/dist/config/yuiConfig.js +1 -0
  22. package/dist/context/currentSkills.js +32 -1
  23. package/dist/context/sessionBootstrapManifest.js +17 -4
  24. package/dist/context/sessionSkillNotice.js +33 -12
  25. package/dist/controlPlaneCli.js +4 -4
  26. package/dist/controller/clientRuntime.js +37 -18
  27. package/dist/controller/controller.js +16 -1
  28. package/dist/controller/fileSchedulerStoreAdapter.js +2 -0
  29. package/dist/controller/hostLifecycle.js +153 -0
  30. package/dist/controller/hostLifecycleChecks.js +27 -0
  31. package/dist/controller/hostUpgrade.js +344 -0
  32. package/dist/controller/jobArtifactControl.js +3 -3
  33. package/dist/controller/jobDefinitionControl.js +1 -1
  34. package/dist/controller/runtime.js +67 -16
  35. package/dist/controller/sessionNotify.js +5 -46
  36. package/dist/execution/codexThreadNaming.js +54 -43
  37. package/dist/executor/fileRoleLaunchPlanner.js +18 -3
  38. package/dist/integration/gitIntegrationService.js +1 -1
  39. package/dist/kernel/builtinCapabilities.js +3 -3
  40. package/dist/repository/gitCommand.js +153 -0
  41. package/dist/repository/gitCompatibility.js +3 -2
  42. package/dist/repository/gitWorkspace.js +579 -563
  43. package/dist/repository/gitWorktreeInventory.js +15 -19
  44. package/dist/repository/projectRefresh.js +1 -1
  45. package/dist/repository/taskBaseFreshness.js +1 -4
  46. package/dist/repository/taskWorkspacePreparer.js +1 -1
  47. package/dist/runtime/agentEndpoint.js +11 -1
  48. package/dist/runtime/agentEndpointIdentity.js +2 -0
  49. package/dist/runtime/agentHost.js +167 -42
  50. package/dist/runtime/agentHostImplementation.js +3 -1
  51. package/dist/runtime/codexAppServerRuntime.js +3 -0
  52. package/dist/runtime/codexInteractiveHost.js +62 -17
  53. package/dist/runtime/codexNativeAccess.js +1 -1
  54. package/dist/runtime/contextRecovery.js +15 -0
  55. package/dist/runtime/hostSessionLifecycle.js +83 -0
  56. package/dist/runtime/hostUpgradeGate.js +43 -0
  57. package/dist/runtime/lifecycleChecks.js +47 -0
  58. package/dist/runtime/lifecycleReservation.js +9 -1
  59. package/dist/runtime/nativeExecutable.js +5 -2
  60. package/dist/runtime/nativeTitleReceipt.js +31 -0
  61. package/dist/runtime/nativeTitleSync.js +192 -0
  62. package/dist/runtime/platform.js +11 -0
  63. package/dist/runtime/providerRetry.js +4 -4
  64. package/dist/runtime/structuredProviderHost.js +42 -10
  65. package/dist/storage/sqliteSchema.js +30 -0
  66. package/dist/storage/storageVersions.js +1 -1
  67. package/dist/storage/taskStore.js +7 -1
  68. package/dist/web/webConversation.js +2 -2
  69. package/dist/web/webMaterials.js +19 -19
  70. package/dist/web/webTaskSurface.js +3 -3
  71. package/dist/workspace/workItemChangeSetManager.js +3 -3
  72. package/docs/managed-turn-and-session-runtime.md +96 -1
  73. package/docs/managed-turn-and-session-runtime.zh-CN.md +70 -1
  74. package/docs/project-refresh.md +3 -2
  75. package/docs/project-refresh.zh-CN.md +2 -1
  76. package/docs/release-workflow.md +86 -2
  77. package/docs/release-workflow.zh-CN.md +65 -1
  78. package/docs/roles-and-configuration.md +34 -0
  79. package/docs/roles-and-configuration.zh-CN.md +27 -0
  80. package/docs/storage-baseline.md +14 -1
  81. package/docs/storage-baseline.zh-CN.md +7 -1
  82. package/docs/testing/verification-levels.md +7 -7
  83. package/docs/testing/verification-levels.zh-CN.md +6 -6
  84. package/i18n/README.zh-CN.md +8 -4
  85. package/package.json +1 -1
  86. package/skills/yui-runtime/SKILL.md +7 -2
  87. package/skills/yui-runtime/VERSION +1 -1
  88. package/dist/runtime/native/darwin-x64/claude-process-owner +0 -0
  89. package/dist/runtime/native/darwin-x64/process-identity +0 -0
@@ -4,7 +4,7 @@ import { createConnection, createServer } from "node:net";
4
4
  import { homedir, tmpdir } from "node:os";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import { createInterface } from "node:readline";
7
- import { createSessionSkillNotice } from "../context/sessionSkillNotice.js";
7
+ import { createSessionSkillNotice, createSessionSkillLifecycleCheck } from "../context/sessionSkillNotice.js";
8
8
  import { publishStructuredConversationRecoverability, publishStructuredProviderAccepted, publishStructuredProviderActivity, publishStructuredProviderAttachmentExit, publishStructuredProviderConnection, publishStructuredProviderDiagnostic, publishStructuredProviderGoal, publishStructuredProviderInputObserved, publishStructuredProviderInputSettlement, publishStructuredProviderOpened, publishStructuredProviderStarted, publishStructuredProviderTerminal, replayStructuredProviderPersistence, structuredProviderEventDelivery } from "../controller/structuredProviderObservation.js";
9
9
  import { callController, controllerCallMayHaveApplied, ControllerClientError } from "../core/controllerClient.js";
10
10
  import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
@@ -16,6 +16,9 @@ import { providerDeliveryFailure, providerDeliveryFailureFrom, redactAgentErrorT
16
16
  import { AGENT_HOST_CONTROL_PROTOCOL } from "./agentHostProtocol.js";
17
17
  import { AGENT_HOST_IMPLEMENTATION } from "./agentHostImplementation.js";
18
18
  import { closeAgentHostEndpoints } from "./agentHostCleanup.js";
19
+ import { createHostUpgradeGate } from "./hostUpgradeGate.js";
20
+ import { createHostSessionLifecycle } from "./hostSessionLifecycle.js";
21
+ import { createNonBlockingChecks } from "./lifecycleChecks.js";
19
22
  import { readAgentRunConfigurationObservation, unknownAgentRunConfiguration } from "./agentRunConfiguration.js";
20
23
  import { CodexPreSubmissionError } from "./codexAppServerRuntime.js";
21
24
  import { runCodexInteractiveHost } from "./codexInteractiveHost.js";
@@ -83,6 +86,8 @@ export async function runAgentHost(input) {
83
86
  }
84
87
  let session;
85
88
  const skillNotice = createSessionSkillNotice(input.home);
89
+ const upgrade = createHostUpgradeGate();
90
+ let upgradeDetaching = false;
86
91
  // This process really holds the Endpoint code its Session runs on. The lease
87
92
  // spans the Session's Turns, so installing a newer generation does not change
88
93
  // the implementation under a Session that is still using this one.
@@ -112,6 +117,16 @@ export async function runAgentHost(input) {
112
117
  });
113
118
  };
114
119
  let snapshot = hostSnapshot("idle");
120
+ const lifecycleChecks = createNonBlockingChecks([
121
+ createSessionSkillLifecycleCheck(input.home, () => (sessionPayload ?? payload).environment), {
122
+ id: "controller-lifecycle-hint",
123
+ run: (_transition, signal) => {
124
+ if (!signal.aborted)
125
+ return signalRoleMailbox(input.home, sessionPayload ?? payload);
126
+ }
127
+ }
128
+ ], error => process.stderr.write(`Session lifecycle check failed: ${errorText(error)}\n`));
129
+ const lifecycle = createHostSessionLifecycle({ after: lifecycleChecks });
115
130
  let dispatchTail = Promise.resolve();
116
131
  let promptHuman = () => { };
117
132
  let humanConsole;
@@ -120,10 +135,23 @@ export async function runAgentHost(input) {
120
135
  let modelChanging = false;
121
136
  const recentSteerAttempts = new Map();
122
137
  await replayExitOutbox(input.home);
138
+ const hostEvent = (event, fields) => ({
139
+ event: { ...event, processInstanceId: fields.processInstanceId, nativeSessionId: fields.nativeSessionId,
140
+ attemptId: fields.attemptId, nativeTurnId: fields.nativeTurnId },
141
+ fields
142
+ });
123
143
  const updateSnapshot = (next) => {
124
- const { appliedGeneratedTitle: _oldTitle, ...rest } = next;
144
+ // Validate the payload before committing the FSM; an invalid observation
145
+ // must not leave the public snapshot and lifecycle on different states.
146
+ validateSnapshot(hostSnapshot(lifecycle.state, next.fields));
147
+ const result = lifecycle.send(next.event);
148
+ if (result.status === "ignored")
149
+ return;
150
+ if (result.status === "invalid")
151
+ throw new Error(result.reason);
152
+ const { appliedGeneratedTitle: _oldTitle, ...rest } = next.fields;
125
153
  snapshot = validateSnapshot({
126
- ...rest,
154
+ ...hostSnapshot(result.state, rest),
127
155
  ...(session?.appliedGeneratedTitle === undefined ? {} : {
128
156
  appliedGeneratedTitle: session.appliedGeneratedTitle
129
157
  })
@@ -150,7 +178,7 @@ export async function runAgentHost(input) {
150
178
  await publishStructuredProviderAccepted({
151
179
  home: input.home, environment: observedPayload.environment, receipt
152
180
  });
153
- updateSnapshot(hostSnapshot("ready", {
181
+ updateSnapshot(hostEvent({ type: "accepted" }, {
154
182
  adapterId: session.adapterId,
155
183
  processInstanceId: session.processInstanceId,
156
184
  nativeSessionId: receipt.nativeSessionId,
@@ -178,7 +206,7 @@ export async function runAgentHost(input) {
178
206
  if (started.clientOwned || observedPayload === undefined)
179
207
  return;
180
208
  const observedAuthority = authority === undefined ? undefined : { ...authority };
181
- updateSnapshot(hostSnapshot("busy", {
209
+ updateSnapshot(hostEvent({ type: "native-started" }, {
182
210
  adapterId: observedPayload.providerControl?.adapterId,
183
211
  processInstanceId: session?.processInstanceId,
184
212
  nativeSessionId: started.nativeSessionId,
@@ -239,7 +267,7 @@ export async function runAgentHost(input) {
239
267
  terminal
240
268
  });
241
269
  if (session !== undefined && terminal.conversationId === session.conversationId) {
242
- updateSnapshot(hostSnapshot(session.inspect().activeNativeTurnId === undefined ? "idle" : "busy", {
270
+ updateSnapshot(hostEvent({ type: "settled", occupancy: session.inspect().activeNativeTurnId === undefined ? "idle" : "busy" }, {
243
271
  adapterId: session.adapterId,
244
272
  processInstanceId: session.processInstanceId,
245
273
  nativeSessionId: session.nativeSessionId,
@@ -282,7 +310,7 @@ export async function runAgentHost(input) {
282
310
  || activeRunAttemptId !== terminalAttemptId)
283
311
  return;
284
312
  if (session !== undefined) {
285
- updateSnapshot(hostSnapshot("settling", {
313
+ updateSnapshot(hostEvent({ type: "terminal" }, {
286
314
  adapterId: terminalPayload.providerControl.adapterId,
287
315
  processInstanceId: session.processInstanceId,
288
316
  nativeSessionId: terminal.nativeSessionId,
@@ -306,7 +334,7 @@ export async function runAgentHost(input) {
306
334
  if (session === undefined)
307
335
  return;
308
336
  const currentPayload = sessionPayload ?? terminalPayload;
309
- updateSnapshot(hostSnapshot(session.inspect().activeNativeTurnId === undefined ? "idle" : "busy", {
337
+ updateSnapshot(hostEvent({ type: "settled", occupancy: session.inspect().activeNativeTurnId === undefined ? "idle" : "busy" }, {
310
338
  adapterId: currentPayload.providerControl.adapterId,
311
339
  processInstanceId: session.processInstanceId,
312
340
  nativeSessionId: terminal.nativeSessionId,
@@ -317,7 +345,7 @@ export async function runAgentHost(input) {
317
345
  promptHuman();
318
346
  }
319
347
  catch (error) {
320
- updateSnapshot(hostSnapshot("failed", {
348
+ updateSnapshot(hostEvent({ type: "failure", outcome: "failed" }, {
321
349
  adapterId: terminalPayload.providerControl.adapterId,
322
350
  processInstanceId: session?.processInstanceId,
323
351
  nativeSessionId: terminal.nativeSessionId,
@@ -433,10 +461,10 @@ export async function runAgentHost(input) {
433
461
  conversationRecoverability = "recoverable";
434
462
  codexClientAttachedAt = Date.now();
435
463
  observeExit(started.session, currentPayload);
436
- const reconnectState = activeRunPayload !== undefined
437
- ? ownedTurn === undefined ? "delivery-unknown" : "ready"
464
+ const occupancy = activeRunPayload !== undefined
465
+ ? ownedTurn === undefined ? "unknown" : "owned"
438
466
  : started.session.inspect().activeNativeTurnId === undefined ? "idle" : "busy";
439
- updateSnapshot(hostSnapshot(reconnectState, {
467
+ updateSnapshot(hostEvent({ type: "connected", occupancy }, {
440
468
  adapterId: "codex",
441
469
  processInstanceId: started.session.processInstanceId,
442
470
  nativeSessionId: started.session.nativeSessionId,
@@ -470,7 +498,7 @@ export async function runAgentHost(input) {
470
498
  lastError = error;
471
499
  }
472
500
  }
473
- updateSnapshot(hostSnapshot("failed", {
501
+ updateSnapshot(hostEvent({ type: "failure", outcome: "failed" }, {
474
502
  adapterId: "codex",
475
503
  processInstanceId: disconnectedSession.processInstanceId,
476
504
  nativeSessionId: disconnectedSession.nativeSessionId,
@@ -483,6 +511,10 @@ export async function runAgentHost(input) {
483
511
  };
484
512
  const observeExit = (providerSession, launched) => {
485
513
  void providerSession.waitForExit().then((result) => enqueueSerialized(async () => {
514
+ // A drained attachment is not an ended native Session and must not
515
+ // trigger reconnect, cancellation, or session lifecycle replacement.
516
+ if (upgradeDetaching)
517
+ return;
486
518
  const ownsCurrentSession = session === providerSession;
487
519
  const currentPayload = ownsCurrentSession
488
520
  ? activeRunPayload ?? lastTerminalPayload ?? sessionPayload ?? launched : launched;
@@ -501,7 +533,7 @@ export async function runAgentHost(input) {
501
533
  }
502
534
  consecutiveCodexDisconnects += 1;
503
535
  if (consecutiveCodexDisconnects > MAX_CONSECUTIVE_CODEX_DISCONNECTS) {
504
- updateSnapshot(hostSnapshot("failed", {
536
+ updateSnapshot(hostEvent({ type: "failure", outcome: "failed" }, {
505
537
  adapterId: "codex",
506
538
  processInstanceId: result.processInstanceId,
507
539
  nativeSessionId: providerSession.nativeSessionId,
@@ -513,7 +545,7 @@ export async function runAgentHost(input) {
513
545
  }));
514
546
  return;
515
547
  }
516
- updateSnapshot(hostSnapshot("starting", {
548
+ updateSnapshot(hostEvent({ type: "disconnected" }, {
517
549
  adapterId: "codex",
518
550
  processInstanceId: result.processInstanceId,
519
551
  nativeSessionId: providerSession.nativeSessionId,
@@ -583,7 +615,7 @@ export async function runAgentHost(input) {
583
615
  }
584
616
  if (hostStopRequested || !ownsCurrentSession)
585
617
  return;
586
- updateSnapshot(hostSnapshot(failures.length === 0 ? "exited" : "failed", {
618
+ updateSnapshot(hostEvent(failures.length === 0 ? { type: "exit" } : { type: "failure", outcome: "failed" }, {
587
619
  adapterId: currentPayload.providerControl?.adapterId,
588
620
  processInstanceId: result.processInstanceId,
589
621
  nativeSessionId: providerSession.nativeSessionId,
@@ -595,7 +627,7 @@ export async function runAgentHost(input) {
595
627
  ? {}
596
628
  : { detail: "Provider process exited before the active AgentRun reached a terminal boundary." })
597
629
  }));
598
- })).catch((error) => updateSnapshot(hostSnapshot("failed", {
630
+ })).catch((error) => updateSnapshot(hostEvent({ type: "failure", outcome: "failed" }, {
599
631
  adapterId: launched.providerControl?.adapterId,
600
632
  processInstanceId: providerSession.processInstanceId,
601
633
  ...authorityFields(),
@@ -610,7 +642,7 @@ export async function runAgentHost(input) {
610
642
  if (activeRunPayload !== undefined
611
643
  || sessionPayload !== undefined
612
644
  && (structuredProviderEventDelivery(input.home, sessionPayload.environment).unpersisted ?? 0) > 0
613
- || ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
645
+ || !lifecycle.can({ type: "launch" })) {
614
646
  throw new Error("Agent Host still owns an unsettled Provider Turn.");
615
647
  }
616
648
  const requestedAuthority = validateProviderAuthorityFence(providerControl.authority);
@@ -624,7 +656,7 @@ export async function runAgentHost(input) {
624
656
  && !sameProviderAuthorityFence(authority, requestedAuthority)) {
625
657
  throw new Error("Agent Host launch carries a stale Provider authority fence.");
626
658
  }
627
- updateSnapshot(hostSnapshot("starting", {
659
+ updateSnapshot(hostEvent({ type: "launch" }, {
628
660
  adapterId: providerControl.adapterId,
629
661
  ...(session === undefined ? {} : {
630
662
  processInstanceId: session.processInstanceId,
@@ -734,10 +766,10 @@ export async function runAgentHost(input) {
734
766
  const restoredOwnedRun = providerControl.kind === "restore"
735
767
  ? providerControl.ownedTurn
736
768
  : undefined;
737
- const providerState = restoredOwnedRun !== undefined
738
- ? "ready"
769
+ const occupancy = restoredOwnedRun !== undefined
770
+ ? "owned"
739
771
  : session.inspect().activeNativeTurnId === undefined ? "idle" : "busy";
740
- updateSnapshot(hostSnapshot(providerState, {
772
+ updateSnapshot(hostEvent({ type: "connected", occupancy }, {
741
773
  adapterId: providerControl.adapterId,
742
774
  processInstanceId: session.processInstanceId,
743
775
  nativeSessionId: session.nativeSessionId,
@@ -769,7 +801,7 @@ export async function runAgentHost(input) {
769
801
  const state = error instanceof ProviderTurnBusyError
770
802
  ? "busy"
771
803
  : error instanceof ProviderTurnRejectedError ? "rejected" : "failed";
772
- updateSnapshot(hostSnapshot(state, {
804
+ updateSnapshot(hostEvent({ type: "failure", outcome: state }, {
773
805
  adapterId: providerControl.adapterId,
774
806
  processInstanceId: session?.processInstanceId,
775
807
  nativeSessionId: session?.nativeSessionId ?? providerControl.nativeSessionId,
@@ -790,6 +822,8 @@ export async function runAgentHost(input) {
790
822
  };
791
823
  const enqueueDispatch = (next) => (enqueueSerialized(() => dispatch(next)));
792
824
  const submitRun = async (request, operation) => {
825
+ if (upgrade.fenced)
826
+ throw new ProviderTurnBusyError("Host upgrade admission is fenced.", request.run.attemptId);
793
827
  if (session === undefined || sessionPayload === undefined) {
794
828
  throw new Error("Agent Host has no live Provider Conversation.");
795
829
  }
@@ -800,7 +834,7 @@ export async function runAgentHost(input) {
800
834
  || !sameProviderAuthorityFence(authority, request.authority)) {
801
835
  throw new Error("Agent Host rejected a stale Provider writer fence.");
802
836
  }
803
- if (activeRunPayload !== undefined || snapshot.state === "settling"
837
+ if (activeRunPayload !== undefined || !lifecycle.can({ type: "submit" })
804
838
  || (structuredProviderEventDelivery(input.home, sessionPayload.environment).unpersisted ?? 0) > 0
805
839
  || session.inspect().activeNativeTurnId !== undefined) {
806
840
  operation.failure = providerDeliveryFailure({
@@ -830,7 +864,7 @@ export async function runAgentHost(input) {
830
864
  };
831
865
  activeRunAttemptId = request.run.attemptId;
832
866
  activeNativeTurnId = undefined;
833
- updateSnapshot(hostSnapshot("starting", {
867
+ updateSnapshot(hostEvent({ type: "submit" }, {
834
868
  adapterId: session.adapterId,
835
869
  processInstanceId: session.processInstanceId,
836
870
  nativeSessionId: session.nativeSessionId,
@@ -853,6 +887,7 @@ export async function runAgentHost(input) {
853
887
  && typeof retry.text === "string")
854
888
  recoveryInput = {
855
889
  text: retry.text,
890
+ ...(typeof retry.context === "string" ? { context: retry.context } : {}),
856
891
  ...(typeof retry.expectedFailedNativeTurnId === "string"
857
892
  ? { expectedFailedNativeTurnId: retry.expectedFailedNativeTurnId } : {})
858
893
  };
@@ -871,8 +906,8 @@ export async function runAgentHost(input) {
871
906
  const registrationUnknown = !registrationSettled
872
907
  && (error instanceof ControllerAcknowledgementUnknownError
873
908
  || error instanceof ProviderDeliveryUnknownError);
874
- updateSnapshot(hostSnapshot(registrationUnknown ? "delivery-unknown"
875
- : error instanceof ProviderTurnBusyError ? "busy" : "failed", {
909
+ updateSnapshot(hostEvent({ type: "failure", outcome: registrationUnknown ? "delivery-unknown"
910
+ : error instanceof ProviderTurnBusyError ? "busy" : "failed" }, {
876
911
  adapterId: session.adapterId,
877
912
  processInstanceId: session.processInstanceId,
878
913
  nativeSessionId: session.nativeSessionId,
@@ -912,10 +947,20 @@ export async function runAgentHost(input) {
912
947
  try {
913
948
  // beginDurableProviderTurn validated current execution-environment,
914
949
  // authority and Session identity before this native write.
915
- const notice = skillNotice.prepare(recoveryInput?.text ?? request.run.boundedText, activeRunPayload.environment, session.nativeSessionId, { recovery: recoveryInput !== undefined });
950
+ const notice = skillNotice.prepare(session.adapterId === "codex" ? request.run.boundedText : recoveryInput?.text ?? request.run.boundedText, activeRunPayload.environment, session.nativeSessionId, { contextEpoch: session.contextEpoch });
916
951
  const receipt = endpointReceipt(await session.submit({
917
952
  ...request.run,
918
- boundedText: notice.text,
953
+ boundedText: session.adapterId === "codex" ? notice.inputText : notice.text,
954
+ ...(session.adapterId !== "codex" || notice.context === undefined && recoveryInput?.context === undefined ? {} : {
955
+ additionalContext: {
956
+ ...(notice.context === undefined ? {} : {
957
+ "yui.session-skills": { kind: "application", value: notice.context }
958
+ }),
959
+ ...(recoveryInput?.context === undefined ? {} : {
960
+ "yui.recovery": { kind: "application", value: recoveryInput.context }
961
+ })
962
+ }
963
+ }),
919
964
  ...(recoveryInput === undefined ? {} : { requireQuiescent: true }),
920
965
  ...(recoveryInput?.expectedFailedNativeTurnId === undefined ? {} : {
921
966
  expectedFailedNativeTurnId: recoveryInput.expectedFailedNativeTurnId
@@ -942,7 +987,7 @@ export async function runAgentHost(input) {
942
987
  await resolveProviderTurnSubmission(input.home, durableRun, unknown);
943
988
  throw unknown;
944
989
  }
945
- updateSnapshot(hostSnapshot("ready", {
990
+ updateSnapshot(hostEvent({ type: "accepted" }, {
946
991
  adapterId: session.adapterId,
947
992
  processInstanceId: session.processInstanceId,
948
993
  nativeSessionId: receipt.nativeSessionId,
@@ -973,7 +1018,7 @@ export async function runAgentHost(input) {
973
1018
  : error instanceof ProviderTurnBusyError
974
1019
  ? "busy"
975
1020
  : error instanceof ProviderTurnRejectedError ? "rejected" : "failed";
976
- updateSnapshot(hostSnapshot(state, {
1021
+ updateSnapshot(hostEvent({ type: "failure", outcome: state }, {
977
1022
  adapterId: session.adapterId,
978
1023
  processInstanceId: session.processInstanceId,
979
1024
  nativeSessionId: session.nativeSessionId,
@@ -1035,10 +1080,13 @@ export async function runAgentHost(input) {
1035
1080
  }
1036
1081
  try {
1037
1082
  await assertHostExecutionEnvironment(input.home, sessionPayload, session.nativeSessionId);
1038
- const notice = skillNotice.prepare(request.run.boundedText, sessionPayload.environment, session.nativeSessionId);
1083
+ const notice = skillNotice.prepare(request.run.boundedText, sessionPayload.environment, session.nativeSessionId, { contextEpoch: session.contextEpoch });
1039
1084
  const receipt = endpointReceipt(await session.steer({
1040
1085
  ...request.run,
1041
- boundedText: notice.text,
1086
+ boundedText: session.adapterId === "codex" ? notice.inputText : notice.text,
1087
+ ...(session.adapterId !== "codex" || notice.context === undefined ? {} : {
1088
+ additionalContext: { "yui.session-skills": { kind: "application", value: notice.context } }
1089
+ }),
1042
1090
  inputRef: request.run.attemptId
1043
1091
  }), request.run.attemptId);
1044
1092
  if (receipt.nativeTurnId !== request.nativeTurnId) {
@@ -1067,7 +1115,7 @@ export async function runAgentHost(input) {
1067
1115
  if (activeRunPayload !== undefined || session.inspect().activeNativeTurnId !== undefined
1068
1116
  || sessionPayload !== undefined
1069
1117
  && (structuredProviderEventDelivery(input.home, sessionPayload.environment).unpersisted ?? 0) > 0
1070
- || ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
1118
+ || !lifecycle.can({ type: "authority-changed" })) {
1071
1119
  throw new Error("Agent Host authority cannot transfer while a AgentRun is unsettled.");
1072
1120
  }
1073
1121
  const next = validateProviderAuthorityFence(request.authority);
@@ -1079,7 +1127,7 @@ export async function runAgentHost(input) {
1079
1127
  }
1080
1128
  }
1081
1129
  authority = next;
1082
- updateSnapshot(hostSnapshot("idle", {
1130
+ updateSnapshot(hostEvent({ type: "authority-changed" }, {
1083
1131
  adapterId: session.adapterId,
1084
1132
  processInstanceId: session.processInstanceId,
1085
1133
  nativeSessionId: session.nativeSessionId,
@@ -1138,6 +1186,64 @@ export async function runAgentHost(input) {
1138
1186
  return { turn: { id: accepted.nativeTurnId, items: [], status: "inProgress", error: null } };
1139
1187
  };
1140
1188
  const handleControl = async (request) => {
1189
+ if (request.type === "prepare-upgrade" || request.type === "finish-upgrade") {
1190
+ upgrade.request(request, {
1191
+ nativeSessionId: snapshot.nativeSessionId ?? "",
1192
+ processInstanceId: snapshot.processInstanceId ?? "",
1193
+ hostGeneration: AGENT_HOST_IMPLEMENTATION.generation
1194
+ });
1195
+ if (request.type === "prepare-upgrade") {
1196
+ // Fence synchronously, then join the same lane as submission and
1197
+ // terminal publication. No check-then-submit gap on Host-owned paths.
1198
+ await enqueueSerialized(async () => {
1199
+ upgrade.request(request, {
1200
+ nativeSessionId: snapshot.nativeSessionId ?? "", processInstanceId: snapshot.processInstanceId ?? "",
1201
+ hostGeneration: AGENT_HOST_IMPLEMENTATION.generation
1202
+ });
1203
+ await upgrade.drain({
1204
+ busy: session === undefined || activeRunPayload !== undefined || activeNativeTurnId !== undefined
1205
+ || snapshot.state !== "idle" || nativeTui !== undefined || nativeOpening !== undefined
1206
+ || modelChanging || session.inspect().activeNativeTurnId !== undefined
1207
+ || session.inspect().submissions.some(s => s.disposition.status === "unknown"
1208
+ || s.disposition.status === "pending" && s.disposition.reason === "submitting"),
1209
+ events: structuredProviderEventDelivery(input.home, sessionPayload?.environment ?? payload.environment, snapshot.nativeSessionId),
1210
+ checkQuiescent: async () => {
1211
+ if (authority?.owner !== "controller" || conversationRecoverability !== "recoverable") {
1212
+ throw new Error("Upgrade requires an exactly resumable controller-owned native Session.");
1213
+ }
1214
+ return await session.checkQuiescent()
1215
+ && session.inspect().activeNativeTurnId === undefined;
1216
+ },
1217
+ release: async () => {
1218
+ const events = structuredProviderEventDelivery(input.home, sessionPayload.environment, snapshot.nativeSessionId);
1219
+ if (events.pending !== 0 || events.unpersisted !== 0 || events.pendingTerminals !== 0 || events.failure
1220
+ || session.inspect().activeNativeTurnId !== undefined
1221
+ || session.nativeSessionId !== request.nativeSessionId
1222
+ || session.processInstanceId !== request.processInstanceId) {
1223
+ throw new Error("Native execution or event delivery changed before Endpoint release.");
1224
+ }
1225
+ upgradeDetaching = true;
1226
+ session.detach();
1227
+ await endpointLease?.release();
1228
+ const drain = await endpointOwner.stop(endpointAdapterId, ENDPOINT_DRAIN_TIMEOUT_MS);
1229
+ if (!drain.quiescent)
1230
+ throw new Error("Endpoint release did not prove owned client/implementation drain.");
1231
+ }
1232
+ });
1233
+ });
1234
+ }
1235
+ else {
1236
+ if (upgrade.observation?.state !== "drained")
1237
+ throw new Error("Host has not drained.");
1238
+ // The Controller persists the drain receipt before this request.
1239
+ // Delay close until this connection has had a chance to receive its ACK.
1240
+ setImmediate(() => { hostStopRequested = true; stopResolve(); });
1241
+ }
1242
+ return controlResult("accepted", { ...snapshot, hostUpgrade: upgrade.observation });
1243
+ }
1244
+ if (upgrade.fenced && ["launch", "submit-turn", "steer-turn", "set-authority", "native-open", "native-model"].includes(request.type)) {
1245
+ return controlResult("busy", { ...snapshot, hostUpgrade: upgrade.observation });
1246
+ }
1141
1247
  if (request.type === "native-model") {
1142
1248
  return enqueueSerialized(async () => {
1143
1249
  const current = session;
@@ -1226,7 +1332,7 @@ export async function runAgentHost(input) {
1226
1332
  const persisted = await replayStructuredProviderPersistence(input.home, sessionPayload.environment);
1227
1333
  if (persisted.length > 0 && activeRunPayload === undefined) {
1228
1334
  const active = session.inspect().activeNativeTurnId;
1229
- updateSnapshot(hostSnapshot(active === undefined ? "idle" : "busy", {
1335
+ updateSnapshot(hostEvent({ type: "settled", occupancy: active === undefined ? "idle" : "busy" }, {
1230
1336
  adapterId: session.adapterId, processInstanceId: session.processInstanceId,
1231
1337
  nativeSessionId: session.nativeSessionId, conversationId: session.conversationId,
1232
1338
  nativeTurnId: active, ...authorityFields()
@@ -1270,7 +1376,7 @@ export async function runAgentHost(input) {
1270
1376
  activeRunPayload = undefined;
1271
1377
  activeRunAttemptId = undefined;
1272
1378
  activeNativeTurnId = undefined;
1273
- updateSnapshot(hostSnapshot(session.inspect().activeNativeTurnId === undefined ? "idle" : "busy", {
1379
+ updateSnapshot(hostEvent({ type: "settled", occupancy: session.inspect().activeNativeTurnId === undefined ? "idle" : "busy" }, {
1274
1380
  adapterId: session.adapterId, processInstanceId: session.processInstanceId,
1275
1381
  nativeSessionId: session.nativeSessionId, conversationId: session.conversationId,
1276
1382
  ...authorityFields()
@@ -1293,9 +1399,10 @@ export async function runAgentHost(input) {
1293
1399
  return requestBytes <= 8000;
1294
1400
  }).slice(0, 8);
1295
1401
  return controlResult("status", session === undefined
1296
- ? snapshot
1402
+ ? { ...snapshot, hostUpgrade: upgrade.observation }
1297
1403
  : validateSnapshot({
1298
1404
  ...snapshot,
1405
+ hostUpgrade: upgrade.observation,
1299
1406
  nativeTui: !!nativeTui,
1300
1407
  ...(nativeRequests === undefined ? {} : { nativeRequests,
1301
1408
  nativeRequestsOmitted: nativeRequests.length !== session.nativeAccess.requests.length }),
@@ -1403,7 +1510,7 @@ export async function runAgentHost(input) {
1403
1510
  throw new Error("Unsupported Host request.");
1404
1511
  const redeemed = await redeem(input.home, request.ticket);
1405
1512
  if (activeRunPayload !== undefined
1406
- || ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
1513
+ || !lifecycle.can({ type: "launch" })) {
1407
1514
  if (redeemed.providerControl?.mode === "resume"
1408
1515
  && redeemed.providerControl.nativeSessionId === snapshot.nativeSessionId) {
1409
1516
  return controlResult("accepted", snapshot);
@@ -1490,6 +1597,7 @@ export async function runAgentHost(input) {
1490
1597
  return 0;
1491
1598
  }
1492
1599
  finally {
1600
+ lifecycleChecks.close();
1493
1601
  for (const [signal, handler] of handlers)
1494
1602
  process.removeListener(signal, handler);
1495
1603
  if (forceKillTimer !== undefined)
@@ -1577,6 +1685,9 @@ export async function sendAgentHostCancelControl(input) {
1577
1685
  export async function sendAgentHostNativeControl(input) {
1578
1686
  return sendAgentHostControl(input);
1579
1687
  }
1688
+ export async function sendAgentHostUpgradeControl(input) {
1689
+ return sendAgentHostControl(input);
1690
+ }
1580
1691
  export async function inspectAgentHost(input) {
1581
1692
  const result = await sendAgentHostControl({
1582
1693
  ...input,
@@ -1758,7 +1869,8 @@ export async function openAgentHostControl(home, payload, snapshot, dispatch) {
1758
1869
  hostImplementation: AGENT_HOST_IMPLEMENTATION,
1759
1870
  controlCapabilities: payload.providerControl === undefined ? ["status"] : [
1760
1871
  "status", "launch", "submit-turn", "steer-turn", "cancel", "set-authority",
1761
- "native-open", "native-respond", "native-model", "reconcile-turn"
1872
+ "native-open", "native-respond", "native-model", "reconcile-turn",
1873
+ ...(payload.providerControl.adapterId === "codex" ? ["prepare-upgrade", "finish-upgrade"] : [])
1762
1874
  ],
1763
1875
  ...(!["task", "global"].includes(payload.environment.YUI_SESSION_SCOPE ?? "") ? {} : {
1764
1876
  eventDelivery: structuredProviderEventDelivery(home, payload.environment, result.snapshot.nativeSessionId)
@@ -1909,6 +2021,12 @@ function validateControl(control) {
1909
2021
  }
1910
2022
  if (control.type === "status")
1911
2023
  return Object.freeze({ ...control });
2024
+ if (control.type === "prepare-upgrade" || control.type === "finish-upgrade") {
2025
+ for (const key of ["requestId", "nativeSessionId", "processInstanceId", "hostGeneration"]) {
2026
+ validateIdentity(control[key], key);
2027
+ }
2028
+ return Object.freeze({ ...control });
2029
+ }
1912
2030
  if (control.type === "reconcile-turn") {
1913
2031
  validateIdentity(control.nativeSessionId, "native Session id");
1914
2032
  validateIdentity(control.attemptId, "Provider input attempt id");
@@ -2025,6 +2143,13 @@ function validateSnapshot(snapshot) {
2025
2143
  || value.length === 0 || value.length > 64 || value.includes("\0")))) {
2026
2144
  throw new Error("Agent Host control capability advertisement is invalid.");
2027
2145
  }
2146
+ if (snapshot.hostUpgrade !== undefined) {
2147
+ for (const key of ["requestId", "nativeSessionId", "processInstanceId", "hostGeneration"]) {
2148
+ validateIdentity(snapshot.hostUpgrade[key], key);
2149
+ }
2150
+ if (!["waiting", "drained", "failed"].includes(snapshot.hostUpgrade.state))
2151
+ throw new Error("Invalid Host drain state.");
2152
+ }
2028
2153
  const authorityFields = [
2029
2154
  snapshot.authorityEpoch,
2030
2155
  snapshot.authorityOwner,
@@ -2124,11 +2249,11 @@ function signalRoleMailbox(home, payload) {
2124
2249
  const taskId = payload.environment.YUI_TASK_ID;
2125
2250
  const roleName = payload.environment.YUI_ROLE;
2126
2251
  if (roleName === undefined)
2127
- return;
2252
+ return Promise.resolve();
2128
2253
  const key = taskId === undefined
2129
2254
  ? `global-role:${encodeURIComponent(roleName)}`
2130
2255
  : `role:${encodeURIComponent(taskId)}/${encodeURIComponent(roleName)}`;
2131
- void callController(home, "scheduler.signal", { key }).catch(() => {
2256
+ return callController(home, "scheduler.signal", { key }).then(() => undefined).catch(() => {
2132
2257
  // This is a low-latency hint. Durable mailbox state and periodic
2133
2258
  // reconciliation remain the recovery path across a Controller handover.
2134
2259
  });
@@ -8,7 +8,9 @@ const release = detectRunningRelease(fileURLToPath(import.meta.url));
8
8
  const modules = [
9
9
  "agentHost.js", "agentHostProtocol.js", "agentHostImplementation.js",
10
10
  "codexInteractiveHost.js", "../controller/structuredProviderObservation.js",
11
- "../controller/runtimeEventInbox.js"
11
+ "../controller/runtimeEventInbox.js", "../context/sessionSkillNotice.js",
12
+ "../context/sessionBootstrapManifest.js", "../context/currentSkills.js", "contextRecovery.js", "hostUpgradeGate.js",
13
+ "hostSessionLifecycle.js", "lifecycleChecks.js"
12
14
  ];
13
15
  export const AGENT_HOST_IMPLEMENTATION = Object.freeze({
14
16
  id: "yui.agent-host",
@@ -171,6 +171,7 @@ export class CodexAppServerRuntime {
171
171
  return this.startTurn({
172
172
  conversationId: input.conversationId,
173
173
  text: input.text,
174
+ ...(input.additionalContext === undefined ? {} : { additionalContext: input.additionalContext }),
174
175
  expectedNoActiveTurn: input.expectedNoActiveTurn,
175
176
  ...(input.requireQuiescent ? { requireQuiescent: true } : {}),
176
177
  ...(input.expectedFailedNativeTurnId === undefined ? {} : {
@@ -230,6 +231,7 @@ export class CodexAppServerRuntime {
230
231
  try {
231
232
  const result = await this.transport.request("turn/start", {
232
233
  threadId,
234
+ ...(input.additionalContext === undefined ? {} : { additionalContext: input.additionalContext }),
233
235
  ...(input.clientUserMessageId === undefined
234
236
  ? {}
235
237
  : { clientUserMessageId: text(input.clientUserMessageId, "Codex input attempt id") }),
@@ -265,6 +267,7 @@ export class CodexAppServerRuntime {
265
267
  try {
266
268
  const result = await this.transport.request("turn/steer", {
267
269
  threadId,
270
+ ...(input.additionalContext === undefined ? {} : { additionalContext: input.additionalContext }),
268
271
  expectedTurnId,
269
272
  ...(input.clientUserMessageId === undefined
270
273
  ? {}