@zq-silk/yui 0.6.2 → 0.6.4
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 +28 -4
- package/README.md +60 -97
- package/dist/agent/argumentPolicy.js +1 -1
- package/dist/agent/managedRuntimeEnvironment.js +1 -0
- package/dist/cli/commandCatalog.js +19 -9
- package/dist/cli/interactionPolicy.js +4 -2
- package/dist/cli.js +77 -32
- package/dist/commands/taskCommands.js +46 -11
- package/dist/commands/taskContextCommand.js +1 -1
- package/dist/commands/taskRoleRuntimeStatus.js +170 -10
- package/dist/controller/agentRuntimeObserver.js +210 -0
- package/dist/controller/clientRuntime.js +3 -21
- package/dist/controller/controller.js +47 -7
- package/dist/controller/fileSchedulerStoreAdapter.js +522 -388
- package/dist/controller/runtime.js +9 -3
- package/dist/controller/runtimeEventInbox.js +49 -295
- package/dist/controller/runtimeEventProcessor.js +184 -321
- package/dist/controller/runtimeHookRunFence.js +226 -0
- package/dist/controller/runtimeLaunchCoordinator.js +91 -26
- package/dist/controller/runtimeObservationHook.js +112 -0
- package/dist/core/controllerServer.js +5 -0
- package/dist/executor/agentAdapter.js +18 -3
- package/dist/executor/fileRoleLaunchPlanner.js +64 -15
- package/dist/executor/managedClaudeRunner.js +121 -0
- package/dist/observability/executionAudit.js +6 -3
- package/dist/repository/taskWorkspacePreparer.js +1 -4
- package/dist/run/providerRetryConfig.js +8 -3
- package/dist/runtime/agentDriver.js +229 -0
- package/dist/runtime/agentDriverObservation.js +57 -0
- package/dist/runtime/builtinAgentDrivers.js +235 -0
- package/dist/runtime/builtinTranscriptObserver.js +290 -0
- package/dist/runtime/builtinTranscriptUsage.js +97 -0
- package/dist/runtime/exactControlPlane.js +2 -2
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/ports.js +12 -1
- package/dist/runtime/runtimeObservation.js +297 -0
- package/dist/runtime/runtimeProjection.js +277 -0
- package/dist/runtime/sessionTerminationGuard.js +78 -22
- package/dist/runtime/tmuxAdapters.js +35 -0
- package/dist/scheduler/activeRoleRunDelivery.js +28 -13
- package/dist/scheduler/leaderWakeupProcessor.js +21 -2
- package/dist/scheduler/roleRunLiveness.js +2 -2
- package/dist/scheduler/roleRunStall.js +62 -114
- package/dist/storage/migration/productionRegistry.js +85 -0
- package/dist/storage/sqliteStore.js +3 -3
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +2 -1
- package/dist/storage/upgrade/sqliteStateMigration.js +123 -0
- package/dist/telemetry/sqliteTelemetryStore.js +0 -28
- package/dist/telemetry/telemetryCompaction.js +1 -0
- package/dist/telemetry/telemetryConfig.js +4 -5
- package/dist/tmux/tmuxManager.js +136 -22
- package/dist/web/assets/client/view.js +1 -1
- package/dist/web/tmuxWebTerminal.js +17 -12
- package/dist/web/webSnapshot.js +1 -1
- package/dist/worktree/managedWorkspace.js +14 -0
- package/i18n/README.zh-CN.md +12 -7
- package/package.json +1 -1
- package/dist/controller/claudeLifecycleHook.js +0 -203
- package/dist/controller/codexLifecycleHook.js +0 -108
- package/dist/controller/providerHookRunFence.js +0 -156
- package/dist/lifecycle/providerLifecycleMapping.js +0 -190
- package/dist/telemetry/telemetryRouter.js +0 -32
|
@@ -20,6 +20,14 @@ export async function terminateSessionOwners(owner, records, ports, options = {}
|
|
|
20
20
|
const pollMs = positiveDuration(options.pollMs, DEFAULT_TERMINATION_POLL_MS, "pollMs");
|
|
21
21
|
const now = ports.now();
|
|
22
22
|
ports.emit({ stage: "stop-requested", owner, at: now });
|
|
23
|
+
// A launch-fence scan is a point-in-time observation, not a durable owner
|
|
24
|
+
// inventory: /proc entries can disappear between the directory and
|
|
25
|
+
// environment reads. Retain every exact child identity observed during the
|
|
26
|
+
// stop so one later scan miss cannot be mistaken for physical zero.
|
|
27
|
+
const trackedChildren = new Map(records.map((record) => [record.launchId, new Map()]));
|
|
28
|
+
for (const record of records) {
|
|
29
|
+
refreshTrackedFencedChildren(record, ports, trackedChildren);
|
|
30
|
+
}
|
|
23
31
|
let gracefulOk = true;
|
|
24
32
|
try {
|
|
25
33
|
gracefulOk = await ports.gracefulStop(owner);
|
|
@@ -50,7 +58,7 @@ export async function terminateSessionOwners(owner, records, ports, options = {}
|
|
|
50
58
|
};
|
|
51
59
|
}
|
|
52
60
|
// Bounded graceful grace: poll the exact owned trees for exit.
|
|
53
|
-
if (await waitForTreesAbsent(records, ports, gracefulGraceMs, pollMs)) {
|
|
61
|
+
if (await waitForTreesAbsent(records, ports, trackedChildren, gracefulGraceMs, pollMs)) {
|
|
54
62
|
ports.emit({ stage: "stop-confirmed", owner, at: ports.now() });
|
|
55
63
|
return {
|
|
56
64
|
outcome: "stop-confirmed",
|
|
@@ -64,24 +72,24 @@ export async function terminateSessionOwners(owner, records, ports, options = {}
|
|
|
64
72
|
// safety requires pairing the PID with its start identity.
|
|
65
73
|
ports.emit({ stage: "forced-stop", owner, at: ports.now() });
|
|
66
74
|
for (const record of records) {
|
|
67
|
-
escalateRecord(record, ports, "SIGTERM");
|
|
75
|
+
escalateRecord(record, ports, trackedChildren, "SIGTERM");
|
|
68
76
|
}
|
|
69
|
-
await waitForTreesAbsent(records, ports, forcedGraceMs, pollMs);
|
|
77
|
+
await waitForTreesAbsent(records, ports, trackedChildren, forcedGraceMs, pollMs);
|
|
70
78
|
for (const record of records) {
|
|
71
|
-
escalateRecord(record, ports, "SIGKILL");
|
|
79
|
+
escalateRecord(record, ports, trackedChildren, "SIGKILL");
|
|
72
80
|
}
|
|
73
|
-
await waitForTreesAbsent(records, ports, forcedGraceMs, pollMs);
|
|
81
|
+
await waitForTreesAbsent(records, ports, trackedChildren, forcedGraceMs, pollMs);
|
|
74
82
|
const remaining = [];
|
|
75
83
|
const confirmed = [];
|
|
76
84
|
let verificationGap;
|
|
77
85
|
for (const record of records) {
|
|
78
|
-
const verdict = treeVerdict(record, ports);
|
|
86
|
+
const verdict = treeVerdict(record, ports, trackedChildren);
|
|
79
87
|
if (verdict.kind === "absent") {
|
|
80
88
|
confirmed.push(record);
|
|
81
89
|
continue;
|
|
82
90
|
}
|
|
83
91
|
if (verdict.kind === "gap") {
|
|
84
|
-
verificationGap = `/proc unreadable for
|
|
92
|
+
verificationGap = `/proc unreadable for launch ${record.launchId}`;
|
|
85
93
|
remaining.push({ record, detail: verificationGap });
|
|
86
94
|
continue;
|
|
87
95
|
}
|
|
@@ -137,19 +145,59 @@ function rootVerdict(record, ports) {
|
|
|
137
145
|
* `/proc` is unreadable, because that would claim physical zero without
|
|
138
146
|
* proof. The final verdict turns a persistent gap into `stop-blocked`.
|
|
139
147
|
*/
|
|
140
|
-
function treeVerdict(record, ports) {
|
|
148
|
+
function treeVerdict(record, ports, trackedChildren) {
|
|
149
|
+
const scanGap = refreshTrackedFencedChildren(record, ports, trackedChildren);
|
|
141
150
|
const root = rootVerdict(record, ports);
|
|
142
|
-
|
|
151
|
+
const children = trackedChildrenVerdict(record, ports, trackedChildren);
|
|
152
|
+
if (root.kind === "live" || children.kind === "live")
|
|
143
153
|
return { kind: "live" };
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (
|
|
154
|
+
if (root.kind === "gap" || children.kind === "gap" || scanGap)
|
|
155
|
+
return { kind: "gap" };
|
|
156
|
+
return { kind: "absent" };
|
|
157
|
+
}
|
|
158
|
+
function refreshTrackedFencedChildren(record, ports, trackedChildren) {
|
|
159
|
+
const tracked = trackedChildren.get(record.launchId);
|
|
160
|
+
if (tracked === undefined)
|
|
161
|
+
return true;
|
|
162
|
+
let verificationGap = false;
|
|
163
|
+
for (const pid of ports.listLaunchFencedProcesses(record.launchId)) {
|
|
164
|
+
if (pid === record.providerRoot.pid)
|
|
165
|
+
continue;
|
|
166
|
+
const identity = ports.processIdentity(pid);
|
|
167
|
+
if (identity === undefined) {
|
|
168
|
+
if (ports.procEntryExists(pid))
|
|
169
|
+
verificationGap = true;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (identity.state === "Z")
|
|
173
|
+
continue;
|
|
174
|
+
tracked.set(pid, identity.startIdentity);
|
|
175
|
+
}
|
|
176
|
+
return verificationGap;
|
|
177
|
+
}
|
|
178
|
+
function trackedChildrenVerdict(record, ports, trackedChildren) {
|
|
179
|
+
const tracked = trackedChildren.get(record.launchId);
|
|
180
|
+
if (tracked === undefined)
|
|
181
|
+
return { kind: "gap" };
|
|
182
|
+
let verificationGap = false;
|
|
183
|
+
for (const [pid, startIdentity] of tracked) {
|
|
184
|
+
const current = ports.processIdentity(pid);
|
|
185
|
+
if (current === undefined) {
|
|
186
|
+
if (ports.procEntryExists(pid)) {
|
|
187
|
+
verificationGap = true;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
tracked.delete(pid);
|
|
191
|
+
}
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (current.state === "Z" || current.startIdentity !== startIdentity) {
|
|
195
|
+
tracked.delete(pid);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
151
198
|
return { kind: "live" };
|
|
152
|
-
|
|
199
|
+
}
|
|
200
|
+
return verificationGap ? { kind: "gap" } : { kind: "absent" };
|
|
153
201
|
}
|
|
154
202
|
/**
|
|
155
203
|
* Signals the exact root (only when its identity is verified) and, while the
|
|
@@ -157,7 +205,7 @@ function treeVerdict(record, ports) {
|
|
|
157
205
|
* fenced child survives, each fenced child is signaled individually: the
|
|
158
206
|
* group can no longer be proven ours once its leader is gone.
|
|
159
207
|
*/
|
|
160
|
-
function escalateRecord(record, ports, signal) {
|
|
208
|
+
function escalateRecord(record, ports, trackedChildren, signal) {
|
|
161
209
|
const root = rootVerdict(record, ports);
|
|
162
210
|
if (root.kind === "live") {
|
|
163
211
|
try {
|
|
@@ -180,9 +228,17 @@ function escalateRecord(record, ports, signal) {
|
|
|
180
228
|
}
|
|
181
229
|
if (root.kind !== "absent")
|
|
182
230
|
return;
|
|
183
|
-
|
|
184
|
-
|
|
231
|
+
refreshTrackedFencedChildren(record, ports, trackedChildren);
|
|
232
|
+
const tracked = trackedChildren.get(record.launchId);
|
|
233
|
+
if (tracked === undefined)
|
|
234
|
+
return;
|
|
235
|
+
for (const [pid, startIdentity] of tracked) {
|
|
236
|
+
const current = ports.processIdentity(pid);
|
|
237
|
+
if (current === undefined
|
|
238
|
+
|| current.state === "Z"
|
|
239
|
+
|| current.startIdentity !== startIdentity) {
|
|
185
240
|
continue;
|
|
241
|
+
}
|
|
186
242
|
try {
|
|
187
243
|
ports.signalProcess(pid, signal);
|
|
188
244
|
}
|
|
@@ -191,10 +247,10 @@ function escalateRecord(record, ports, signal) {
|
|
|
191
247
|
}
|
|
192
248
|
}
|
|
193
249
|
}
|
|
194
|
-
async function waitForTreesAbsent(records, ports, timeoutMs, pollMs) {
|
|
250
|
+
async function waitForTreesAbsent(records, ports, trackedChildren, timeoutMs, pollMs) {
|
|
195
251
|
const start = Date.now();
|
|
196
252
|
for (;;) {
|
|
197
|
-
if (records.every((record) => treeVerdict(record, ports).kind === "absent")) {
|
|
253
|
+
if (records.every((record) => treeVerdict(record, ports, trackedChildren).kind === "absent")) {
|
|
198
254
|
return true;
|
|
199
255
|
}
|
|
200
256
|
if (Date.now() - start >= timeoutMs)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { createRuntimeBinding } from "./runtimeBinding.js";
|
|
3
3
|
import { normalizeRuntimeOwner } from "./runtimeOwner.js";
|
|
4
|
+
import { RuntimeHostContentionError } from "./ports.js";
|
|
4
5
|
import { requireSafeIdentity } from "./validation.js";
|
|
5
6
|
/**
|
|
6
7
|
* Runtime lifecycle adapter for the current tmux host. The returned hostRef is
|
|
@@ -145,6 +146,40 @@ export class TmuxSessionHost {
|
|
|
145
146
|
async #launchUnlocked(request, hostId, beforeHostStart) {
|
|
146
147
|
// Validate generated identity before starting an external process.
|
|
147
148
|
const bindingId = requireSafeIdentity(this.#createBindingId(), "Runtime binding id");
|
|
149
|
+
const writableHumanAttached = request.owner.scope === "task"
|
|
150
|
+
&& request.runId !== undefined
|
|
151
|
+
&& (this.tmux.hasWritableClientAsync !== undefined
|
|
152
|
+
? await this.tmux.hasWritableClientAsync(hostId, request.owner.roleName)
|
|
153
|
+
: this.tmux.hasWritableClient?.(hostId, request.owner.roleName) === true);
|
|
154
|
+
if (writableHumanAttached) {
|
|
155
|
+
throw new RuntimeHostContentionError("writable-client", `A writable human is attached to ${request.owner.taskId}/${request.owner.roleName}.`);
|
|
156
|
+
}
|
|
157
|
+
if (request.owner.scope === "task"
|
|
158
|
+
&& request.adapterId === "claude"
|
|
159
|
+
&& request.runId !== undefined
|
|
160
|
+
&& await probeRoleStatus(this.tmux, hostId, request.owner.roleName) === "running") {
|
|
161
|
+
// Managed Claude is process-per-Run. A live Role window here belongs to
|
|
162
|
+
// an earlier process (or to recovery of the exact reserved Run); never
|
|
163
|
+
// plan, persist pre-start state, or inject input into it. The coordinator
|
|
164
|
+
// decides between same-generation recovery and a retry after natural
|
|
165
|
+
// exit from the durable reservation identity.
|
|
166
|
+
return createRuntimeBinding({
|
|
167
|
+
id: bindingId,
|
|
168
|
+
launchId: request.launchId,
|
|
169
|
+
owner: request.owner,
|
|
170
|
+
agentId: request.agentId,
|
|
171
|
+
adapterId: request.adapterId,
|
|
172
|
+
hostRef: encodeHostRef({
|
|
173
|
+
scope: request.owner.scope,
|
|
174
|
+
hostId,
|
|
175
|
+
roleName: request.owner.roleName
|
|
176
|
+
}),
|
|
177
|
+
hostCreated: false,
|
|
178
|
+
...(request.mode === "resume"
|
|
179
|
+
? { nativeSessionId: request.nativeSessionId }
|
|
180
|
+
: {})
|
|
181
|
+
});
|
|
182
|
+
}
|
|
148
183
|
const input = {
|
|
149
184
|
roleName: request.owner.roleName,
|
|
150
185
|
agentId: request.agentId,
|
|
@@ -3,8 +3,7 @@ import { isSchedulerTaskWorkspaceReady } from "./ports.js";
|
|
|
3
3
|
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
4
4
|
import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain } from "../executor/effectiveLaunch.js";
|
|
5
5
|
import { RuntimeLaunchError } from "../runtime/ports.js";
|
|
6
|
-
import {
|
|
7
|
-
import { preInputReadinessCapability } from "../lifecycle/providerLifecycleMapping.js";
|
|
6
|
+
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
8
7
|
/**
|
|
9
8
|
* Delivers durable Work AgentRuns before liveness reconciliation. Task command
|
|
10
9
|
* handlers only record intent; this Controller path is the sole automated
|
|
@@ -95,8 +94,9 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
|
|
|
95
94
|
preStartFencePersisted = true;
|
|
96
95
|
}
|
|
97
96
|
});
|
|
98
|
-
// A
|
|
99
|
-
// launch
|
|
97
|
+
// A managed host may already have submitted the exact Run prompt as
|
|
98
|
+
// part of process launch. Once preparation returns that transport
|
|
99
|
+
// fact, any
|
|
100
100
|
// later readiness or aggregate-write failure is delivery uncertainty,
|
|
101
101
|
// not a launch failure: preserve the Run and its reservation for the
|
|
102
102
|
// matching provider Hook instead of terminalizing it.
|
|
@@ -109,9 +109,7 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
|
|
|
109
109
|
// pre-readiness Session falls back to the existing durable Session;
|
|
110
110
|
// fresh runtime-discovered providers intentionally persist `null`.
|
|
111
111
|
if (!preStartFencePersisted) {
|
|
112
|
-
const preparedFenceSession = prepared.session
|
|
113
|
-
? existingSession
|
|
114
|
-
: validateReadySession(role, run.effective, existingSession, run.mode, { prepared, session: prepared.session });
|
|
112
|
+
const preparedFenceSession = validateLaunchSubmittedRecoverySession(role, run.effective, existingSession, run.mode, prepared, prepared.session);
|
|
115
113
|
preparedSession = preparedFenceSession;
|
|
116
114
|
store.saveRoleRunPrepared({
|
|
117
115
|
task,
|
|
@@ -125,7 +123,7 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
|
|
|
125
123
|
const ready = await delivery.waitUntilReady(prepared);
|
|
126
124
|
deliveryAttempted = deliveryAttempted
|
|
127
125
|
|| ready.prepared.inputSubmittedAtLaunch === true;
|
|
128
|
-
const session =
|
|
126
|
+
const session = validateLaunchSubmittedRecoverySession(role, run.effective, existingSession, run.mode, ready.prepared, ready.session);
|
|
129
127
|
preparedSession = session;
|
|
130
128
|
store.saveRoleRunPrepared({
|
|
131
129
|
task,
|
|
@@ -178,7 +176,8 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
|
|
|
178
176
|
// screen scrape, or pane/PID inference — and fails closed for a
|
|
179
177
|
// supported adapter whose readiness cannot be confirmed.
|
|
180
178
|
if (ready.prepared.sessionStarted
|
|
181
|
-
&&
|
|
179
|
+
&& builtinAgentDriverRegistry().requireByAdapterId(run.effective.adapterId)
|
|
180
|
+
.capabilities.observation.preInputReadiness === "exact"
|
|
182
181
|
&& !providerReadyForPush(store, {
|
|
183
182
|
taskId: task.id,
|
|
184
183
|
roleName: role.name,
|
|
@@ -232,14 +231,15 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
|
|
|
232
231
|
if (error instanceof RuntimeLaunchError) {
|
|
233
232
|
const terminalFailure = roleRunDeliveryFailure(run, processing.batchId, existingSession, error.launchId);
|
|
234
233
|
if (error.retryable) {
|
|
234
|
+
const writerAttached = error.reason === "writable-client";
|
|
235
235
|
results.push({
|
|
236
236
|
taskId: task.id,
|
|
237
237
|
roleName: role.name,
|
|
238
238
|
runId: run.id,
|
|
239
239
|
status: "skipped",
|
|
240
|
-
reason: "runtime-unavailable",
|
|
240
|
+
reason: writerAttached ? "writer-attached" : "runtime-unavailable",
|
|
241
241
|
error: message,
|
|
242
|
-
terminalFailure
|
|
242
|
+
...(writerAttached ? {} : { terminalFailure })
|
|
243
243
|
});
|
|
244
244
|
continue;
|
|
245
245
|
}
|
|
@@ -364,8 +364,23 @@ function preflightSession(role, effective, existing, mode, preflight) {
|
|
|
364
364
|
};
|
|
365
365
|
return validateRoleSession(role, effective, existing, mode, session);
|
|
366
366
|
}
|
|
367
|
-
|
|
368
|
-
|
|
367
|
+
/**
|
|
368
|
+
* A Controller restart can recover an exact launch-submitted Run while its
|
|
369
|
+
* finite provider process is still alive. The host intentionally does not
|
|
370
|
+
* re-plan or re-submit that Run and therefore may return no native identity;
|
|
371
|
+
* retain only the durable Session fenced to the same launch generation.
|
|
372
|
+
*/
|
|
373
|
+
function validateLaunchSubmittedRecoverySession(role, effective, existing, mode, prepared, session) {
|
|
374
|
+
if (session === null
|
|
375
|
+
&& prepared.inputSubmittedAtLaunch === true
|
|
376
|
+
&& prepared.sessionStarted === false
|
|
377
|
+
&& existing?.launchId !== undefined
|
|
378
|
+
&& existing.launchId === prepared.launchId) {
|
|
379
|
+
return validateRoleSession(role, effective, existing, mode, existing);
|
|
380
|
+
}
|
|
381
|
+
return session === undefined
|
|
382
|
+
? existing
|
|
383
|
+
: validateRoleSession(role, effective, existing, mode, session);
|
|
369
384
|
}
|
|
370
385
|
function validateRoleSession(role, effective, existing, mode, session) {
|
|
371
386
|
if (mode === "new" && session === null)
|
|
@@ -7,6 +7,7 @@ import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/life
|
|
|
7
7
|
import { recordLeaderFailure } from "./leaderFailure.js";
|
|
8
8
|
import { createLeaderRecoveryNotification } from "./operatorNotification.js";
|
|
9
9
|
import { isSchedulerTaskWorkspaceReady } from "./ports.js";
|
|
10
|
+
import { RuntimeLaunchError } from "../runtime/ports.js";
|
|
10
11
|
export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
11
12
|
const results = [];
|
|
12
13
|
const wakeups = selection === undefined || selection.full
|
|
@@ -152,8 +153,9 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
152
153
|
preStartFencePersisted = true;
|
|
153
154
|
}
|
|
154
155
|
});
|
|
155
|
-
// A
|
|
156
|
-
// launch
|
|
156
|
+
// A managed host may already have submitted the exact Run prompt as
|
|
157
|
+
// part of process launch. Once preparation returns that transport
|
|
158
|
+
// fact, any
|
|
157
159
|
// later readiness or aggregate-write failure is delivery uncertainty,
|
|
158
160
|
// not a launch failure: preserve the Run and its reservation for the
|
|
159
161
|
// matching provider Hook instead of terminalizing it.
|
|
@@ -262,6 +264,23 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
262
264
|
const detail = error instanceof Error ? error.message : String(error);
|
|
263
265
|
const message = `Leader dispatch failed: ${detail}`;
|
|
264
266
|
if (claimed && run !== null) {
|
|
267
|
+
// A finite managed provider from the preceding Run may still be
|
|
268
|
+
// exiting after its durable yield. Keep this newly-claimed Run as the
|
|
269
|
+
// sole owner of the Role mailbox; active-Run delivery will retry it
|
|
270
|
+
// after the old host disappears. This is runtime backpressure, not a
|
|
271
|
+
// Leader failure and not grounds for allocating another Run.
|
|
272
|
+
if (error instanceof RuntimeLaunchError && error.retryable) {
|
|
273
|
+
results.push({
|
|
274
|
+
taskId: task.id,
|
|
275
|
+
runId: run.id,
|
|
276
|
+
status: "skipped",
|
|
277
|
+
reason: error.reason === "writable-client"
|
|
278
|
+
? "writer-attached"
|
|
279
|
+
: "not-ready",
|
|
280
|
+
error: message
|
|
281
|
+
});
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
265
284
|
// Once delivery begins, a send may have succeeded even when receipt
|
|
266
285
|
// observation or the aggregate write failed. Preserve the exact
|
|
267
286
|
// durable Run and let receipt-backed active delivery recover it.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { selectedSchedulerRoles, selectedSchedulerTasks } from "./ports.js";
|
|
2
2
|
import { formatTaskRecordReference } from "../task/taskRecordReference.js";
|
|
3
3
|
import { queueLeaderWakeup } from "./wakeupQueue.js";
|
|
4
|
-
import { currentRoleRunProgressAt,
|
|
4
|
+
import { currentRoleRunProgressAt, DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS } from "./roleRunStall.js";
|
|
5
5
|
export const EXITED_ROLE_RUN_SUMMARY = "The role's tmux session exited before the run yielded.";
|
|
6
6
|
/**
|
|
7
7
|
* Lightweight liveness only: an active AgentRun whose tmux role is absent is
|
|
@@ -143,5 +143,5 @@ function isResourceCandidate(task, run, now) {
|
|
|
143
143
|
}
|
|
144
144
|
const deliveredAt = Date.parse(run.deliveredAt);
|
|
145
145
|
return Number.isFinite(deliveredAt)
|
|
146
|
-
&& now.getTime() - deliveredAt >=
|
|
146
|
+
&& now.getTime() - deliveredAt >= DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS;
|
|
147
147
|
}
|