@zq-silk/yui 0.16.2 → 1.0.0-alpha.1

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 (111) hide show
  1. package/README.md +6 -4
  2. package/dist/agent/agent.js +4 -9
  3. package/dist/agent/executionComponents.js +4 -4
  4. package/dist/agentRun/agentRun.js +8 -8
  5. package/dist/artifacts/managedGit.js +3 -56
  6. package/dist/brief/taskBrief.js +3 -3
  7. package/dist/cli/updateOrchestrator.js +2 -13
  8. package/dist/cli/updatePorts.js +36 -69
  9. package/dist/cli/upgradeCommand.js +4 -8
  10. package/dist/cli.js +4 -7
  11. package/dist/commands/controllerCommands.js +1 -1
  12. package/dist/commands/taskCommands.js +8 -1
  13. package/dist/commands/taskRoleRuntimeStatus.js +1 -1
  14. package/dist/context/runInputContract.js +1 -1
  15. package/dist/controller/agentRuntimeObserver.js +4 -4
  16. package/dist/controller/clientRuntime.js +19 -44
  17. package/dist/controller/controller.js +16 -4
  18. package/dist/controller/fileSchedulerStoreAdapter.js +6 -6
  19. package/dist/controller/globalInputDelivery.js +3 -1
  20. package/dist/controller/jobSupervisor.js +3 -3
  21. package/dist/controller/providerRetryDelivery.js +128 -118
  22. package/dist/controller/runtime.js +1 -1
  23. package/dist/controller/structuredProviderObservation.js +1 -1
  24. package/dist/coordination/workMailbox.js +4 -4
  25. package/dist/core/controllerIdentity.js +25 -0
  26. package/dist/core/controllerProcessIdentity.js +2 -2
  27. package/dist/core/controllerServer.js +4 -2
  28. package/dist/core/protocol.js +1 -1
  29. package/dist/domain/validation.js +6 -0
  30. package/dist/event/taskEvent.js +3 -3
  31. package/dist/execution/workItemExecution.js +2 -2
  32. package/dist/executor/agentExecutor.js +6 -6
  33. package/dist/executor/effectiveLaunch.js +3 -3
  34. package/dist/grant/capabilityGrant.js +2 -2
  35. package/dist/input/inputRequest.js +4 -4
  36. package/dist/integration/changeSet.js +3 -3
  37. package/dist/integration/integrationAttempt.js +3 -3
  38. package/dist/integration/integrationSourceApplication.js +1 -1
  39. package/dist/job/durableJob.js +1 -1
  40. package/dist/job/jobRunner.js +2 -2
  41. package/dist/message/message.js +8 -6
  42. package/dist/milestone/milestone.js +3 -3
  43. package/dist/profile/agentProfile.js +3 -3
  44. package/dist/release/runtimeRelease.js +9 -7
  45. package/dist/repository/project.js +3 -3
  46. package/dist/resources/liveReferences.js +1 -1
  47. package/dist/resources/sqliteResourceRegistry.js +2 -2
  48. package/dist/review/reviewRound.js +5 -5
  49. package/dist/role/role.js +7 -8
  50. package/dist/runtime/acpProtocol.js +2 -3
  51. package/dist/runtime/agentDriverObservation.js +1 -1
  52. package/dist/runtime/agentHost.js +3 -3
  53. package/dist/runtime/agentHostProtocol.js +2 -2
  54. package/dist/runtime/codexInteractiveHost.js +1 -1
  55. package/dist/runtime/launchBroker.js +1 -1
  56. package/dist/runtime/processExitObservation.js +1 -1
  57. package/dist/runtime/providerContinuationReconciliationService.js +84 -75
  58. package/dist/runtime/providerRuntimeIdentity.js +3 -3
  59. package/dist/runtime/runtimeCoherence.js +7 -3
  60. package/dist/runtime/runtimeObservation.js +3 -3
  61. package/dist/runtime/sessionOwnerIdentity.js +1 -1
  62. package/dist/runtime/taskRuntimeIsolation.js +3 -3
  63. package/dist/runtime/tmuxAdapters.js +3 -3
  64. package/dist/scheduler/taskWake.js +1 -1
  65. package/dist/storage/baselineSchema.js +606 -0
  66. package/dist/storage/homeLayout.js +5 -16
  67. package/dist/storage/recordValidation.js +16 -4
  68. package/dist/storage/sqliteSchema.js +106 -1741
  69. package/dist/storage/sqliteStore.js +16 -14
  70. package/dist/storage/storageSchema.js +8 -7
  71. package/dist/storage/storageVersions.js +23 -16
  72. package/dist/storage/taskStore.js +3 -29
  73. package/dist/storage/upgrade/upgradeOrchestrator.js +14 -102
  74. package/dist/task/task.js +15 -7
  75. package/dist/task/taskActivation.js +5 -4
  76. package/dist/telemetry/sqliteTelemetryStore.js +2 -2
  77. package/dist/verification/gateArtifact.js +5 -3
  78. package/dist/verification/verificationPlan.js +6 -7
  79. package/dist/workItem/workItem.js +6 -6
  80. package/dist/workspace/cleanupInspection.js +1 -9
  81. package/dist/worktree/managedWorkspace.js +3 -3
  82. package/docs/managed-turn-and-session-runtime.md +26 -14
  83. package/docs/managed-turn-and-session-runtime.zh-CN.md +18 -10
  84. package/docs/release-workflow.md +60 -300
  85. package/docs/release-workflow.zh-CN.md +46 -234
  86. package/docs/sqlite-control-plane-design.md +48 -289
  87. package/docs/sqlite-control-plane-design.zh-CN.md +37 -53
  88. package/docs/storage-baseline.md +139 -0
  89. package/docs/storage-baseline.zh-CN.md +111 -0
  90. package/docs/task-delivery.md +3 -4
  91. package/docs/task-delivery.zh-CN.md +3 -3
  92. package/docs/testing/verification-levels.md +40 -180
  93. package/docs/testing/verification-levels.zh-CN.md +27 -131
  94. package/i18n/README.zh-CN.md +5 -4
  95. package/package.json +1 -1
  96. package/dist/storage/migrations/agentFailureContext.js +0 -22
  97. package/dist/storage/migrations/agentRunContract.js +0 -159
  98. package/dist/storage/migrations/artifactsToGit.js +0 -338
  99. package/dist/storage/migrations/collapseWorktreeLayout.js +0 -963
  100. package/dist/storage/migrations/currentInputContract.js +0 -86
  101. package/dist/storage/migrations/currentRuntimeContract.js +0 -228
  102. package/dist/storage/migrations/historicalVerificationPlan.js +0 -35
  103. package/dist/storage/migrations/integrationContinuation.js +0 -105
  104. package/dist/storage/migrations/narrowAgentFailureContext.js +0 -65
  105. package/dist/storage/migrations/notificationOnlyWakes.js +0 -74
  106. package/dist/storage/migrations/removeRuntimeGeneration.js +0 -207
  107. package/dist/storage/migrations/submitIntent.js +0 -126
  108. package/dist/storage/migrations/unifyHomeLayout.js +0 -925
  109. package/dist/storage/migrations/verificationPlanV1.js +0 -162
  110. package/dist/storage/migrations/verificationPolicy.js +0 -74
  111. package/dist/storage/migrations/workItemHistory.js +0 -46
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { findLiveControllerProcessForHome, inspectLiveControllerProcess } from "../core/controllerProcessIdentity.js";
4
4
  import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
