@zq-silk/yui 0.8.3 → 0.8.7
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/ARCHITECTURE.md +40 -22
- package/README.md +66 -19
- package/dist/cli/commandCatalog.js +43 -14
- package/dist/cli/operatorWizard.js +10 -20
- package/dist/cli/updatePorts.js +6 -0
- package/dist/cli.js +252 -37
- package/dist/commands/executionAuditCommands.js +30 -0
- package/dist/commands/globalRoleCommands.js +8 -4
- package/dist/commands/operatorCommands.js +42 -1
- package/dist/commands/taskCommands.js +527 -147
- package/dist/commands/taskCompletionGate.js +36 -24
- package/dist/commands/taskContextCommand.js +11 -4
- package/dist/commands/taskInputCommands.js +48 -10
- package/dist/commands/taskNextActionCommand.js +38 -3
- package/dist/commands/taskOverviewCommand.js +2 -1
- package/dist/commands/taskRoleRuntimeStatus.js +2 -1
- package/dist/context/runContextPack.js +9 -5
- package/dist/context/sessionBootstrapManifest.js +158 -11
- package/dist/context/wakeNotification.js +5 -3
- package/dist/controller/clientRuntime.js +15 -15
- package/dist/controller/controller.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +67 -7
- package/dist/controller/handoverCandidate.js +10 -3
- package/dist/controller/sessionNotify.js +4 -22
- package/dist/executor/agentAdapter.js +2 -2
- package/dist/executor/agentExecutor.js +29 -9
- package/dist/executor/fileRoleLaunchPlanner.js +37 -45
- package/dist/integration/gitIntegrationService.js +50 -2
- package/dist/integration/integrationCheckEvidenceReuse.js +53 -0
- package/dist/observability/executionAudit.js +47 -1
- package/dist/observability/faultClassification.js +6 -4
- package/dist/observability/orchestrationMetrics.js +196 -0
- package/dist/operator/operatorSessionHistory.js +36 -0
- package/dist/release/releaseHandover.js +7 -5
- package/dist/release/runtimeRelease.js +15 -0
- package/dist/repository/taskWorkspaceCoordinator.js +13 -10
- package/dist/review/deltaRecheck.js +3 -2
- package/dist/review/reviewFindingLedger.js +5 -4
- package/dist/review/reviewOutcomeClassifier.js +263 -54
- package/dist/review/taskFinalReviewContractEvent.js +1 -0
- package/dist/review/taskFinalReviewContractRebind.js +367 -0
- package/dist/run/runIdentity.js +10 -70
- package/dist/runtime/agentHost.js +3 -4
- package/dist/runtime/codexAppServerRuntime.js +6 -0
- package/dist/runtime/exactControlPlane.js +47 -37
- package/dist/runtime/firstProgressStopLoss.js +54 -0
- package/dist/runtime/launchBroker.js +10 -2
- package/dist/runtime/runtimeDeadlines.js +14 -0
- package/dist/runtime/sessionTitle.js +24 -12
- package/dist/runtime/structuredProviderHost.js +7 -1
- package/dist/runtime/tmuxAdapters.js +10 -3
- package/dist/scheduler/actionability.js +4 -2
- package/dist/scheduler/activeRoleRunDelivery.js +20 -18
- package/dist/scheduler/activeTaskProgress.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +33 -2
- package/dist/scheduler/taskExecutionProjection.js +13 -4
- package/dist/scheduler/wakeReason.js +1 -0
- package/dist/storage/sqliteStore.js +18 -3
- package/dist/storage/taskStore.js +14 -3
- package/dist/task/completionReadiness.js +48 -19
- package/dist/task/deliveryGuard.js +3 -1
- package/dist/task/nextAction.js +153 -55
- package/dist/task/repairWave.js +14 -1
- package/dist/task/task.js +10 -0
- package/dist/task/taskRecordRetirement.js +72 -0
- package/dist/web/webSnapshot.js +7 -1
- package/dist/workItem/workItem.js +6 -4
- package/i18n/README.zh-CN.md +48 -9
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +73 -31
- package/skills/yui-operator/SKILL.md +58 -10
- package/skills/yui-reviewer/SKILL.md +23 -0
- package/skills/yui-runtime/SKILL.md +6 -6
|
@@ -7,15 +7,12 @@ import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
|
|
|
7
7
|
import { hasRuntimeLifecycleWork } from "../runtime/lifecycleReservation.js";
|
|
8
8
|
import { assertControllerStatusIdentity } from "../runtime/exactControlPlane.js";
|
|
9
9
|
import { EPHEMERAL_DOMAIN_ENVIRONMENT_NAMES } from "./domainIdentity.js";
|
|
10
|
-
import {
|
|
10
|
+
import { yuiVersionIdentity } from "../version.js";
|
|
11
11
|
import { SessionOwnerReconciliation } from "./sessionOwnerReconciliation.js";
|
|
12
12
|
import { WorkspaceCleanupBlockedError } from "../repository/taskWorkspacePreparer.js";
|
|
13
|
+
import { CONTROLLER_SHUTDOWN_TIMEOUT_MS, LIFECYCLE_REQUEST_TIMEOUT_MS } from "../runtime/runtimeDeadlines.js";
|
|
13
14
|
const STARTUP_TIMEOUT_MS = 5_000;
|
|
14
|
-
// A lifecycle RPC may legitimately occupy the Controller for 30 seconds.
|
|
15
|
-
// Restart must allow that request to drain before deciding shutdown is stuck.
|
|
16
|
-
const SHUTDOWN_TIMEOUT_MS = 45_000;
|
|
17
15
|
const POLL_INTERVAL_MS = 50;
|
|
18
|
-
const LIFECYCLE_REQUEST_TIMEOUT_MS = 30_000;
|
|
19
16
|
const ENVIRONMENT_REFRESH_TIMEOUT_MS = 500;
|
|
20
17
|
const CONFIGURATION_REFRESH_TIMEOUT_MS = 500;
|
|
21
18
|
const CONTROLLER_OPERATIONAL_ENVIRONMENT = [
|
|
@@ -140,12 +137,8 @@ function assertCompatibleControllerStatus(status, expectedVersion) {
|
|
|
140
137
|
+ "Run `yui controller restart` before writing new task records.");
|
|
141
138
|
}
|
|
142
139
|
const actualVersion = statusRecord.version;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
? typeof actualVersion === "string" && actualVersion !== expected
|
|
146
|
-
: actualVersion !== expected;
|
|
147
|
-
if (versionMismatch) {
|
|
148
|
-
throw new Error(`Controller version is incompatible (expected ${expected}, found ${typeof actualVersion === "string" ? actualVersion : "unknown"}). `
|
|
140
|
+
if (expectedVersion !== undefined && actualVersion !== expectedVersion) {
|
|
141
|
+
throw new Error(`Controller version is incompatible (expected ${expectedVersion}, found ${typeof actualVersion === "string" ? actualVersion : "unknown"}). `
|
|
149
142
|
+ "Run `yui controller restart` before writing new task records.");
|
|
150
143
|
}
|
|
151
144
|
// Ordinary callers must authenticate the complete control-plane identity.
|
|
@@ -154,7 +147,11 @@ function assertCompatibleControllerStatus(status, expectedVersion) {
|
|
|
154
147
|
// path authenticates its executable, argv, and version immediately after
|
|
155
148
|
// readiness in ensureFileTaskControllerIdentity.
|
|
156
149
|
if (expectedVersion === undefined) {
|
|
157
|
-
|
|
150
|
+
const identity = yuiVersionIdentity();
|
|
151
|
+
assertControllerStatusIdentity(status, {
|
|
152
|
+
...identity,
|
|
153
|
+
version: typeof actualVersion === "string" ? actualVersion : identity.version
|
|
154
|
+
});
|
|
158
155
|
}
|
|
159
156
|
}
|
|
160
157
|
function spawnDetachedFileTaskController(home, environment) {
|
|
@@ -168,7 +165,7 @@ function spawnDetachedFileTaskController(home, environment) {
|
|
|
168
165
|
/** Stops the per-home Controller and waits until its owned discovery is gone. */
|
|
169
166
|
export async function stopFileTaskController(home, options = {}) {
|
|
170
167
|
const call = options.call ?? callController;
|
|
171
|
-
const shutdownTimeoutMs = positive(options.shutdownTimeoutMs,
|
|
168
|
+
const shutdownTimeoutMs = positive(options.shutdownTimeoutMs, CONTROLLER_SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs");
|
|
172
169
|
const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
|
|
173
170
|
const expectedPid = options.expectedPid === undefined
|
|
174
171
|
? undefined
|
|
@@ -224,7 +221,7 @@ export async function stopFileTaskController(home, options = {}) {
|
|
|
224
221
|
/** Restarts only the per-home Controller process; managed tmux sessions remain untouched. */
|
|
225
222
|
export async function restartFileTaskController(home, options = {}) {
|
|
226
223
|
const call = options.call ?? callController;
|
|
227
|
-
const shutdownTimeoutMs = positive(options.shutdownTimeoutMs,
|
|
224
|
+
const shutdownTimeoutMs = positive(options.shutdownTimeoutMs, CONTROLLER_SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs");
|
|
228
225
|
const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
|
|
229
226
|
let current = null;
|
|
230
227
|
let previousPid;
|
|
@@ -529,7 +526,10 @@ export class FileTaskWorkflowRuntime {
|
|
|
529
526
|
&& this.workspacePreparer !== undefined) {
|
|
530
527
|
await this.workspacePreparer.prepareTaskWorkspace(taskId);
|
|
531
528
|
}
|
|
532
|
-
await callFileTaskController(this.home, "scheduler.scan", {},
|
|
529
|
+
await callFileTaskController(this.home, "scheduler.scan", {}, {
|
|
530
|
+
...this.clientOptions,
|
|
531
|
+
requestTimeoutMs: LIFECYCLE_REQUEST_TIMEOUT_MS
|
|
532
|
+
});
|
|
533
533
|
}
|
|
534
534
|
}
|
|
535
535
|
const MANAGED_RUNTIME_ENVIRONMENT = new Set(YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES);
|
|
@@ -10,6 +10,7 @@ import { processOperatorInputNotifications } from "../scheduler/operatorInputNot
|
|
|
10
10
|
import { startControllerServer } from "../core/controllerServer.js";
|
|
11
11
|
import { monotonicMilliseconds } from "../core/controllerTelemetry.js";
|
|
12
12
|
import { isProjectMaintenanceFenced } from "../repository/projectMaintenanceLock.js";
|
|
13
|
+
import { isHandoverLockHeld } from "../release/runtimeRelease.js";
|
|
13
14
|
import { KeyedWorkQueue } from "../coordination/keyedWorkQueue.js";
|
|
14
15
|
import { MailboxScheduler } from "../coordination/mailboxScheduler.js";
|
|
15
16
|
import { nearestDeadlineBatch } from "../coordination/deadlineScheduler.js";
|
|
@@ -45,7 +46,7 @@ const ZERO_DRAIN_METRICS = Object.freeze({
|
|
|
45
46
|
* Runs one lean scheduler pass. Due native Turn completions are folded before
|
|
46
47
|
* liveness, so a valid Hook boundary fences destructive process reconciliation.
|
|
47
48
|
*/
|
|
48
|
-
export async function runControllerSchedulerPass(store, delivery, now, workspacePreparer, scope = { kind: "full" }, includeOperator = true, runtimeCleanupOutcomes = [], lifecycleHost, stallWindowMs = DEFAULT_STALL_WINDOW_MS, maintenanceFence, onMaintenanceFenceDefer, blockedTaskIds = new Set(), inputDeliveryRecoveryCutoff, diagnosticAfterMs = DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS) {
|
|
49
|
+
export async function runControllerSchedulerPass(store, delivery, now, workspacePreparer, scope = { kind: "full" }, includeOperator = true, runtimeCleanupOutcomes = [], lifecycleHost, stallWindowMs = DEFAULT_STALL_WINDOW_MS, maintenanceFence, onMaintenanceFenceDefer, blockedTaskIds = new Set(), inputDeliveryRecoveryCutoff, diagnosticAfterMs = DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS, leaderWakeFence) {
|
|
49
50
|
const compiledSelection = compileReconcileSelection(scope);
|
|
50
51
|
const selection = includeOperator
|
|
51
52
|
? { ...compiledSelection, blockedTaskIds }
|
|
@@ -58,7 +59,9 @@ export async function runControllerSchedulerPass(store, delivery, now, workspace
|
|
|
58
59
|
await controlEventLoopTurn();
|
|
59
60
|
const failedCleanupRoles = await processSelectedRoleRuntimeCleanups(store, delivery, lifecycleHost, scope, now, runtimeCleanupOutcomes, blockedTaskIds);
|
|
60
61
|
const roleSelection = selectionWithoutFailedCleanupRoles(store, selection, failedCleanupRoles);
|
|
61
|
-
const
|
|
62
|
+
const availableWakeupSelection = () => (leaderWakeFence?.() === true
|
|
63
|
+
? exactTaskSelection(new Set())
|
|
64
|
+
: selectionWithoutFailedLeaderCleanupTasks(store, selection, failedCleanupRoles));
|
|
62
65
|
if (selection.full)
|
|
63
66
|
repairOrphanedActiveTasks(store, now, selection);
|
|
64
67
|
const claimedTaskMailboxes = claimSelectedTaskMailboxes(store, selection, now);
|
|
@@ -66,7 +69,7 @@ export async function runControllerSchedulerPass(store, delivery, now, workspace
|
|
|
66
69
|
// Durable Leader work that already has a ready Task workspace belongs to
|
|
67
70
|
// the control path. Dispatch it before any unrelated Task workspace I/O;
|
|
68
71
|
// the processor itself retains the fail-closed workspace-ready guard.
|
|
69
|
-
const initialWakeups = selectedPendingWakeups(store,
|
|
72
|
+
const initialWakeups = selectedPendingWakeups(store, availableWakeupSelection());
|
|
70
73
|
const initialWakeupResults = await processLeaderWakeups(store, delivery, now, exactTaskSelection(new Set(initialWakeups.keys())));
|
|
71
74
|
// Preserve ready-Leader-first ordering, then bound the repeated state
|
|
72
75
|
// projections that follow it in this pass.
|
|
@@ -134,7 +137,8 @@ export async function runControllerSchedulerPass(store, delivery, now, workspace
|
|
|
134
137
|
&& workspacePreparation.ready.has(result.taskId)
|
|
135
138
|
? [result.taskId]
|
|
136
139
|
: [])));
|
|
137
|
-
const laterWakeupTaskIds = new Set([...selectedPendingWakeups(store,
|
|
140
|
+
const laterWakeupTaskIds = new Set([...selectedPendingWakeups(store, availableWakeupSelection())]
|
|
141
|
+
.flatMap(([taskId, wakeup]) => {
|
|
138
142
|
const initial = initialWakeups.get(taskId);
|
|
139
143
|
return initial === undefined
|
|
140
144
|
|| !pendingWakeupsMatch(initial, wakeup)
|
|
@@ -823,6 +827,7 @@ export class FileTaskController {
|
|
|
823
827
|
#onExpiredEphemeralDomain;
|
|
824
828
|
#maintenanceFence;
|
|
825
829
|
#onMaintenanceFenceDefer;
|
|
830
|
+
#leaderWakeFence;
|
|
826
831
|
#jobSupervisor;
|
|
827
832
|
#continuationReconciler;
|
|
828
833
|
#current;
|
|
@@ -867,6 +872,7 @@ export class FileTaskController {
|
|
|
867
872
|
this.#onExpiredEphemeralDomain = options.onExpiredEphemeralDomain;
|
|
868
873
|
this.#maintenanceFence = options.maintenanceFence;
|
|
869
874
|
this.#onMaintenanceFenceDefer = options.onMaintenanceFenceDefer;
|
|
875
|
+
this.#leaderWakeFence = options.leaderWakeFence;
|
|
870
876
|
this.#jobSupervisor = options.jobSupervisor;
|
|
871
877
|
this.#continuationReconciler = options.continuationReconciler;
|
|
872
878
|
this.#signalScheduler = new MailboxScheduler(async (keys) => { await this.#requestPass({ kind: "dirty", keys }); }, {
|
|
@@ -1118,7 +1124,7 @@ export class FileTaskController {
|
|
|
1118
1124
|
// processes Leader wakeups.
|
|
1119
1125
|
this.#jobSupervisor?.reconcile(this.#now());
|
|
1120
1126
|
if (scope.kind === "full") {
|
|
1121
|
-
result = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, scope, false, runtimeCleanupOutcomes, this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, runtimeFailedTaskIds, this.#startedAt, this.#diagnosticAfterMs);
|
|
1127
|
+
result = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, scope, false, runtimeCleanupOutcomes, this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, runtimeFailedTaskIds, this.#startedAt, this.#diagnosticAfterMs, this.#leaderWakeFence);
|
|
1122
1128
|
}
|
|
1123
1129
|
else {
|
|
1124
1130
|
const dirtyPass = await this.#runDirtySchedulerPass(scope, runtimeCleanupOutcomes, runtimeFailedTaskIds);
|
|
@@ -1216,7 +1222,7 @@ export class FileTaskController {
|
|
|
1216
1222
|
const taskScopes = partition.taskScopes.filter((taskScope) => (!blockedTaskIds.has(taskScope.taskId)));
|
|
1217
1223
|
const orderedResults = [];
|
|
1218
1224
|
if (partition.globalKeys.length > 0) {
|
|
1219
|
-
orderedResults.push(await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, { kind: "dirty", keys: partition.globalKeys }, false, runtimeCleanupOutcomes, this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, blockedTaskIds, this.#startedAt, this.#diagnosticAfterMs));
|
|
1225
|
+
orderedResults.push(await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, { kind: "dirty", keys: partition.globalKeys }, false, runtimeCleanupOutcomes, this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, blockedTaskIds, this.#startedAt, this.#diagnosticAfterMs, this.#leaderWakeFence));
|
|
1220
1226
|
}
|
|
1221
1227
|
if (taskScopes.length === 0
|
|
1222
1228
|
|| this.#stopped
|
|
@@ -1249,7 +1255,7 @@ export class FileTaskController {
|
|
|
1249
1255
|
if (this.#stopped || this.#pendingFull)
|
|
1250
1256
|
continue;
|
|
1251
1257
|
try {
|
|
1252
|
-
taskResults[selected.index] = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, { kind: "dirty", keys: selected.taskScope.keys }, false, taskCleanupOutcomes[selected.index], this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, blockedTaskIds, this.#startedAt, this.#diagnosticAfterMs);
|
|
1258
|
+
taskResults[selected.index] = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, { kind: "dirty", keys: selected.taskScope.keys }, false, taskCleanupOutcomes[selected.index], this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, blockedTaskIds, this.#startedAt, this.#diagnosticAfterMs, this.#leaderWakeFence);
|
|
1253
1259
|
this.#clearTaskPassRetry(selected.taskScope.taskId);
|
|
1254
1260
|
}
|
|
1255
1261
|
catch (error) {
|
|
@@ -1676,7 +1682,9 @@ export async function startFileTaskController(home, store, delivery, dispatcher,
|
|
|
1676
1682
|
const runtime = new FileTaskController(store, delivery, {
|
|
1677
1683
|
...options,
|
|
1678
1684
|
maintenanceFence: options.maintenanceFence
|
|
1679
|
-
?? ((projectId) => isProjectMaintenanceFenced(home, projectId))
|
|
1685
|
+
?? ((projectId) => isProjectMaintenanceFenced(home, projectId)),
|
|
1686
|
+
leaderWakeFence: options.leaderWakeFence
|
|
1687
|
+
?? (() => isHandoverLockHeld(home))
|
|
1680
1688
|
});
|
|
1681
1689
|
let stopping = false;
|
|
1682
1690
|
const lifecycleRequests = new Set();
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
-
import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindTaskRoleRun, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, prepareTaskRoleRunRedispatch, recordRoleAgentSession, recordTaskRoleTurnBoundary, rememberRoleAgentCompletedTurn, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
|
|
3
|
+
import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindTaskRoleRun, clearTaskRoleProviderRuntimeForCleanup, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, prepareTaskRoleRunRedispatch, recordRoleAgentSession, recordTaskRoleTurnBoundary, rememberRoleAgentCompletedTurn, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
|
|
4
4
|
import { acceptProviderTurn, beginProviderTurn, createProviderRuntimeBinding, endProviderActivation, currentProviderActivation, currentProviderConversation, markProviderTurnDeliveryUnknown, rejectProviderTurn, settleProviderTurn, startProviderActivation, supersedeProviderConversation, updateProviderConversationRecoverability } from "../runtime/providerRuntimeIdentity.js";
|
|
5
5
|
import { decideProviderRecovery } from "../runtime/providerRecoveryDecision.js";
|
|
6
|
+
import { boundProviderRetryBeforeFirstProgress, projectFirstProgressStopLoss } from "../runtime/firstProgressStopLoss.js";
|
|
6
7
|
import { hasRecentTurnId } from "../executor/turnCompletion.js";
|
|
7
8
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
9
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
8
10
|
import { buildTaskWakeEnvelope } from "../context/wakeNotification.js";
|
|
9
11
|
import { createTaskWake, fallbackWakeCursor, latestTaskWake } from "../scheduler/taskWake.js";
|
|
10
12
|
import { rolloverTaskRoleSessionForContextBudget } from "../lifecycle/contextBudgetRollover.js";
|
|
@@ -595,7 +597,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
595
597
|
const latest = latestTaskWake(reader.listTaskWakes(taskId));
|
|
596
598
|
const fromCursor = latest?.toCursor ?? fallbackWakeCursor({
|
|
597
599
|
taskCreatedAt: task.createdAt,
|
|
598
|
-
leaderRunCreatedAt: reader.listAgentRuns(taskId)
|
|
600
|
+
leaderRunCreatedAt: operationalTaskRecords(reader.listAgentRuns(taskId), reader.listEvents(taskId), "agent-run")
|
|
599
601
|
.filter((run) => run.roleName === "leader")
|
|
600
602
|
.at(-1)?.createdAt
|
|
601
603
|
});
|
|
@@ -674,6 +676,9 @@ export class FileSchedulerStoreAdapter {
|
|
|
674
676
|
: sessions?.sessions[agentId];
|
|
675
677
|
return session === undefined ? null : mapSession(session);
|
|
676
678
|
}
|
|
679
|
+
getTaskRoleSessionSet(taskId, roleName) {
|
|
680
|
+
return this.store.getTaskRoleSessionSet(taskId, roleName);
|
|
681
|
+
}
|
|
677
682
|
listEvents(taskId) {
|
|
678
683
|
return this.#taskReadProjection(taskId).events;
|
|
679
684
|
}
|
|
@@ -1222,7 +1227,14 @@ export class FileSchedulerStoreAdapter {
|
|
|
1222
1227
|
startedAt: now.toISOString()
|
|
1223
1228
|
}), batchId);
|
|
1224
1229
|
}
|
|
1225
|
-
|
|
1230
|
+
const owner = runtimeOwnerFromTarget(target);
|
|
1231
|
+
markRuntimeOwnerSessionStopped(store, owner, now);
|
|
1232
|
+
if (owner.scope === "task") {
|
|
1233
|
+
const sessions = store.getTaskRoleSessionSet(owner.taskId, owner.roleName);
|
|
1234
|
+
if (sessions !== null && sessions.providerBinding !== null) {
|
|
1235
|
+
store.saveTaskRoleSessionSet(clearTaskRoleProviderRuntimeForCleanup(sessions, now));
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1226
1238
|
saveRuntimeLifecycleMailbox(store, mailbox);
|
|
1227
1239
|
return true;
|
|
1228
1240
|
});
|
|
@@ -1288,6 +1300,40 @@ export class FileSchedulerStoreAdapter {
|
|
|
1288
1300
|
clearPendingWakeup(taskId) { this.store.clearPendingWakeup(taskId); }
|
|
1289
1301
|
getLeaderFailure(taskId) { return this.store.getLeaderFailure(taskId); }
|
|
1290
1302
|
getOperatorNotification(taskId) { return this.store.getOperatorNotification(taskId); }
|
|
1303
|
+
saveLeaderFirstProgressStopLoss(input) {
|
|
1304
|
+
return this.store.transaction((store) => {
|
|
1305
|
+
const task = store.getTask(input.taskId);
|
|
1306
|
+
const role = store.getRole(input.taskId, input.roleName);
|
|
1307
|
+
if (task === null || task.status !== "active" || role === null
|
|
1308
|
+
|| store.getLeaderFailure(input.taskId) !== null) {
|
|
1309
|
+
return "state-changed";
|
|
1310
|
+
}
|
|
1311
|
+
const stopLoss = projectFirstProgressStopLoss({
|
|
1312
|
+
sessions: store.getTaskRoleSessionSet(input.taskId, input.roleName),
|
|
1313
|
+
events: store.listEvents(input.taskId),
|
|
1314
|
+
workItems: store.listWorkItems(input.taskId),
|
|
1315
|
+
reviewRounds: store.listReviewRounds(input.taskId),
|
|
1316
|
+
integrations: store.listIntegrationAttempts(input.taskId)
|
|
1317
|
+
});
|
|
1318
|
+
if (!stopLoss.exhausted || stopLoss.fingerprint !== input.expectedFingerprint) {
|
|
1319
|
+
return "state-changed";
|
|
1320
|
+
}
|
|
1321
|
+
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
1322
|
+
const lastSession = sessions === null
|
|
1323
|
+
? undefined
|
|
1324
|
+
: [...(sessions.history ?? []), ...Object.values(sessions.sessions)]
|
|
1325
|
+
.sort((left, right) => left.createdAt.localeCompare(right.createdAt))
|
|
1326
|
+
.at(-1);
|
|
1327
|
+
const message = `Leader first-progress stop-loss: ${stopLoss.reason}`;
|
|
1328
|
+
store.saveRole(input.taskId, updateRoleStatus(role, "failed", input.now));
|
|
1329
|
+
store.saveLeaderFailure(recordLeaderFailure(input.taskId, lastSession?.nativeSessionId ?? "(unregistered)", message, input.now, null));
|
|
1330
|
+
store.saveOperatorNotification(createLeaderRecoveryNotification(input.taskId, message, input.now, store.getOperatorNotification(input.taskId)));
|
|
1331
|
+
enqueueWork(store, { kind: "operator" }, "leader-first-progress-stop-loss", input.now, [
|
|
1332
|
+
{ type: "task", id: input.taskId }
|
|
1333
|
+
]);
|
|
1334
|
+
return "recorded";
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1291
1337
|
saveLeaderDispatch(input) {
|
|
1292
1338
|
return this.store.transaction((store) => {
|
|
1293
1339
|
const task = store.getTask(input.task.id);
|
|
@@ -2241,7 +2287,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
2241
2287
|
failedNativeTurnId: input.nativeTurnId,
|
|
2242
2288
|
lastErrorSummary: summary,
|
|
2243
2289
|
...(input.retryAfterMs === undefined ? {} : { retryAfterMs: input.retryAfterMs })
|
|
2244
|
-
}, now, configuredProviderRetryPolicy(store));
|
|
2290
|
+
}, now, configuredProviderRetryPolicy(store, input.taskId, input.roleName));
|
|
2245
2291
|
if (retryDecision.outcome === "exhausted") {
|
|
2246
2292
|
this.recordProviderRetryClassified(store, input, classification.errorClass, {
|
|
2247
2293
|
wouldRetry: "false",
|
|
@@ -2253,7 +2299,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
2253
2299
|
if (run.providerRetry === undefined)
|
|
2254
2300
|
return null;
|
|
2255
2301
|
const finalized = finalizeProviderRetryDeadline(store, run, retryDecision.reason === "attempts"
|
|
2256
|
-
? `Provider retry failed after all ${
|
|
2302
|
+
? `Provider retry failed after all ${run.providerRetry.maxRetries} in-Session continuation attempts.`
|
|
2257
2303
|
: retryDecision.reason === "retry-after-window"
|
|
2258
2304
|
? `Provider Retry-After exceeded the bounded ${config.maxWindowMs / 1_000}-second episode window.`
|
|
2259
2305
|
: `Provider retry did not recover within the bounded ${config.maxWindowMs / 1_000}-second episode window.`, retryDecision.reason === "attempts" ? "attempts-exhausted" : "episode-window-exhausted", now);
|
|
@@ -3616,6 +3662,7 @@ function mapSession(session) {
|
|
|
3616
3662
|
adapterId: session.adapterId,
|
|
3617
3663
|
nativeSessionId: session.nativeSessionId,
|
|
3618
3664
|
...(session.launchId === undefined ? {} : { launchId: session.launchId }),
|
|
3665
|
+
...(session.title === undefined ? {} : { title: session.title }),
|
|
3619
3666
|
status: session.status,
|
|
3620
3667
|
effective: session.effective,
|
|
3621
3668
|
updatedAt: session.updatedAt
|
|
@@ -3629,6 +3676,7 @@ function saveTaskSession(store, role, session, status, now, launchId) {
|
|
|
3629
3676
|
adapterId: session.adapterId,
|
|
3630
3677
|
nativeSessionId: session.nativeSessionId,
|
|
3631
3678
|
...(launchId === undefined ? {} : { launchId }),
|
|
3679
|
+
...(session.title === undefined ? {} : { title: session.title }),
|
|
3632
3680
|
policy: "fixed",
|
|
3633
3681
|
status,
|
|
3634
3682
|
effective: session.effective
|
|
@@ -3659,7 +3707,7 @@ function bindTaskRoleRunInFlight(store, role, run, now) {
|
|
|
3659
3707
|
agentId,
|
|
3660
3708
|
runId: run.id,
|
|
3661
3709
|
receiptId: agentRunDeliveryReceiptId(run)
|
|
3662
|
-
}, now);
|
|
3710
|
+
}, now, run.mode);
|
|
3663
3711
|
store.saveRoleSessionSet(updated);
|
|
3664
3712
|
}
|
|
3665
3713
|
function markTaskRoleRunPushedInFlight(store, role, run, now) {
|
|
@@ -4021,7 +4069,19 @@ function compareCanonicalObservationOrder(left, right) {
|
|
|
4021
4069
|
|| (left.ordinal ?? -1) - (right.ordinal ?? -1)
|
|
4022
4070
|
|| left.eventId.localeCompare(right.eventId);
|
|
4023
4071
|
}
|
|
4024
|
-
function configuredProviderRetryPolicy(store) {
|
|
4072
|
+
function configuredProviderRetryPolicy(store, taskId, roleName) {
|
|
4025
4073
|
const config = providerRetryConfig(store.getConfig());
|
|
4074
|
+
if (taskId !== undefined && roleName === "leader") {
|
|
4075
|
+
const progress = projectFirstProgressStopLoss({
|
|
4076
|
+
sessions: store.getTaskRoleSessionSet(taskId, roleName),
|
|
4077
|
+
events: store.listEvents(taskId),
|
|
4078
|
+
workItems: store.listWorkItems(taskId),
|
|
4079
|
+
reviewRounds: store.listReviewRounds(taskId),
|
|
4080
|
+
integrations: store.listIntegrationAttempts(taskId)
|
|
4081
|
+
});
|
|
4082
|
+
if (progress.firstProgressAt === undefined) {
|
|
4083
|
+
return boundProviderRetryBeforeFirstProgress(config, progress);
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4026
4086
|
return { delaysMs: config.delaysMs, maxWindowMs: config.maxWindowMs };
|
|
4027
4087
|
}
|
|
@@ -19,6 +19,7 @@ import { resolveTaskStoreBackendForHome } from "../storage/sqliteStore.js";
|
|
|
19
19
|
import { detectRunningRelease, isOwnerLive, readActiveReleasePointer, readHandoverFence, removeCandidateDiscovery, removeHandoverFence, writeCandidateDiscovery, writeHandoverFence, writeHandoverReceipt, writeRuntimeIdentity } from "../release/runtimeRelease.js";
|
|
20
20
|
import { startFileTaskControllerRuntime } from "./runtime.js";
|
|
21
21
|
import { readLinuxProcessStartIdentity } from "./domainIdentity.js";
|
|
22
|
+
import { RELEASE_HANDOVER_OLD_OWNER_GRACE_MS } from "../runtime/runtimeDeadlines.js";
|
|
22
23
|
export const CONTROLLER_CANDIDATE_ENV = "YUI_CONTROLLER_CANDIDATE";
|
|
23
24
|
export const CONTROLLER_HANDOVER_ID_ENV = "YUI_CONTROLLER_HANDOVER_ID";
|
|
24
25
|
const DEFAULT_POLL_INTERVAL_MS = 100;
|
|
@@ -29,7 +30,7 @@ const DEFAULT_POLL_INTERVAL_MS = 100;
|
|
|
29
30
|
* a second independent grace (see `DEFAULT_DUAL_OWNER_GRACE_MS` in
|
|
30
31
|
* `releaseHandover.ts`, which is only an optional confirmation debounce).
|
|
31
32
|
*/
|
|
32
|
-
export const DEFAULT_DUAL_OWNER_GRACE_MS =
|
|
33
|
+
export const DEFAULT_DUAL_OWNER_GRACE_MS = RELEASE_HANDOVER_OLD_OWNER_GRACE_MS;
|
|
33
34
|
/** Reads the candidate configuration from the process environment. */
|
|
34
35
|
export function handoverCandidateFromEnvironment(environment) {
|
|
35
36
|
if (environment[CONTROLLER_CANDIDATE_ENV] !== "1")
|
|
@@ -85,7 +86,6 @@ export async function runHandoverCandidate(home, handoverId, options = {}) {
|
|
|
85
86
|
dualOwner: false
|
|
86
87
|
}));
|
|
87
88
|
advanceFence(home, fence, "candidate-ready", now().toISOString());
|
|
88
|
-
const committedAt = Date.now();
|
|
89
89
|
let dualOwnerReported = false;
|
|
90
90
|
for (;;) {
|
|
91
91
|
const current = readHandoverFence(home);
|
|
@@ -111,7 +111,7 @@ export async function runHandoverCandidate(home, handoverId, options = {}) {
|
|
|
111
111
|
}
|
|
112
112
|
if (current.phase === "committed"
|
|
113
113
|
&& !oldDead
|
|
114
|
-
&&
|
|
114
|
+
&& committedFenceAgeMs(current, now()) > dualOwnerGraceMs
|
|
115
115
|
&& !dualOwnerReported) {
|
|
116
116
|
// The old Controller was told to exit but is still live. Stay read-only
|
|
117
117
|
// and make the dual-owner condition visible; never write concurrently.
|
|
@@ -129,6 +129,13 @@ export async function runHandoverCandidate(home, handoverId, options = {}) {
|
|
|
129
129
|
await delay(pollIntervalMs);
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
+
function committedFenceAgeMs(fence, observedAt) {
|
|
133
|
+
const committedAt = Date.parse(fence.updatedAt);
|
|
134
|
+
if (!Number.isFinite(committedAt)) {
|
|
135
|
+
throw new Error("Committed handover fence timestamp is invalid.");
|
|
136
|
+
}
|
|
137
|
+
return observedAt.getTime() - committedAt;
|
|
138
|
+
}
|
|
132
139
|
async function promote(home, fence, environment, now) {
|
|
133
140
|
removeCandidateDiscovery(home);
|
|
134
141
|
const controller = await startFileTaskControllerRuntime(home, { environment });
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { callController } from "../core/controllerClient.js";
|
|
2
2
|
import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
|
|
3
|
-
import { yuiRunBodyFromInputMessage, yuiRunIdFromInputMessages } from "../run/runIdentity.js";
|
|
4
3
|
import { runtimeLifecycleSignalKey } from "../runtime/lifecycleReservation.js";
|
|
5
4
|
import { setCodexThreadName } from "../execution/codexThreadNaming.js";
|
|
6
5
|
import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
|
|
@@ -50,9 +49,11 @@ export function parseCodexSessionNotification(payloadArgument, environment) {
|
|
|
50
49
|
const nativeSessionId = requireText(payload["thread-id"], "Codex thread-id");
|
|
51
50
|
const turnId = requireText(payload["turn-id"], "Codex turn-id");
|
|
52
51
|
const lastAssistantMessage = requireAssistantMessage(payload["last-assistant-message"]);
|
|
53
|
-
const runId =
|
|
52
|
+
const runId = environment.YUI_RUN_ID === undefined
|
|
53
|
+
? undefined
|
|
54
|
+
: requireText(environment.YUI_RUN_ID, "YUI_RUN_ID");
|
|
54
55
|
const title = environment.YUI_SESSION_TITLE === undefined
|
|
55
|
-
?
|
|
56
|
+
? undefined
|
|
56
57
|
: requireText(environment.YUI_SESSION_TITLE, "YUI_SESSION_TITLE");
|
|
57
58
|
const scope = environment.YUI_SESSION_SCOPE;
|
|
58
59
|
if (scope !== "task" && scope !== "global") {
|
|
@@ -117,19 +118,6 @@ function requireAssistantMessage(value) {
|
|
|
117
118
|
}
|
|
118
119
|
return text;
|
|
119
120
|
}
|
|
120
|
-
function sessionTitleFromInputMessages(value) {
|
|
121
|
-
if (!Array.isArray(value))
|
|
122
|
-
return undefined;
|
|
123
|
-
for (const entry of value) {
|
|
124
|
-
if (typeof entry !== "string")
|
|
125
|
-
continue;
|
|
126
|
-
const body = yuiRunBodyFromInputMessage(entry);
|
|
127
|
-
const normalized = body.trim().replaceAll(/\s+/g, " ");
|
|
128
|
-
if (normalized.length > 0)
|
|
129
|
-
return truncateSessionText(normalized);
|
|
130
|
-
}
|
|
131
|
-
return undefined;
|
|
132
|
-
}
|
|
133
121
|
function shouldSetThreadName(home, params) {
|
|
134
122
|
if (params.scope !== "task"
|
|
135
123
|
|| params.title === undefined
|
|
@@ -168,12 +156,6 @@ function threadNameRequest(params, environment) {
|
|
|
168
156
|
return null;
|
|
169
157
|
}
|
|
170
158
|
}
|
|
171
|
-
function truncateSessionText(value) {
|
|
172
|
-
const truncated = value.slice(0, 1_024);
|
|
173
|
-
return /[\uD800-\uDBFF]$/.test(truncated)
|
|
174
|
-
? truncated.slice(0, -1)
|
|
175
|
-
: truncated;
|
|
176
|
-
}
|
|
177
159
|
function isObject(value) {
|
|
178
160
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
179
161
|
}
|
|
@@ -276,7 +276,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
276
276
|
return ["--append-system-prompt-file", input.managedContextFile];
|
|
277
277
|
}
|
|
278
278
|
compileResume(input) {
|
|
279
|
-
const launch =
|
|
279
|
+
const launch = this.compileNew(input);
|
|
280
280
|
return { ...launch, argv: [...launch.argv, "--resume", nativeId(input.nativeSessionId)] };
|
|
281
281
|
}
|
|
282
282
|
compileManagedControl(input, mode, nativeSessionId) {
|
|
@@ -285,7 +285,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
285
285
|
}
|
|
286
286
|
const sessionId = nativeId(nativeSessionId);
|
|
287
287
|
const launch = mode === "new"
|
|
288
|
-
?
|
|
288
|
+
? this.compileNew(input)
|
|
289
289
|
: this.compileResume({ ...input, nativeSessionId: sessionId });
|
|
290
290
|
return {
|
|
291
291
|
...launch,
|
|
@@ -173,9 +173,29 @@ export function retireTaskRoleSessionsForWorkspace(set, now) {
|
|
|
173
173
|
// receive a fresh identity after the Role workspace changes.
|
|
174
174
|
history: [...(set.history ?? []), ...Object.values(set.sessions)],
|
|
175
175
|
sessions: {},
|
|
176
|
+
providerBinding: null,
|
|
176
177
|
updatedAt: timestamp
|
|
177
178
|
});
|
|
178
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Clears the Provider transport identity after its physical runtime is proven
|
|
182
|
+
* stopped, without retiring workspace-bound Session records. Workspace
|
|
183
|
+
* retirement remains a separate, stricter transaction after every supported
|
|
184
|
+
* placeholder has been terminalized.
|
|
185
|
+
*/
|
|
186
|
+
export function clearTaskRoleProviderRuntimeForCleanup(set, now) {
|
|
187
|
+
validateRoleSessionSet(set);
|
|
188
|
+
if (set.inFlight !== null) {
|
|
189
|
+
throw new Error("Cannot clear a Task Role Provider runtime with unsettled Run state.");
|
|
190
|
+
}
|
|
191
|
+
if (set.providerBinding === null)
|
|
192
|
+
return set;
|
|
193
|
+
return validateRoleSessionSet({
|
|
194
|
+
...set,
|
|
195
|
+
providerBinding: null,
|
|
196
|
+
updatedAt: requireDate(now, "Provider Runtime cleanup timestamp")
|
|
197
|
+
});
|
|
198
|
+
}
|
|
179
199
|
/**
|
|
180
200
|
* Terminalizes only the aggregate-16 Claude placeholder shape after the
|
|
181
201
|
* caller has fenced the Task store and proved that the exact Role has no live
|
|
@@ -261,18 +281,18 @@ export function roleAgentSessionResumeMode(set, agentId, desired, workspace) {
|
|
|
261
281
|
}
|
|
262
282
|
return "new";
|
|
263
283
|
}
|
|
284
|
+
if (session.status === "stopped" || session.status === "broken") {
|
|
285
|
+
return "new";
|
|
286
|
+
}
|
|
264
287
|
const compatible = set.owner.scope === "task"
|
|
265
288
|
? effectiveLaunchSnapshotsCompatibleForTaskMain(session.effective, desired, workspace)
|
|
266
289
|
: effectiveLaunchSnapshotsCompatible(session.effective, desired);
|
|
267
290
|
if (compatible)
|
|
268
291
|
return "resume";
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
+ "Stop the existing native process before starting a fresh Session.");
|
|
272
|
-
}
|
|
273
|
-
return "new";
|
|
292
|
+
throw new Error(`Role Agent session is incompatible with the next effective launch: ${agentId}. `
|
|
293
|
+
+ "Stop the existing native process before starting a fresh Session.");
|
|
274
294
|
}
|
|
275
|
-
export function bindTaskRoleRun(set, fence, preparedAt) {
|
|
295
|
+
export function bindTaskRoleRun(set, fence, preparedAt, mode) {
|
|
276
296
|
validateRoleSessionSet(set);
|
|
277
297
|
assertTaskRoleSessionSet(set);
|
|
278
298
|
const normalized = normalizeTaskRoleRunFence(fence);
|
|
@@ -285,9 +305,9 @@ export function bindTaskRoleRun(set, fence, preparedAt) {
|
|
|
285
305
|
throw new Error("Task Role session set already has an in-flight Run.");
|
|
286
306
|
}
|
|
287
307
|
const timestamp = requireDate(preparedAt, "Task Role Run preparedAt");
|
|
288
|
-
const providerBinding = set.providerBinding
|
|
289
|
-
?
|
|
290
|
-
:
|
|
308
|
+
const providerBinding = mode === "resume" && set.providerBinding !== null
|
|
309
|
+
? rebindProviderRuntimeRun(set.providerBinding, normalized.runId)
|
|
310
|
+
: null;
|
|
291
311
|
const updated = {
|
|
292
312
|
...set,
|
|
293
313
|
inFlight: { ...normalized, preparedAt: timestamp },
|