@zq-silk/yui 0.6.0 → 0.6.2
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 +5 -5
- package/dist/agent/managedRuntimeEnvironment.js +2 -1
- package/dist/cli/commandCatalog.js +251 -13
- package/dist/cli/updateOrchestrator.js +8 -0
- package/dist/cli/updatePorts.js +76 -22
- package/dist/cli.js +264 -20
- package/dist/commands/configCommands.js +83 -9
- package/dist/commands/controllerCommands.js +103 -0
- package/dist/commands/deliveryGuardPreflight.js +30 -0
- package/dist/commands/durableJobCommands.js +231 -0
- package/dist/commands/executionAuditCommands.js +193 -0
- package/dist/commands/grantCommands.js +374 -0
- package/dist/commands/projectCommands.js +119 -81
- package/dist/commands/releaseCommands.js +444 -0
- package/dist/commands/resourcesCommands.js +274 -0
- package/dist/commands/sessionCommands.js +104 -0
- package/dist/commands/taskActor.js +117 -0
- package/dist/commands/taskChangeSetCommands.js +60 -0
- package/dist/commands/taskCommands.js +618 -202
- package/dist/commands/taskCompletionGate.js +78 -1
- package/dist/commands/taskContextCommand.js +33 -6
- package/dist/commands/taskInputCommands.js +1 -1
- package/dist/commands/taskIntegrationCommands.js +136 -33
- package/dist/commands/taskIntegrationQueueCommands.js +228 -0
- package/dist/commands/taskNextActionCommand.js +100 -0
- package/dist/commands/taskOverlapCommands.js +120 -0
- package/dist/commands/taskOverviewCommand.js +36 -8
- package/dist/commands/telemetryCommands.js +330 -0
- package/dist/commands/workflowCommands.js +415 -0
- package/dist/config/yuiConfig.js +62 -0
- package/dist/controller/clientRuntime.js +42 -1
- package/dist/controller/controller.js +402 -56
- package/dist/controller/controllerMain.js +25 -2
- package/dist/controller/domainIdentity.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
- package/dist/controller/handoverCandidate.js +168 -0
- package/dist/controller/jobClient.js +102 -0
- package/dist/controller/jobControl.js +613 -0
- package/dist/controller/jobSupervisor.js +498 -0
- package/dist/controller/providerHookRunFence.js +34 -5
- package/dist/controller/resourceCleanupLinux.js +18 -9
- package/dist/controller/resourceInventoryLinux.js +90 -39
- package/dist/controller/runtime.js +165 -15
- package/dist/controller/runtimeEventInbox.js +234 -57
- package/dist/controller/runtimeEventProcessor.js +297 -58
- package/dist/controller/sessionOwnerReconciliation.js +321 -0
- package/dist/core/controllerServer.js +416 -27
- package/dist/core/controllerTelemetry.js +167 -0
- package/dist/doctor/doctor.js +113 -16
- package/dist/domain/validation.js +9 -0
- package/dist/execution/executionGroup.js +40 -3
- package/dist/executor/agentExecutor.js +6 -3
- package/dist/executor/effectiveLaunch.js +52 -0
- package/dist/executor/executorRegistry.js +50 -0
- package/dist/executor/fileRoleLaunchPlanner.js +61 -6
- package/dist/grant/capabilityGrant.js +282 -0
- package/dist/integration/changeSet.js +16 -3
- package/dist/integration/changeSetManifest.js +46 -0
- package/dist/integration/gitIntegrationService.js +528 -147
- package/dist/integration/integrationAttempt.js +54 -5
- package/dist/integration/integrationQueueEntry.js +221 -0
- package/dist/integration/integrationQueueService.js +955 -0
- package/dist/integration/manifestTags.js +99 -0
- package/dist/integration/overlapDiagnostics.js +211 -0
- package/dist/job/durableJob.js +449 -0
- package/dist/job/jobRunner.js +350 -0
- package/dist/lifecycle/exactRunTerminalization.js +24 -2
- package/dist/lifecycle/providerErrorClass.js +126 -0
- package/dist/message/message.js +16 -3
- package/dist/observability/executionAudit.js +545 -0
- package/dist/observability/faultClassification.js +160 -0
- package/dist/observability/runtimeIdentity.js +367 -0
- package/dist/release/fakeReleasePorts.js +55 -0
- package/dist/release/releaseHandover.js +475 -0
- package/dist/release/releaseIdempotencyStore.js +165 -0
- package/dist/release/releaseWorkflow.js +459 -0
- package/dist/release/releaseWorkflowEngine.js +688 -0
- package/dist/release/releaseWorkflowPorts.js +1720 -0
- package/dist/release/runtimeRelease.js +495 -0
- package/dist/release/workflowFileLock.js +218 -0
- package/dist/repository/gitWorkspace.js +177 -1
- package/dist/repository/projectMaintenanceLock.js +315 -0
- package/dist/repository/taskWorkspaceCoordinator.js +87 -17
- package/dist/repository/taskWorkspacePreparer.js +1091 -517
- package/dist/resources/autoResourceGc.js +116 -0
- package/dist/resources/liveReferences.js +574 -0
- package/dist/resources/resourceDiscovery.js +477 -0
- package/dist/resources/resourceGc.js +645 -0
- package/dist/resources/resourceRegistrar.js +256 -0
- package/dist/resources/resourceRegistry.js +150 -0
- package/dist/resources/resourceRegistryStore.js +41 -0
- package/dist/resources/resourceTypes.js +42 -0
- package/dist/resources/sqliteResourceRegistry.js +111 -0
- package/dist/review/reviewConfig.js +10 -0
- package/dist/review/reviewFinding.js +240 -0
- package/dist/review/reviewFindingLedger.js +545 -0
- package/dist/review/reviewOutcomeClassifier.js +61 -0
- package/dist/review/reviewRound.js +56 -4
- package/dist/run/agentRun.js +80 -4
- package/dist/run/providerRetry.js +84 -0
- package/dist/run/providerRetryConfig.js +63 -0
- package/dist/run/yieldReceipt.js +65 -0
- package/dist/runtime/exactControlPlane.js +79 -2
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/sessionOwnerIdentity.js +269 -0
- package/dist/runtime/sessionOwnerRegistry.js +132 -0
- package/dist/runtime/sessionReconciliation.js +93 -0
- package/dist/runtime/sessionTerminationGuard.js +211 -0
- package/dist/runtime/taskRuntimeIsolation.js +13 -0
- package/dist/runtime/tmuxAdapters.js +34 -1
- package/dist/scheduler/actionability.js +155 -0
- package/dist/scheduler/activeRoleRunDelivery.js +14 -5
- package/dist/scheduler/activeTaskProgress.js +60 -0
- package/dist/scheduler/leaderWakeupProcessor.js +22 -11
- package/dist/scheduler/roleRunStall.js +135 -29
- package/dist/scheduler/taskExecutionProjection.js +11 -0
- package/dist/storage/compatibleTaskStore.js +112 -5
- package/dist/storage/migration/productionRegistry.js +736 -1
- package/dist/storage/sqliteSchema.js +264 -3
- package/dist/storage/sqliteStore.js +487 -13
- package/dist/storage/storeRpc.js +21 -0
- package/dist/storage/taskStore.js +974 -21
- package/dist/storage/upgrade/homeClassification.js +120 -2
- package/dist/storage/upgrade/migrationReceipt.js +67 -0
- package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
- package/dist/storage/upgrade/recordVersions.js +10 -1
- package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
- package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
- package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
- package/dist/task/deliveryGuard.js +226 -0
- package/dist/task/nextAction.js +738 -0
- package/dist/task/repairWave.js +137 -0
- package/dist/task/taskRecordReference.js +6 -1
- package/dist/telemetry/sqliteTelemetryStore.js +387 -0
- package/dist/telemetry/telemetryCompaction.js +251 -0
- package/dist/telemetry/telemetryConfig.js +64 -0
- package/dist/telemetry/telemetryRouter.js +32 -0
- package/dist/telemetry/telemetryStore.js +19 -0
- package/dist/telemetry/telemetryWiring.js +33 -0
- package/dist/tmux/tmuxManager.js +20 -1
- package/dist/tmux/tmuxSocketEndpoint.js +20 -0
- package/dist/verification/gateArtifact.js +216 -0
- package/dist/verification/gateArtifactStore.js +87 -0
- package/dist/verification/verificationGateService.js +414 -0
- package/dist/verification/verificationPlan.js +308 -0
- package/dist/workspace/gitChangeSetCapture.js +12 -2
- package/dist/workspace/workItemChangeSetManager.js +60 -3
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +8 -0
package/dist/run/agentRun.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
|
|
2
2
|
import { validateTaskRecordReference } from "../task/taskRecordReference.js";
|
|
3
|
+
import { markProviderRetryInFlight, validateAgentRunProviderRetry } from "./providerRetry.js";
|
|
4
|
+
import { validateYieldReceipt } from "./yieldReceipt.js";
|
|
3
5
|
import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
|
|
4
6
|
export function createAgentRun(id, taskId, roleName, mode, input, now, context) {
|
|
5
7
|
if (mode !== "new" && mode !== "resume") {
|
|
@@ -7,7 +9,7 @@ export function createAgentRun(id, taskId, roleName, mode, input, now, context)
|
|
|
7
9
|
}
|
|
8
10
|
const timestamp = now.toISOString();
|
|
9
11
|
return {
|
|
10
|
-
schemaVersion:
|
|
12
|
+
schemaVersion: 7,
|
|
11
13
|
id: requireSafeIdentity(id, "Agent run id"),
|
|
12
14
|
taskId: requireSafeIdentity(taskId, "Task id"),
|
|
13
15
|
roleName: requireSafeIdentity(roleName, "Role name"),
|
|
@@ -64,8 +66,8 @@ export function markAgentRunDelivered(run, now) {
|
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
export function validateAgentRun(run) {
|
|
67
|
-
if (run.schemaVersion !==
|
|
68
|
-
throw new Error("Agent run must use schemaVersion
|
|
69
|
+
if (run.schemaVersion !== 7)
|
|
70
|
+
throw new Error("Agent run must use schemaVersion 7.");
|
|
69
71
|
validateTaskRecordReference({ taskId: run.taskId, localId: run.id }, "agentRun");
|
|
70
72
|
requireSafeIdentity(run.roleName, "Role name");
|
|
71
73
|
if (run.mode !== "new" && run.mode !== "resume") {
|
|
@@ -188,10 +190,40 @@ export function validateAgentRun(run) {
|
|
|
188
190
|
if (run.summary !== undefined || run.endedAt !== undefined) {
|
|
189
191
|
throw new Error("An active Agent run cannot have terminal metadata.");
|
|
190
192
|
}
|
|
193
|
+
if (run.disposition !== undefined
|
|
194
|
+
|| run.observedActionabilityDigest !== undefined
|
|
195
|
+
|| run.waitReason !== undefined) {
|
|
196
|
+
throw new Error("An active Agent run cannot carry Leader receipt metadata.");
|
|
197
|
+
}
|
|
191
198
|
}
|
|
192
199
|
else {
|
|
193
200
|
requireText(run.summary ?? "", "Agent run summary");
|
|
194
201
|
requireTimestamp(run.endedAt ?? "", "Agent run endedAt");
|
|
202
|
+
if (run.disposition !== undefined
|
|
203
|
+
&& !["progress", "waiting", "blocked", "completed"].includes(run.disposition)) {
|
|
204
|
+
throw new Error(`Agent run disposition is invalid: ${String(run.disposition)}.`);
|
|
205
|
+
}
|
|
206
|
+
if (run.observedActionabilityDigest !== undefined) {
|
|
207
|
+
requireText(run.observedActionabilityDigest, "Agent run observedActionabilityDigest");
|
|
208
|
+
}
|
|
209
|
+
if (run.waitReason !== undefined) {
|
|
210
|
+
requireText(run.waitReason.kind, "Agent run waitReason kind");
|
|
211
|
+
if (run.waitReason.ref !== undefined) {
|
|
212
|
+
requireText(run.waitReason.ref, "Agent run waitReason ref");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (run.providerRetry !== undefined) {
|
|
217
|
+
validateAgentRunProviderRetry(run.providerRetry);
|
|
218
|
+
if (run.status !== "active") {
|
|
219
|
+
throw new Error("A terminal Agent run cannot carry a providerRetry projection.");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (run.yieldReceipt !== undefined) {
|
|
223
|
+
validateYieldReceipt(run.yieldReceipt);
|
|
224
|
+
if (run.status !== "yielded") {
|
|
225
|
+
throw new Error("An Agent run yield receipt requires a yielded Run.");
|
|
226
|
+
}
|
|
195
227
|
}
|
|
196
228
|
return run;
|
|
197
229
|
}
|
|
@@ -206,13 +238,57 @@ function finishAgentRun(run, status, summary, now) {
|
|
|
206
238
|
throw new Error(`Agent run is already terminal: ${run.id}.`);
|
|
207
239
|
}
|
|
208
240
|
const timestamp = now.toISOString();
|
|
209
|
-
|
|
241
|
+
const terminal = {
|
|
210
242
|
...run,
|
|
211
243
|
status,
|
|
212
244
|
summary,
|
|
213
245
|
updatedAt: timestamp,
|
|
214
246
|
endedAt: timestamp
|
|
215
247
|
};
|
|
248
|
+
// A terminal Run no longer waits for a retry; the projection is cleared so
|
|
249
|
+
// old and new readers agree the Run is settled.
|
|
250
|
+
if (terminal.providerRetry !== undefined) {
|
|
251
|
+
delete terminal.providerRetry;
|
|
252
|
+
}
|
|
253
|
+
return terminal;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Attaches (or advances) the Issue 04 in-place retry projection on an active
|
|
257
|
+
* Run. The Run stays `active`; only the projection changes.
|
|
258
|
+
*/
|
|
259
|
+
export function withProviderRetry(run, retry) {
|
|
260
|
+
if (run.status !== "active") {
|
|
261
|
+
throw new Error(`Cannot schedule provider retry on a terminal Agent run: ${run.id}.`);
|
|
262
|
+
}
|
|
263
|
+
return validateAgentRun({ ...run, providerRetry: retry });
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Reopens an active retry-waiting Run for another in-place attempt on its
|
|
267
|
+
* original Native Session: transport markers are reset so the existing
|
|
268
|
+
* delivery path re-pushes the exact same input, the dispatch mode becomes
|
|
269
|
+
* `resume`, and the projection is marked in-flight.
|
|
270
|
+
*/
|
|
271
|
+
export function reopenRunForProviderRetry(run, now) {
|
|
272
|
+
if (run.status !== "active" || run.providerRetry === undefined) {
|
|
273
|
+
throw new Error(`Agent run is not waiting for a provider retry: ${run.id}.`);
|
|
274
|
+
}
|
|
275
|
+
const timestamp = now.toISOString();
|
|
276
|
+
// Omit the transport markers entirely (not `undefined`, which the file
|
|
277
|
+
// store rejects) so the delivery path sees an un-pushed Run.
|
|
278
|
+
const { providerRetry, pushedAt: _pushedAt, deliveredAt: _deliveredAt, ...rest } = run;
|
|
279
|
+
return validateAgentRun({
|
|
280
|
+
...rest,
|
|
281
|
+
mode: "resume",
|
|
282
|
+
updatedAt: timestamp,
|
|
283
|
+
providerRetry: markProviderRetryInFlight(providerRetry, now)
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
/** Attaches the Issue 04 yield receipt to a yielded Run. */
|
|
287
|
+
export function withYieldReceipt(run, receipt) {
|
|
288
|
+
if (run.status !== "yielded") {
|
|
289
|
+
throw new Error(`Cannot attach a yield receipt to a non-yielded Agent run: ${run.id}.`);
|
|
290
|
+
}
|
|
291
|
+
return validateAgentRun({ ...run, yieldReceipt: receipt });
|
|
216
292
|
}
|
|
217
293
|
function requireSafeIdentity(value, label) {
|
|
218
294
|
const normalized = requireText(value, label);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
|
|
2
|
+
export const PROVIDER_RETRY_BASE_DELAY_MS = 1_000;
|
|
3
|
+
export const PROVIDER_RETRY_MAX_DELAY_MS = 60_000;
|
|
4
|
+
/**
|
|
5
|
+
* Bounded exponential backoff. The delay is uncapped in attempt count (Issue
|
|
6
|
+
* 04 retries transient failures indefinitely with backoff) but capped in
|
|
7
|
+
* interval so a hot loop can never occupy the control plane.
|
|
8
|
+
*/
|
|
9
|
+
export function nextProviderRetryDelayMs(attempt) {
|
|
10
|
+
if (!Number.isSafeInteger(attempt) || attempt < 1) {
|
|
11
|
+
throw new Error(`Provider retry attempt must be a positive integer: ${String(attempt)}.`);
|
|
12
|
+
}
|
|
13
|
+
const exponent = Math.min(attempt - 1, 10);
|
|
14
|
+
return Math.min(PROVIDER_RETRY_MAX_DELAY_MS, PROVIDER_RETRY_BASE_DELAY_MS * (2 ** exponent));
|
|
15
|
+
}
|
|
16
|
+
export function validateAgentRunProviderRetry(value) {
|
|
17
|
+
if (value.schemaVersion !== 1) {
|
|
18
|
+
throw new Error("Agent run providerRetry must use schemaVersion 1.");
|
|
19
|
+
}
|
|
20
|
+
if (!Number.isSafeInteger(value.attempt) || value.attempt < 1) {
|
|
21
|
+
throw new Error("Agent run providerRetry attempt must be a positive integer.");
|
|
22
|
+
}
|
|
23
|
+
requireTimestamp(value.firstFailureAt, "Agent run providerRetry firstFailureAt");
|
|
24
|
+
requireTimestamp(value.lastFailureAt, "Agent run providerRetry lastFailureAt");
|
|
25
|
+
if (Date.parse(value.lastFailureAt) < Date.parse(value.firstFailureAt)) {
|
|
26
|
+
throw new Error("Agent run providerRetry lastFailureAt must not precede firstFailureAt.");
|
|
27
|
+
}
|
|
28
|
+
if (value.nextAttemptAt !== undefined) {
|
|
29
|
+
requireTimestamp(value.nextAttemptAt, "Agent run providerRetry nextAttemptAt");
|
|
30
|
+
}
|
|
31
|
+
if (value.launchId !== undefined) {
|
|
32
|
+
requireIdentity(value.launchId, "Agent run providerRetry launchId");
|
|
33
|
+
}
|
|
34
|
+
if (value.nativeSessionId !== undefined) {
|
|
35
|
+
requireIdentity(value.nativeSessionId, "Agent run providerRetry nativeSessionId");
|
|
36
|
+
}
|
|
37
|
+
requireText(value.lastErrorSummary, "Agent run providerRetry lastErrorSummary");
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Records one more transient failure on a Run lineage. A fresh lineage starts
|
|
42
|
+
* at attempt 1; an existing projection increments its attempt and keeps the
|
|
43
|
+
* original `firstFailureAt`.
|
|
44
|
+
*/
|
|
45
|
+
export function scheduleProviderRetry(previous, input, now) {
|
|
46
|
+
const timestamp = now.toISOString();
|
|
47
|
+
const attempt = (previous?.attempt ?? 0) + 1;
|
|
48
|
+
const scheduleNextAttempt = input.scheduleNextAttempt ?? true;
|
|
49
|
+
return validateAgentRunProviderRetry({
|
|
50
|
+
schemaVersion: 1,
|
|
51
|
+
errorClass: input.errorClass,
|
|
52
|
+
attempt,
|
|
53
|
+
firstFailureAt: previous?.firstFailureAt ?? timestamp,
|
|
54
|
+
lastFailureAt: timestamp,
|
|
55
|
+
...(scheduleNextAttempt
|
|
56
|
+
? {
|
|
57
|
+
nextAttemptAt: new Date(now.getTime() + nextProviderRetryDelayMs(attempt)).toISOString()
|
|
58
|
+
}
|
|
59
|
+
: {}),
|
|
60
|
+
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
61
|
+
...(input.nativeSessionId === undefined ? {} : { nativeSessionId: input.nativeSessionId }),
|
|
62
|
+
lastErrorSummary: input.lastErrorSummary
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Marks the projection in-flight: the retry has been dispatched and no
|
|
67
|
+
* `nextAttemptAt` is due until the next outcome arrives.
|
|
68
|
+
*/
|
|
69
|
+
export function markProviderRetryInFlight(value, now) {
|
|
70
|
+
if (value.nextAttemptAt === undefined)
|
|
71
|
+
return value;
|
|
72
|
+
// Omit `nextAttemptAt` entirely so the projection is durably in-flight
|
|
73
|
+
// until the next outcome.
|
|
74
|
+
const { nextAttemptAt: _nextAttemptAt, ...rest } = value;
|
|
75
|
+
return validateAgentRunProviderRetry({
|
|
76
|
+
...rest,
|
|
77
|
+
lastFailureAt: now.toISOString()
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** True when the projection is due for another attempt at `now`. */
|
|
81
|
+
export function providerRetryIsDue(value, now) {
|
|
82
|
+
return value.nextAttemptAt !== undefined
|
|
83
|
+
&& Date.parse(value.nextAttemptAt) <= now.getTime();
|
|
84
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const KNOWN_ADAPTERS = ["claude", "codex"];
|
|
2
|
+
function parseAdapters(value) {
|
|
3
|
+
if (value === undefined)
|
|
4
|
+
return [];
|
|
5
|
+
const adapters = [];
|
|
6
|
+
for (const raw of value.split(",")) {
|
|
7
|
+
const token = raw.trim().toLowerCase();
|
|
8
|
+
if (token === "")
|
|
9
|
+
continue;
|
|
10
|
+
if (token === "all") {
|
|
11
|
+
for (const adapter of KNOWN_ADAPTERS) {
|
|
12
|
+
if (!adapters.includes(adapter))
|
|
13
|
+
adapters.push(adapter);
|
|
14
|
+
}
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (!KNOWN_ADAPTERS.includes(token)) {
|
|
18
|
+
throw new Error(`Unknown Provider retry adapter: ${token}.`);
|
|
19
|
+
}
|
|
20
|
+
if (!adapters.includes(token)) {
|
|
21
|
+
adapters.push(token);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return adapters;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolves the Issue 04 flags from the process environment.
|
|
28
|
+
*
|
|
29
|
+
* - `YUI_PROVIDER_RETRY_IN_PLACE` — comma-separated adapter ids (`claude`,
|
|
30
|
+
* `codex`, `all`). Unset/empty disables the feature entirely.
|
|
31
|
+
* - `YUI_PROVIDER_RETRY_MODE` — `shadow` (default when adapters are listed)
|
|
32
|
+
* records classification and "would retry" facts without changing behavior;
|
|
33
|
+
* `enforce` keeps the Run active and retries in place.
|
|
34
|
+
* - `YUI_YIELD_RECEIPT_REPLAY` — `0` disables receipt replay; default `1`.
|
|
35
|
+
*/
|
|
36
|
+
export function providerRetryConfig(environment = process.env) {
|
|
37
|
+
const adapters = parseAdapters(environment.YUI_PROVIDER_RETRY_IN_PLACE);
|
|
38
|
+
const modeValue = environment.YUI_PROVIDER_RETRY_MODE?.trim().toLowerCase();
|
|
39
|
+
let mode;
|
|
40
|
+
if (adapters.length === 0) {
|
|
41
|
+
mode = "off";
|
|
42
|
+
}
|
|
43
|
+
else if (modeValue === "enforce") {
|
|
44
|
+
mode = "enforce";
|
|
45
|
+
}
|
|
46
|
+
else if (modeValue === undefined || modeValue === "" || modeValue === "shadow") {
|
|
47
|
+
mode = "shadow";
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
throw new Error(`Unknown Provider retry mode: ${modeValue}.`);
|
|
51
|
+
}
|
|
52
|
+
const replayValue = environment.YUI_YIELD_RECEIPT_REPLAY?.trim();
|
|
53
|
+
const yieldReceiptReplay = replayValue === undefined || replayValue === "" || replayValue === "1";
|
|
54
|
+
if (!["0", "1"].includes(replayValue ?? "1")) {
|
|
55
|
+
throw new Error(`YUI_YIELD_RECEIPT_REPLAY must be 0 or 1: ${replayValue}.`);
|
|
56
|
+
}
|
|
57
|
+
return { mode, adapters, yieldReceiptReplay };
|
|
58
|
+
}
|
|
59
|
+
/** Whether the adapter has in-place retry enabled in the given mode. */
|
|
60
|
+
export function providerRetryEnabledForAdapter(config, adapterId, mode) {
|
|
61
|
+
return config.mode === mode
|
|
62
|
+
&& config.adapters.includes(adapterId);
|
|
63
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
|
|
3
|
+
const DIGEST_PREFIX_LENGTH = 16;
|
|
4
|
+
function canonicalDigestValue(value) {
|
|
5
|
+
if (value === null || typeof value !== "object")
|
|
6
|
+
return JSON.stringify(value) ?? "null";
|
|
7
|
+
if (Array.isArray(value)) {
|
|
8
|
+
return `[${value.map(canonicalDigestValue).join(",")}]`;
|
|
9
|
+
}
|
|
10
|
+
const entries = Object.entries(value)
|
|
11
|
+
.filter(([, entry]) => entry !== undefined)
|
|
12
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
|
|
13
|
+
return `{${entries
|
|
14
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalDigestValue(entry)}`)
|
|
15
|
+
.join(",")}}`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Computes the terminal outcome digest. The canonical form sorts object keys
|
|
19
|
+
* and drops `undefined` fields so the same outcome always hashes identically
|
|
20
|
+
* regardless of object construction order.
|
|
21
|
+
*/
|
|
22
|
+
export function computeYieldOutcomeDigest(input) {
|
|
23
|
+
return createHash("sha256")
|
|
24
|
+
.update(canonicalDigestValue(input))
|
|
25
|
+
.digest("hex");
|
|
26
|
+
}
|
|
27
|
+
export function formatYieldReceiptRequestId(taskId, runId, outcomeDigest) {
|
|
28
|
+
return `yield:${taskId}:${runId}:${outcomeDigest.slice(0, DIGEST_PREFIX_LENGTH)}`;
|
|
29
|
+
}
|
|
30
|
+
export function formatYieldReceiptId(taskId, runId, outcomeDigest) {
|
|
31
|
+
return `yield-receipt:${taskId}:${runId}:${outcomeDigest.slice(0, DIGEST_PREFIX_LENGTH)}`;
|
|
32
|
+
}
|
|
33
|
+
export function createYieldReceipt(taskId, runId, outcome, now) {
|
|
34
|
+
const outcomeDigest = computeYieldOutcomeDigest(outcome);
|
|
35
|
+
return validateYieldReceipt({
|
|
36
|
+
schemaVersion: 1,
|
|
37
|
+
requestId: formatYieldReceiptRequestId(taskId, runId, outcomeDigest),
|
|
38
|
+
receiptId: formatYieldReceiptId(taskId, runId, outcomeDigest),
|
|
39
|
+
outcomeDigest,
|
|
40
|
+
committedAt: now.toISOString()
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export function validateYieldReceipt(value) {
|
|
44
|
+
if (value.schemaVersion !== 1) {
|
|
45
|
+
throw new Error("Yield receipt must use schemaVersion 1.");
|
|
46
|
+
}
|
|
47
|
+
requireIdentity(value.requestId, "Yield receipt requestId");
|
|
48
|
+
requireIdentity(value.receiptId, "Yield receipt receiptId");
|
|
49
|
+
requireText(value.outcomeDigest, "Yield receipt outcomeDigest");
|
|
50
|
+
requireTimestamp(value.committedAt, "Yield receipt committedAt");
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Matches a presented outcome against a terminal Run's committed receipt.
|
|
55
|
+
* The same digest replays the receipt; a different digest fails closed.
|
|
56
|
+
*/
|
|
57
|
+
export function matchYieldReceipt(receipt, outcome) {
|
|
58
|
+
if (receipt === undefined)
|
|
59
|
+
return null;
|
|
60
|
+
const presented = computeYieldOutcomeDigest(outcome);
|
|
61
|
+
if (presented === receipt.outcomeDigest) {
|
|
62
|
+
return { kind: "replayed", receipt };
|
|
63
|
+
}
|
|
64
|
+
return { kind: "digest-mismatch", existing: receipt };
|
|
65
|
+
}
|
|
@@ -9,6 +9,7 @@ import { hasRuntimeCleanupObligation, isRuntimeLaunchReservation, runtimeLifecyc
|
|
|
9
9
|
import { nativeSessionIdForLaunch } from "./preallocatedNativeSession.js";
|
|
10
10
|
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
11
11
|
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
12
|
+
import { readActiveReleasePointer } from "../release/runtimeRelease.js";
|
|
12
13
|
export const EXACT_CONTROL_ARGUMENT = "--yui-control";
|
|
13
14
|
export const YUI_CONTROL_PLANE_DESCRIPTOR = "YUI_CONTROL_PLANE_DESCRIPTOR";
|
|
14
15
|
export const YUI_TASK_RUNTIME_DESCRIPTOR = "YUI_TASK_RUNTIME_DESCRIPTOR";
|
|
@@ -20,7 +21,11 @@ export function createExactControlPlaneDescriptor(input) {
|
|
|
20
21
|
executable: canonicalPath(input.executable),
|
|
21
22
|
cliEntry: canonicalPath(input.cliEntry),
|
|
22
23
|
yuiHome: canonicalPath(input.yuiHome),
|
|
23
|
-
identity: Object.freeze({ ...identity })
|
|
24
|
+
identity: Object.freeze({ ...identity }),
|
|
25
|
+
...(input.buildId === undefined ? {} : { buildId: input.buildId }),
|
|
26
|
+
...(input.activeReleaseDigest === undefined
|
|
27
|
+
? {}
|
|
28
|
+
: { activeReleaseDigest: input.activeReleaseDigest })
|
|
24
29
|
});
|
|
25
30
|
}
|
|
26
31
|
export function createExactTaskRuntimeDescriptor(input) {
|
|
@@ -55,7 +60,11 @@ export function parseExactControlPlaneDescriptor(value) {
|
|
|
55
60
|
executable: requireText(record.executable, "Control-plane executable"),
|
|
56
61
|
cliEntry: requireText(record.cliEntry, "Control-plane CLI entry"),
|
|
57
62
|
yuiHome: requireText(record.yuiHome, "Control-plane YUI_HOME"),
|
|
58
|
-
identity: validateVersionIdentity(record.identity)
|
|
63
|
+
identity: validateVersionIdentity(record.identity),
|
|
64
|
+
...(typeof record.buildId !== "string" ? {} : { buildId: record.buildId }),
|
|
65
|
+
...(typeof record.activeReleaseDigest !== "string"
|
|
66
|
+
? {}
|
|
67
|
+
: { activeReleaseDigest: record.activeReleaseDigest })
|
|
59
68
|
});
|
|
60
69
|
}
|
|
61
70
|
export function parseExactTaskRuntimeDescriptor(value) {
|
|
@@ -165,6 +174,31 @@ export async function assertExactControlPlanePreflight(input, options = {}) {
|
|
|
165
174
|
+ `(expected ${descriptor.identity.aggregateSchemaVersion}, found `
|
|
166
175
|
+ `${storage.currentAggregateSchemaVersion ?? "unknown"}).`);
|
|
167
176
|
}
|
|
177
|
+
// Issue 02: bind the descriptor to the active release. A descriptor that
|
|
178
|
+
// carries a build ID must match the Home's active release pointer; a
|
|
179
|
+
// descriptor without a build ID is rejected once a Home has an active
|
|
180
|
+
// release (old Sessions cannot mutate a released control plane). Homes
|
|
181
|
+
// without an active release pointer keep the legacy continuity contract.
|
|
182
|
+
const readActiveRelease = options.readActiveRelease ?? defaultReadActiveRelease;
|
|
183
|
+
const activeRelease = readActiveRelease(descriptor.yuiHome);
|
|
184
|
+
if (descriptor.buildId !== undefined) {
|
|
185
|
+
if (activeRelease === null) {
|
|
186
|
+
throw new Error("Exact control-plane descriptor names a release build but the Home has "
|
|
187
|
+
+ "no active release pointer.");
|
|
188
|
+
}
|
|
189
|
+
if (activeRelease.buildId !== descriptor.buildId) {
|
|
190
|
+
throw new Error("Exact control-plane release changed since the descriptor was frozen "
|
|
191
|
+
+ `(expected ${descriptor.buildId}, active ${activeRelease.buildId}).`);
|
|
192
|
+
}
|
|
193
|
+
if (descriptor.activeReleaseDigest !== undefined
|
|
194
|
+
&& activeRelease.packageDigest !== descriptor.activeReleaseDigest) {
|
|
195
|
+
throw new Error("Exact control-plane release digest does not match the active release pointer.");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else if (activeRelease !== null) {
|
|
199
|
+
throw new Error("Exact control-plane descriptor predates the active release; old Sessions "
|
|
200
|
+
+ "cannot mutate a released control plane. Re-launch through the current release.");
|
|
201
|
+
}
|
|
168
202
|
if (options.checkController !== false) {
|
|
169
203
|
const call = options.callController ?? defaultCallController;
|
|
170
204
|
try {
|
|
@@ -314,6 +348,36 @@ export function refreshExactTaskRuntimeDescriptorSource(source, home, store) {
|
|
|
314
348
|
writeTextFileAtomically(resolve(source), `${serializeExactDescriptor(refreshed)}\n`);
|
|
315
349
|
return refreshed;
|
|
316
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Advances a reused Hook's own exact source to the current durable Run/launch
|
|
353
|
+
* generation before the volatile fence. The stable identity (control digest,
|
|
354
|
+
* Task, Role, Agent, adapter, workspace) and the native Session are preserved;
|
|
355
|
+
* only the volatile Run/launch fields advance. A source may never jump to a
|
|
356
|
+
* replacement native Session.
|
|
357
|
+
*/
|
|
358
|
+
export function refreshReusedTaskRuntimeDescriptorSource(source, home, store, current) {
|
|
359
|
+
const resolved = readTaskRuntimeSource(source, home);
|
|
360
|
+
if (!resolved.fromFile) {
|
|
361
|
+
throw new Error("A managed Task runtime descriptor must use its stable file source.");
|
|
362
|
+
}
|
|
363
|
+
const previous = resolved.descriptor;
|
|
364
|
+
if (previous.nativeSessionId === undefined
|
|
365
|
+
|| previous.nativeSessionId !== current.nativeSessionId) {
|
|
366
|
+
throw new Error("Task runtime descriptor source cannot jump to a replacement native Session.");
|
|
367
|
+
}
|
|
368
|
+
const refreshed = createExactTaskRuntimeDescriptor({
|
|
369
|
+
...previous,
|
|
370
|
+
runId: current.runId,
|
|
371
|
+
launchId: current.launchId,
|
|
372
|
+
nativeSessionId: current.nativeSessionId
|
|
373
|
+
});
|
|
374
|
+
if (exactTaskRuntimeDescriptorPath(home, refreshed) !== resolve(source)) {
|
|
375
|
+
throw new Error("Refreshed Task runtime descriptor changed its stable identity.");
|
|
376
|
+
}
|
|
377
|
+
assertExactTaskRuntimeState(refreshed, store);
|
|
378
|
+
writeTextFileAtomically(resolve(source), `${serializeExactDescriptor(refreshed)}\n`);
|
|
379
|
+
return refreshed;
|
|
380
|
+
}
|
|
317
381
|
function readTaskRuntimeSource(source, home) {
|
|
318
382
|
const value = requireText(source, "Exact Task runtime descriptor source");
|
|
319
383
|
if (value.trimStart().startsWith("{")) {
|
|
@@ -470,3 +534,16 @@ function isRecord(value) {
|
|
|
470
534
|
function isDefinitelyNotRunning(error) {
|
|
471
535
|
return isRecord(error) && error.code === "CONTROLLER_NOT_RUNNING";
|
|
472
536
|
}
|
|
537
|
+
function defaultReadActiveRelease(home) {
|
|
538
|
+
try {
|
|
539
|
+
const pointer = readActiveReleasePointer(home);
|
|
540
|
+
return pointer === null
|
|
541
|
+
? null
|
|
542
|
+
: { buildId: pointer.buildId, packageDigest: pointer.packageDigest };
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
// A damaged pointer fails closed: treat it as an active release the
|
|
546
|
+
// descriptor cannot match, rather than silently skipping the gate.
|
|
547
|
+
return { buildId: "unknown", packageDigest: "unknown" };
|
|
548
|
+
}
|
|
549
|
+
}
|
package/dist/runtime/index.js
CHANGED
|
@@ -6,3 +6,7 @@ export { createPendingTurnCompletion, DEFAULT_RECENT_TURN_ID_LIMIT, hasRecentTur
|
|
|
6
6
|
export { RuntimeLaunchError } from "./ports.js";
|
|
7
7
|
export { TmuxPromptPushAdapter, TmuxSessionHost } from "./tmuxAdapters.js";
|
|
8
8
|
export { FileTaskRuntimeIsolation, YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR, YUI_TASK_RUNTIME_SERVICE_NAMESPACE, assertTaskRuntimeIsolationPreflight, createTaskRuntimeIsolationDescriptor, parseTaskRuntimeIsolationDescriptor, planTaskRuntimeCleanup, taskRuntimeIsolationEnvironment, taskRuntimeIsolationFingerprint } from "./taskRuntimeIsolation.js";
|
|
9
|
+
export { createSessionOwnerIdentity, discoverProviderRootByLaunchEnv, isLinuxProcessLive, listLaunchFencedProcesses, listOwnedProcessTree, readLinuxProcessIdentity } from "./sessionOwnerIdentity.js";
|
|
10
|
+
export { FileSessionOwnerRegistry } from "./sessionOwnerRegistry.js";
|
|
11
|
+
export { DEFAULT_FORCED_GRACE_MS, DEFAULT_GRACEFUL_GRACE_MS, terminateSessionOwners } from "./sessionTerminationGuard.js";
|
|
12
|
+
export { reconcileSessionOwners } from "./sessionReconciliation.js";
|