5
+ import { parseControllerIdentity } from "../core/controllerIdentity.js";
5
6
  import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, operationalAgentEnvironment, selectEnvironment, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
6
7
  import { callController, ControllerClientError, readControllerDiscovery, stopOrphanedFileTaskController } from "../core/controllerClient.js";
7
8
  import { FILE_TASK_CONTROLLER_PROTOCOL_VERSION } from "../core/protocol.js";
@@ -53,7 +54,7 @@ export async function ensureFileTaskController(home, options = {}) {
53
54
  await waitForForeignHandover(home, options);
54
55
  try {
55
56
  const status = await call(home, "controller.status", {});
56
- assertCompatibleControllerStatus(status, options.expectedVersion);
57
+ assertCompatibleControllerStatus(status, options.expectedIdentity);
57
58
  return status;
58
59
  }
59
60
  catch (error) {
@@ -78,7 +79,7 @@ export async function ensureFileTaskController(home, options = {}) {
78
79
  for (;;) {
79
80
  try {
80
81
  const status = await call(home, "controller.status", {});
81
- assertCompatibleControllerStatus(status, options.expectedVersion);
82
+ assertCompatibleControllerStatus(status, options.expectedIdentity);
82
83
  return status;
83
84
  }
84
85
  catch (error) {
@@ -95,10 +96,10 @@ export async function ensureFileTaskController(home, options = {}) {
95
96
  }
96
97
  /** Start and await readiness for a captured Controller identity. */
97
98
  export async function ensureFileTaskControllerIdentity(home, identity, options = {}) {
98
- assertExpectedControllerRuntimeProcessIdentity(identity);
99
+ const captured = parseControllerIdentity(identity);
99
100
  const status = await ensureFileTaskController(home, {
100
101
  ...options,
101
- expectedVersion: identity.version
102
+ expectedIdentity: captured
102
103
  });
103
104
  // A same-version status is not an identity proof: another Controller binary
104
105
  // may own the Home after a restart or a stale discovery race. Authenticate
@@ -109,39 +110,24 @@ export async function ensureFileTaskControllerIdentity(home, identity, options =
109
110
  const actual = await call(home, "controller.identity", {}, {
110
111
  timeoutMs: options.requestTimeoutMs
111
112
  });
112
- assertControllerRuntimeProcessIdentity(actual, identity);
113
+ assertControllerRuntimeProcessIdentity(actual, captured);
113
114
  return status;
114
115
  }
115
- function assertExpectedControllerRuntimeProcessIdentity(identity) {
116
- if (typeof identity.executablePath !== "string"
117
- || identity.executablePath.length === 0
118
- || !Array.isArray(identity.args)
119
- || identity.args.some((arg) => typeof arg !== "string")
120
- || typeof identity.version !== "string"
121
- || identity.version.length === 0) {
122
- throw new Error("Expected Controller runtime identity is malformed; refusing to start or accept a Controller.");
123
- }
124
- }
125
- function assertControllerRuntimeProcessIdentity(actual, expected) {
126
- if (!isJsonRecord(actual)) {
127
- throw new Error("Authenticated Controller runtime identity is malformed; refusing to accept readiness.");
128
- }
116
+ function assertControllerRuntimeProcessIdentity(value, expected) {
117
+ const actual = parseControllerIdentity(value);
129
118
  const actualArgs = actual.args;
130
- if (typeof actual.executablePath !== "string"
131
- || !Array.isArray(actualArgs)
132
- || actualArgs.some((arg) => typeof arg !== "string")
133
- || typeof actual.version !== "string") {
134
- throw new Error("Authenticated Controller runtime identity is malformed; refusing to accept readiness.");
135
- }
136
119
  const argsMatch = actualArgs.length === expected.args.length
137
120
  && actualArgs.every((arg, index) => arg === expected.args[index]);
138
121
  if (actual.executablePath !== expected.executablePath
139
122
  || !argsMatch
140
- || actual.version !== expected.version) {
141
- throw new Error("Authenticated Controller runtime identity does not match the captured executable, argv, and version; refusing readiness.");
123
+ || actual.version !== expected.version
124
+ || actual.controllerProtocolVersion !== expected.controllerProtocolVersion
125
+ || actual.storageVersion !== expected.storageVersion
126
+ || actual.minimumStorageVersion !== expected.minimumStorageVersion) {
127
+ throw new Error("Authenticated Controller runtime identity does not match the captured executable, argv, package, protocol and storage versions; refusing readiness.");
142
128
  }
143
129
  }
144
- function assertCompatibleControllerStatus(status, expectedVersion) {
130
+ function assertCompatibleControllerStatus(status, expectedIdentity) {
145
131
  const statusRecord = isJsonRecord(status) && status.running === true ? status : null;
146
132
  const actual = statusRecord?.protocolVersion;
147
133
  if (statusRecord === null || actual !== FILE_TASK_CONTROLLER_PROTOCOL_VERSION) {
@@ -149,22 +135,11 @@ function assertCompatibleControllerStatus(status, expectedVersion) {
149
135
  + "Run `yui controller restart` before writing new task records.");
150
136
  }
151
137
  const actualVersion = statusRecord.version;
152
- if (expectedVersion !== undefined && actualVersion !== expectedVersion) {
153
- throw new Error(`Controller version is incompatible (expected ${expectedVersion}, found ${typeof actualVersion === "string" ? actualVersion : "unknown"}). `
154
- + "Run `yui controller restart` before writing new task records.");
155
- }
156
- // Ordinary callers must authenticate the complete control-plane identity.
157
- // An update/upgrade restore instead targets a previously captured Controller
158
- // binary, which may predate the current storage-layout status fields; that
159
- // path authenticates its executable, argv, and version immediately after
160
- // readiness in ensureFileTaskControllerIdentity.
161
- if (expectedVersion === undefined) {
162
- const identity = yuiVersionIdentity();
163
- assertControllerStatusIdentity(status, {
164
- ...identity,
165
- version: typeof actualVersion === "string" ? actualVersion : identity.version
166
- });
167
- }
138
+ const current = yuiVersionIdentity();
139
+ assertControllerStatusIdentity(status, expectedIdentity === undefined ? {
140
+ ...current,
141
+ version: typeof actualVersion === "string" ? actualVersion : current.version
142
+ } : parseControllerIdentity(expectedIdentity));
168
143
  }
169
144
  function spawnDetachedFileTaskController(_home, environment) {
170
145
  const child = spawn(process.execPath, [fileURLToPath(new URL("./controllerMain.js", import.meta.url))], {
@@ -948,6 +948,18 @@ export class FileTaskController {
948
948
  #requestBackgroundPump() {
949
949
  void this.pump().catch(this.#onError);
950
950
  }
951
+ /** Optional maintenance is not execution admission. Keep its failure visible
952
+ * without bypassing the scheduler's own ownership and delivery fences. */
953
+ async #observeMaintenance(phase, action) {
954
+ try {
955
+ return await action();
956
+ }
957
+ catch (error) {
958
+ this.#onError(new Error(`${phase} failed: ${error instanceof Error ? error.message : String(error)}. `
959
+ + "No successful cleanup or quiescence is inferred; inspect the original failure.", { cause: error }));
960
+ return undefined;
961
+ }
962
+ }
951
963
  async #runCoalesced() {
952
964
  let result = emptyControllerSchedulerResult();
953
965
  let pendingRuntimeDrain = false;
@@ -966,15 +978,15 @@ export class FileTaskController {
966
978
  if (scope.kind === "full")
967
979
  this.reloadReconciliationInterval();
968
980
  if (scope.kind === "full" && this.#resourceReaper !== undefined) {
969
- const reap = await this.#resourceReaper();
970
- for (const failure of reap.failed) {
981
+ const reap = await this.#observeMaintenance("Ephemeral runtime reap", this.#resourceReaper);
982
+ for (const failure of reap?.failed ?? []) {
971
983
  this.#onError(new Error(`Ephemeral runtime reap failed for ${failure.id}: ${failure.message}`));
972
984
  }
973
985
  // The reaper owns the exact resource fences. Once a whole expired
974
986
  // domain converges, let the detached Controller close itself on a
975
987
  // later turn; never close while this reconciliation pass is still
976
988
  // the in-flight server request.
977
- if (reap.failed.length === 0 && (reap.expiredDomains?.length ?? 0) > 0) {
989
+ if (reap !== undefined && reap.failed.length === 0 && (reap.expiredDomains?.length ?? 0) > 0) {
978
990
  for (const domain of reap.expiredDomains ?? []) {
979
991
  queueMicrotask(() => {
980
992
  this.#onExpiredEphemeralDomain?.(domain);
@@ -992,7 +1004,7 @@ export class FileTaskController {
992
1004
  // full pass. It queries only identities already present in Task
993
1005
  // facts and cannot start a model AgentRun or scan unknown children.
994
1006
  if (scope.kind === "full" && this.#continuationReconciler !== undefined) {
995
- await this.#continuationReconciler.reconcile(this.#now());
1007
+ await this.#observeMaintenance("Continuation reconciliation", () => this.#continuationReconciler.reconcile(this.#now()));
996
1008
  }
997
1009
  // DurableJob reconciliation runs before the scheduler pass so a
998
1010
  // terminal job's Leader wakeup is enqueued in the same pass that
@@ -635,7 +635,7 @@ export class FileSchedulerStoreAdapter {
635
635
  const key = providerContinuationKey(continuation.identity);
636
636
  const identityDigest = createHash("sha256").update(key).digest("hex");
637
637
  const detached = createRuntimeObservation({
638
- schemaVersion: 4,
638
+ schemaVersion: 1,
639
639
  eventId: `derived-continuation-detached:${identityDigest}`,
640
640
  semanticKey: `continuation-detached:${identityDigest}`,
641
641
  kind: "continuation.started",
@@ -724,7 +724,7 @@ export class FileSchedulerStoreAdapter {
724
724
  .update(`${input.semanticKey}\u0000${identityKey}`)
725
725
  .digest("hex");
726
726
  const settled = createRuntimeObservation({
727
- schemaVersion: 4,
727
+ schemaVersion: 1,
728
728
  eventId: `native-snapshot-settled:${digest}`,
729
729
  semanticKey: `native-snapshot-settled:${digest}`,
730
730
  kind: "continuation.settled",
@@ -2271,7 +2271,7 @@ export class FileSchedulerStoreAdapter {
2271
2271
  && event.payload.observationId === observationId)))
2272
2272
  return;
2273
2273
  const observation = validateRuntimeProcessExitObservation({
2274
- schemaVersion: 2,
2274
+ schemaVersion: 1,
2275
2275
  observationId,
2276
2276
  hostSequence: 1,
2277
2277
  hostInstanceId: `tmux-${input.roleName}`,
@@ -2720,7 +2720,7 @@ export class FileSchedulerStoreAdapter {
2720
2720
  // this Global Role, settle it to the observed terminal so the scope-generic
2721
2721
  // resolver and the interrupt-then gate see the real stop on the live binding
2722
2722
  // — the same edge Task settles via settleStructuredProviderTurn. This is a
2723
- // strict guarded no-op for an older unmanaged Session or a
2723
+ // strict guarded no-op for a Session without a controlled binding or a
2724
2724
  // binding holding a different/already-terminal Turn, and the
2725
2725
  // durable native terminal recorded just above remains the proof of record.
2726
2726
  current = settleGlobalRoleRuntimeTurn(current, input.nativeTurnId, input.providerStatus, now);
@@ -3483,12 +3483,12 @@ function settleStructuredProviderTurn(sessions, runId, status, now, attemptId) {
3483
3483
  * It is a strict guarded no-op unless a controller-owned Turn with that exact
3484
3484
  * native id is bound and still in flight (`accepted`) — an absent binding, a
3485
3485
  * different/only-submitting Turn, or an already-terminal one is left untouched, so
3486
- * an older unmanaged Session without a controlled binding settles nothing
3486
+ * a Session without a controlled binding settles nothing
3487
3487
  * and the durable `recentCompletedTurnIds` terminal remains the proof of record.
3488
3488
  */
3489
3489
  function settleGlobalRoleRuntimeTurn(sessions, nativeTurnId, status, now) {
3490
3490
  const binding = sessions.providerBinding;
3491
- if (binding === null || binding === undefined || binding.run === null
3491
+ if (binding === null || binding.run === null
3492
3492
  || binding.run.nativeTurnId !== nativeTurnId
3493
3493
  || binding.run.status !== "accepted") {
3494
3494
  return sessions;
@@ -147,7 +147,9 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
147
147
  }
148
148
  }
149
149
  catch (error) {
150
- onError(error);
150
+ onError(new Error(`Global input delivery failed for ${role.name}: `
151
+ + `${error instanceof Error ? error.message : String(error)}. `
152
+ + "Inspect the persisted Message and attempt before retrying; delivery is not inferred.", { cause: error }));
151
153
  }
152
154
  }
153
155
  }
@@ -50,9 +50,9 @@ export class DurableJobSupervisor {
50
50
  this.#reconcileJob(job, now);
51
51
  }
52
52
  catch (error) {
53
- this.#onError(error instanceof Error
54
- ? error
55
- : new Error(`DurableJob supervision failed for ${job.id}: ${String(error)}`));
53
+ this.#onError(new Error(`DurableJob supervision failed for ${job.taskId}/${job.id}: `
54
+ + `${error instanceof Error ? error.message : String(error)}. `
55
+ + "Inspect retained process/artifact evidence before retry or cancellation; effects remain unconfirmed.", { cause: error }));
56
56
  }
57
57
  }
58
58
  }
@@ -90,134 +90,144 @@ export function createProviderRetryHooks(home, store, ports = {}) {
90
90
  inspectedUnknown.delete(id);
91
91
  for (const set of current) {
92
92
  const owner = set.owner;
93
- const binding = set.providerBinding;
94
- const retry = binding.retry;
95
- const at = now(retry);
96
- const scoped = { ...(owner.scope === "task" ? { taskId: owner.taskId } : {}), roleName: owner.roleName };
97
- const session = set.sessions[set.activeAgentId];
98
- const blocker = retryIntentBlocker(store, scoped, retry);
99
- if (blocker !== undefined && blocker !== "background-work-unsettled") {
100
- stop(set, blocker, at);
101
- continue;
102
- }
103
- if (binding.retryDisabled && retry.successorAutomatic !== false || binding.authority.owner !== "controller"
104
- || binding.authority.epoch !== retry.authorityEpoch
105
- || currentProviderConversation(binding).conversationId !== retry.nativeSessionId
106
- || session?.status !== "active" || session.nativeSessionId !== retry.nativeSessionId
107
- || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget(owner)))) {
108
- stop(set, "session-or-authority-changed", at);
109
- continue;
110
- }
111
- // Never replay an admitted attempt. Existing exact runtime queries and
112
- // terminal ingress resolve it, even across Controller restart.
113
- if (retry.status === "in-flight") {
114
- if (binding.run?.status === "delivery-unknown" && retry.currentAttemptId !== undefined
115
- && !inspectedUnknown.has(retry.currentAttemptId)) {
116
- inspectedUnknown.add(retry.currentAttemptId);
117
- let detail;
118
- try {
119
- const host = await inspect({ home, ...scoped, scope: owner.scope });
120
- detail = host.nativeSessionId === retry.nativeSessionId && host.attemptId === retry.currentAttemptId
121
- ? `Exact Host query: ${host.state}; awaiting original canonical Provider evidence.`
122
- : "Host query did not prove the original Session/input identity; no replay.";
123
- }
124
- catch (error) {
125
- detail = `Original Host query unavailable; no replay: ${redactAgentErrorText(error instanceof Error ? error.message : String(error))}`;
126
- }
127
- store.transaction(() => {
128
- const latest = load(owner);
129
- if (latest === null || latest.providerBinding == null || latest.providerBinding.run === null
130
- || latest.providerBinding.run.attemptId !== retry.currentAttemptId
131
- || latest.providerBinding.run.status !== "delivery-unknown")
132
- return;
133
- save({ ...latest, providerBinding: { ...latest.providerBinding,
134
- retry: { ...latest.providerBinding.retry, reason: detail } } });
135
- if (owner.scope === "global") {
136
- recordGlobalRuntimeAttention(store, owner.roleName, `${retry.currentAttemptId}:unconfirmed`, providerRetryProjection(load(owner)?.providerBinding), new Date(at));
93
+ try {
94
+ const binding = set.providerBinding;
95
+ const retry = binding.retry;
96
+ const at = now(retry);
97
+ const scoped = { ...(owner.scope === "task" ? { taskId: owner.taskId } : {}), roleName: owner.roleName };
98
+ const session = set.sessions[set.activeAgentId];
99
+ const blocker = retryIntentBlocker(store, scoped, retry);
100
+ if (blocker !== undefined && blocker !== "background-work-unsettled") {
101
+ stop(set, blocker, at);
102
+ continue;
103
+ }
104
+ if (binding.retryDisabled && retry.successorAutomatic !== false || binding.authority.owner !== "controller"
105
+ || binding.authority.epoch !== retry.authorityEpoch
106
+ || currentProviderConversation(binding).conversationId !== retry.nativeSessionId
107
+ || session?.status !== "active" || session.nativeSessionId !== retry.nativeSessionId
108
+ || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget(owner)))) {
109
+ stop(set, "session-or-authority-changed", at);
110
+ continue;
111
+ }
112
+ // Never replay an admitted attempt. Existing exact runtime queries and
113
+ // terminal ingress resolve it, even across Controller restart.
114
+ if (retry.status === "in-flight") {
115
+ if (binding.run?.status === "delivery-unknown" && retry.currentAttemptId !== undefined
116
+ && !inspectedUnknown.has(retry.currentAttemptId)) {
117
+ inspectedUnknown.add(retry.currentAttemptId);
118
+ let detail;
119
+ try {
120
+ const host = await inspect({ home, ...scoped, scope: owner.scope });
121
+ detail = host.nativeSessionId === retry.nativeSessionId && host.attemptId === retry.currentAttemptId
122
+ ? `Exact Host query: ${host.state}; awaiting original canonical Provider evidence.`
123
+ : "Host query did not prove the original Session/input identity; no replay.";
137
124
  }
138
- if (owner.scope === "task") {
139
- const event = createTaskEvent(store.nextEventId(owner.taskId), owner.taskId, "provider.retry-unconfirmed", {
140
- roleName: owner.roleName, attemptId: retry.currentAttemptId, nativeSessionId: retry.nativeSessionId, detail
141
- }, new Date(at));
142
- store.saveEvent(owner.taskId, event);
143
- enqueueWork(store, owner.roleName === "leader" ? { kind: "operator" }
144
- : { kind: "role", taskId: owner.taskId, roleName: "leader" }, "provider-retry-needs-attention", new Date(at), [{ type: "event", taskId: owner.taskId, id: event.id }]);
125
+ catch (error) {
126
+ detail = `Original Host query unavailable; no replay: ${redactAgentErrorText(error instanceof Error ? error.message : String(error))}`;
145
127
  }
146
- });
147
- }
148
- continue;
149
- }
150
- if (retry.successorAutomatic !== false && at >= Date.parse(retry.deadline)) {
151
- stop(set, "deadline-exhausted", at, true);
152
- continue;
153
- }
154
- if (at < Date.parse(retry.nextEligibleAt) || blocker !== undefined || binding.goal?.status === "active")
155
- continue;
156
- if (binding.run?.attemptId !== retry.failedAttemptId) {
157
- stop(set, "superseded-input", at);
158
- continue;
159
- }
160
- try {
161
- if (owner.scope === "task" && retry.previousRunId !== undefined) {
162
- if (retry.successorRunId !== undefined || retry.successorReviewRoundId !== undefined)
163
- continue;
164
- const previous = store.getRun(owner.taskId, retry.previousRunId);
165
- if (previous?.status === "active")
166
- continue; // ordinary rejected-delivery terminal fold
167
- if (previous?.status !== "failed") {
168
- stop(set, "run-no-longer-failed", at);
169
- continue;
170
- }
171
- const actualTaskReviewCandidate = await ports.snapshotTaskCandidate?.(previous);
172
- store.transaction(() => {
173
- const latest = load(owner)?.providerBinding?.retry;
174
- if (latest?.chainId !== retry.chainId || latest.failedAttemptId !== retry.failedAttemptId
175
- || latest.status !== "waiting")
176
- return;
177
- const reason = retryIntentBlocker(store, scoped, latest);
178
- if (reason !== undefined)
179
- throw new Error(reason);
180
- runTaskCommand(["run", "retry", `${owner.taskId}/${previous.id}`], store, {
181
- now: () => new Date(at), environment: {}, providerRetryChainId: retry.chainId,
182
- ...(actualTaskReviewCandidate === undefined ? {} : { actualTaskReviewCandidate })
128
+ store.transaction(() => {
129
+ const latest = load(owner);
130
+ if (latest === null || latest.providerBinding == null || latest.providerBinding.run === null
131
+ || latest.providerBinding.run.attemptId !== retry.currentAttemptId
132
+ || latest.providerBinding.run.status !== "delivery-unknown")
133
+ return;
134
+ save({ ...latest, providerBinding: { ...latest.providerBinding,
135
+ retry: { ...latest.providerBinding.retry, reason: detail } } });
136
+ if (owner.scope === "global") {
137
+ recordGlobalRuntimeAttention(store, owner.roleName, `${retry.currentAttemptId}:unconfirmed`, providerRetryProjection(load(owner)?.providerBinding), new Date(at));
138
+ }
139
+ if (owner.scope === "task") {
140
+ const event = createTaskEvent(store.nextEventId(owner.taskId), owner.taskId, "provider.retry-unconfirmed", {
141
+ roleName: owner.roleName, attemptId: retry.currentAttemptId, nativeSessionId: retry.nativeSessionId, detail
142
+ }, new Date(at));
143
+ store.saveEvent(owner.taskId, event);
144
+ enqueueWork(store, owner.roleName === "leader" ? { kind: "operator" }
145
+ : { kind: "role", taskId: owner.taskId, roleName: "leader" }, "provider-retry-needs-attention", new Date(at), [{ type: "event", taskId: owner.taskId, id: event.id }]);
146
+ }
183
147
  });
184
- });
148
+ }
149
+ continue;
150
+ }
151
+ if (retry.successorAutomatic !== false && at >= Date.parse(retry.deadline)) {
152
+ stop(set, "deadline-exhausted", at, true);
153
+ continue;
185
154
  }
186
- else {
187
- const original = retry.input.kind === "text" ? retry.input.text
188
- : retry.input.kind === "wake"
189
- ? `Yui Task notification: task=${retry.input.taskId} wake=${retry.input.wakeId}.\nRead current Task context and that fixed wake window, including original Messages in full. This is not an execution assignment.`
190
- : retry.input.kind === "message"
191
- ? `Yui Global Message: role=${retry.input.roleName} message=${retry.input.messageId}.\nRead your Session Context and referenced original Message in full.`
192
- : undefined;
193
- if (original === undefined) {
194
- stop(set, "original-input-unavailable", at);
195
- continue;
155
+ if (at < Date.parse(retry.nextEligibleAt) || blocker !== undefined || binding.goal?.status === "active")
156
+ continue;
157
+ if (binding.run?.attemptId !== retry.failedAttemptId) {
158
+ stop(set, "superseded-input", at);
159
+ continue;
160
+ }
161
+ try {
162
+ if (owner.scope === "task" && retry.previousRunId !== undefined) {
163
+ if (retry.successorRunId !== undefined || retry.successorReviewRoundId !== undefined)
164
+ continue;
165
+ const previous = store.getRun(owner.taskId, retry.previousRunId);
166
+ if (previous?.status === "active")
167
+ continue; // ordinary rejected-delivery terminal fold
168
+ if (previous?.status !== "failed") {
169
+ stop(set, "run-no-longer-failed", at);
170
+ continue;
171
+ }
172
+ const actualTaskReviewCandidate = await ports.snapshotTaskCandidate?.(previous);
173
+ store.transaction(() => {
174
+ const latest = load(owner)?.providerBinding?.retry;
175
+ if (latest?.chainId !== retry.chainId || latest.failedAttemptId !== retry.failedAttemptId
176
+ || latest.status !== "waiting")
177
+ return;
178
+ const reason = retryIntentBlocker(store, scoped, latest);
179
+ if (reason !== undefined)
180
+ throw new Error(reason);
181
+ runTaskCommand(["run", "retry", `${owner.taskId}/${previous.id}`], store, {
182
+ now: () => new Date(at), environment: {}, providerRetryChainId: retry.chainId,
183
+ ...(actualTaskReviewCandidate === undefined ? {} : { actualTaskReviewCandidate })
184
+ });
185
+ });
196
186
  }
197
- const result = await (ports.submit ?? sendAgentHostRunControl)({
198
- home, ...scoped, scope: owner.scope,
199
- control: {
200
- protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "submit-turn", nativeSessionId: retry.nativeSessionId,
201
- authority: { epoch: binding.authority.epoch, owner: "controller", holderId: binding.authority.holderId },
202
- run: { attemptId: providerRetryAttemptId(retry), boundedText: original }
187
+ else {
188
+ const original = retry.input.kind === "text" ? retry.input.text
189
+ : retry.input.kind === "wake"
190
+ ? `Yui Task notification: task=${retry.input.taskId} wake=${retry.input.wakeId}.\nRead current Task context and that fixed wake window, including original Messages in full. This is not an execution assignment.`
191
+ : retry.input.kind === "message"
192
+ ? `Yui Global Message: role=${retry.input.roleName} message=${retry.input.messageId}.\nRead your Session Context and referenced original Message in full.`
193
+ : undefined;
194
+ if (original === undefined) {
195
+ stop(set, "original-input-unavailable", at);
196
+ continue;
197
+ }
198
+ const result = await (ports.submit ?? sendAgentHostRunControl)({
199
+ home, ...scoped, scope: owner.scope,
200
+ control: {
201
+ protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "submit-turn", nativeSessionId: retry.nativeSessionId,
202
+ authority: { epoch: binding.authority.epoch, owner: "controller", holderId: binding.authority.holderId },
203
+ run: { attemptId: providerRetryAttemptId(retry), boundedText: original }
204
+ }
205
+ });
206
+ // Durable settlement belongs to Host/Controller ingress. In
207
+ // particular a lost response is never a reason to replay.
208
+ if (result.outcome === "rejected" && result.failure?.registrationDisposition === "not-committed"
209
+ && result.failure.errorName !== "ProviderTurnBusyError") {
210
+ stop(set, result.failure.detail, at);
203
211
  }
204
- });
205
- // Durable settlement belongs to Host/Controller ingress. In
206
- // particular a lost response is never a reason to replay.
207
- if (result.outcome === "rejected" && result.failure?.registrationDisposition === "not-committed"
208
- && result.failure.errorName !== "ProviderTurnBusyError") {
209
- stop(set, result.failure.detail, at);
210
212
  }
211
213
  }
214
+ catch (error) {
215
+ // A send failure may have applied. A durable submitting/unknown
216
+ // identity fences it; do not manufacture a successor to uncertainty.
217
+ const latest = load(owner);
218
+ if (latest?.providerBinding?.run?.attemptId === retry.failedAttemptId) {
219
+ stop(set, error instanceof Error ? error.message : String(error), at);
220
+ }
221
+ throw error;
222
+ }
212
223
  }
213
224
  catch (error) {
214
- // A send failure may have applied. A durable submitting/unknown
215
- // identity fences it; do not manufacture a successor to uncertainty.
216
- const latest = load(owner);
217
- if (latest?.providerBinding?.run?.attemptId === retry.failedAttemptId) {
218
- stop(set, error instanceof Error ? error.message : String(error), at);
219
- }
220
- ports.onError?.(error);
225
+ const failure = new Error(`Provider retry failed for ${owner.scope === "task" ? owner.taskId : "global"}/${owner.roleName}: `
226
+ + `${error instanceof Error ? error.message : String(error)}. `
227
+ + "Inspect the current attempt; failed observation does not authorize replay.", { cause: error });
228
+ if (ports.onError === undefined)
229
+ throw failure;
230
+ ports.onError(failure);
221
231
  }
222
232
  }
223
233
  }
@@ -431,7 +431,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
431
431
  const jobControl = kernel.jobs;
432
432
  const continuationReconciler = options.continuationMetadata === undefined
433
433
  ? undefined
434
- : new ProviderContinuationReconciliationService(store, schedulerStore, options.continuationMetadata);
434
+ : new ProviderContinuationReconciliationService(store, schedulerStore, options.continuationMetadata, options.onError);
435
435
  const running = await startFileTaskController(home, schedulerStore, delivery, (method, params) => method === "task.role-capabilities"
436
436
  ? readTaskAgentCapabilities(store, catalogs, params)
437
437
  : method === "web.start" || method === "web.stop" || method === "web.status"
@@ -381,7 +381,7 @@ function observation(input) {
381
381
  payload: input.payload ?? {}
382
382
  };
383
383
  return createRuntimeObservation({
384
- schemaVersion: 4,
384
+ schemaVersion: 1,
385
385
  eventId,
386
386
  semanticKey: runtimeObservationSemanticKey(partial),
387
387
  kind: input.kind,
@@ -2,7 +2,7 @@ import { validateTaskRecordReference } from "../task/taskRecordReference.js";
2
2
  const RECENT_DEDUPE_KEY_LIMIT = 256;
3
3
  export function createWorkMailbox(target) {
4
4
  return {
5
- schemaVersion: 5,
5
+ schemaVersion: 1,
6
6
  target: copyTarget(target),
7
7
  nextSequence: 1,
8
8
  processing: null,
@@ -13,8 +13,8 @@ export function createWorkMailbox(target) {
13
13
  export function validateWorkMailbox(value) {
14
14
  const mailbox = record(value, "WorkMailbox");
15
15
  exact(mailbox, ["schemaVersion", "target", "nextSequence", "processing", "pending", "recentDedupeKeys"], "WorkMailbox");
16
- if (mailbox.schemaVersion !== 5)
17
- throw new Error("WorkMailbox must use schemaVersion 5");
16
+ if (mailbox.schemaVersion !== 1)
17
+ throw new Error("WorkMailbox must use schemaVersion 1");
18
18
  const target = parseTarget(mailbox.target);
19
19
  const nextSequence = requireInteger(mailbox.nextSequence, 1, "WorkMailbox nextSequence");
20
20
  const processing = mailbox.processing === null ? null : parseProcessing(mailbox.processing);
@@ -38,7 +38,7 @@ export function validateWorkMailbox(value) {
38
38
  }
39
39
  }
40
40
  return {
41
- schemaVersion: 5,
41
+ schemaVersion: 1,
42
42
  target,
43
43
  nextSequence,
44
44
  processing,
@@ -0,0 +1,25 @@
1
+ import { compareStorageVersions, isStorageVersion } from "../storage/storageVersions.js";
2
+ export function isControllerIdentity(value) {
3
+ if (typeof value !== "object" || value === null || Array.isArray(value))
4
+ return false;
5
+ const record = value;
6
+ return typeof record.executablePath === "string" && record.executablePath.length > 0
7
+ && Array.isArray(record.args) && record.args.every(arg => typeof arg === "string")
8
+ && typeof record.version === "string" && record.version.length > 0
9
+ && Number.isSafeInteger(record.controllerProtocolVersion) && record.controllerProtocolVersion > 0
10
+ && isStorageVersion(record.storageVersion) && isStorageVersion(record.minimumStorageVersion)
11
+ && compareStorageVersions(record.minimumStorageVersion, record.storageVersion) <= 0;
12
+ }
13
+ export function parseControllerIdentity(value) {
14
+ if (!isControllerIdentity(value)) {
15
+ throw new Error("Controller identity must include the exact executable, argv, package, protocol and storage versions.");
16
+ }
17
+ return {
18
+ executablePath: value.executablePath,
19
+ args: [...value.args],
20
+ version: value.version,
21
+ controllerProtocolVersion: value.controllerProtocolVersion,
22
+ storageVersion: value.storageVersion,
23
+ minimumStorageVersion: value.minimumStorageVersion
24
+ };
25
+ }
@@ -1,7 +1,7 @@
1
1
  import { readFileSync, readdirSync } from "node:fs";
2
2
  import { readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
3
3
  import { readHomeFilesystemId, validateHomeFilesystemId } from "./homeFilesystemIdentity.js";
4
- /** Exact live-process fence used only at Controller startup and v3 handoff. */
4
+ /** Exact live-process fence used at Controller startup and handover. */
5
5
  export function inspectLiveControllerProcess(pid, homeFilesystemId, expectedProcessStartIdentity) {
6
6
  if (!Number.isSafeInteger(pid) || pid < 1 || pid === process.pid)
7
7
  return undefined;
@@ -39,7 +39,7 @@ export function inspectLiveControllerProcess(pid, homeFilesystemId, expectedProc
39
39
  return undefined;
40
40
  }
41
41
  }
42
- /** Finds an older/current Controller whose discovery record was lost. */
42
+ /** Finds a Controller whose discovery record was lost. */
43
43
  export function findLiveControllerProcessForHome(homeFilesystemId) {
44
44
  validateHomeFilesystemId(homeFilesystemId);
45
45
  if (process.platform !== "linux")