@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.
- package/README.md +6 -4
- package/dist/agent/agent.js +4 -9
- package/dist/agent/executionComponents.js +4 -4
- package/dist/agentRun/agentRun.js +8 -8
- package/dist/artifacts/managedGit.js +3 -56
- package/dist/brief/taskBrief.js +3 -3
- package/dist/cli/updateOrchestrator.js +2 -13
- package/dist/cli/updatePorts.js +36 -69
- package/dist/cli/upgradeCommand.js +4 -8
- package/dist/cli.js +4 -7
- package/dist/commands/controllerCommands.js +1 -1
- package/dist/commands/taskCommands.js +8 -1
- package/dist/commands/taskRoleRuntimeStatus.js +1 -1
- package/dist/context/runInputContract.js +1 -1
- package/dist/controller/agentRuntimeObserver.js +4 -4
- package/dist/controller/clientRuntime.js +19 -44
- package/dist/controller/controller.js +16 -4
- package/dist/controller/fileSchedulerStoreAdapter.js +6 -6
- package/dist/controller/globalInputDelivery.js +3 -1
- package/dist/controller/jobSupervisor.js +3 -3
- package/dist/controller/providerRetryDelivery.js +128 -118
- package/dist/controller/runtime.js +1 -1
- package/dist/controller/structuredProviderObservation.js +1 -1
- package/dist/coordination/workMailbox.js +4 -4
- package/dist/core/controllerIdentity.js +25 -0
- package/dist/core/controllerProcessIdentity.js +2 -2
- package/dist/core/controllerServer.js +4 -2
- package/dist/core/protocol.js +1 -1
- package/dist/domain/validation.js +6 -0
- package/dist/event/taskEvent.js +3 -3
- package/dist/execution/workItemExecution.js +2 -2
- package/dist/executor/agentExecutor.js +6 -6
- package/dist/executor/effectiveLaunch.js +3 -3
- package/dist/grant/capabilityGrant.js +2 -2
- package/dist/input/inputRequest.js +4 -4
- package/dist/integration/changeSet.js +3 -3
- package/dist/integration/integrationAttempt.js +3 -3
- package/dist/integration/integrationSourceApplication.js +1 -1
- package/dist/job/durableJob.js +1 -1
- package/dist/job/jobRunner.js +2 -2
- package/dist/message/message.js +8 -6
- package/dist/milestone/milestone.js +3 -3
- package/dist/profile/agentProfile.js +3 -3
- package/dist/release/runtimeRelease.js +9 -7
- package/dist/repository/project.js +3 -3
- package/dist/resources/liveReferences.js +1 -1
- package/dist/resources/sqliteResourceRegistry.js +2 -2
- package/dist/review/reviewRound.js +5 -5
- package/dist/role/role.js +7 -8
- package/dist/runtime/acpProtocol.js +2 -3
- package/dist/runtime/agentDriverObservation.js +1 -1
- package/dist/runtime/agentHost.js +3 -3
- package/dist/runtime/agentHostProtocol.js +2 -2
- package/dist/runtime/codexInteractiveHost.js +1 -1
- package/dist/runtime/launchBroker.js +1 -1
- package/dist/runtime/processExitObservation.js +1 -1
- package/dist/runtime/providerContinuationReconciliationService.js +84 -75
- package/dist/runtime/providerRuntimeIdentity.js +3 -3
- package/dist/runtime/runtimeCoherence.js +7 -3
- package/dist/runtime/runtimeObservation.js +3 -3
- package/dist/runtime/sessionOwnerIdentity.js +1 -1
- package/dist/runtime/taskRuntimeIsolation.js +3 -3
- package/dist/runtime/tmuxAdapters.js +3 -3
- package/dist/scheduler/taskWake.js +1 -1
- package/dist/storage/baselineSchema.js +606 -0
- package/dist/storage/homeLayout.js +5 -16
- package/dist/storage/recordValidation.js +16 -4
- package/dist/storage/sqliteSchema.js +106 -1741
- package/dist/storage/sqliteStore.js +16 -14
- package/dist/storage/storageSchema.js +8 -7
- package/dist/storage/storageVersions.js +23 -16
- package/dist/storage/taskStore.js +3 -29
- package/dist/storage/upgrade/upgradeOrchestrator.js +14 -102
- package/dist/task/task.js +15 -7
- package/dist/task/taskActivation.js +5 -4
- package/dist/telemetry/sqliteTelemetryStore.js +2 -2
- package/dist/verification/gateArtifact.js +5 -3
- package/dist/verification/verificationPlan.js +6 -7
- package/dist/workItem/workItem.js +6 -6
- package/dist/workspace/cleanupInspection.js +1 -9
- package/dist/worktree/managedWorkspace.js +3 -3
- package/docs/managed-turn-and-session-runtime.md +26 -14
- package/docs/managed-turn-and-session-runtime.zh-CN.md +18 -10
- package/docs/release-workflow.md +60 -300
- package/docs/release-workflow.zh-CN.md +46 -234
- package/docs/sqlite-control-plane-design.md +48 -289
- package/docs/sqlite-control-plane-design.zh-CN.md +37 -53
- package/docs/storage-baseline.md +139 -0
- package/docs/storage-baseline.zh-CN.md +111 -0
- package/docs/task-delivery.md +3 -4
- package/docs/task-delivery.zh-CN.md +3 -3
- package/docs/testing/verification-levels.md +40 -180
- package/docs/testing/verification-levels.zh-CN.md +27 -131
- package/i18n/README.zh-CN.md +5 -4
- package/package.json +1 -1
- package/dist/storage/migrations/agentFailureContext.js +0 -22
- package/dist/storage/migrations/agentRunContract.js +0 -159
- package/dist/storage/migrations/artifactsToGit.js +0 -338
- package/dist/storage/migrations/collapseWorktreeLayout.js +0 -963
- package/dist/storage/migrations/currentInputContract.js +0 -86
- package/dist/storage/migrations/currentRuntimeContract.js +0 -228
- package/dist/storage/migrations/historicalVerificationPlan.js +0 -35
- package/dist/storage/migrations/integrationContinuation.js +0 -105
- package/dist/storage/migrations/narrowAgentFailureContext.js +0 -65
- package/dist/storage/migrations/notificationOnlyWakes.js +0 -74
- package/dist/storage/migrations/removeRuntimeGeneration.js +0 -207
- package/dist/storage/migrations/submitIntent.js +0 -126
- package/dist/storage/migrations/unifyHomeLayout.js +0 -925
- package/dist/storage/migrations/verificationPlanV1.js +0 -162
- package/dist/storage/migrations/verificationPolicy.js +0 -74
- 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.
|
|
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.
|
|
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
|
-
|
|
99
|
+
const captured = parseControllerIdentity(identity);
|
|
99
100
|
const status = await ensureFileTaskController(home, {
|
|
100
101
|
...options,
|
|
101
|
-
|
|
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,
|
|
113
|
+
assertControllerRuntimeProcessIdentity(actual, captured);
|
|
113
114
|
return status;
|
|
114
115
|
}
|
|
115
|
-
function
|
|
116
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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(
|
|
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(
|
|
54
|
-
? error
|
|
55
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|| binding.authority.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
&&
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (
|
|
218
|
-
|
|
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"
|
|
@@ -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
|
+
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 !==
|
|
17
|
-
throw new Error("WorkMailbox must use schemaVersion
|
|
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:
|
|
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
|
|
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
|
|
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")
|