@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
@@ -0,0 +1,153 @@
1
+ import { createRuntimeBinding } from "../runtime/runtimeBinding.js";
2
+ import { normalizeRuntimeOwner } from "../runtime/runtimeOwner.js";
3
+ import { RuntimeHostContentionError } from "../runtime/ports.js";
4
+ const states = ["unavailable", "stopped", "starting", "running", "stopping"];
5
+ export const CONTROLLER_HOST_TRANSITIONS = {
6
+ launch: { from: ["unavailable", "stopped", "running"], to: "starting" },
7
+ launched: { from: ["starting"], to: "running" },
8
+ stop: { from: states, to: "stopping" },
9
+ "exit-sent": { from: ["stopping"], to: "stopping" },
10
+ stopped: { from: ["stopping"], to: "stopped" },
11
+ failed: { from: ["starting", "stopping"], to: "unavailable" },
12
+ observed: { from: states, to: (current, observed) => current === "stopping" && (observed.state === "running" || observed.state === "starting")
13
+ ? "stopping" : observed.state }
14
+ };
15
+ /** All physical effects still require their existing durable caller authority.
16
+ * This process-local FSM gates competing effects at the shared Host port.
17
+ * Recovery observes current custody; it never invents a launch or replays one.
18
+ * Observation revisions reject late reads, not external process identities:
19
+ * exact OS/native identities remain owned by SessionOwner/Host fences. */
20
+ export function createControllerHostLifecycle(raw, hooks = {}) {
21
+ const entries = new Map();
22
+ let nextOperation = 0;
23
+ const entryFor = (input) => {
24
+ const owner = normalizeRuntimeOwner(input);
25
+ const key = JSON.stringify(owner);
26
+ let entry = entries.get(key);
27
+ if (!entry) {
28
+ entry = { owner: Object.freeze(owner), state: "unavailable", revision: 0 };
29
+ entries.set(key, entry);
30
+ }
31
+ return entry;
32
+ };
33
+ const send = (entry, event, observed, binding) => {
34
+ const rule = CONTROLLER_HOST_TRANSITIONS[event];
35
+ if (!rule.from.includes(entry.state))
36
+ throw new RuntimeHostContentionError("previous-process", `Host lifecycle cannot ${event} from ${entry.state}.`);
37
+ const to = typeof rule.to === "function" ? rule.to(entry.state, observed) : rule.to;
38
+ const nativeSessionId = to === "stopped" ? undefined
39
+ : binding?.nativeSessionId ?? observed?.nativeSessionId ?? entry.nativeSessionId;
40
+ const bindingId = to === "stopped" ? undefined
41
+ : observed?.nativeSessionId && observed.nativeSessionId !== entry.nativeSessionId
42
+ ? undefined : binding?.id ?? entry.bindingId;
43
+ if (event === "observed" && to === entry.state && nativeSessionId === entry.nativeSessionId)
44
+ return;
45
+ const transition = Object.freeze({ owner: entry.owner, from: entry.state, to, event,
46
+ revision: entry.revision + 1, operationId: entry.operationId, nativeSessionId, bindingId });
47
+ hooks.before?.notify(transition);
48
+ entry.state = to;
49
+ entry.revision = transition.revision;
50
+ entry.nativeSessionId = nativeSessionId;
51
+ entry.bindingId = bindingId;
52
+ hooks.after?.notify(transition);
53
+ };
54
+ const observe = (entry, revision, inspection) => {
55
+ if (entry.revision === revision && entry.operationId === undefined)
56
+ send(entry, "observed", inspection);
57
+ return inspection;
58
+ };
59
+ const effect = async (owner, event, run) => {
60
+ const entry = entryFor(owner);
61
+ if (entry.operationId !== undefined)
62
+ throw new RuntimeHostContentionError("previous-process", `Host lifecycle already has an in-flight operation for ${owner.roleName}.`);
63
+ // Check state before reserving the operation; a rejected edge owns nothing.
64
+ if (!CONTROLLER_HOST_TRANSITIONS[event].from.includes(entry.state))
65
+ throw new RuntimeHostContentionError("previous-process", `Host lifecycle cannot ${event} from ${entry.state}.`);
66
+ entry.operationId = ++nextOperation;
67
+ send(entry, event);
68
+ try {
69
+ return await run(entry);
70
+ }
71
+ catch (error) {
72
+ send(entry, "failed");
73
+ throw error;
74
+ }
75
+ finally {
76
+ entry.operationId = undefined;
77
+ }
78
+ };
79
+ const inspectOwner = async (owner) => {
80
+ const entry = entryFor(owner), revision = entry.revision;
81
+ return observe(entry, revision, await raw.inspectOwner(owner));
82
+ };
83
+ const launch = async (request, before) => {
84
+ await inspectOwner(request.owner);
85
+ return effect(request.owner, "launch", async (entry) => {
86
+ const binding = createRuntimeBinding(request.mode === "new"
87
+ ? await raw.start(request, before) : await raw.restore(request, before));
88
+ if (JSON.stringify(normalizeRuntimeOwner(binding.owner)) !== JSON.stringify(entry.owner)
89
+ || binding.agentId !== request.agentId || binding.adapterId !== request.adapterId
90
+ || request.mode === "resume" && binding.nativeSessionId !== request.nativeSessionId) {
91
+ throw new Error("Host lifecycle launch handshake has mismatched owner/native identity.");
92
+ }
93
+ send(entry, "launched", undefined, binding);
94
+ return binding;
95
+ });
96
+ };
97
+ const port = {
98
+ start: launch, restore: launch, inspectOwner,
99
+ inspect: async (binding) => {
100
+ const entry = entryFor(binding.owner), revision = entry.revision;
101
+ const inspection = await raw.inspect(binding);
102
+ if (entry.bindingId && entry.bindingId !== binding.id
103
+ || entry.nativeSessionId && binding.nativeSessionId !== entry.nativeSessionId)
104
+ return inspection;
105
+ return observe(entry, revision, inspection);
106
+ },
107
+ ...(raw.inspectOwners === undefined ? {} : {
108
+ inspectOwners: async (owners) => {
109
+ const revisions = new Map(owners.map(owner => {
110
+ const entry = entryFor(owner);
111
+ return [entry, entry.revision];
112
+ }));
113
+ const results = await raw.inspectOwners(owners);
114
+ for (const { owner, inspection } of results) {
115
+ const entry = entryFor(owner), revision = revisions.get(entry);
116
+ if (revision !== undefined)
117
+ observe(entry, revision, inspection);
118
+ }
119
+ return results;
120
+ }
121
+ }),
122
+ stop: async (binding) => {
123
+ const current = entryFor(binding.owner);
124
+ if (current.nativeSessionId && binding.nativeSessionId !== current.nativeSessionId
125
+ || current.bindingId && binding.id !== current.bindingId) {
126
+ throw new Error("Host lifecycle stop carries a stale binding identity.");
127
+ }
128
+ return effect(binding.owner, "stop", async (entry) => {
129
+ await raw.stop(binding);
130
+ const observed = await raw.inspect(binding);
131
+ if (observed.state !== "stopped")
132
+ throw new Error("Host lifecycle stop has no physical exit proof.");
133
+ send(entry, "stopped");
134
+ });
135
+ },
136
+ stopOwner: owner => effect(owner, "stop", async (entry) => {
137
+ const stopped = await (hooks.stopOwner ? hooks.stopOwner(owner) : raw.stopOwner(owner));
138
+ send(entry, stopped ? "stopped" : "failed");
139
+ return stopped;
140
+ })
141
+ };
142
+ return {
143
+ port,
144
+ snapshot: (owner) => Object.freeze({ ...entryFor(owner) }),
145
+ // Graceful exit acknowledgement is not exit proof. A later physical
146
+ // observation moves stopping -> stopped; no timer or automatic replay.
147
+ requestExit: (owner, run) => effect(owner, "stop", async (entry) => {
148
+ const result = await run();
149
+ send(entry, "exit-sent");
150
+ return result;
151
+ })
152
+ };
153
+ }
@@ -0,0 +1,27 @@
1
+ import { createNonBlockingChecks } from "../runtime/lifecycleChecks.js";
2
+ /** Checks produce a recommendation; only the separately fenced coordinator
3
+ * can admit/execute it. Hints contain no target identity or restart authority. */
4
+ export function createHostLifecycleChecks(coordinator, onError = () => { }) {
5
+ const checks = createNonBlockingChecks([{
6
+ id: "host-version",
7
+ run: async (_hint, signal) => {
8
+ const observations = await coordinator.status();
9
+ if (signal.aborted)
10
+ return;
11
+ const recommended = coordinator.hasPending() || observations.some(entry => entry.status === "waiting" || entry.status === "unknown" || entry.status === "failed");
12
+ if (!recommended)
13
+ return;
14
+ // This await belongs exclusively to this optional check lane. Neither
15
+ // Host transitions nor Controller event folding wait for this operation.
16
+ const results = await coordinator.request();
17
+ for (const entry of results) {
18
+ if (entry.status === "failed" || entry.status === "unknown") {
19
+ onError(new Error(`Host upgrade ${entry.owner.roleName}/${entry.nativeSessionId}: ${entry.detail}`));
20
+ }
21
+ }
22
+ }
23
+ }], onError);
24
+ const close = checks.close;
25
+ checks.close = () => { close(); coordinator.stop(); };
26
+ return checks;
27
+ }
@@ -0,0 +1,344 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isDeepStrictEqual } from "node:util";
3
+ import { isEnabled } from "../role/availability.js";
4
+ import { createWorkMailbox, enqueueSignal, claimPending, completeProcessing, mailboxHasWork } from "../coordination/workMailbox.js";
5
+ import { RUNTIME_HOST_UPGRADE_REASON, RUNTIME_HOST_UPGRADE_DRAINED as DRAINED, RUNTIME_HOST_UPGRADE_FAILED as FAILED, runtimeLifecycleTarget, runtimeCleanupDisposition } from "../runtime/lifecycleReservation.js";
6
+ const SOURCE = "host-upgrade/v1:";
7
+ /** Reuse the durable role-runtime lane and its fixed claim/settlement contract.
8
+ * The versioned signal source is a bounded exact request, not a second queue
9
+ * or writable Session state. No environment, credentials or inputs are copied.
10
+ * The drain signal is persisted BEFORE asking the old process to exit. */
11
+ export class HostUpgradeCoordinator {
12
+ store;
13
+ target;
14
+ ports;
15
+ #tail = Promise.resolve();
16
+ #closed = false;
17
+ constructor(store, target, ports) {
18
+ this.store = store;
19
+ this.target = target;
20
+ this.ports = ports;
21
+ }
22
+ hasPending() {
23
+ return !this.#closed && this.store.listWorkMailboxes().some(mailbox => reasons(mailbox).includes(RUNTIME_HOST_UPGRADE_REASON));
24
+ }
25
+ stop() { this.#closed = true; }
26
+ async close() {
27
+ this.stop();
28
+ await this.#tail;
29
+ }
30
+ /** Read-only follow-up: never admits, drains, restores, or settles a request. */
31
+ async status() {
32
+ const results = [];
33
+ for (const { owner, nativeSessionId, adapterId } of this.store.listRuntimeSessionCandidates()) {
34
+ if (this.#closed)
35
+ break;
36
+ const result = (status, detail, snapshot) => ({
37
+ owner, nativeSessionId, status, detail, hostImplementation: snapshot?.hostImplementation,
38
+ endpointImplementation: snapshot?.endpointImplementation
39
+ });
40
+ try {
41
+ const mailbox = this.store.getWorkMailbox(runtimeLifecycleTarget(owner));
42
+ const request = readRequest(mailbox);
43
+ if (!this.enabled(owner)) {
44
+ results.push(result("paused", "Role/Task remains paused."));
45
+ continue;
46
+ }
47
+ const state = await this.ports.hostState(owner);
48
+ const live = state === "running" ? await this.ports.inspect(owner) : undefined;
49
+ if (live && this.verified(live, nativeSessionId, adapterId)) {
50
+ results.push(result("current", "Loaded Host and Endpoint match this Controller.", live));
51
+ }
52
+ else if (request && (request.nativeSessionId !== nativeSessionId || live
53
+ && (live.nativeSessionId !== request.nativeSessionId || live.processInstanceId !== request.processInstanceId))) {
54
+ results.push(result("unknown", `Request ${request.requestId} does not match the observed Host/Session.`, live));
55
+ }
56
+ else if (request) {
57
+ results.push(result(reasons(mailbox).includes(FAILED) || live?.hostUpgrade?.state === "failed" ? "failed"
58
+ : state === "unavailable" ? "unknown" : "waiting", `Request ${request.requestId}; target=${request.targetGeneration}; Host=${state}; drain=${live?.hostUpgrade?.state
59
+ ?? (reasons(mailbox).includes(DRAINED) ? "persisted" : "unconfirmed")}. ${live?.hostUpgrade?.detail ?? ""}`, live));
60
+ }
61
+ else {
62
+ results.push(result(live && (!live.controlCapabilities?.includes("prepare-upgrade")
63
+ || !live.controlCapabilities.includes("finish-upgrade")) ? "unsupported" : "unknown", "No confirmed switch to this Controller's generation.", live));
64
+ }
65
+ }
66
+ catch (error) {
67
+ results.push(result("unknown", message(error)));
68
+ }
69
+ }
70
+ return results;
71
+ }
72
+ request() {
73
+ return this.serial(async () => {
74
+ const results = [];
75
+ for (const candidate of this.store.listRuntimeSessionCandidates()) {
76
+ if (this.#closed)
77
+ break;
78
+ const { owner, nativeSessionId } = candidate;
79
+ const result = (status, detail, snapshot) => ({
80
+ owner, nativeSessionId, status, detail, hostImplementation: snapshot?.hostImplementation,
81
+ endpointImplementation: snapshot?.endpointImplementation
82
+ });
83
+ try {
84
+ const existing = this.store.getWorkMailbox(runtimeLifecycleTarget(owner));
85
+ if (readRequest(existing)) {
86
+ results.push(await this.step(owner));
87
+ continue;
88
+ }
89
+ if (!this.enabled(owner)) {
90
+ results.push(result("paused", "Role/Task remains paused; no Host is started."));
91
+ continue;
92
+ }
93
+ const snapshot = await this.ports.inspect(owner);
94
+ if (this.#closed)
95
+ break;
96
+ if (snapshot.nativeSessionId !== nativeSessionId) {
97
+ results.push(result("unknown", "Live Host does not match the recorded native Session.", snapshot));
98
+ continue;
99
+ }
100
+ if (this.verified(snapshot, nativeSessionId, candidate.adapterId)) {
101
+ results.push(result("current", undefined, snapshot));
102
+ continue;
103
+ }
104
+ if (this.sameImplementations(snapshot, candidate.adapterId)) {
105
+ results.push(result("waiting", "Loaded implementations match; lifecycle/event readiness is not a version mismatch.", snapshot));
106
+ continue;
107
+ }
108
+ if (!snapshot.controlCapabilities?.includes("prepare-upgrade")
109
+ || !snapshot.controlCapabilities.includes("finish-upgrade")
110
+ || !snapshot.hostImplementation?.generation || !snapshot.endpointImplementation?.generation
111
+ || !snapshot.processInstanceId) {
112
+ results.push(result("unsupported", "Old Host has no graceful-upgrade capability; retained without sending unsupported commands.", snapshot));
113
+ continue;
114
+ }
115
+ if (existing && mailboxHasWork(existing)) {
116
+ results.push(result("waiting", "Another exact lifecycle operation owns this Role.", snapshot));
117
+ continue;
118
+ }
119
+ const session = this.session(owner, candidate.agentId);
120
+ if (!session || session.nativeSessionId !== nativeSessionId)
121
+ throw new Error("Session changed before upgrade admission.");
122
+ const body = {
123
+ nativeSessionId, processInstanceId: snapshot.processInstanceId,
124
+ hostGeneration: snapshot.hostImplementation.generation, targetGeneration: this.target.generation,
125
+ agentId: candidate.agentId, adapterId: candidate.adapterId, effectiveDigest: digest(session.effective)
126
+ };
127
+ const request = { requestId: `host-upgrade-${digest([owner, body])}`, ...body };
128
+ this.store.transaction(tx => {
129
+ const latest = tx.getWorkMailbox(runtimeLifecycleTarget(owner));
130
+ if (latest && mailboxHasWork(latest))
131
+ throw new Error("Runtime lane changed before upgrade admission.");
132
+ const current = this.session(owner, candidate.agentId);
133
+ if (!current || current.nativeSessionId !== nativeSessionId || digest(current.effective) !== request.effectiveDigest) {
134
+ throw new Error("Session changed before upgrade admission.");
135
+ }
136
+ tx.saveWorkMailbox(enqueueSignal(latest ?? createWorkMailbox(runtimeLifecycleTarget(owner)), {
137
+ reason: RUNTIME_HOST_UPGRADE_REASON, refs: [{ type: "session", id: nativeSessionId }],
138
+ source: SOURCE + JSON.stringify(request), dedupeKey: request.requestId, occurredAt: new Date().toISOString()
139
+ }));
140
+ });
141
+ results.push(await this.step(owner));
142
+ }
143
+ catch (error) {
144
+ results.push(result("unknown", message(error)));
145
+ }
146
+ }
147
+ return results;
148
+ });
149
+ }
150
+ reconcile() {
151
+ return this.serial(async () => {
152
+ const results = [];
153
+ for (const mailbox of this.store.listWorkMailboxes()) {
154
+ if (this.#closed)
155
+ break;
156
+ const target = mailbox.target;
157
+ if (target.kind !== "role-runtime" && target.kind !== "global-role-runtime")
158
+ continue;
159
+ const owner = target.kind === "role-runtime"
160
+ ? { scope: "task", taskId: target.taskId, roleName: target.roleName }
161
+ : { scope: "global", roleName: target.roleName };
162
+ try {
163
+ if (!readRequest(mailbox))
164
+ continue;
165
+ results.push(await this.step(owner));
166
+ }
167
+ catch (error) {
168
+ results.push({ owner, nativeSessionId: "", status: "unknown", detail: message(error) });
169
+ }
170
+ }
171
+ return results;
172
+ });
173
+ }
174
+ async step(owner) {
175
+ let mailbox = this.store.getWorkMailbox(runtimeLifecycleTarget(owner));
176
+ const request = readRequest(mailbox);
177
+ const result = (status, detail, snapshot) => ({
178
+ owner, nativeSessionId: request.nativeSessionId, status, detail,
179
+ hostImplementation: snapshot?.hostImplementation, endpointImplementation: snapshot?.endpointImplementation
180
+ });
181
+ try {
182
+ if (runtimeCleanupDisposition(mailbox) !== "upgrade-host")
183
+ return result("waiting", "Explicit lifecycle cleanup supersedes upgrade.");
184
+ if (request.targetGeneration !== this.target.generation)
185
+ return result("waiting", "Controller generation differs from the pinned update target.");
186
+ if (!this.enabled(owner))
187
+ return result("paused", "Role/Task remains paused; original upgrade intent retained.");
188
+ const assertCurrent = () => {
189
+ const session = this.session(owner, request.agentId);
190
+ const currentMailbox = this.store.getWorkMailbox(runtimeLifecycleTarget(owner));
191
+ if (this.#closed || !session || session.status !== "active" || session.nativeSessionId !== request.nativeSessionId
192
+ || digest(session.effective) !== request.effectiveDigest || !this.enabled(owner)
193
+ || runtimeCleanupDisposition(currentMailbox) !== "upgrade-host"
194
+ || !isDeepStrictEqual(readRequest(currentMailbox), request)) {
195
+ throw new Error("Upgrade no longer owns the original Session/configuration/lifecycle lane.");
196
+ }
197
+ return session;
198
+ };
199
+ const session = assertCurrent();
200
+ let live;
201
+ const hostState = await this.ports.hostState(owner);
202
+ if (hostState === "running") {
203
+ live = await this.ports.inspect(owner);
204
+ if (this.verified(live, request.nativeSessionId, request.adapterId)) {
205
+ assertCurrent();
206
+ this.complete(owner, request);
207
+ return result("upgraded", undefined, live);
208
+ }
209
+ if (reasons(mailbox).includes(FAILED))
210
+ return result("failed", "Prior upgrade failed; exact request and drain facts retained. Inspect before retrying.", live);
211
+ if (live.nativeSessionId !== request.nativeSessionId || live.processInstanceId !== request.processInstanceId
212
+ || live.hostImplementation?.generation !== request.hostGeneration) {
213
+ return result("unknown", "Host changed without the target handshake; no process was stopped.", live);
214
+ }
215
+ if (!reasons(mailbox).includes(DRAINED)) {
216
+ assertCurrent();
217
+ live = await this.ports.control(owner, "prepare-upgrade", request);
218
+ const drain = live.hostUpgrade;
219
+ if (drain?.requestId !== request.requestId)
220
+ return result("unknown", "Drain acknowledgement is missing or mismatched.", live);
221
+ if (drain.state === "failed") {
222
+ this.signal(owner, request, FAILED);
223
+ return result("failed", drain.detail, live);
224
+ }
225
+ if (drain.state !== "drained")
226
+ return result("waiting", "Admission fenced; waiting for native terminal, event persistence and owned-resource drain.", live);
227
+ assertCurrent();
228
+ this.signal(owner, request, DRAINED);
229
+ mailbox = this.store.getWorkMailbox(runtimeLifecycleTarget(owner));
230
+ }
231
+ if (live.hostUpgrade?.requestId !== request.requestId || live.hostUpgrade.state !== "drained") {
232
+ return result("unknown", "Persisted drain receipt does not match this live Host.", live);
233
+ }
234
+ assertCurrent();
235
+ await this.ports.control(owner, "finish-upgrade", request);
236
+ return result("waiting", "Old Host drained; awaiting its graceful process exit.", live);
237
+ }
238
+ if (reasons(mailbox).includes(FAILED))
239
+ return result("failed", "Prior launch is unconfirmed; retained request is not automatically replayed.");
240
+ if (hostState !== "stopped" || !reasons(mailbox).includes(DRAINED)) {
241
+ return result("unknown", "No proven old Host drain/exit; original inputs and Session retained.");
242
+ }
243
+ assertCurrent();
244
+ // A failed or unknown launch is NOT automatically replayed. The existing
245
+ // Host/pane and the exact mailbox request remain available for inspection.
246
+ try {
247
+ await this.ports.restore(owner, request, session.effective, assertCurrent);
248
+ }
249
+ catch (error) {
250
+ this.signal(owner, request, FAILED);
251
+ return result("failed", `Target Host launch/handshake failed: ${message(error)}`);
252
+ }
253
+ live = await this.ports.inspect(owner);
254
+ if (!this.verified(live, request.nativeSessionId, request.adapterId)) {
255
+ this.signal(owner, request, FAILED);
256
+ return result("failed", "Target Host did not prove native identity, implementations and control/event capabilities.", live);
257
+ }
258
+ assertCurrent();
259
+ this.complete(owner, request);
260
+ return result("upgraded", undefined, live);
261
+ }
262
+ catch (error) {
263
+ return result("unknown", message(error));
264
+ }
265
+ }
266
+ verified(snapshot, nativeSessionId, adapterId) {
267
+ return snapshot.nativeSessionId === nativeSessionId && snapshot.adapterId === adapterId
268
+ && this.sameImplementations(snapshot, adapterId)
269
+ && snapshot.controlCapabilities?.includes("reconcile-turn")
270
+ && snapshot.eventDelivery !== undefined && snapshot.eventDelivery.unpersisted === 0
271
+ && snapshot.eventDelivery.failure === undefined && ["idle", "ready", "busy"].includes(snapshot.state);
272
+ }
273
+ sameImplementations(snapshot, adapterId) {
274
+ return isDeepStrictEqual(snapshot.hostImplementation, this.target)
275
+ && isDeepStrictEqual(snapshot.endpointImplementation, this.ports.endpoint(adapterId));
276
+ }
277
+ session(owner, agentId) {
278
+ const set = owner.scope === "task" ? this.store.getTaskRoleSessionSet(owner.taskId, owner.roleName)
279
+ : this.store.getGlobalRoleSessionSet(owner.roleName);
280
+ return set?.activeAgentId === agentId ? set.sessions[agentId] : undefined;
281
+ }
282
+ enabled(owner) {
283
+ const role = owner.scope === "task" ? this.store.getRole(owner.taskId, owner.roleName) : this.store.getGlobalRole(owner.roleName);
284
+ if (!role || !isEnabled(role))
285
+ return false;
286
+ if (owner.scope === "global")
287
+ return true;
288
+ const task = this.store.getTask(owner.taskId);
289
+ return !!task && ["active", "draft"].includes(task.status) && task.executionGate.state === "enabled";
290
+ }
291
+ signal(owner, request, reason) {
292
+ this.store.transaction(tx => {
293
+ const mailbox = tx.getWorkMailbox(runtimeLifecycleTarget(owner));
294
+ if (!mailbox || !isDeepStrictEqual(readRequest(mailbox), request))
295
+ throw new Error("Upgrade mailbox changed.");
296
+ tx.saveWorkMailbox(enqueueSignal(mailbox, { reason, refs: [], source: "host-upgrade",
297
+ dedupeKey: `${request.requestId}:${reason}`, occurredAt: new Date().toISOString() }));
298
+ });
299
+ }
300
+ complete(owner, request) {
301
+ this.store.transaction(tx => {
302
+ let mailbox = tx.getWorkMailbox(runtimeLifecycleTarget(owner));
303
+ if (!mailbox || !isDeepStrictEqual(readRequest(mailbox), request)
304
+ || runtimeCleanupDisposition(mailbox) !== "upgrade-host")
305
+ throw new Error("Upgrade mailbox changed.");
306
+ if (mailbox.processing)
307
+ mailbox = completeProcessing(mailbox, mailbox.processing.batchId);
308
+ if (mailbox.pending) {
309
+ const batchId = request.requestId;
310
+ mailbox = completeProcessing(claimPending(mailbox, {
311
+ batchId, owner: "runtime-lifecycle", startedAt: new Date().toISOString()
312
+ }), batchId);
313
+ }
314
+ tx.saveWorkMailbox(mailbox); // retain bounded dedupe receipts for repeated update
315
+ });
316
+ }
317
+ serial(run) {
318
+ const next = this.#tail.then(run);
319
+ this.#tail = next.catch(() => undefined);
320
+ return next;
321
+ }
322
+ }
323
+ function reasons(mailbox) {
324
+ return [...mailbox?.pending?.reasons ?? [], ...mailbox?.processing?.batch.reasons ?? []];
325
+ }
326
+ function readRequest(mailbox) {
327
+ if (!reasons(mailbox).includes(RUNTIME_HOST_UPGRADE_REASON))
328
+ return undefined;
329
+ const sources = [...mailbox?.pending?.sources ?? [], ...mailbox?.processing?.batch.sources ?? []]
330
+ .filter(source => source.startsWith(SOURCE));
331
+ const unique = [...new Set(sources)];
332
+ if (unique.length !== 1)
333
+ throw new Error("Host upgrade request is missing or ambiguous.");
334
+ const request = JSON.parse(unique[0].slice(SOURCE.length));
335
+ for (const key of ["requestId", "nativeSessionId", "processInstanceId", "hostGeneration",
336
+ "targetGeneration", "agentId", "adapterId", "effectiveDigest"]) {
337
+ if (typeof request[key] !== "string" || !request[key] || request[key].includes("\0")) {
338
+ throw new Error("Host upgrade request is invalid.");
339
+ }
340
+ }
341
+ return request;
342
+ }
343
+ function digest(value) { return createHash("sha256").update(JSON.stringify(value)).digest("hex"); }
344
+ function message(error) { return error instanceof Error ? error.message : String(error); }
@@ -19,7 +19,7 @@ export async function controlJobArtifact(store, home, value, now) {
19
19
  op: "run-get", scope: input.scope, caller: input.caller, id: input.id, runId: input.runId
20
20
  }, now);
21
21
  const run = current(); // Both definition and historical Assignment authorization.
22
- const repo = openJobArtifactRepository(home, jobArtifactOwner(run), runReference(run));
22
+ const repo = openJobArtifactRepository(home, jobArtifactOwner(run), runReference(run), () => store.getConfig());
23
23
  if (input.op === "artifact-list") {
24
24
  const head = await repo.head();
25
25
  const files = head === null ? [] : await repo.list(head);
@@ -45,7 +45,7 @@ export async function controlJobArtifact(store, home, value, now) {
45
45
  if (!(run.artifacts ?? []).some(item => isDeepStrictEqual(item, ref))) {
46
46
  throw new Error("Artifact reference is not registered for this exact Run.");
47
47
  }
48
- const content = await readJobArtifactReference(home, ref);
48
+ const content = await readJobArtifactReference(home, ref, () => store.getConfig());
49
49
  current(); // A replaced Session during I/O must not receive the material.
50
50
  return boundedJobArtifactPayload({ reference: ref, ...(input.op === "artifact-preview"
51
51
  ? previewArtifactContent(content, input.offset) : presentArtifactContent(content)) }, "Artifact response", MAX_JOB_ARTIFACT_RESPONSE_BYTES);
@@ -79,7 +79,7 @@ export async function controlJobArtifact(store, home, value, now) {
79
79
  }
80
80
  assertCallerAuthorized(store, input.caller, ref.owner.taskId);
81
81
  }
82
- await readJobArtifactReference(home, ref);
82
+ await readJobArtifactReference(home, ref, () => store.getConfig());
83
83
  }
84
84
  try {
85
85
  return store.transaction(tx => {
@@ -303,7 +303,7 @@ export function authorizeGlobalJobStart(store, run) {
303
303
  const definition = store.getJobDefinition({ kind: "global" }, run.definitionId);
304
304
  if (definition?.builtin)
305
305
  requireProjectSyncTarget(store, definition);
306
- const project = requireRefreshProject({ listProjects: () => projects, rootDirectory: () => home }, run.projectId, true);
306
+ const project = requireRefreshProject({ listProjects: () => projects }, run.projectId, true);
307
307
  if (projectRefreshTargetDigest(project) !== projectRefreshDigest({
308
308
  kind: "builtin", action: run.action, projectId: run.projectId, input: run.actionInput
309
309
  }))