@zq-silk/yui 0.13.4 → 0.13.5
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 +8 -8
- package/dist/cli/commandCatalog.js +22 -21
- package/dist/cli/interactionPolicy.js +0 -14
- package/dist/cli.js +42 -0
- package/dist/commands/executionAuditCommands.js +1 -1
- package/dist/commands/taskCommands.js +60 -326
- package/dist/commands/taskContextCommand.js +3 -14
- package/dist/commands/taskExecutionCommands.js +254 -0
- package/dist/commands/taskNextActionCommand.js +1 -3
- package/dist/commands/taskOverviewCommand.js +9 -2
- package/dist/commands/taskRoleRuntimeStatus.js +2 -25
- package/dist/controller/agentRuntimeObserver.js +4 -2
- package/dist/controller/clientRuntime.js +45 -2
- package/dist/controller/controller.js +6 -3
- package/dist/controller/fileSchedulerStoreAdapter.js +59 -188
- package/dist/controller/jobControl.js +3 -2
- package/dist/controller/runtime.js +6 -33
- package/dist/controller/runtimeEventProcessor.js +8 -4
- package/dist/controller/runtimeHookRunFence.js +4 -10
- package/dist/execution/executionHealth.js +8 -16
- package/dist/executor/agentExecutor.js +13 -14
- package/dist/executor/fileRoleLaunchPlanner.js +9 -16
- package/dist/lifecycle/exactRunTerminalization.js +24 -322
- package/dist/repository/taskWorkspaceCoordinator.js +0 -9
- package/dist/runtime/agentHost.js +20 -80
- package/dist/runtime/exactControlPlane.js +15 -9
- package/dist/runtime/providerContinuationReconciliationService.js +1 -1
- package/dist/runtime/providerRecoveryDecision.js +1 -1
- package/dist/runtime/providerRuntimeIdentity.js +25 -15
- package/dist/scheduler/activeRoleRunDelivery.js +1 -19
- package/dist/scheduler/leaderWakeupProcessor.js +12 -63
- package/dist/scheduler/ports.js +3 -2
- package/dist/scheduler/roleRunLiveness.js +4 -1
- package/dist/scheduler/roleRunStall.js +0 -2
- package/dist/scheduler/taskExecutionProjection.js +18 -1
- package/dist/scheduler/wakeupQueue.js +2 -1
- package/dist/storage/migration/productionRegistry.js +65 -0
- package/dist/storage/sqliteStore.js +10 -2
- package/dist/storage/taskStore.js +11 -3
- package/dist/task/completionReadiness.js +0 -67
- package/dist/task/nextAction.js +16 -32
- package/dist/task/task.js +38 -3
- package/dist/web/assets/client/i18n.js +0 -4
- package/dist/web/assets/client/view.js +0 -18
- package/dist/web/webSnapshot.js +7 -13
- package/package.json +1 -1
- package/dist/run/recoveryProjection.js +0 -252
- package/dist/runtime/conversationSwitch.js +0 -277
|
@@ -14,7 +14,7 @@ import { ProviderDeliveryUnknownError, ProviderConversationMissingError, Provide
|
|
|
14
14
|
import { sameProviderAuthorityFence, validateProviderAuthorityFence } from "./providerAuthorityFence.js";
|
|
15
15
|
import { validateRuntimeProcessExitObservation } from "./processExitObservation.js";
|
|
16
16
|
import { persistRuntimeProcessExitObservation, replayRuntimeProcessExitOutbox } from "./processExitOutbox.js";
|
|
17
|
-
import { readRuntimeStopReceipt, removeRuntimeStopReceipt
|
|
17
|
+
import { readRuntimeStopReceipt, removeRuntimeStopReceipt } from "./runtimeStopReceipt.js";
|
|
18
18
|
import { AGENT_HOST_CONTROL_TIMEOUT_MS, AGENT_HOST_READY_TIMEOUT_MS } from "./runtimeDeadlines.js";
|
|
19
19
|
export const AGENT_HOST_CONTROL_PROTOCOL = "yui-agent-host/v2";
|
|
20
20
|
const HOST_CONTROL_MAX_BYTES = 32 * 1024;
|
|
@@ -34,7 +34,6 @@ export async function runAgentHost(input) {
|
|
|
34
34
|
let activeNativeTurnId;
|
|
35
35
|
let codexClientAttachedAt;
|
|
36
36
|
let consecutiveCodexDisconnects = 0;
|
|
37
|
-
const switchDetachedSessions = new WeakSet();
|
|
38
37
|
let activationId;
|
|
39
38
|
let conversationRecoverability = "unknown";
|
|
40
39
|
let authority;
|
|
@@ -228,8 +227,7 @@ export async function runAgentHost(input) {
|
|
|
228
227
|
const reconnectableCodexClient = ownsCurrentSession
|
|
229
228
|
&& providerSession.adapterId === "codex"
|
|
230
229
|
&& !hostStopRequested
|
|
231
|
-
&& stopReceipt === null
|
|
232
|
-
&& !switchDetachedSessions.has(providerSession);
|
|
230
|
+
&& stopReceipt === null;
|
|
233
231
|
if (reconnectableCodexClient) {
|
|
234
232
|
session = undefined;
|
|
235
233
|
if (codexClientAttachedAt !== undefined
|
|
@@ -274,21 +272,19 @@ export async function runAgentHost(input) {
|
|
|
274
272
|
hostSequence += 1;
|
|
275
273
|
const observedAt = new Date().toISOString();
|
|
276
274
|
const failures = [];
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
failures.push(`activation terminal: ${errorText(error)}`);
|
|
291
|
-
}
|
|
275
|
+
try {
|
|
276
|
+
await publishStructuredProviderActivationTerminal({
|
|
277
|
+
home: input.home,
|
|
278
|
+
environment: currentPayload.environment,
|
|
279
|
+
conversationId: providerSession.conversationId,
|
|
280
|
+
nativeSessionId: providerSession.nativeSessionId,
|
|
281
|
+
activationId: currentActivationId,
|
|
282
|
+
status: stopReceipt !== null || hostStopRequested ? "ended" : "failed",
|
|
283
|
+
observedAt
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
failures.push(`activation terminal: ${errorText(error)}`);
|
|
292
288
|
}
|
|
293
289
|
let exitPersisted = false;
|
|
294
290
|
try {
|
|
@@ -357,10 +353,12 @@ export async function runAgentHost(input) {
|
|
|
357
353
|
}
|
|
358
354
|
const requestedAuthority = validateProviderAuthorityFence(providerControl.authority);
|
|
359
355
|
const replacesCurrentConversation = session !== undefined && providerControl.kind === "new";
|
|
360
|
-
if (
|
|
356
|
+
if (replacesCurrentConversation) {
|
|
357
|
+
throw new Error("Agent Host cannot replace a live Provider Session; stop it before starting a fresh Session.");
|
|
358
|
+
}
|
|
359
|
+
if (authority === undefined)
|
|
361
360
|
authority = requestedAuthority;
|
|
362
|
-
else if (
|
|
363
|
-
&& authority !== undefined
|
|
361
|
+
else if (authority !== undefined
|
|
364
362
|
&& !sameProviderAuthorityFence(authority, requestedAuthority)) {
|
|
365
363
|
throw new Error("Agent Host launch carries a stale Provider authority fence.");
|
|
366
364
|
}
|
|
@@ -380,43 +378,6 @@ export async function runAgentHost(input) {
|
|
|
380
378
|
let providerAcceptedAttemptId;
|
|
381
379
|
let durableInitialTurn;
|
|
382
380
|
try {
|
|
383
|
-
if (replacesCurrentConversation) {
|
|
384
|
-
const previousSession = session;
|
|
385
|
-
const previousPayload = sessionPayload;
|
|
386
|
-
const previousActivationId = activationId ?? previousPayload?.launchId;
|
|
387
|
-
if (previousPayload === undefined || previousActivationId === undefined
|
|
388
|
-
|| authority?.owner !== "controller") {
|
|
389
|
-
throw new Error("Agent Host cannot detach an inexact Provider Activation.");
|
|
390
|
-
}
|
|
391
|
-
await detachDurableProviderForConversationSwitch(input.home, {
|
|
392
|
-
taskId: requiredEnvironment(next.environment.YUI_TASK_ID, "Task id"),
|
|
393
|
-
roleName: requiredEnvironment(next.environment.YUI_ROLE, "Role name"),
|
|
394
|
-
runId: requiredEnvironment(next.environment.YUI_RUN_ID, "Run id"),
|
|
395
|
-
agentId: requiredEnvironment(next.environment.YUI_AGENT_ID, "Agent id"),
|
|
396
|
-
launchId: next.launchId,
|
|
397
|
-
previousConversationId: previousSession.conversationId,
|
|
398
|
-
previousNativeSessionId: previousSession.nativeSessionId,
|
|
399
|
-
previousActivationId,
|
|
400
|
-
nextAuthorityEpoch: requestedAuthority.epoch,
|
|
401
|
-
nextAuthorityHolderId: requestedAuthority.holderId,
|
|
402
|
-
observedAt: new Date().toISOString()
|
|
403
|
-
});
|
|
404
|
-
switchDetachedSessions.add(previousSession);
|
|
405
|
-
// The persistent Provider child belongs to the Activation that first
|
|
406
|
-
// created it, not to the most recent resume Run payload. The exit
|
|
407
|
-
// observer carries that Activation launch id, so the stop receipt must
|
|
408
|
-
// use the same identity or the expected switch detach is misclassified
|
|
409
|
-
// as an abnormal child exit and generic cleanup can kill the Host.
|
|
410
|
-
writeRuntimeStopReceipt(input.home, previousActivationId, new Date());
|
|
411
|
-
authority = undefined;
|
|
412
|
-
await terminateProviderSessionForConversationSwitch(previousSession);
|
|
413
|
-
if (session === previousSession)
|
|
414
|
-
session = undefined;
|
|
415
|
-
sessionPayload = undefined;
|
|
416
|
-
activationId = undefined;
|
|
417
|
-
conversationRecoverability = "unknown";
|
|
418
|
-
authority = requestedAuthority;
|
|
419
|
-
}
|
|
420
381
|
if (session !== undefined) {
|
|
421
382
|
if (session.adapterId !== providerControl.adapterId
|
|
422
383
|
|| providerControl.mode !== "resume"
|
|
@@ -1123,9 +1084,6 @@ async function beginDurableProviderTurn(home, durableTurn) {
|
|
|
1123
1084
|
throw error;
|
|
1124
1085
|
}
|
|
1125
1086
|
}
|
|
1126
|
-
async function detachDurableProviderForConversationSwitch(home, request) {
|
|
1127
|
-
await callControllerIdempotently(home, "runtime.conversation-switch-detach", request);
|
|
1128
|
-
}
|
|
1129
1087
|
async function callControllerIdempotently(home, method, request) {
|
|
1130
1088
|
try {
|
|
1131
1089
|
await callAgentController(home, method, request);
|
|
@@ -1152,24 +1110,6 @@ async function callControllerIdempotently(home, method, request) {
|
|
|
1152
1110
|
class ControllerAcknowledgementUnknownError extends Error {
|
|
1153
1111
|
name = "ControllerAcknowledgementUnknownError";
|
|
1154
1112
|
}
|
|
1155
|
-
async function terminateProviderSessionForConversationSwitch(providerSession) {
|
|
1156
|
-
providerSession.terminate("SIGTERM");
|
|
1157
|
-
const forceKill = setTimeout(() => providerSession.terminate("SIGKILL"), 3_000);
|
|
1158
|
-
forceKill.unref();
|
|
1159
|
-
let hardTimeout;
|
|
1160
|
-
const timeout = new Promise((_resolve, reject) => {
|
|
1161
|
-
hardTimeout = setTimeout(() => reject(new Error("Old Provider Activation did not exit after its switch authority was revoked.")), 8_000);
|
|
1162
|
-
hardTimeout.unref();
|
|
1163
|
-
});
|
|
1164
|
-
try {
|
|
1165
|
-
await Promise.race([providerSession.waitForExit(), timeout]);
|
|
1166
|
-
}
|
|
1167
|
-
finally {
|
|
1168
|
-
clearTimeout(forceKill);
|
|
1169
|
-
if (hardTimeout !== undefined)
|
|
1170
|
-
clearTimeout(hardTimeout);
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
1113
|
async function resolveProviderTurnSubmission(home, durableTurn, error) {
|
|
1174
1114
|
const attemptId = durableTurn.attemptId;
|
|
1175
1115
|
if (typeof attemptId !== "string") {
|
|
@@ -268,7 +268,9 @@ export function assertExactTaskRuntimeEnvironment(runtimeSource, environment, ex
|
|
|
268
268
|
/** Fences a descriptor to the one currently active durable Task runtime. */
|
|
269
269
|
export function assertExactTaskRuntimeState(runtime, store, options = {}) {
|
|
270
270
|
const task = store.getTask(runtime.taskId);
|
|
271
|
-
if (task === null
|
|
271
|
+
if (task === null
|
|
272
|
+
|| task.status !== "active"
|
|
273
|
+
|| task.executionGate.state !== "enabled") {
|
|
272
274
|
throw new Error("Exact Task runtime Task is not current and active.");
|
|
273
275
|
}
|
|
274
276
|
const role = store.getRole(runtime.taskId, runtime.roleName);
|
|
@@ -304,17 +306,21 @@ export function assertExactTaskRuntimeState(runtime, store, options = {}) {
|
|
|
304
306
|
&& session?.launchId === runtime.launchId;
|
|
305
307
|
const executionRef = lifecycleMailbox?.processing?.executionRef;
|
|
306
308
|
const preallocated = options.preallocatedDriverSessionReservation;
|
|
307
|
-
const
|
|
308
|
-
&& runtime.adapterId === preallocated.adapterId
|
|
309
|
-
&& runtime.runId !== undefined
|
|
309
|
+
const exactRunLaunchReservation = runtime.runId !== undefined
|
|
310
310
|
&& runtime.launchId !== undefined
|
|
311
|
-
&& runtime.nativeSessionId !== undefined
|
|
312
|
-
&& session === undefined
|
|
313
311
|
&& reservation
|
|
314
312
|
&& !hasRuntimeCleanupObligation(lifecycleMailbox)
|
|
315
313
|
&& executionRef?.type === "run"
|
|
316
314
|
&& executionRef.taskId === runtime.taskId
|
|
317
|
-
&& executionRef.id === runtime.runId
|
|
315
|
+
&& executionRef.id === runtime.runId;
|
|
316
|
+
const terminalSessionReplacementReservation = exactRunLaunchReservation
|
|
317
|
+
&& session !== undefined
|
|
318
|
+
&& (session.status === "stopped" || session.status === "broken");
|
|
319
|
+
const exactPreallocatedReservation = preallocated !== undefined
|
|
320
|
+
&& runtime.adapterId === preallocated.adapterId
|
|
321
|
+
&& exactRunLaunchReservation
|
|
322
|
+
&& runtime.nativeSessionId !== undefined
|
|
323
|
+
&& (session === undefined || terminalSessionReplacementReservation)
|
|
318
324
|
&& runtime.nativeSessionId === nativeSessionIdForLaunch(preallocated.yuiHome, runtime.launchId, runtime.agentId, runtime.adapterId);
|
|
319
325
|
const preallocatedBeforeInFlightProjection = exactPreallocatedReservation
|
|
320
326
|
&& (sessions?.inFlight === null || sessions?.inFlight === undefined);
|
|
@@ -327,11 +333,11 @@ export function assertExactTaskRuntimeState(runtime, store, options = {}) {
|
|
|
327
333
|
throw new Error("Exact Task runtime launch fence is not current.");
|
|
328
334
|
}
|
|
329
335
|
if (runtime.nativeSessionId === undefined) {
|
|
330
|
-
if (session !== undefined) {
|
|
336
|
+
if (session !== undefined && !terminalSessionReplacementReservation) {
|
|
331
337
|
throw new Error("Exact Task runtime native Session fence is missing.");
|
|
332
338
|
}
|
|
333
339
|
}
|
|
334
|
-
else if (session === undefined) {
|
|
340
|
+
else if (session === undefined || terminalSessionReplacementReservation) {
|
|
335
341
|
if (!exactPreallocatedReservation) {
|
|
336
342
|
throw new Error("Exact Task runtime native Session fence is not current.");
|
|
337
343
|
}
|
|
@@ -20,7 +20,7 @@ export class ProviderContinuationReconciliationService {
|
|
|
20
20
|
}
|
|
21
21
|
async reconcile(now) {
|
|
22
22
|
const changedTaskIds = new Set();
|
|
23
|
-
for (const task of this.store.listTasks().filter((entry) => entry.status === "active")) {
|
|
23
|
+
for (const task of this.store.listTasks().filter((entry) => (entry.status === "active" && entry.executionGate.state === "enabled"))) {
|
|
24
24
|
const events = this.store.listEvents(task.id);
|
|
25
25
|
const groups = groupDetachedContinuations(projectProviderContinuations(events), events.map(runtimeObservationFromTaskEvent)
|
|
26
26
|
.filter((entry) => entry !== null));
|
|
@@ -47,7 +47,7 @@ export function decideProviderRecovery(input) {
|
|
|
47
47
|
reason: "Provider Conversation is missing but its Activation writer has not ended."
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
-
return { action: "
|
|
50
|
+
return { action: "restart-run", conversationId: conversation.conversationId };
|
|
51
51
|
}
|
|
52
52
|
function providerTurnIsUnsettled(binding) {
|
|
53
53
|
return binding.turn !== null
|
|
@@ -294,21 +294,30 @@ export function updateProviderConversationRecoverability(raw, recoverability) {
|
|
|
294
294
|
export function supersedeProviderConversation(raw, input) {
|
|
295
295
|
const binding = validateProviderRuntimeBinding(raw);
|
|
296
296
|
const current = currentProviderConversation(binding);
|
|
297
|
-
const basis = input.basis
|
|
298
|
-
if (basis !== "
|
|
299
|
-
throw new Error("Provider Conversation
|
|
297
|
+
const basis = input.basis;
|
|
298
|
+
if (basis !== "terminal-session") {
|
|
299
|
+
throw new Error("Provider Conversation replacement basis is invalid.");
|
|
300
300
|
}
|
|
301
|
-
|
|
302
|
-
throw new Error("Current Provider Conversation is not exactly unrecoverable.");
|
|
303
|
-
}
|
|
304
|
-
if (!input.noUnsettledInputDelivery) {
|
|
305
|
-
throw new Error("Cannot replace a Provider Conversation with unsettled input delivery.");
|
|
306
|
-
}
|
|
307
|
-
if (binding.authority.owner !== "none" || currentProviderActivation(binding) !== null) {
|
|
308
|
-
throw new Error("Cannot replace a Provider Conversation while its writer umbrella is owned.");
|
|
309
|
-
}
|
|
310
|
-
const switchedAt = timestamp(input.switchedAt, "Provider Conversation switch timestamp");
|
|
301
|
+
const switchedAt = timestamp(input.switchedAt, "Provider Conversation replacement timestamp");
|
|
311
302
|
const epoch = current.epoch + 1;
|
|
303
|
+
const terminalReason = "terminal-session-replaced";
|
|
304
|
+
const activations = binding.activations.map((entry) => entry.status === "active"
|
|
305
|
+
? {
|
|
306
|
+
...entry,
|
|
307
|
+
status: "failed",
|
|
308
|
+
endedAt: switchedAt,
|
|
309
|
+
terminalReason
|
|
310
|
+
}
|
|
311
|
+
: entry);
|
|
312
|
+
const turn = binding.turn !== null
|
|
313
|
+
&& ["submitting", "accepted", "running", "delivery-unknown"].includes(binding.turn.status)
|
|
314
|
+
? {
|
|
315
|
+
...binding.turn,
|
|
316
|
+
status: binding.turn.turnId === undefined ? "rejected" : "failed",
|
|
317
|
+
updatedAt: switchedAt,
|
|
318
|
+
terminalReason
|
|
319
|
+
}
|
|
320
|
+
: binding.turn;
|
|
312
321
|
return validateProviderRuntimeBinding({
|
|
313
322
|
...binding,
|
|
314
323
|
currentConversationEpoch: epoch,
|
|
@@ -324,7 +333,7 @@ export function supersedeProviderConversation(raw, input) {
|
|
|
324
333
|
createdAt: switchedAt
|
|
325
334
|
}
|
|
326
335
|
],
|
|
327
|
-
activations: [...
|
|
336
|
+
activations: [...activations, {
|
|
328
337
|
activationId: identity(input.activationId, "Provider Activation id"),
|
|
329
338
|
conversationId: input.conversationId,
|
|
330
339
|
generation: 1,
|
|
@@ -336,7 +345,8 @@ export function supersedeProviderConversation(raw, input) {
|
|
|
336
345
|
owner: "controller",
|
|
337
346
|
holderId: input.activationId,
|
|
338
347
|
changedAt: switchedAt
|
|
339
|
-
}
|
|
348
|
+
},
|
|
349
|
+
turn
|
|
340
350
|
});
|
|
341
351
|
}
|
|
342
352
|
export function validateProviderRuntimeBinding(value) {
|
|
@@ -8,7 +8,7 @@ import { RuntimeLaunchFailure } from "../runtime/launchDiagnostics.js";
|
|
|
8
8
|
import { mailboxHasWork, nextPendingBatch } from "../coordination/workMailbox.js";
|
|
9
9
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
10
10
|
import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
11
|
-
import {
|
|
11
|
+
import { RuntimeLifecycleBusyError } from "../runtime/lifecycleReservation.js";
|
|
12
12
|
/**
|
|
13
13
|
* Delivers durable Work AgentRuns before liveness reconciliation. Task command
|
|
14
14
|
* handlers only record intent; this Controller path is the sole automated
|
|
@@ -48,24 +48,6 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
|
|
|
48
48
|
});
|
|
49
49
|
continue;
|
|
50
50
|
}
|
|
51
|
-
// Single-flight: a Role runtime lifecycle lane that already holds a
|
|
52
|
-
// launch reservation or cleanup obligation must not be entered by a
|
|
53
|
-
// second delivery. The Run stays active-unpushed and is retried after
|
|
54
|
-
// the lane settles; the contention is never terminalized as a failure.
|
|
55
|
-
if (hasRuntimeLifecycleWork(store.getWorkMailbox(runtimeLifecycleTarget({
|
|
56
|
-
scope: "task",
|
|
57
|
-
taskId: task.id,
|
|
58
|
-
roleName: role.name
|
|
59
|
-
})))) {
|
|
60
|
-
results.push({
|
|
61
|
-
taskId: task.id,
|
|
62
|
-
roleName: role.name,
|
|
63
|
-
runId: run.id,
|
|
64
|
-
status: "skipped",
|
|
65
|
-
reason: "runtime-unavailable"
|
|
66
|
-
});
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
51
|
const existingSession = store.getRoleSession(task.id, role.name, run.effective.agentId);
|
|
70
52
|
const receiptId = agentRunDeliveryReceiptId(run);
|
|
71
53
|
const target = { kind: "role", taskId: task.id, roleName: role.name };
|
|
@@ -2,11 +2,11 @@ import { createAgentRun } from "../run/agentRun.js";
|
|
|
2
2
|
import { createRunAssignment } from "../context/runContextContract.js";
|
|
3
3
|
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
4
4
|
import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskSession } from "../executor/effectiveLaunch.js";
|
|
5
|
-
import {
|
|
5
|
+
import { roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
|
|
6
|
+
import { RuntimeLifecycleBusyError } from "../runtime/lifecycleReservation.js";
|
|
6
7
|
import { recordLeaderFailure } from "./leaderFailure.js";
|
|
7
8
|
import { isSchedulerTaskWorkspaceReady } from "./ports.js";
|
|
8
9
|
import { RuntimeLaunchError } from "../runtime/ports.js";
|
|
9
|
-
import { roleSessionDispatchModeWithConversationSwitch } from "../runtime/conversationSwitch.js";
|
|
10
10
|
export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
11
11
|
const results = [];
|
|
12
12
|
const wakeups = selection === undefined || selection.full
|
|
@@ -20,7 +20,10 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
20
20
|
for (const wakeup of wakeups) {
|
|
21
21
|
const task = store.getTask(wakeup.taskId);
|
|
22
22
|
const role = store.getRole(wakeup.taskId, "leader");
|
|
23
|
-
if (task === null
|
|
23
|
+
if (task === null
|
|
24
|
+
|| task.status !== "active"
|
|
25
|
+
|| task.executionGate.state !== "enabled"
|
|
26
|
+
|| role === null) {
|
|
24
27
|
results.push({ taskId: wakeup.taskId, status: "skipped", reason: "unavailable" });
|
|
25
28
|
continue;
|
|
26
29
|
}
|
|
@@ -29,10 +32,6 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
29
32
|
results.push({ taskId: task.id, status: "skipped", reason: "workspace-not-ready" });
|
|
30
33
|
continue;
|
|
31
34
|
}
|
|
32
|
-
if (store.getLeaderFailure(task.id) !== null) {
|
|
33
|
-
results.push({ taskId: task.id, status: "skipped", reason: "recovery-blocked" });
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
35
|
if (store.hasOpenInputRequest(task.id)) {
|
|
37
36
|
results.push({ taskId: task.id, status: "skipped", reason: "waiting-input" });
|
|
38
37
|
continue;
|
|
@@ -48,20 +47,6 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
48
47
|
results.push({ taskId: task.id, status: "skipped", reason: "busy" });
|
|
49
48
|
continue;
|
|
50
49
|
}
|
|
51
|
-
// Single-flight: a Role runtime lifecycle lane that already holds a
|
|
52
|
-
// launch reservation or cleanup obligation must not be entered by a
|
|
53
|
-
// second wake. The wake stays durable (pendingWakeup is not consumed) and
|
|
54
|
-
// is retried after the lane settles; the suppression is recorded for the
|
|
55
|
-
// audit instead of manufacturing a failed Run.
|
|
56
|
-
if (hasRuntimeLifecycleWork(store.getWorkMailbox(runtimeLifecycleTarget({
|
|
57
|
-
scope: "task",
|
|
58
|
-
taskId: task.id,
|
|
59
|
-
roleName: role.name
|
|
60
|
-
})))) {
|
|
61
|
-
store.recordWakeSuppression?.(task.id, "lifecycle-busy", now);
|
|
62
|
-
results.push({ taskId: task.id, status: "skipped", reason: "recovery-blocked" });
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
50
|
const reopening = wakeup.reasons.includes("task-reopened");
|
|
66
51
|
let existingSession = store.getRoleSession(task.id, role.name, reopening ? undefined : role.effective.agentId);
|
|
67
52
|
let effectiveSession = existingSession;
|
|
@@ -71,43 +56,6 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
71
56
|
let prepared;
|
|
72
57
|
let preStartFencePersisted = false;
|
|
73
58
|
try {
|
|
74
|
-
if (existingSession !== null && !hasNativeSession(existingSession)) {
|
|
75
|
-
const sessionIsTerminal = existingSession.status === "stopped"
|
|
76
|
-
|| existingSession.status === "broken";
|
|
77
|
-
if (!sessionIsTerminal) {
|
|
78
|
-
// An opaque live Session has no provider identity that can be safely
|
|
79
|
-
// rebound. A host absence observation is not a verified stop; keep
|
|
80
|
-
// the wake durable until an explicit exact cleanup/reset settles it.
|
|
81
|
-
if (existingSession.launchId !== undefined) {
|
|
82
|
-
await delivery.inspectRole({
|
|
83
|
-
taskId: task.id,
|
|
84
|
-
roleName: role.name,
|
|
85
|
-
agentId: existingSession.agentId,
|
|
86
|
-
adapterId: existingSession.adapterId
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
results.push({
|
|
90
|
-
taskId: task.id,
|
|
91
|
-
status: "skipped",
|
|
92
|
-
reason: "recovery-blocked"
|
|
93
|
-
});
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
if (hasRuntimeLifecycleWork(store.getWorkMailbox(runtimeLifecycleTarget({
|
|
97
|
-
scope: "task",
|
|
98
|
-
taskId: task.id,
|
|
99
|
-
roleName: role.name
|
|
100
|
-
})))) {
|
|
101
|
-
// A stopped/broken Session is eligible for a fresh mode only after
|
|
102
|
-
// its exact runtime cleanup/reservation lane has settled.
|
|
103
|
-
results.push({
|
|
104
|
-
taskId: task.id,
|
|
105
|
-
status: "skipped",
|
|
106
|
-
reason: "recovery-blocked"
|
|
107
|
-
});
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
59
|
const compatibleSession = existingSession !== null
|
|
112
60
|
&& (reopening
|
|
113
61
|
? effectiveLaunchSnapshotsCompatible(existingSession.effective, role.effective)
|
|
@@ -123,15 +71,14 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
123
71
|
const resumableSession = hasNativeSession(existingSession)
|
|
124
72
|
&& existingSession.status !== "stopped"
|
|
125
73
|
&& existingSession.status !== "broken";
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
// request (or exact terminal/missing evidence) authorizes a fresh one.
|
|
74
|
+
// Reuse a healthy Session. A terminal Session is disposable and the next
|
|
75
|
+
// Run starts fresh without consulting historical recovery records.
|
|
129
76
|
const sessionSet = store.getTaskRoleSessionSet?.(task.id, role.name) ?? null;
|
|
130
77
|
const mode = reopenIdentityDrift
|
|
131
78
|
? "new"
|
|
132
79
|
: sessionSet === null
|
|
133
80
|
? resumableSession && compatibleSession ? "resume" : "new"
|
|
134
|
-
:
|
|
81
|
+
: roleAgentSessionResumeMode(sessionSet, role.effective.agentId, role.effective);
|
|
135
82
|
const runId = store.peekNextAgentRunId(task.id);
|
|
136
83
|
const wakeEnvelope = resolveLeaderWakeEnvelope(store, task.id);
|
|
137
84
|
const contextSnapshot = store.freezeLeaderContextSnapshot?.(task.id, role.name, now);
|
|
@@ -226,7 +173,9 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
226
173
|
|| ready.prepared.turnAcceptedDuringLaunch === true
|
|
227
174
|
|| ready.prepared.turnDeliveryUnknownDuringLaunch === true;
|
|
228
175
|
const latestTask = store.getTask(task.id);
|
|
229
|
-
if (latestTask === null
|
|
176
|
+
if (latestTask === null
|
|
177
|
+
|| latestTask.status !== "active"
|
|
178
|
+
|| latestTask.executionGate.state !== "enabled") {
|
|
230
179
|
delivery.forgetPrepared?.({
|
|
231
180
|
taskId: task.id,
|
|
232
181
|
roleName: role.name,
|
package/dist/scheduler/ports.js
CHANGED
|
@@ -25,13 +25,14 @@ export function selectedActiveSchedulerTasks(store, selection) {
|
|
|
25
25
|
const indexedTaskIds = store.listActiveTaskIds?.();
|
|
26
26
|
if (indexedTaskIds === undefined) {
|
|
27
27
|
return store.listTasks().filter((task) => (task.status === "active"
|
|
28
|
+
&& task.executionGate.state === "enabled"
|
|
28
29
|
&& !selection?.blockedTaskIds?.has(task.id)));
|
|
29
30
|
}
|
|
30
31
|
return [...indexedTaskIds].flatMap((taskId) => {
|
|
31
32
|
if (selection?.blockedTaskIds?.has(taskId))
|
|
32
33
|
return [];
|
|
33
34
|
const task = store.getTask(taskId);
|
|
34
|
-
return task?.status === "active" ? [task] : [];
|
|
35
|
+
return task?.status === "active" && task.executionGate.state === "enabled" ? [task] : [];
|
|
35
36
|
});
|
|
36
37
|
}
|
|
37
38
|
const taskIds = selection.taskIds;
|
|
@@ -39,7 +40,7 @@ export function selectedActiveSchedulerTasks(store, selection) {
|
|
|
39
40
|
if (selection.blockedTaskIds?.has(taskId))
|
|
40
41
|
return [];
|
|
41
42
|
const task = store.getTask(taskId);
|
|
42
|
-
return task?.status === "active" ? [task] : [];
|
|
43
|
+
return task?.status === "active" && task.executionGate.state === "enabled" ? [task] : [];
|
|
43
44
|
});
|
|
44
45
|
}
|
|
45
46
|
/** Resolves either every Role in a selected Task or only explicit Role keys. */
|
|
@@ -196,7 +196,10 @@ function exactBatchInventory(batch, candidates) {
|
|
|
196
196
|
return { statuses, resources, hostExits };
|
|
197
197
|
}
|
|
198
198
|
function isResourceCandidate(task, run, now) {
|
|
199
|
-
if (task.status !== "active"
|
|
199
|
+
if (task.status !== "active"
|
|
200
|
+
|| task.executionGate.state !== "enabled"
|
|
201
|
+
|| run.status !== "active"
|
|
202
|
+
|| run.deliveredAt === undefined) {
|
|
200
203
|
return false;
|
|
201
204
|
}
|
|
202
205
|
const deliveredAt = Date.parse(run.deliveredAt);
|
|
@@ -14,8 +14,6 @@ export const RUN_STALLED_EVENT = "run.stalled";
|
|
|
14
14
|
export const RUN_RECOVERED_EVENT = "run.recovered";
|
|
15
15
|
export const RUN_DIAGNOSTIC_FINISHED_EVENT = "runtime.diagnostic-finished";
|
|
16
16
|
/** Structured, non-Message recovery evidence written by an explicit Leader. */
|
|
17
|
-
export const RUN_RECOVERY_REQUESTED_EVENT = "run.recovery-requested";
|
|
18
|
-
export const RUN_RECOVERY_APPLIED_EVENT = "run.recovery-applied";
|
|
19
17
|
/** Workflow-semantic events that count for the durable progress clock. */
|
|
20
18
|
const ACTIVITY_EVENT_TYPES = new Set([
|
|
21
19
|
RUN_PROGRESS_EVENT,
|
|
@@ -132,12 +132,29 @@ export function projectTaskExecution(facts) {
|
|
|
132
132
|
...(run.executionGroupId === undefined ? {} : { executionGroupId: run.executionGroupId }),
|
|
133
133
|
...(run.executionLaneId === undefined ? {} : { executionLaneId: run.executionLaneId })
|
|
134
134
|
}));
|
|
135
|
-
const monitoring = task.
|
|
135
|
+
const monitoring = task.executionGate.state === "stopped"
|
|
136
|
+
|| task.status === "completed"
|
|
136
137
|
|| task.status === "retired"
|
|
137
138
|
|| task.status === "archived"
|
|
138
139
|
? "stopped"
|
|
139
140
|
: "active";
|
|
140
141
|
if (monitoring === "stopped") {
|
|
142
|
+
if (task.executionGate.state === "stopped" && task.status === "active") {
|
|
143
|
+
return render({
|
|
144
|
+
task,
|
|
145
|
+
status: "stopped",
|
|
146
|
+
owner: "operator",
|
|
147
|
+
action: "start-execution",
|
|
148
|
+
summary: `Task ${task.id} execution is stopped; durable progress is preserved.`,
|
|
149
|
+
reason: "execution-stopped",
|
|
150
|
+
monitoring,
|
|
151
|
+
failClosed: false,
|
|
152
|
+
activeRuns: activeRunViews,
|
|
153
|
+
attention: [],
|
|
154
|
+
blockers: [],
|
|
155
|
+
pendingWakeup
|
|
156
|
+
});
|
|
157
|
+
}
|
|
141
158
|
const stoppedStatus = task.status;
|
|
142
159
|
return render({
|
|
143
160
|
task,
|
|
@@ -11,7 +11,8 @@ export function queueLeaderWakeup(store, taskId, reason, now) {
|
|
|
11
11
|
export function queueLeaderWakeupAfterYield(store, task, run, now) {
|
|
12
12
|
if (run.taskId !== task.id)
|
|
13
13
|
throw new Error(`AgentRun belongs to another Task: ${run.taskId}.`);
|
|
14
|
-
if (task.status !== "active" || run.roleName === "leader")
|
|
14
|
+
if (task.status !== "active" || task.executionGate.state !== "enabled" || run.roleName === "leader") {
|
|
15
15
|
return null;
|
|
16
|
+
}
|
|
16
17
|
return queueLeaderWakeup(store, task.id, wakeReason("role-result"), now);
|
|
17
18
|
}
|
|
@@ -24,6 +24,8 @@ const TASK_FROM_VERSION = 3;
|
|
|
24
24
|
const TASK_TO_VERSION = 4;
|
|
25
25
|
const TASK_INTENT_FROM_VERSION = 4;
|
|
26
26
|
const TASK_INTENT_TO_VERSION = 5;
|
|
27
|
+
const TASK_EXECUTION_GATE_FROM_VERSION = 5;
|
|
28
|
+
const TASK_EXECUTION_GATE_TO_VERSION = 6;
|
|
27
29
|
const WORK_ITEM_FROM_VERSION = 6;
|
|
28
30
|
const WORK_ITEM_TO_VERSION = 7;
|
|
29
31
|
const WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION = 7;
|
|
@@ -161,6 +163,7 @@ export function createProductionStorageRegistry() {
|
|
|
161
163
|
.registerCompatible(projectLifecycleStep())
|
|
162
164
|
.registerOfflineMigration(taskWorkspaceIdentityStep())
|
|
163
165
|
.registerOfflineMigration(taskIntentStep())
|
|
166
|
+
.registerOfflineMigration(taskExecutionGateStep())
|
|
164
167
|
.registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_FROM_VERSION, WORK_ITEM_TO_VERSION, "workItems"))
|
|
165
168
|
.registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION, WORK_ITEM_GIT_SNAPSHOT_TO_VERSION, "workItems"))
|
|
166
169
|
.registerOfflineMigration(workItemExecutionGroupHistoryStep())
|
|
@@ -3375,6 +3378,68 @@ function migrateTaskV4ToV5(snapshot) {
|
|
|
3375
3378
|
state: { ...snapshot.state, tasks: nextTasks }
|
|
3376
3379
|
};
|
|
3377
3380
|
}
|
|
3381
|
+
/** Task v6 separates semantic lifecycle from the current execution admission gate. */
|
|
3382
|
+
function taskExecutionGateStep() {
|
|
3383
|
+
return {
|
|
3384
|
+
axis: "record",
|
|
3385
|
+
recordKind: "task",
|
|
3386
|
+
fromVersion: TASK_EXECUTION_GATE_FROM_VERSION,
|
|
3387
|
+
toVersion: TASK_EXECUTION_GATE_TO_VERSION,
|
|
3388
|
+
preconditions: requireTaskV5Family,
|
|
3389
|
+
transform: migrateTaskV5ToV6,
|
|
3390
|
+
declaredEffects: []
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
function requireTaskV5Family(snapshot) {
|
|
3394
|
+
const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
|
|
3395
|
+
if (manifestVersions.task !== TASK_EXECUTION_GATE_FROM_VERSION) {
|
|
3396
|
+
throw new Error(`Record task migration requires manifest version ${TASK_EXECUTION_GATE_FROM_VERSION}.`);
|
|
3397
|
+
}
|
|
3398
|
+
if (snapshot.state === null)
|
|
3399
|
+
return;
|
|
3400
|
+
const tasks = asObject(snapshot.state.tasks, "state tasks");
|
|
3401
|
+
for (const [taskId, rawTask] of Object.entries(tasks)) {
|
|
3402
|
+
const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
|
|
3403
|
+
if (aggregate.task === undefined)
|
|
3404
|
+
continue;
|
|
3405
|
+
const record = asObject(aggregate.task, `Task ${taskId}`);
|
|
3406
|
+
if (record.schemaVersion !== TASK_EXECUTION_GATE_FROM_VERSION) {
|
|
3407
|
+
throw new Error(`Task ${taskId} must use schemaVersion ${TASK_EXECUTION_GATE_FROM_VERSION}.`);
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
function migrateTaskV5ToV6(snapshot) {
|
|
3412
|
+
requireTaskV5Family(snapshot);
|
|
3413
|
+
const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
|
|
3414
|
+
const schemaManifest = {
|
|
3415
|
+
...snapshot.schemaManifest,
|
|
3416
|
+
recordVersions: { ...manifestVersions, task: TASK_EXECUTION_GATE_TO_VERSION }
|
|
3417
|
+
};
|
|
3418
|
+
if (snapshot.state === null)
|
|
3419
|
+
return { schemaManifest, state: null };
|
|
3420
|
+
const tasks = asObject(snapshot.state.tasks, "state tasks");
|
|
3421
|
+
const nextTasks = {};
|
|
3422
|
+
for (const [taskId, rawTask] of Object.entries(tasks)) {
|
|
3423
|
+
const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
|
|
3424
|
+
if (aggregate.task === undefined) {
|
|
3425
|
+
nextTasks[taskId] = { ...aggregate };
|
|
3426
|
+
continue;
|
|
3427
|
+
}
|
|
3428
|
+
const task = asObject(aggregate.task, `Task ${taskId}`);
|
|
3429
|
+
nextTasks[taskId] = {
|
|
3430
|
+
...aggregate,
|
|
3431
|
+
task: {
|
|
3432
|
+
...task,
|
|
3433
|
+
schemaVersion: TASK_EXECUTION_GATE_TO_VERSION,
|
|
3434
|
+
executionGate: { state: "enabled" }
|
|
3435
|
+
}
|
|
3436
|
+
};
|
|
3437
|
+
}
|
|
3438
|
+
return {
|
|
3439
|
+
schemaManifest,
|
|
3440
|
+
state: { ...snapshot.state, tasks: nextTasks }
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
3378
3443
|
/**
|
|
3379
3444
|
* A version bump is deliverable only when the shared planner resolves the full
|
|
3380
3445
|
* adjacent path. This also covers target-only record families as explicit 0->1
|