@zq-silk/yui 0.6.13 → 0.6.14
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 +34 -5
- package/dist/cli/commandCatalog.js +173 -57
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli.js +99 -11
- package/dist/commands/configCommands.js +521 -171
- package/dist/commands/deliveryGuardPreflight.js +2 -2
- package/dist/commands/executionAuditCommands.js +56 -3
- package/dist/commands/projectCommands.js +504 -2
- package/dist/commands/releaseCommands.js +0 -1
- package/dist/commands/taskActor.js +17 -0
- package/dist/commands/taskBaseCommands.js +29 -0
- package/dist/commands/taskCommands.js +577 -126
- package/dist/commands/taskContextCommand.js +36 -2
- package/dist/commands/taskNextActionCommand.js +48 -3
- package/dist/commands/taskPublicationCommands.js +319 -0
- package/dist/commands/taskRoleRuntimeStatus.js +83 -59
- package/dist/commands/telemetryCommands.js +14 -13
- package/dist/config/yuiConfig.js +161 -8
- package/dist/context/sessionContextBudget.js +68 -0
- package/dist/context/wakeNotification.js +65 -0
- package/dist/controller/clientRuntime.js +2 -1
- package/dist/controller/ephemeralResourceReaper.js +2 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +183 -16
- package/dist/controller/jobSupervisor.js +5 -4
- package/dist/controller/resourceCleanupLinux.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +3 -3
- package/dist/controller/runtime.js +88 -10
- package/dist/controller/updateReconciliation.js +4 -3
- package/dist/doctor/doctor.js +26 -7
- package/dist/executor/agentConfigurationCatalog.js +18 -0
- package/dist/executor/fileRoleLaunchPlanner.js +3 -3
- package/dist/lifecycle/contextBudgetRollover.js +81 -0
- package/dist/lifecycle/exactRunTerminalization.js +11 -1
- package/dist/lifecycle/providerErrorClass.js +33 -12
- package/dist/observability/executionAudit.js +214 -6
- package/dist/output/table.js +18 -0
- package/dist/repository/gitWorkspace.js +92 -0
- package/dist/repository/project.js +218 -4
- package/dist/repository/taskBaseFreshness.js +318 -0
- package/dist/repository/taskWorkspacePreparer.js +16 -2
- package/dist/review/deltaRecheck.js +232 -0
- package/dist/review/reviewConfig.js +31 -0
- package/dist/review/reviewFindingLedger.js +5 -1
- package/dist/review/reviewRound.js +156 -1
- package/dist/run/providerRetry.js +21 -3
- package/dist/run/providerRetryConfig.js +13 -60
- package/dist/run/recoveryProjection.js +199 -0
- package/dist/runtime/builtinAgentDrivers.js +3 -0
- package/dist/runtime/builtinTranscriptUsage.js +76 -32
- package/dist/runtime/continuationManager.js +17 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/launchDiagnostics.js +154 -0
- package/dist/runtime/lifecycleReservation.js +13 -0
- package/dist/runtime/providerContinuation.js +38 -0
- package/dist/runtime/providerContinuationReconciliationService.js +1 -0
- package/dist/runtime/providerErrorCodes.js +278 -0
- package/dist/runtime/runtimeHealthPolicy.js +20 -0
- package/dist/runtime/runtimeObservation.js +1 -0
- package/dist/runtime/runtimeProjection.js +115 -23
- package/dist/runtime/tmuxAdapters.js +242 -48
- package/dist/scheduler/activeRoleRunDelivery.js +139 -3
- package/dist/scheduler/activeTaskProgress.js +4 -3
- package/dist/scheduler/leaderWakeupProcessor.js +105 -15
- package/dist/scheduler/roleRunLiveness.js +2 -1
- package/dist/scheduler/roleRunStall.js +3 -2
- package/dist/scheduler/taskWake.js +72 -0
- package/dist/scheduler/wakeReason.js +64 -0
- package/dist/scheduler/wakeupQueue.js +2 -1
- package/dist/setup/setupCommand.js +1 -1
- package/dist/storage/migration/productionRegistry.js +325 -1
- package/dist/storage/sqliteSchema.js +61 -2
- package/dist/storage/sqliteStore.js +129 -2
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/taskStore.js +262 -5
- package/dist/storage/upgrade/recordVersions.js +6 -1
- package/dist/storage/upgrade/sqliteStateMigration.js +22 -2
- package/dist/task/completionReadiness.js +282 -0
- package/dist/task/publicationReference.js +123 -0
- package/dist/task/taskRecordReference.js +3 -1
- package/dist/telemetry/telemetryConfig.js +23 -18
- package/dist/telemetry/telemetryWiring.js +8 -8
- package/dist/tmux/tmuxManager.js +50 -9
- package/dist/web/assets/client/i18n.js +4 -0
- package/dist/web/assets/client/view.js +18 -0
- package/dist/web/webSnapshot.js +100 -10
- package/i18n/README.zh-CN.md +5 -5
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +49 -10
- package/skills/yui-operator/SKILL.md +13 -3
- package/skills/yui-worker/SKILL.md +8 -0
|
@@ -4,13 +4,17 @@ import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindTaskRoleRu
|
|
|
4
4
|
import { createProviderRuntimeBinding, endProviderActivation, startProviderActivation, updateProviderConversationRecoverability } from "../runtime/providerRuntimeIdentity.js";
|
|
5
5
|
import { hasRecentTurnId } from "../executor/turnCompletion.js";
|
|
6
6
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
7
|
+
import { buildTaskWakeEnvelope } from "../context/wakeNotification.js";
|
|
8
|
+
import { createTaskWake, fallbackWakeCursor, latestTaskWake } from "../scheduler/taskWake.js";
|
|
9
|
+
import { rolloverTaskRoleSessionForContextBudget } from "../lifecycle/contextBudgetRollover.js";
|
|
10
|
+
import { resolveContextBudget } from "../config/yuiConfig.js";
|
|
7
11
|
import { answerInputRequest } from "../input/inputRequest.js";
|
|
8
12
|
import { activeRoleAgentBinding, updateRoleStatus } from "../role/role.js";
|
|
9
13
|
import { effectiveLaunchWithTaskMainWorkspace, effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain, resolveEffectiveLaunch, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
|
|
10
14
|
import { SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
|
|
11
15
|
import { failAgentRun, markAgentRunDelivered, markAgentRunPushed, reopenRunForProviderRetry, withProviderRetry } from "../run/agentRun.js";
|
|
12
16
|
import { classifyProviderError, isRetryableProviderErrorClass } from "../lifecycle/providerErrorClass.js";
|
|
13
|
-
import { providerRetryIsDue, scheduleProviderRetry } from "../run/providerRetry.js";
|
|
17
|
+
import { providerRetryBudgetExhausted, providerRetryIsDue, scheduleProviderRetry } from "../run/providerRetry.js";
|
|
14
18
|
import { providerRetryConfig } from "../run/providerRetryConfig.js";
|
|
15
19
|
import { terminalizeExactTaskRun } from "../lifecycle/exactRunTerminalization.js";
|
|
16
20
|
import { createCanonicalLifecycleEvent, foldCanonicalLifecycleEvent } from "../lifecycle/canonicalLifecycleEvent.js";
|
|
@@ -18,6 +22,7 @@ import { recordLeaderFailure } from "../scheduler/leaderFailure.js";
|
|
|
18
22
|
import { createLeaderRecoveryNotification, createLeaderStallNotification } from "../scheduler/operatorNotification.js";
|
|
19
23
|
import { pendingWakeupsMatch } from "../scheduler/pendingWakeup.js";
|
|
20
24
|
import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
|
|
25
|
+
import { wakeReason } from "../scheduler/wakeReason.js";
|
|
21
26
|
import { foldRunProgressFacts, latestRunDurableProgressAt, latestRunEventTime, latestStallEvidenceKey, clearMatchingLeaderStallAttention, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT, RUN_STALLED_EVENT } from "../scheduler/roleRunStall.js";
|
|
22
27
|
import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
23
28
|
import { providerContinuationKey } from "../runtime/providerContinuation.js";
|
|
@@ -26,7 +31,7 @@ import { recordExecutionLaneResult } from "../execution/executionGroup.js";
|
|
|
26
31
|
import { formatAgentRunReceiptId, formatTaskRecordReference } from "../task/taskRecordReference.js";
|
|
27
32
|
import { bindExecution, claimPending, claimInputDelivery as claimMailboxInputDelivery, completeInputDelivery as completeMailboxInputDelivery, completeProcessing, mailboxHasPending, mailboxHasWork, pendingLane, markInputDeliveryPushed as markMailboxInputDeliveryPushed, markInputDeliveryUnknown as markMailboxInputDeliveryUnknown, releaseInputDelivery as releaseMailboxInputDelivery, resolveInputDeliveryNotAccepted as resolveMailboxInputDeliveryNotAccepted, releaseProcessing } from "../coordination/workMailbox.js";
|
|
28
33
|
import { enqueueWork } from "../coordination/workMailboxQueue.js";
|
|
29
|
-
import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_LAUNCH_RESERVED_REASON, RUNTIME_LIFECYCLE_OWNER, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
34
|
+
import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_LAUNCH_RESERVED_REASON, RUNTIME_LIFECYCLE_OWNER, RuntimeLifecycleBusyError, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
30
35
|
import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
|
|
31
36
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
32
37
|
import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation, runtimeObservationFenceMatches, runtimeObservationFromTaskEvent, runtimeObservationRunFenceMatches, runtimeObservationTaskEventPayload } from "../runtime/runtimeObservation.js";
|
|
@@ -138,6 +143,9 @@ export class FileSchedulerStoreAdapter {
|
|
|
138
143
|
launchId: input.fence.launchId,
|
|
139
144
|
nativeSessionId: input.fence.nativeSessionId,
|
|
140
145
|
runId: input.fence.runId,
|
|
146
|
+
...(failureEvidence.errorCode === undefined
|
|
147
|
+
? {}
|
|
148
|
+
: { errorCode: failureEvidence.errorCode }),
|
|
141
149
|
error: failureEvidence.code,
|
|
142
150
|
...(failureEvidence.details === undefined
|
|
143
151
|
? {}
|
|
@@ -535,6 +543,35 @@ export class FileSchedulerStoreAdapter {
|
|
|
535
543
|
getTaskBrief(taskId) { return this.store.getTaskBrief(taskId); }
|
|
536
544
|
listDecisions(taskId) { return this.store.listDecisions(taskId); }
|
|
537
545
|
listMilestones(taskId) { return this.store.listMilestones(taskId); }
|
|
546
|
+
getTaskWakeEnvelope(taskId) {
|
|
547
|
+
return this.store.transaction((reader) => {
|
|
548
|
+
const pending = reader.getPendingWakeup(taskId);
|
|
549
|
+
if (pending === null)
|
|
550
|
+
return null;
|
|
551
|
+
const task = reader.getTask(taskId);
|
|
552
|
+
if (task === null)
|
|
553
|
+
return null;
|
|
554
|
+
const latest = latestTaskWake(reader.listTaskWakes(taskId));
|
|
555
|
+
const fromCursor = latest?.toCursor ?? fallbackWakeCursor({
|
|
556
|
+
taskCreatedAt: task.createdAt,
|
|
557
|
+
leaderRunCreatedAt: reader.listAgentRuns(taskId)
|
|
558
|
+
.filter((run) => run.roleName === "leader")
|
|
559
|
+
.at(-1)?.createdAt
|
|
560
|
+
});
|
|
561
|
+
return buildTaskWakeEnvelope(reader, {
|
|
562
|
+
taskId,
|
|
563
|
+
wakeId: reader.peekNextTaskWakeId(taskId),
|
|
564
|
+
reasons: pending.reasons,
|
|
565
|
+
fromCursor
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
rolloverTaskRoleSessionForContextBudget(input) {
|
|
570
|
+
return this.store.transaction((reader) => rolloverTaskRoleSessionForContextBudget(reader, input.taskId, input.roleName, { peakTokens: input.peakTokens, hardTokens: input.hardTokens }, input.now));
|
|
571
|
+
}
|
|
572
|
+
getContextBudget() {
|
|
573
|
+
return resolveContextBudget(this.store.getConfig().contextBudget);
|
|
574
|
+
}
|
|
538
575
|
listRoles(taskId) {
|
|
539
576
|
return this.store.listRoles(taskId).map((role) => mapRole(this.store, role));
|
|
540
577
|
}
|
|
@@ -583,7 +620,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
583
620
|
const answered = answerInputRequest(request, { choiceKey }, "agent-timeout", now);
|
|
584
621
|
store.saveInputRequest(task.id, answered);
|
|
585
622
|
store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, "input.auto-answered", { requestId: request.id, choiceKey }, now));
|
|
586
|
-
queueLeaderWakeup(store, task.id,
|
|
623
|
+
queueLeaderWakeup(store, task.id, wakeReason("input-timeout", request.id), now);
|
|
587
624
|
resolved.push({ inputRequestId: request.id, taskId: task.id, choiceKey });
|
|
588
625
|
}
|
|
589
626
|
return resolved;
|
|
@@ -684,7 +721,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
684
721
|
]);
|
|
685
722
|
}
|
|
686
723
|
else {
|
|
687
|
-
queueLeaderWakeup(store, task.id, "role-run-stalled", input.now);
|
|
724
|
+
queueLeaderWakeup(store, task.id, wakeReason("role-run-stalled"), input.now);
|
|
688
725
|
}
|
|
689
726
|
return "raised";
|
|
690
727
|
});
|
|
@@ -809,6 +846,20 @@ export class FileSchedulerStoreAdapter {
|
|
|
809
846
|
enqueueLeaderWakeup(taskId, reason, now) {
|
|
810
847
|
return this.store.transaction((store) => (queueLeaderWakeup(store, taskId, reason, now)));
|
|
811
848
|
}
|
|
849
|
+
/**
|
|
850
|
+
* Records that a Leader wake was suppressed by scheduler single-flight
|
|
851
|
+
* (the Role runtime lifecycle lane was busy). The wake stays durable and
|
|
852
|
+
* is retried after the lane settles; this event is the audit trail that
|
|
853
|
+
* separates scheduler backpressure from real Run failures.
|
|
854
|
+
*/
|
|
855
|
+
recordWakeSuppression(taskId, reason, now) {
|
|
856
|
+
this.store.transaction((store) => {
|
|
857
|
+
const task = store.getTask(taskId);
|
|
858
|
+
if (task === null)
|
|
859
|
+
return;
|
|
860
|
+
store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, "wake.suppressed", { reason }, now));
|
|
861
|
+
});
|
|
862
|
+
}
|
|
812
863
|
listActiveDurableJobs() {
|
|
813
864
|
return this.store.listActiveDurableJobs();
|
|
814
865
|
}
|
|
@@ -1135,6 +1186,24 @@ export class FileSchedulerStoreAdapter {
|
|
|
1135
1186
|
store.saveRole(input.task.id, updateRoleStatus(role, "running", input.now));
|
|
1136
1187
|
bindTaskRoleRunInFlight(store, role, input.run, input.now);
|
|
1137
1188
|
store.saveEvent(input.task.id, createTaskEvent(store.nextEventId(input.task.id), input.task.id, "run.dispatched", runLaunchEventPayload(input.run), input.now));
|
|
1189
|
+
if (input.wakeId !== undefined && input.wakeFromCursor !== undefined) {
|
|
1190
|
+
if (store.peekNextTaskWakeId(input.task.id) !== input.wakeId) {
|
|
1191
|
+
return "state-changed";
|
|
1192
|
+
}
|
|
1193
|
+
const allocatedWakeId = store.nextTaskWakeId(input.task.id);
|
|
1194
|
+
if (allocatedWakeId !== input.wakeId) {
|
|
1195
|
+
throw new Error(`Leader TaskWake allocation changed unexpectedly: ${input.task.id}.`);
|
|
1196
|
+
}
|
|
1197
|
+
store.saveTaskWake(input.task.id, createTaskWake({
|
|
1198
|
+
id: allocatedWakeId,
|
|
1199
|
+
taskId: input.task.id,
|
|
1200
|
+
reasons: input.wakeup.reasons,
|
|
1201
|
+
fromCursor: input.wakeFromCursor,
|
|
1202
|
+
toCursor: input.now.toISOString(),
|
|
1203
|
+
runId: input.run.id,
|
|
1204
|
+
now: input.now
|
|
1205
|
+
}));
|
|
1206
|
+
}
|
|
1138
1207
|
if (input.session !== null && input.session.nativeSessionId !== undefined) {
|
|
1139
1208
|
saveTaskSession(store, role, {
|
|
1140
1209
|
...input.session,
|
|
@@ -1405,7 +1474,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1405
1474
|
clearMatchingLeaderStallAttention(store, task.id, currentRun.id);
|
|
1406
1475
|
store.saveRole(task.id, updateRoleStatus(role, "exited", input.now));
|
|
1407
1476
|
stopTaskSessionIfPresent(store, task.id, role.name, currentRun.effective.agentId, input.now);
|
|
1408
|
-
queueLeaderWakeup(store, task.id, "review-failed", input.now);
|
|
1477
|
+
queueLeaderWakeup(store, task.id, wakeReason("review-failed"), input.now);
|
|
1409
1478
|
return "failed";
|
|
1410
1479
|
}
|
|
1411
1480
|
const target = { kind: "role", taskId: task.id, roleName: role.name };
|
|
@@ -1453,7 +1522,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1453
1522
|
}
|
|
1454
1523
|
store.saveRole(task.id, updateRoleStatus(role, "exited", input.now));
|
|
1455
1524
|
stopTaskSessionIfPresent(store, task.id, role.name, currentRun.effective.agentId, input.now);
|
|
1456
|
-
queueLeaderWakeup(store, task.id, role.name === "leader" ? "leader-run-failed" : "role-run-failed", input.now);
|
|
1525
|
+
queueLeaderWakeup(store, task.id, wakeReason(role.name === "leader" ? "leader-run-failed" : "role-run-failed"), input.now);
|
|
1457
1526
|
return "failed";
|
|
1458
1527
|
});
|
|
1459
1528
|
}
|
|
@@ -1486,7 +1555,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1486
1555
|
}
|
|
1487
1556
|
if (existing !== null
|
|
1488
1557
|
&& (existing.processing !== null || existing.pending !== null)) {
|
|
1489
|
-
throw new
|
|
1558
|
+
throw new RuntimeLifecycleBusyError("Runtime lifecycle work is already pending.");
|
|
1490
1559
|
}
|
|
1491
1560
|
enqueueWork(store, target, RUNTIME_LAUNCH_RESERVED_REASON, now, input.owner.scope === "task"
|
|
1492
1561
|
? [{ type: "task", id: input.owner.taskId }]
|
|
@@ -1747,6 +1816,18 @@ export class FileSchedulerStoreAdapter {
|
|
|
1747
1816
|
sessions = rememberRoleAgentCompletedTurn(sessions, input.agentId, input.nativeSessionId, input.turnId, now);
|
|
1748
1817
|
store.saveTaskRoleSessionSet(sessions);
|
|
1749
1818
|
completeRuntimeHookReservation(store, owner, input.launchId);
|
|
1819
|
+
// The in-flight fence was cleared by an in-place provider retry
|
|
1820
|
+
// decision. If the active Run is still in its retry window and this
|
|
1821
|
+
// completion belongs to it, the recovered CLI finished the original
|
|
1822
|
+
// turn: clear the retry projection so the deadline does not re-push
|
|
1823
|
+
// the prompt. The Run stays active for the normal completion path.
|
|
1824
|
+
const activeRun = store.getActiveAgentRun(task.id, role.name);
|
|
1825
|
+
if (activeRun !== null
|
|
1826
|
+
&& activeRun.providerRetry !== undefined
|
|
1827
|
+
&& input.runId === activeRun.id) {
|
|
1828
|
+
const { providerRetry: _removed, ...rest } = activeRun;
|
|
1829
|
+
store.saveAgentRun({ ...rest, updatedAt: now.toISOString() });
|
|
1830
|
+
}
|
|
1750
1831
|
return { session: sessions.sessions[input.agentId], duplicate: false };
|
|
1751
1832
|
}
|
|
1752
1833
|
// A provider Turn is an activation boundary, not a Yui Run outcome.
|
|
@@ -1871,11 +1952,12 @@ export class FileSchedulerStoreAdapter {
|
|
|
1871
1952
|
* the Run, or `null` to fall through to the existing terminalization path.
|
|
1872
1953
|
*/
|
|
1873
1954
|
providerRetryDecision(store, input, summary, now) {
|
|
1874
|
-
const config = providerRetryConfig();
|
|
1955
|
+
const config = providerRetryConfig(this.store.getConfig());
|
|
1875
1956
|
if (config.mode === "off")
|
|
1876
1957
|
return null;
|
|
1877
1958
|
const classification = classifyProviderError({
|
|
1878
1959
|
adapterId: input.adapterId,
|
|
1960
|
+
...(input.errorCode === undefined ? {} : { errorCode: input.errorCode }),
|
|
1879
1961
|
error: input.error,
|
|
1880
1962
|
errorDetails: input.errorDetails,
|
|
1881
1963
|
summary: input.lastAssistantMessage
|
|
@@ -1890,6 +1972,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1890
1972
|
if (adapterEnabled) {
|
|
1891
1973
|
this.recordProviderRetryClassified(store, input, classification.errorClass, {
|
|
1892
1974
|
wouldRetry: String(retryable),
|
|
1975
|
+
basis: classification.basis,
|
|
1893
1976
|
shadow: String(shadow)
|
|
1894
1977
|
}, now);
|
|
1895
1978
|
}
|
|
@@ -1903,6 +1986,20 @@ export class FileSchedulerStoreAdapter {
|
|
|
1903
1986
|
const run = store.getAgentRun(input.taskId, input.runId);
|
|
1904
1987
|
if (run === null || run.status !== "active")
|
|
1905
1988
|
return null;
|
|
1989
|
+
// A retry lineage that has used its total budget terminalizes with one
|
|
1990
|
+
// structured failure instead of looping. The Run falls through to the
|
|
1991
|
+
// normal terminalization path below.
|
|
1992
|
+
if (run.providerRetry !== undefined
|
|
1993
|
+
&& providerRetryBudgetExhausted(run.providerRetry, now, config.maxWindowMs)) {
|
|
1994
|
+
this.recordProviderRetryClassified(store, input, classification.errorClass, {
|
|
1995
|
+
wouldRetry: "false",
|
|
1996
|
+
shadow: "false",
|
|
1997
|
+
basis: classification.basis,
|
|
1998
|
+
note: "retry-budget-exhausted",
|
|
1999
|
+
attempt: String(run.providerRetry.attempt)
|
|
2000
|
+
}, now);
|
|
2001
|
+
return null;
|
|
2002
|
+
}
|
|
1906
2003
|
const retry = scheduleProviderRetry(run.providerRetry, {
|
|
1907
2004
|
errorClass: classification.errorClass,
|
|
1908
2005
|
launchId: input.launchId,
|
|
@@ -1911,9 +2008,9 @@ export class FileSchedulerStoreAdapter {
|
|
|
1911
2008
|
}, now);
|
|
1912
2009
|
store.saveAgentRun(withProviderRetry(run, retry));
|
|
1913
2010
|
// Clear the in-flight fence so the original Session can be re-bound and
|
|
1914
|
-
// re-pushed by the delivery path; the Session itself stays alive.
|
|
1915
2011
|
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
1916
|
-
|
|
2012
|
+
// re-pushed by the delivery path; the Session itself stays alive.
|
|
2013
|
+
if (sessions !== null && sessions.inFlight !== null) {
|
|
1917
2014
|
store.saveTaskRoleSessionSet(clearTaskRoleRun(sessions, {
|
|
1918
2015
|
agentId: input.agentId,
|
|
1919
2016
|
runId: input.runId,
|
|
@@ -1939,6 +2036,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1939
2036
|
this.recordProviderRetryClassified(store, input, classification.errorClass, {
|
|
1940
2037
|
wouldRetry: "true",
|
|
1941
2038
|
shadow: "false",
|
|
2039
|
+
basis: classification.basis,
|
|
1942
2040
|
attempt: String(retry.attempt),
|
|
1943
2041
|
nextAttemptAt: retry.nextAttemptAt
|
|
1944
2042
|
}, now);
|
|
@@ -2001,6 +2099,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
2001
2099
|
: [...taskIds].sort((left, right) => (left.localeCompare(right, undefined, { numeric: true })));
|
|
2002
2100
|
const due = this.listPendingProviderRetries(selectedTaskIds).filter((entry) => Date.parse(entry.nextAttemptAt) <= now.getTime());
|
|
2003
2101
|
const reopened = [];
|
|
2102
|
+
const config = providerRetryConfig(this.store.getConfig());
|
|
2004
2103
|
for (const entry of due) {
|
|
2005
2104
|
this.store.transaction((store) => {
|
|
2006
2105
|
const run = store.getAgentRun(entry.taskId, entry.runId);
|
|
@@ -2013,6 +2112,36 @@ export class FileSchedulerStoreAdapter {
|
|
|
2013
2112
|
}
|
|
2014
2113
|
const sessions = store.getTaskRoleSessionSet(entry.taskId, entry.roleName);
|
|
2015
2114
|
const session = sessions?.sessions[run.effective.agentId];
|
|
2115
|
+
// Budget exhausted: one structured terminal failure, never a loop.
|
|
2116
|
+
if (providerRetryBudgetExhausted(run.providerRetry, now, config.maxWindowMs)) {
|
|
2117
|
+
const summary = `Provider retry budget exhausted after ${run.providerRetry.attempt} attempt(s) `
|
|
2118
|
+
+ `(run ${run.id}, error class ${run.providerRetry.errorClass}, `
|
|
2119
|
+
+ `last error: ${run.providerRetry.lastErrorSummary}). `
|
|
2120
|
+
+ `The original Session requires an explicit Leader recovery decision.`;
|
|
2121
|
+
const result = terminalizeExactTaskRun(store, {
|
|
2122
|
+
taskId: entry.taskId,
|
|
2123
|
+
roleName: entry.roleName,
|
|
2124
|
+
agentId: run.effective.agentId,
|
|
2125
|
+
runId: run.id,
|
|
2126
|
+
receiptId: formatAgentRunReceiptId(entry.taskId, run.id),
|
|
2127
|
+
...(run.providerRetry.nativeSessionId === undefined
|
|
2128
|
+
? {}
|
|
2129
|
+
: { nativeSessionId: run.providerRetry.nativeSessionId }),
|
|
2130
|
+
...(run.providerRetry.launchId === undefined
|
|
2131
|
+
? {}
|
|
2132
|
+
: { launchId: run.providerRetry.launchId }),
|
|
2133
|
+
outcome: { status: "failed", summary }
|
|
2134
|
+
}, now);
|
|
2135
|
+
if (result.disposition !== "applied")
|
|
2136
|
+
return;
|
|
2137
|
+
store.saveEvent(entry.taskId, createTaskEvent(store.nextEventId(entry.taskId), entry.taskId, "runtime.provider-retry-budget-exhausted", {
|
|
2138
|
+
runId: run.id,
|
|
2139
|
+
attempt: String(run.providerRetry.attempt),
|
|
2140
|
+
errorClass: run.providerRetry.errorClass
|
|
2141
|
+
}, now));
|
|
2142
|
+
enqueueWork(store, { kind: "role", taskId: entry.taskId, roleName: "leader" }, "role-run-failed", now, [{ type: "run", taskId: entry.taskId, id: run.id }]);
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2016
2145
|
const identityMatches = session !== undefined
|
|
2017
2146
|
&& session.adapterId === run.effective.adapterId
|
|
2018
2147
|
&& (run.providerRetry.nativeSessionId === undefined
|
|
@@ -2022,10 +2151,41 @@ export class FileSchedulerStoreAdapter {
|
|
|
2022
2151
|
const sessionAlive = identityMatches
|
|
2023
2152
|
&& session.status !== "stopped"
|
|
2024
2153
|
&& session.status !== "broken";
|
|
2025
|
-
if (
|
|
2026
|
-
//
|
|
2027
|
-
//
|
|
2154
|
+
if (sessionAlive) {
|
|
2155
|
+
// The CLI process is still alive. Enter a grace period: let the
|
|
2156
|
+
// Provider reconnect and finish the turn itself instead of sending
|
|
2157
|
+
// a duplicate prompt. Reschedule with the next backoff; a late
|
|
2158
|
+
// completion is attributed to this Run by observeRuntimeTurnCompleted.
|
|
2159
|
+
const retry = scheduleProviderRetry(run.providerRetry, {
|
|
2160
|
+
errorClass: run.providerRetry.errorClass,
|
|
2161
|
+
...(run.providerRetry.launchId === undefined
|
|
2162
|
+
? {}
|
|
2163
|
+
: { launchId: run.providerRetry.launchId }),
|
|
2164
|
+
...(run.providerRetry.nativeSessionId === undefined
|
|
2165
|
+
? {}
|
|
2166
|
+
: { nativeSessionId: run.providerRetry.nativeSessionId }),
|
|
2167
|
+
lastErrorSummary: run.providerRetry.lastErrorSummary
|
|
2168
|
+
}, now);
|
|
2169
|
+
store.saveAgentRun(withProviderRetry(run, retry));
|
|
2170
|
+
store.saveEvent(entry.taskId, createTaskEvent(store.nextEventId(entry.taskId), entry.taskId, "runtime.provider-retry-waiting", {
|
|
2171
|
+
runId: run.id,
|
|
2172
|
+
attempt: String(retry.attempt),
|
|
2173
|
+
...(retry.nextAttemptAt === undefined
|
|
2174
|
+
? {}
|
|
2175
|
+
: { nextAttemptAt: retry.nextAttemptAt })
|
|
2176
|
+
}, now));
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
// The process is gone. If the adapter supports resume and the
|
|
2180
|
+
// projection carries a native Session id, relaunch with resume: the
|
|
2181
|
+
// conversation identity stays the same, only the launch generation
|
|
2182
|
+
// changes. Otherwise terminalize with a precise replacement blocker.
|
|
2183
|
+
const canResume = run.providerRetry.nativeSessionId !== undefined
|
|
2184
|
+
&& this.drivers.requireByAdapterId(run.effective.adapterId)
|
|
2185
|
+
.capabilities.control.resume;
|
|
2186
|
+
if (!canResume) {
|
|
2028
2187
|
const summary = `Provider retry stopped: the original Session is gone `
|
|
2188
|
+
+ `and the adapter cannot resume it `
|
|
2029
2189
|
+ `(run ${run.id}, attempt ${run.providerRetry.attempt}, `
|
|
2030
2190
|
+ `last error: ${run.providerRetry.lastErrorSummary}). `
|
|
2031
2191
|
+ `A replacement Session requires an explicit Leader recovery decision.`;
|
|
@@ -2049,10 +2209,17 @@ export class FileSchedulerStoreAdapter {
|
|
|
2049
2209
|
enqueueWork(store, { kind: "role", taskId: entry.taskId, roleName: "leader" }, "role-run-failed", now, [{ type: "run", taskId: entry.taskId, id: run.id }]);
|
|
2050
2210
|
return;
|
|
2051
2211
|
}
|
|
2052
|
-
//
|
|
2053
|
-
//
|
|
2054
|
-
//
|
|
2212
|
+
// Resume relaunch: reopen the Run in resume mode on the same native
|
|
2213
|
+
// Session. The delivery pass relaunches the process with resume and
|
|
2214
|
+
// re-pushes the exact same input; only the launch generation changes.
|
|
2055
2215
|
store.saveAgentRun(reopenRunForProviderRetry(run, now));
|
|
2216
|
+
store.saveEvent(entry.taskId, createTaskEvent(store.nextEventId(entry.taskId), entry.taskId, "runtime.provider-retry-resume", {
|
|
2217
|
+
runId: run.id,
|
|
2218
|
+
attempt: String(run.providerRetry.attempt),
|
|
2219
|
+
...(run.providerRetry.nativeSessionId === undefined
|
|
2220
|
+
? {}
|
|
2221
|
+
: { nativeSessionId: run.providerRetry.nativeSessionId })
|
|
2222
|
+
}, now));
|
|
2056
2223
|
// The retry decision settled the original delivery claim; re-queue the
|
|
2057
2224
|
// Role mailbox so the delivery pass can claim and re-push the Run.
|
|
2058
2225
|
enqueueWork(store, { kind: "role", taskId: entry.taskId, roleName: entry.roleName }, "provider-retry-repush", now, [{ type: "run", taskId: entry.taskId, id: run.id }]);
|
|
@@ -18,6 +18,7 @@ import { fileURLToPath } from "node:url";
|
|
|
18
18
|
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
19
19
|
import { cancelQueuedDurableJob, completeDurableJob, isDurableJobTerminal, markDurableJobUnknown, markDurableJobWakeupNotified, startDurableJob, touchDurableJobHeartbeat } from "../job/durableJob.js";
|
|
20
20
|
import { readLinuxProcessStartIdentity } from "./domainIdentity.js";
|
|
21
|
+
import { wakeReason } from "../scheduler/wakeReason.js";
|
|
21
22
|
const DEFAULT_STEP_TIMEOUT_MS = 30 * 60_000;
|
|
22
23
|
const HEARTBEAT_STALE_MS = 2 * 60_000;
|
|
23
24
|
const SIGKILL_GRACE_MS = 30_000;
|
|
@@ -114,7 +115,7 @@ export class DurableJobSupervisor {
|
|
|
114
115
|
// atomic (same transaction). Compose cancel + wakeupNotified and
|
|
115
116
|
// pass the wakeup param so the adapter enqueues the Leader mailbox
|
|
116
117
|
// entry in the same transaction.
|
|
117
|
-
const terminal = this.#store.transitionDurableJob(job.taskId, job.id, (current) => markDurableJobWakeupNotified(cancelQueuedDurableJob(current, now), now), now, { reason: "job-finished", refs: wakeupRefs(job) });
|
|
118
|
+
const terminal = this.#store.transitionDurableJob(job.taskId, job.id, (current) => markDurableJobWakeupNotified(cancelQueuedDurableJob(current, now), now), now, { reason: wakeReason("job-finished"), refs: wakeupRefs(job) });
|
|
118
119
|
this.#deliverTerminalEvent(terminal);
|
|
119
120
|
return;
|
|
120
121
|
}
|
|
@@ -290,7 +291,7 @@ export class DurableJobSupervisor {
|
|
|
290
291
|
// the adapter enqueues the Leader mailbox entry in the same transaction.
|
|
291
292
|
// A separate #notifyWakeup pass would lose the wakeup if the Controller
|
|
292
293
|
// died between the terminal write and the flag flip.
|
|
293
|
-
const terminal = this.#store.transitionDurableJob(job.taskId, job.id, (current) => markDurableJobWakeupNotified(completeDurableJob(current, result, now), now), now, { reason: "job-finished", refs: wakeupRefs(job) });
|
|
294
|
+
const terminal = this.#store.transitionDurableJob(job.taskId, job.id, (current) => markDurableJobWakeupNotified(completeDurableJob(current, result, now), now), now, { reason: wakeReason("job-finished"), refs: wakeupRefs(job) });
|
|
294
295
|
this.#deliverTerminalEvent(terminal);
|
|
295
296
|
this.#sigkillAt.delete(this.#sigkillKey(job));
|
|
296
297
|
}
|
|
@@ -303,12 +304,12 @@ export class DurableJobSupervisor {
|
|
|
303
304
|
// The runner died without exit.json, but its step checkpoint covered
|
|
304
305
|
// every planned step: the outcome is still provable from durable
|
|
305
306
|
// evidence. No false pass — the checkpoint is the runner's own record.
|
|
306
|
-
const terminal = this.#store.transitionDurableJob(job.taskId, job.id, (current) => markDurableJobWakeupNotified(completeDurableJob(current, { ...proven, evidenceSource: "checkpoint" }, now), now), now, { reason: "job-finished", refs: wakeupRefs(job) });
|
|
307
|
+
const terminal = this.#store.transitionDurableJob(job.taskId, job.id, (current) => markDurableJobWakeupNotified(completeDurableJob(current, { ...proven, evidenceSource: "checkpoint" }, now), now), now, { reason: wakeReason("job-finished"), refs: wakeupRefs(job) });
|
|
307
308
|
this.#deliverTerminalEvent(terminal);
|
|
308
309
|
}
|
|
309
310
|
else {
|
|
310
311
|
const completedSteps = checkpoint?.completedSteps ?? [];
|
|
311
|
-
const terminal = this.#store.transitionDurableJob(job.taskId, job.id, (current) => markDurableJobWakeupNotified(markDurableJobUnknown(current, "runner process exited without writing exit.json", completedSteps, now), now), now, { reason: "job-finished", refs: wakeupRefs(job) });
|
|
312
|
+
const terminal = this.#store.transitionDurableJob(job.taskId, job.id, (current) => markDurableJobWakeupNotified(markDurableJobUnknown(current, "runner process exited without writing exit.json", completedSteps, now), now), now, { reason: wakeReason("job-finished"), refs: wakeupRefs(job) });
|
|
312
313
|
this.#deliverTerminalEvent(terminal);
|
|
313
314
|
}
|
|
314
315
|
this.#sigkillAt.delete(this.#sigkillKey(job));
|
|
@@ -14,7 +14,7 @@ export async function cleanControllerResource(resource, options = {}) {
|
|
|
14
14
|
}
|
|
15
15
|
assertEphemeralDomainFence(resource);
|
|
16
16
|
const environment = options.environment ?? process.env;
|
|
17
|
-
const ports = options.ports ?? linuxCleanupPorts(environment);
|
|
17
|
+
const ports = options.ports ?? linuxCleanupPorts(environment, options.tmuxBin ?? "tmux");
|
|
18
18
|
const cleanup = async () => {
|
|
19
19
|
if (resource.artifact !== undefined) {
|
|
20
20
|
cleanArtifact(resource, ports, environment);
|
|
@@ -151,7 +151,7 @@ async function waitForProcesses(resource, ports, graceMs, pollMs) {
|
|
|
151
151
|
await ports.sleep(Math.min(pollMs, graceMs - elapsed));
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
-
function linuxCleanupPorts(environment) {
|
|
154
|
+
function linuxCleanupPorts(environment, tmuxBin) {
|
|
155
155
|
return {
|
|
156
156
|
processStartIdentity: readProcessStartIdentity,
|
|
157
157
|
signal: (pid, signal) => process.kill(pid, signal),
|
|
@@ -169,7 +169,7 @@ function linuxCleanupPorts(environment) {
|
|
|
169
169
|
socketActive: unixSocketIsActive,
|
|
170
170
|
removeArtifact: (path) => unlinkSync(path),
|
|
171
171
|
inspectTmuxServerPanes: async (resource) => {
|
|
172
|
-
const manager = tmuxManagerForResource(resource, environment);
|
|
172
|
+
const manager = tmuxManagerForResource(resource, environment, tmuxBin);
|
|
173
173
|
return manager.inspectRolePaneInventory().map((pane) => (`${pane.taskId}/${pane.roleName}`));
|
|
174
174
|
},
|
|
175
175
|
killPane: async (resource) => {
|
|
@@ -177,7 +177,7 @@ function linuxCleanupPorts(environment) {
|
|
|
177
177
|
if (target === undefined) {
|
|
178
178
|
throw new Error(`Role pane identity is unavailable: ${resource.id}.`);
|
|
179
179
|
}
|
|
180
|
-
const manager = tmuxManagerForResource(resource, environment);
|
|
180
|
+
const manager = tmuxManagerForResource(resource, environment, tmuxBin);
|
|
181
181
|
const pane = manager.inspectRolePaneInventory().find((candidate) => (candidate.target === target));
|
|
182
182
|
if (pane === undefined)
|
|
183
183
|
return;
|
|
@@ -200,14 +200,14 @@ function linuxCleanupPorts(environment) {
|
|
|
200
200
|
}
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
|
-
function tmuxManagerForResource(resource, environment) {
|
|
203
|
+
function tmuxManagerForResource(resource, environment, tmuxBin) {
|
|
204
204
|
const home = resource.yuiHome;
|
|
205
205
|
if (home === undefined) {
|
|
206
206
|
throw new Error(`Tmux resource home is unavailable: ${resource.id}.`);
|
|
207
207
|
}
|
|
208
208
|
const executor = new NodeCommandExecutor();
|
|
209
209
|
const commandEnvironment = tmuxSocketEnvironment(environment);
|
|
210
|
-
return new TmuxManager(
|
|
210
|
+
return new TmuxManager(tmuxBin, {
|
|
211
211
|
run: (command, args, options) => executor.run(command, args, {
|
|
212
212
|
...options,
|
|
213
213
|
environment: {
|
|
@@ -53,7 +53,7 @@ export async function scanControllerResourceInventory(options) {
|
|
|
53
53
|
const panes = options.panes !== undefined
|
|
54
54
|
? options.panes
|
|
55
55
|
: tmuxArtifact?.active === true
|
|
56
|
-
? inspectHomePanes(home, environment, warnings)
|
|
56
|
+
? inspectHomePanes(home, environment, options.tmuxBin ?? "tmux", warnings)
|
|
57
57
|
: [];
|
|
58
58
|
const discovery = await inspectDiscovery(home, matchingProcesses, activeSockets);
|
|
59
59
|
if (discovery.status === "valid"
|
|
@@ -726,11 +726,11 @@ function loadHomeState(home, warnings, options) {
|
|
|
726
726
|
function numericCompare(left, right) {
|
|
727
727
|
return left.localeCompare(right, undefined, { numeric: true });
|
|
728
728
|
}
|
|
729
|
-
function inspectHomePanes(home, environment, warnings) {
|
|
729
|
+
function inspectHomePanes(home, environment, tmuxBin, warnings) {
|
|
730
730
|
try {
|
|
731
731
|
const executor = new NodeCommandExecutor();
|
|
732
732
|
const commandEnvironment = tmuxSocketEnvironment(environment);
|
|
733
|
-
return new TmuxManager(
|
|
733
|
+
return new TmuxManager(tmuxBin, {
|
|
734
734
|
run: (command, args, options) => executor.run(command, args, {
|
|
735
735
|
...options,
|
|
736
736
|
environment: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { reconciliationIntervalMilliseconds } from "../config/yuiConfig.js";
|
|
1
|
+
import { reconciliationIntervalMilliseconds, resolveTmuxBin } from "../config/yuiConfig.js";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -7,7 +7,8 @@ import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_M
|
|
|
7
7
|
import { hasRuntimeCleanupObligation, runtimeLifecycleSignalKey, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
8
8
|
import { agentProcessReadinessProbe, ExecutorRegistry } from "../executor/executorRegistry.js";
|
|
9
9
|
import { activeLiveRoleAgentSession, roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
|
|
10
|
-
import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain, resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
10
|
+
import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain, effectiveLaunchConfig, resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
11
|
+
import { AgentConfigurationCatalogService, validateAgentLaunchConfiguration } from "../executor/agentConfigurationCatalog.js";
|
|
11
12
|
import { isTaskOwnedWorkspace, managedWorkspaceIdentity, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
|
|
12
13
|
import { FileRoleLaunchPlanner } from "../executor/fileRoleLaunchPlanner.js";
|
|
13
14
|
import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
|
|
@@ -100,13 +101,13 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
100
101
|
? new ResourceInventoryClient()
|
|
101
102
|
: undefined;
|
|
102
103
|
const schedulerStore = options.schedulerStore
|
|
103
|
-
?? new FileSchedulerStoreAdapter(store, openSchedulerTelemetry(home,
|
|
104
|
+
?? new FileSchedulerStoreAdapter(store, openSchedulerTelemetry(home, store.getConfig()));
|
|
104
105
|
const domainIdentity = options.domainIdentity
|
|
105
106
|
?? ephemeralDomainFromEnvironment(options.environment ?? process.env);
|
|
106
107
|
const planner = options.planner ?? new FileRoleLaunchPlanner(home, store, {
|
|
107
108
|
environment: options.environment
|
|
108
109
|
});
|
|
109
|
-
const tmux = options.tmux ?? new TmuxManager(
|
|
110
|
+
const tmux = options.tmux ?? new TmuxManager(resolveTmuxBin(store.getConfig().tmuxBin), new NodeCommandExecutor(), {
|
|
110
111
|
yuiHome: home,
|
|
111
112
|
...(domainIdentity === undefined
|
|
112
113
|
? {}
|
|
@@ -125,7 +126,57 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
125
126
|
tmux,
|
|
126
127
|
onWarning: options.onError
|
|
127
128
|
});
|
|
129
|
+
// The runtime inbox is also the low-latency discovery source during a
|
|
130
|
+
// scheduler-owned launch. Waiting only for the post-pass durable projection
|
|
131
|
+
// would deadlock behind the same pass that is currently starting the host.
|
|
132
|
+
const runtimeEventInbox = new FileRuntimeEventInbox(home);
|
|
133
|
+
const catalogs = options.catalogs
|
|
134
|
+
?? new AgentConfigurationCatalogService(home, {
|
|
135
|
+
environment: options.environment ?? process.env
|
|
136
|
+
});
|
|
128
137
|
const sessionHost = options.sessionHost ?? new TmuxSessionHost(planner, tmux, {
|
|
138
|
+
validateLaunch: async (request) => {
|
|
139
|
+
const agent = store.getConfiguredAgent(request.agentId);
|
|
140
|
+
if (agent === null)
|
|
141
|
+
return;
|
|
142
|
+
const resolved = await catalogs.resolve({
|
|
143
|
+
agent,
|
|
144
|
+
cwd: request.workspace,
|
|
145
|
+
config: effectiveLaunchConfig(request.effective)
|
|
146
|
+
});
|
|
147
|
+
validateAgentLaunchConfiguration(resolved.catalog, effectiveLaunchConfig(request.effective));
|
|
148
|
+
},
|
|
149
|
+
waitForNativeSession: async (request, signal) => {
|
|
150
|
+
const owner = request.owner;
|
|
151
|
+
if (owner.scope !== "task") {
|
|
152
|
+
throw new Error("Native session discovery requires a Task runtime owner.");
|
|
153
|
+
}
|
|
154
|
+
while (!signal.aborted) {
|
|
155
|
+
const session = schedulerStore.getRoleSession(owner.taskId, owner.roleName, request.agentId);
|
|
156
|
+
if (session !== null
|
|
157
|
+
&& session.launchId === request.launchId
|
|
158
|
+
&& typeof session.nativeSessionId === "string"
|
|
159
|
+
&& session.nativeSessionId.trim().length > 0) {
|
|
160
|
+
return session.nativeSessionId;
|
|
161
|
+
}
|
|
162
|
+
for (const event of runtimeEventInbox.list()) {
|
|
163
|
+
if (event.type === "runtime-observation"
|
|
164
|
+
&& event.observation.kind === "session.started"
|
|
165
|
+
&& event.observation.fence.taskId === owner.taskId
|
|
166
|
+
&& event.observation.fence.roleName === owner.roleName
|
|
167
|
+
&& event.observation.fence.agentId === request.agentId
|
|
168
|
+
&& event.observation.fence.launchId === request.launchId
|
|
169
|
+
&& typeof event.observation.fence.nativeSessionId === "string"
|
|
170
|
+
&& event.observation.fence.nativeSessionId.trim().length > 0) {
|
|
171
|
+
return event.observation.fence.nativeSessionId;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
await abortableDelay(50, signal);
|
|
175
|
+
}
|
|
176
|
+
throw new Error("Native session discovery was aborted.");
|
|
177
|
+
},
|
|
178
|
+
inactivityTimeoutMs: positiveIntegerOption(options.environment?.YUI_LAUNCH_INACTIVITY_TIMEOUT_MS
|
|
179
|
+
?? process.env.YUI_LAUNCH_INACTIVITY_TIMEOUT_MS, 300_000),
|
|
129
180
|
onHostCreated: ({ binding, pane }) => {
|
|
130
181
|
sessionOwners.recordHostOwner({
|
|
131
182
|
owner: binding.owner,
|
|
@@ -216,6 +267,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
216
267
|
currentHome: home,
|
|
217
268
|
scope: "current",
|
|
218
269
|
panes,
|
|
270
|
+
tmuxBin: resolveTmuxBin(store.getConfig().tmuxBin),
|
|
219
271
|
...(options.environment === undefined
|
|
220
272
|
? {}
|
|
221
273
|
: { environment: options.environment })
|
|
@@ -304,6 +356,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
304
356
|
// explicit `controller cleanup --all` inventory operation.
|
|
305
357
|
scope: "current",
|
|
306
358
|
environment: options.environment,
|
|
359
|
+
tmuxBin: resolveTmuxBin(store.getConfig().tmuxBin),
|
|
307
360
|
// When the worker backend is active, the reaper's scan runs in the
|
|
308
361
|
// inventory worker too (same cadence, same inventory shape).
|
|
309
362
|
...(inventoryClient === undefined
|
|
@@ -328,12 +381,11 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
328
381
|
environment: options.environment
|
|
329
382
|
});
|
|
330
383
|
const lifecycleDispatcher = createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, options.dispatcher, signalRuntimeCleanup, launchCoordinator, planner);
|
|
331
|
-
// f7/rr5:
|
|
332
|
-
//
|
|
333
|
-
// supervisor enqueues a durable-job-terminal event; the processor drains
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
const runtimeEventInbox = new FileRuntimeEventInbox(home);
|
|
384
|
+
// f7/rr5: This same inbox feeds the supervisor's terminal channel and the
|
|
385
|
+
// runtime event processor. When a Job reaches a terminal state, the
|
|
386
|
+
// supervisor enqueues a durable-job-terminal event; the processor drains it
|
|
387
|
+
// on the next pass, waking the Controller immediately instead of waiting for
|
|
388
|
+
// the poll interval.
|
|
337
389
|
const jobSupervisor = new DurableJobSupervisor({
|
|
338
390
|
store: schedulerStore,
|
|
339
391
|
process: createLinuxProcessPort(),
|
|
@@ -874,6 +926,32 @@ function requiredParam(value) {
|
|
|
874
926
|
}
|
|
875
927
|
return value;
|
|
876
928
|
}
|
|
929
|
+
function abortableDelay(milliseconds, signal) {
|
|
930
|
+
return new Promise((resolve, reject) => {
|
|
931
|
+
const timer = setTimeout(() => {
|
|
932
|
+
signal.removeEventListener("abort", onAbort);
|
|
933
|
+
resolve();
|
|
934
|
+
}, milliseconds);
|
|
935
|
+
const onAbort = () => {
|
|
936
|
+
clearTimeout(timer);
|
|
937
|
+
reject(new Error("Native session discovery was aborted."));
|
|
938
|
+
};
|
|
939
|
+
if (signal.aborted) {
|
|
940
|
+
onAbort();
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
function positiveIntegerOption(value, fallback) {
|
|
947
|
+
if (value === undefined || value.trim().length === 0)
|
|
948
|
+
return fallback;
|
|
949
|
+
const parsed = Number(value);
|
|
950
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
951
|
+
throw new Error(`Invalid native session discovery timeout: ${value}`);
|
|
952
|
+
}
|
|
953
|
+
return parsed;
|
|
954
|
+
}
|
|
877
955
|
function applicationError(code, message) {
|
|
878
956
|
const error = Object.assign(new Error(message), { code });
|
|
879
957
|
error.name = "CoreApplicationError";
|