@zq-silk/yui 0.12.3 → 0.12.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/dist/cli/commandCatalog.js +2 -2
- package/dist/commands/durableJobCommands.js +5 -5
- package/dist/commands/taskActor.js +18 -0
- package/dist/commands/taskCommands.js +235 -53
- package/dist/commands/taskIntegrationCommands.js +30 -19
- package/dist/commands/taskIntegrationQueueCommands.js +14 -14
- package/dist/context/runContextPack.js +15 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +17 -1
- package/dist/executor/workspacePreflightClassification.js +6 -5
- package/dist/lifecycle/exactRunTerminalization.js +146 -6
- package/dist/run/rejectedYieldAttempt.js +221 -0
- package/dist/runtime/tmuxAdapters.js +12 -3
- package/dist/scheduler/activeRoleRunDelivery.js +4 -0
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
|
6
6
|
import { createIntegrationAttempt, recordResolutionDecision, supersedeIntegration, updateIntegrationAttempt } from "../integration/integrationAttempt.js";
|
|
7
7
|
import { GitIntegrationService } from "../integration/gitIntegrationService.js";
|
|
8
8
|
import { runTaskIntegrationQueueCommand } from "./taskIntegrationQueueCommands.js";
|
|
9
|
-
import {
|
|
9
|
+
import { taskLocalActor } from "./taskActor.js";
|
|
10
10
|
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
11
11
|
import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
|
|
12
12
|
export async function runTaskIntegrationCommand(args, store, home, options = {}) {
|
|
@@ -18,12 +18,13 @@ export async function runTaskIntegrationCommand(args, store, home, options = {})
|
|
|
18
18
|
return continueIntegration(rest, store, home, now, options);
|
|
19
19
|
}
|
|
20
20
|
if (command === "resolve") {
|
|
21
|
-
return resolveDecision(rest, store, now, options.environment);
|
|
21
|
+
return resolveDecision(rest, store, now, options.environment, home);
|
|
22
22
|
}
|
|
23
23
|
if (command === "abort")
|
|
24
|
-
return abortIntegration(rest, store, now(), options);
|
|
25
|
-
if (command === "supersede")
|
|
26
|
-
return supersedeIntegrationCommand(rest, store, now(), options.environment);
|
|
24
|
+
return abortIntegration(rest, store, now(), options, home);
|
|
25
|
+
if (command === "supersede") {
|
|
26
|
+
return supersedeIntegrationCommand(rest, store, now(), options.environment, home);
|
|
27
|
+
}
|
|
27
28
|
if (command === "cleanup") {
|
|
28
29
|
return cleanupIntegration(rest, store, home, options.environment);
|
|
29
30
|
}
|
|
@@ -43,6 +44,7 @@ async function cleanupIntegration(args, store, home, environment) {
|
|
|
43
44
|
throw usageError("Task Integration cleanup usage: yui task integration cleanup <task>/<integration>.");
|
|
44
45
|
}
|
|
45
46
|
const integration = requireIntegration(store, args[0], environment);
|
|
47
|
+
taskLocalActor(store, environment, integration.taskId, home);
|
|
46
48
|
if (integration.status !== "committed"
|
|
47
49
|
&& integration.status !== "superseded"
|
|
48
50
|
&& integration.status !== "failed") {
|
|
@@ -83,6 +85,7 @@ async function start(args, store, home, now, options) {
|
|
|
83
85
|
if (task.status !== "active") {
|
|
84
86
|
throw usageError(`Task is not active: ${task.id}/${task.status}.`);
|
|
85
87
|
}
|
|
88
|
+
taskLocalActor(store, options.environment, task.id, home);
|
|
86
89
|
const changeSetIds = parsed.many.get("--change-set") ?? [];
|
|
87
90
|
if (changeSetIds.length === 0)
|
|
88
91
|
throw usageError("--change-set is required.", usage);
|
|
@@ -135,6 +138,7 @@ async function start(args, store, home, now, options) {
|
|
|
135
138
|
// record creation are atomic, so a concurrent Leader cannot sneak a
|
|
136
139
|
// duplicate Integration between the guard and the write.
|
|
137
140
|
const integration = store.transaction((tx) => {
|
|
141
|
+
taskLocalActor(tx, options.environment, task.id, home);
|
|
138
142
|
const guard = runDeliveryGuardPreflight(tx, task.id, {
|
|
139
143
|
kind: "integration-start",
|
|
140
144
|
projectId: project.id,
|
|
@@ -165,6 +169,7 @@ async function continueIntegration(args, store, home, now, options) {
|
|
|
165
169
|
throw usageError(usage);
|
|
166
170
|
const integration = requireIntegration(store, parsed.positionals[0], options.environment);
|
|
167
171
|
requireActiveIntegrationTask(store, integration);
|
|
172
|
+
taskLocalActor(store, options.environment, integration.taskId, home);
|
|
168
173
|
if (integration.status !== "validating"
|
|
169
174
|
&& integration.status !== "running"
|
|
170
175
|
&& (integration.status !== "blocked"
|
|
@@ -184,25 +189,28 @@ async function runIntegration(store, home, integration, now, options) {
|
|
|
184
189
|
: `Integration ${result.attempt.id} failed; target ref was not advanced\n`;
|
|
185
190
|
return { output, data: result };
|
|
186
191
|
}
|
|
187
|
-
function resolveDecision(args, store, now, environment) {
|
|
192
|
+
function resolveDecision(args, store, now, environment, home) {
|
|
188
193
|
const usage = "Task Integration resolve usage: yui task integration resolve <task>/<integration> --option <manual-resolution|reject> --rationale <text>.";
|
|
189
194
|
const parsed = parseRepeatable(args, new Set(), new Set(["--option", "--rationale"]), usage);
|
|
190
195
|
if (parsed.positionals.length !== 1)
|
|
191
196
|
throw usageError(usage);
|
|
192
|
-
const integration = requireIntegration(store, parsed.positionals[0], environment);
|
|
193
|
-
const task = requireActiveIntegrationTask(store, integration);
|
|
194
|
-
if (taskActor(environment, task.id) !== "leader") {
|
|
195
|
-
throw usageError("Only the Task Leader can resolve an Integration conflict.");
|
|
196
|
-
}
|
|
197
197
|
const selectedOption = parsed.one.get("--option");
|
|
198
198
|
const rationale = parsed.one.get("--rationale");
|
|
199
199
|
if (selectedOption === undefined || rationale === undefined)
|
|
200
200
|
throw usageError(usage);
|
|
201
|
-
const resolved =
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
201
|
+
const resolved = store.transaction((tx) => {
|
|
202
|
+
const integration = requireIntegration(tx, parsed.positionals[0], environment);
|
|
203
|
+
const task = requireActiveIntegrationTask(tx, integration);
|
|
204
|
+
if (taskLocalActor(tx, environment, task.id, home) !== "leader") {
|
|
205
|
+
throw usageError("Only the Task Leader can resolve an Integration conflict.");
|
|
206
|
+
}
|
|
207
|
+
const updated = recordResolutionDecision(integration, {
|
|
208
|
+
action: selectedOption,
|
|
209
|
+
rationale
|
|
210
|
+
}, now());
|
|
211
|
+
tx.saveIntegrationAttempt(updated.taskId, updated);
|
|
212
|
+
return updated;
|
|
213
|
+
});
|
|
206
214
|
return {
|
|
207
215
|
output: selectedOption === "manual-resolution"
|
|
208
216
|
? `Leader selected manual resolution for ${resolved.id}; resolve its candidate worktree, then run integration continue\n`
|
|
@@ -210,13 +218,14 @@ function resolveDecision(args, store, now, environment) {
|
|
|
210
218
|
data: { integration: resolved }
|
|
211
219
|
};
|
|
212
220
|
}
|
|
213
|
-
async function abortIntegration(args, store, now, options) {
|
|
221
|
+
async function abortIntegration(args, store, now, options, home) {
|
|
214
222
|
const usage = "Task Integration abort usage: yui task integration abort <task>/<integration> --reason <text>.";
|
|
215
223
|
const parsed = parseRepeatable(args, new Set(), new Set(["--reason"]), usage);
|
|
216
224
|
if (parsed.positionals.length !== 1)
|
|
217
225
|
throw usageError(usage);
|
|
218
226
|
const integration = requireIntegration(store, parsed.positionals[0], options.environment);
|
|
219
227
|
requireActiveIntegrationTask(store, integration);
|
|
228
|
+
taskLocalActor(store, options.environment, integration.taskId, home);
|
|
220
229
|
if (integration.status !== "running" && integration.status !== "blocked") {
|
|
221
230
|
throw usageError(`Integration cannot be aborted from ${integration.status}: ${integration.id}.`);
|
|
222
231
|
}
|
|
@@ -238,6 +247,7 @@ async function abortIntegration(args, store, now, options) {
|
|
|
238
247
|
if (current.status !== "running" && current.status !== "blocked") {
|
|
239
248
|
throw usageError(`Integration cannot be aborted from ${current.status}: ${current.id}.`);
|
|
240
249
|
}
|
|
250
|
+
taskLocalActor(tx, options.environment, current.taskId, home);
|
|
241
251
|
const aborted = updateIntegrationAttempt(current, {
|
|
242
252
|
status: "failed",
|
|
243
253
|
checks: [
|
|
@@ -252,7 +262,7 @@ async function abortIntegration(args, store, now, options) {
|
|
|
252
262
|
};
|
|
253
263
|
});
|
|
254
264
|
}
|
|
255
|
-
function supersedeIntegrationCommand(args, store, now, environment) {
|
|
265
|
+
function supersedeIntegrationCommand(args, store, now, environment, home) {
|
|
256
266
|
const usage = "Task Integration supersede usage: yui task integration supersede <task>/<integration> --reason <text>.";
|
|
257
267
|
const parsed = parseRepeatable(args, new Set(), new Set(["--reason"]), usage);
|
|
258
268
|
if (parsed.positionals.length !== 1)
|
|
@@ -267,7 +277,7 @@ function supersedeIntegrationCommand(args, store, now, environment) {
|
|
|
267
277
|
throw usageError(usage);
|
|
268
278
|
// Only the Task Leader (or Operator/user) may supersede a committed
|
|
269
279
|
// Integration: it rewrites delivery-baseline evidence and audit history.
|
|
270
|
-
|
|
280
|
+
taskLocalActor(store, environment, integration.taskId, home);
|
|
271
281
|
// A queue-backed committed Attempt cannot be superseded: the queue entry
|
|
272
282
|
// would remain in its current status while its Attempt becomes "superseded",
|
|
273
283
|
// leaving contradictory terminal records that never converge. This covers
|
|
@@ -287,6 +297,7 @@ function supersedeIntegrationCommand(args, store, now, environment) {
|
|
|
287
297
|
if (current.status !== "committed") {
|
|
288
298
|
throw usageError(`Integration cannot be superseded from ${current.status}: ${current.id}.`);
|
|
289
299
|
}
|
|
300
|
+
taskLocalActor(tx, environment, current.taskId, home);
|
|
290
301
|
const superseded = supersedeIntegration(current, reason, now);
|
|
291
302
|
tx.saveIntegrationAttempt(superseded.taskId, superseded);
|
|
292
303
|
return {
|
|
@@ -3,7 +3,7 @@ import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
|
3
3
|
import { resolveProject } from "../repository/project.js";
|
|
4
4
|
import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
|
|
5
5
|
import { enqueueIntegrationQueueEntry, processIntegrationQueue, reconcileIntegrationQueueEntry, requeueIntegrationQueueEntry, supersedeIntegrationQueueEntry } from "../integration/integrationQueueService.js";
|
|
6
|
-
import {
|
|
6
|
+
import { taskLocalActor } from "./taskActor.js";
|
|
7
7
|
import { parseRepeatable } from "./taskIntegrationCommands.js";
|
|
8
8
|
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
9
9
|
/**
|
|
@@ -22,11 +22,11 @@ export async function runTaskIntegrationQueueCommand(args, store, home, options
|
|
|
22
22
|
if (command === "process")
|
|
23
23
|
return process(rest, store, home, options);
|
|
24
24
|
if (command === "supersede")
|
|
25
|
-
return supersede(rest, store, options);
|
|
25
|
+
return supersede(rest, store, options, home);
|
|
26
26
|
if (command === "requeue")
|
|
27
|
-
return requeue(rest, store, options);
|
|
27
|
+
return requeue(rest, store, options, home);
|
|
28
28
|
if (command === "reconcile")
|
|
29
|
-
return reconcile(rest, store, options);
|
|
29
|
+
return reconcile(rest, store, options, home);
|
|
30
30
|
throw usageError(command === undefined
|
|
31
31
|
? "Task Integration queue command is required."
|
|
32
32
|
: `Unknown command: task integration queue ${command}`);
|
|
@@ -37,7 +37,7 @@ async function enqueue(args, store, options, home) {
|
|
|
37
37
|
if (parsed.positionals.length !== 1)
|
|
38
38
|
throw usageError(usage);
|
|
39
39
|
const task = requireActiveTask(store, parsed.positionals[0]);
|
|
40
|
-
requireLeader(options.environment, task.id);
|
|
40
|
+
requireLeader(store, options.environment, task.id, home);
|
|
41
41
|
const projectRef = parsed.one.get("--project");
|
|
42
42
|
const changeSetId = parsed.one.get("--change-set");
|
|
43
43
|
if (projectRef === undefined || changeSetId === undefined)
|
|
@@ -120,7 +120,7 @@ async function process(args, store, home, options) {
|
|
|
120
120
|
if (parsed.positionals.length !== 1)
|
|
121
121
|
throw usageError(usage);
|
|
122
122
|
const task = requireActiveTask(store, parsed.positionals[0]);
|
|
123
|
-
requireLeader(options.environment, task.id);
|
|
123
|
+
requireLeader(store, options.environment, task.id, home);
|
|
124
124
|
const limitValue = parsed.one.get("--limit");
|
|
125
125
|
const limit = limitValue === undefined ? undefined : Number.parseInt(limitValue, 10);
|
|
126
126
|
if (limitValue !== undefined && (!Number.isSafeInteger(limit) || limit < 1)) {
|
|
@@ -148,13 +148,13 @@ async function process(args, store, home, options) {
|
|
|
148
148
|
: `${lines.join("\n")}\n`;
|
|
149
149
|
return { output, data: { processed } };
|
|
150
150
|
}
|
|
151
|
-
function supersede(args, store, options) {
|
|
151
|
+
function supersede(args, store, options, home) {
|
|
152
152
|
const usage = "Task Integration queue supersede usage: yui task integration queue supersede <task>/<entry> --reason <text>.";
|
|
153
153
|
const parsed = parseRepeatable(args, new Set(), new Set(["--reason"]), usage);
|
|
154
154
|
if (parsed.positionals.length !== 1)
|
|
155
155
|
throw usageError(usage);
|
|
156
156
|
const entry = requireQueueEntry(store, parsed.positionals[0], options.environment);
|
|
157
|
-
requireLeader(options.environment, entry.taskId);
|
|
157
|
+
requireLeader(store, options.environment, entry.taskId, home);
|
|
158
158
|
const reason = parsed.one.get("--reason");
|
|
159
159
|
if (reason === undefined)
|
|
160
160
|
throw usageError(usage);
|
|
@@ -164,24 +164,24 @@ function supersede(args, store, options) {
|
|
|
164
164
|
data: { entry: superseded }
|
|
165
165
|
};
|
|
166
166
|
}
|
|
167
|
-
function requeue(args, store, options) {
|
|
167
|
+
function requeue(args, store, options, home) {
|
|
168
168
|
const usage = "Task Integration queue requeue usage: yui task integration queue requeue <task>/<entry>.";
|
|
169
169
|
if (args.length !== 1)
|
|
170
170
|
throw usageError(usage);
|
|
171
171
|
const entry = requireQueueEntry(store, args[0], options.environment);
|
|
172
|
-
requireLeader(options.environment, entry.taskId);
|
|
172
|
+
requireLeader(store, options.environment, entry.taskId, home);
|
|
173
173
|
const waiting = requeueIntegrationQueueEntry(store, entry.taskId, entry.id, options.now ?? (() => new Date()));
|
|
174
174
|
return {
|
|
175
175
|
output: `Requeued ${waiting.id}; it will be processed again on the next queue run\n`,
|
|
176
176
|
data: { entry: waiting }
|
|
177
177
|
};
|
|
178
178
|
}
|
|
179
|
-
async function reconcile(args, store, options) {
|
|
179
|
+
async function reconcile(args, store, options, home) {
|
|
180
180
|
const usage = "Task Integration queue reconcile usage: yui task integration queue reconcile <task>/<entry>.";
|
|
181
181
|
if (args.length !== 1)
|
|
182
182
|
throw usageError(usage);
|
|
183
183
|
const entry = requireQueueEntry(store, args[0], options.environment);
|
|
184
|
-
requireLeader(options.environment, entry.taskId);
|
|
184
|
+
requireLeader(store, options.environment, entry.taskId, home);
|
|
185
185
|
const committed = await reconcileIntegrationQueueEntry(store, entry.taskId, entry.id, new NodeGitWorkspace(), options.now ?? (() => new Date()));
|
|
186
186
|
return {
|
|
187
187
|
output: `Reconciled ${committed.id} as committed -> ${committed.targetAfter ?? "-"}\n`,
|
|
@@ -221,8 +221,8 @@ function requireQueueEntry(store, value, environment) {
|
|
|
221
221
|
}
|
|
222
222
|
return entry;
|
|
223
223
|
}
|
|
224
|
-
function requireLeader(environment, taskId) {
|
|
225
|
-
if (
|
|
224
|
+
function requireLeader(store, environment, taskId, home) {
|
|
225
|
+
if (taskLocalActor(store, environment, taskId, home) !== "leader") {
|
|
226
226
|
throw usageError("Only the Task Leader can change the integration queue.");
|
|
227
227
|
}
|
|
228
228
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rejectedYieldAttemptFromTaskEvent, RUN_YIELD_REJECTED_EVENT } from "../run/rejectedYieldAttempt.js";
|
|
1
2
|
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
2
3
|
import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
|
|
3
4
|
import { RUN_BOOTSTRAP_MAX_DELTAS } from "./runContextContract.js";
|
|
@@ -426,6 +427,20 @@ function collectAuthorizedContext(store, run) {
|
|
|
426
427
|
for (const event of publishedTreeAuthorizations.reverse().slice(-16)) {
|
|
427
428
|
result.push(materialize("L4", "task-event", event.id, event));
|
|
428
429
|
}
|
|
430
|
+
for (const event of events.filter(({ type }) => (type === RUN_YIELD_REJECTED_EVENT)).slice(-16)) {
|
|
431
|
+
result.push(materialize("L4", "task-event", event.id, event));
|
|
432
|
+
const attempt = rejectedYieldAttemptFromTaskEvent(event);
|
|
433
|
+
if (attempt === null)
|
|
434
|
+
continue;
|
|
435
|
+
const rejectedRun = store.getAgentRun(task.id, attempt.runId);
|
|
436
|
+
if (rejectedRun !== null) {
|
|
437
|
+
result.push(materialize("L4", "agent-run", rejectedRun.id, rejectedRun));
|
|
438
|
+
}
|
|
439
|
+
const rejectedRound = store.getReviewRound(task.id, attempt.reviewRoundId);
|
|
440
|
+
if (rejectedRound !== null) {
|
|
441
|
+
result.push(materialize("L3", "review-round", rejectedRound.id, rejectedRound));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
429
444
|
for (const request of store.listOpenInputRequests([task.id])) {
|
|
430
445
|
result.push(materialize("L4", "input-request", request.id, request));
|
|
431
446
|
}
|
|
@@ -1714,8 +1714,24 @@ export class FileSchedulerStoreAdapter {
|
|
|
1714
1714
|
}
|
|
1715
1715
|
}
|
|
1716
1716
|
}
|
|
1717
|
-
|
|
1717
|
+
const blocksAutomaticLeaderRecovery = role.name === "leader"
|
|
1718
|
+
&& input.leaderRecovery === "blocked";
|
|
1719
|
+
store.saveRole(task.id, updateRoleStatus(role, blocksAutomaticLeaderRecovery ? "failed" : "exited", input.now));
|
|
1718
1720
|
stopTaskSessionIfPresent(store, task.id, role.name, currentRun.effective.agentId, input.now);
|
|
1721
|
+
if (blocksAutomaticLeaderRecovery) {
|
|
1722
|
+
const failure = recordLeaderFailure(task.id, input.session?.nativeSessionId ?? "(unregistered)", input.summary, input.now, store.getLeaderFailure(task.id));
|
|
1723
|
+
store.saveLeaderFailure(failure);
|
|
1724
|
+
recordLeaderAttentionRequired(store, {
|
|
1725
|
+
taskId: task.id,
|
|
1726
|
+
reason: "leader-recovery-failed",
|
|
1727
|
+
payload: {
|
|
1728
|
+
message: failure.message,
|
|
1729
|
+
runId: currentRun.id
|
|
1730
|
+
},
|
|
1731
|
+
now: input.now
|
|
1732
|
+
});
|
|
1733
|
+
return "failed";
|
|
1734
|
+
}
|
|
1719
1735
|
queueLeaderWakeup(store, task.id, wakeReason(role.name === "leader" ? "leader-run-failed" : "role-run-failed"), input.now);
|
|
1720
1736
|
return "failed";
|
|
1721
1737
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isTaskOwnedWorkspace } from "../worktree/managedWorkspace.js";
|
|
1
|
+
import { isTaskOwnedWorkspace, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
|
|
3
2
|
function ownerLabel(owner) {
|
|
4
3
|
switch (owner.type) {
|
|
5
4
|
case "task":
|
|
@@ -43,11 +42,13 @@ export function classifyWorkspacePreflight(store, task, roleName, activeRun, ins
|
|
|
43
42
|
};
|
|
44
43
|
}
|
|
45
44
|
// 2. Run snapshot validation: when the active Run carries a workspace
|
|
46
|
-
// snapshot
|
|
47
|
-
//
|
|
45
|
+
// snapshot its launch-stable identity must match the durable
|
|
46
|
+
// ManagedWorkspace. Audit timestamps may change without changing the
|
|
47
|
+
// owner, root, or Project entries authorized for the Run.
|
|
48
48
|
if (activeRun?.workspace !== undefined) {
|
|
49
49
|
const durableRunWorkspace = store.getManagedWorkspace(activeRun.workspace.owner);
|
|
50
|
-
if (durableRunWorkspace === null
|
|
50
|
+
if (durableRunWorkspace === null
|
|
51
|
+
|| !sameManagedWorkspaceIdentity(durableRunWorkspace, activeRun.workspace)) {
|
|
51
52
|
const diff = [];
|
|
52
53
|
if (durableRunWorkspace === null) {
|
|
53
54
|
diff.push(`durable ManagedWorkspace for owner ${JSON.stringify(activeRun.workspace.owner)} is missing`);
|
|
@@ -9,7 +9,7 @@ import { finishReviewRound, updateReviewExecutionGroup } from "../review/reviewR
|
|
|
9
9
|
import { reconcileReviewFindingsAfterReview } from "../review/reviewFindingLedger.js";
|
|
10
10
|
import { agentRunDeliveryReceiptId, failAgentRun, withYieldReceipt, yieldAgentRun } from "../run/agentRun.js";
|
|
11
11
|
import { createYieldReceipt } from "../run/yieldReceipt.js";
|
|
12
|
-
import { recordExecutionLaneResult } from "../execution/executionGroup.js";
|
|
12
|
+
import { recordExecutionLaneResult, resolveExecutionGroup } from "../execution/executionGroup.js";
|
|
13
13
|
import { isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
14
14
|
import { runOwnsBlockingProviderContinuation } from "../runtime/runtimeContinuationProjection.js";
|
|
15
15
|
import { runHasActiveRuntimeOperations } from "../runtime/runtimeObservation.js";
|
|
@@ -145,7 +145,7 @@ export function terminalizeExactRunReviewRound(store, input, now) {
|
|
|
145
145
|
return validation;
|
|
146
146
|
}
|
|
147
147
|
const reviewRound = validation.round;
|
|
148
|
-
|
|
148
|
+
let groupedRound = input.run.executionGroupId !== undefined
|
|
149
149
|
&& input.run.executionLaneId !== undefined
|
|
150
150
|
&& reviewRound.executionGroup !== undefined
|
|
151
151
|
? updateReviewExecutionGroup(reviewRound, recordExecutionLaneResult(reviewRound.executionGroup, input.run.executionLaneId, {
|
|
@@ -174,10 +174,25 @@ export function terminalizeExactRunReviewRound(store, input, now) {
|
|
|
174
174
|
: { gitSnapshot: input.reviewResult.gitSnapshot })
|
|
175
175
|
}, input.outcome.status === "yielded" ? "completed" : "failed", now))
|
|
176
176
|
: reviewRound;
|
|
177
|
+
if (input.settleFailedExecutionGroup === true
|
|
178
|
+
&& input.outcome.status === "failed"
|
|
179
|
+
&& groupedRound.executionGroup !== undefined
|
|
180
|
+
&& groupedRound.executionGroup.resolution === undefined
|
|
181
|
+
&& groupedRound.executionGroup.lanes.every((lane) => (lane.status === "yielded"
|
|
182
|
+
|| lane.status === "completed"
|
|
183
|
+
|| lane.status === "failed"
|
|
184
|
+
|| lane.status === "skipped"))) {
|
|
185
|
+
groupedRound = updateReviewExecutionGroup(groupedRound, resolveExecutionGroup(groupedRound.executionGroup, {
|
|
186
|
+
decision: "blocked",
|
|
187
|
+
summary: input.outcome.summary
|
|
188
|
+
}, now));
|
|
189
|
+
}
|
|
177
190
|
const groupedMultiLane = groupedRound.executionGroup !== undefined
|
|
178
191
|
&& (groupedRound.executionGroup.lanes.length > 1
|
|
179
192
|
|| groupedRound.executionGroup.strategy.mode === "adaptive");
|
|
180
|
-
if (groupedMultiLane
|
|
193
|
+
if (groupedMultiLane
|
|
194
|
+
&& groupedRound.executionGroup !== undefined
|
|
195
|
+
&& groupedRound.executionGroup.resolution === undefined) {
|
|
181
196
|
// A panel Lane only contributes evidence. The Leader must see every
|
|
182
197
|
// terminal Lane and explicitly resolve the Group before this ReviewRound
|
|
183
198
|
// can become terminal.
|
|
@@ -193,6 +208,108 @@ export function terminalizeExactRunReviewRound(store, input, now) {
|
|
|
193
208
|
}
|
|
194
209
|
return { disposition: "applied", round: terminal };
|
|
195
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Retire one stranded active Run only after its exact Provider Turn is
|
|
213
|
+
* terminal and every durable execution fence is quiet. The caller owns the
|
|
214
|
+
* surrounding aggregate transaction so the Run, ReviewRound/Lane, mailbox,
|
|
215
|
+
* Session, and append-only retirement record commit together.
|
|
216
|
+
*/
|
|
217
|
+
export function retireExactActiveAgentRun(store, input, now) {
|
|
218
|
+
const current = store.getAgentRun(input.taskId, input.runId);
|
|
219
|
+
const stateChanged = (reason) => ({
|
|
220
|
+
disposition: "state-changed",
|
|
221
|
+
run: current,
|
|
222
|
+
...(current === null ? {} : { progressAt: latestRunDurableProgressAt(store, input.taskId, input.roleName, input.runId)?.progressAt }),
|
|
223
|
+
reason
|
|
224
|
+
});
|
|
225
|
+
const task = store.getTask(input.taskId);
|
|
226
|
+
if (task === null)
|
|
227
|
+
return stateChanged("task-missing");
|
|
228
|
+
if (task.status !== "active")
|
|
229
|
+
return stateChanged("task-terminal");
|
|
230
|
+
if (current === null)
|
|
231
|
+
return stateChanged("run-missing");
|
|
232
|
+
if (current.status !== "active")
|
|
233
|
+
return stateChanged("run-terminal");
|
|
234
|
+
if (current.taskId !== input.taskId || current.roleName !== input.roleName) {
|
|
235
|
+
return stateChanged("run-owner-mismatch");
|
|
236
|
+
}
|
|
237
|
+
if (current.effective.agentId !== input.agentId
|
|
238
|
+
|| current.effective.adapterId !== input.adapterId) {
|
|
239
|
+
return stateChanged("run-launch-identity-mismatch");
|
|
240
|
+
}
|
|
241
|
+
const active = current.executionGroupId !== undefined && current.executionLaneId !== undefined
|
|
242
|
+
? store.getActiveExecutionLaneRun(input.taskId, current.executionGroupId, current.executionLaneId)
|
|
243
|
+
: store.getActiveAgentRun(input.taskId, input.roleName);
|
|
244
|
+
if (active?.id !== current.id)
|
|
245
|
+
return stateChanged("active-run-mismatch");
|
|
246
|
+
const progress = latestRunDurableProgressAt(store, input.taskId, input.roleName, input.runId);
|
|
247
|
+
if (progress === null)
|
|
248
|
+
return stateChanged("progress-unavailable");
|
|
249
|
+
if (progress.progressAt !== input.expectedProgressAt) {
|
|
250
|
+
return { ...stateChanged("progress-fence-mismatch"), progressAt: progress.progressAt };
|
|
251
|
+
}
|
|
252
|
+
if (!matchesRecoverySessionFence(store, {
|
|
253
|
+
...input,
|
|
254
|
+
action: "terminate",
|
|
255
|
+
providerAcceptance: current.deliveredAt === undefined ? "rejected" : "accepted",
|
|
256
|
+
now
|
|
257
|
+
}))
|
|
258
|
+
return stateChanged("session-or-launch-fence-mismatch");
|
|
259
|
+
const blocker = exactRecoveryExecutionBlocker(store, current);
|
|
260
|
+
if (blocker !== null) {
|
|
261
|
+
return {
|
|
262
|
+
disposition: "blocked",
|
|
263
|
+
run: current,
|
|
264
|
+
progressAt: progress.progressAt,
|
|
265
|
+
reason: blocker
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
269
|
+
const session = sessions?.sessions[input.agentId];
|
|
270
|
+
const providerBinding = sessions?.providerBinding;
|
|
271
|
+
const providerTurn = providerBinding?.turn;
|
|
272
|
+
const providerSettled = providerBinding?.runId === current.id
|
|
273
|
+
&& (providerTurn?.status === "completed"
|
|
274
|
+
|| providerTurn?.status === "failed"
|
|
275
|
+
|| providerTurn?.status === "cancelled"
|
|
276
|
+
|| providerTurn?.status === "rejected");
|
|
277
|
+
if (session?.status !== "stopped" && session?.status !== "broken" && !providerSettled) {
|
|
278
|
+
return {
|
|
279
|
+
disposition: "blocked",
|
|
280
|
+
run: current,
|
|
281
|
+
progressAt: progress.progressAt,
|
|
282
|
+
reason: "runtime-not-terminal"
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const terminal = terminalizeExactTaskRun(store, {
|
|
286
|
+
taskId: input.taskId,
|
|
287
|
+
roleName: input.roleName,
|
|
288
|
+
agentId: input.agentId,
|
|
289
|
+
runId: input.runId,
|
|
290
|
+
receiptId: agentRunDeliveryReceiptId(current),
|
|
291
|
+
...(input.nativeSessionId === undefined ? {} : { nativeSessionId: input.nativeSessionId }),
|
|
292
|
+
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
293
|
+
settleFailedExecutionGroup: true,
|
|
294
|
+
outcome: { status: "failed", summary: input.reason }
|
|
295
|
+
}, now);
|
|
296
|
+
if (terminal.disposition !== "applied" || terminal.run === null) {
|
|
297
|
+
return stateChanged(terminal.reason ?? "terminalization-fence-mismatch");
|
|
298
|
+
}
|
|
299
|
+
if (terminal.run.purpose === "execution" && terminal.run.workItemId !== undefined) {
|
|
300
|
+
const item = store.getWorkItem(input.taskId, terminal.run.workItemId);
|
|
301
|
+
if (item !== null
|
|
302
|
+
&& !["completed", "failed", "retired"].includes(item.status)
|
|
303
|
+
&& !workItemOwnsUnresolvedExecutionLane(item, terminal.run.executionGroupId, terminal.run.executionLaneId)) {
|
|
304
|
+
store.saveWorkItem(input.taskId, updateWorkItemStatus(item, "failed", now, input.reason));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
disposition: "applied",
|
|
309
|
+
run: terminal.run,
|
|
310
|
+
progressAt: progress.progressAt
|
|
311
|
+
};
|
|
312
|
+
}
|
|
196
313
|
/**
|
|
197
314
|
* Applies one exact application-level terminal fact inside the caller's
|
|
198
315
|
* FileTaskStore transaction. All caller-owned outcome records can therefore
|
|
@@ -279,7 +396,10 @@ export function terminalizeExactTaskRun(store, input, now) {
|
|
|
279
396
|
taskId: input.taskId,
|
|
280
397
|
run,
|
|
281
398
|
outcome: input.outcome,
|
|
282
|
-
reviewResult: input.reviewResult
|
|
399
|
+
reviewResult: input.reviewResult,
|
|
400
|
+
...(input.settleFailedExecutionGroup === undefined
|
|
401
|
+
? {}
|
|
402
|
+
: { settleFailedExecutionGroup: input.settleFailedExecutionGroup })
|
|
283
403
|
}, now);
|
|
284
404
|
if (reviewRoundTerminalization.disposition !== "applied") {
|
|
285
405
|
return obsolete(run, reviewRoundTerminalization.reason ?? "review-round-mismatch");
|
|
@@ -313,7 +433,7 @@ export function terminalizeExactTaskRun(store, input, now) {
|
|
|
313
433
|
? undefined
|
|
314
434
|
: workItemExecutionGroupById(item, run.executionGroupId);
|
|
315
435
|
if (item !== null && group !== undefined) {
|
|
316
|
-
|
|
436
|
+
let grouped = recordExecutionLaneResult(group, run.executionLaneId, {
|
|
317
437
|
summary: input.outcome.summary,
|
|
318
438
|
...(input.reviewResult?.report === undefined ? {} : { report: input.reviewResult.report }),
|
|
319
439
|
...(input.reviewResult?.checks === undefined ? {} : { checks: input.reviewResult.checks }),
|
|
@@ -322,6 +442,18 @@ export function terminalizeExactTaskRun(store, input, now) {
|
|
|
322
442
|
...(input.reviewResult?.evidenceCommit === undefined ? {} : { evidenceCommit: input.reviewResult.evidenceCommit }),
|
|
323
443
|
...(input.reviewResult?.gitSnapshot === undefined ? {} : { gitSnapshot: input.reviewResult.gitSnapshot })
|
|
324
444
|
}, input.outcome.status === "yielded" ? "completed" : "failed", now);
|
|
445
|
+
if (input.settleFailedExecutionGroup === true
|
|
446
|
+
&& input.outcome.status === "failed"
|
|
447
|
+
&& grouped.resolution === undefined
|
|
448
|
+
&& grouped.lanes.every((lane) => (lane.status === "yielded"
|
|
449
|
+
|| lane.status === "completed"
|
|
450
|
+
|| lane.status === "failed"
|
|
451
|
+
|| lane.status === "skipped"))) {
|
|
452
|
+
grouped = resolveExecutionGroup(grouped, {
|
|
453
|
+
decision: "blocked",
|
|
454
|
+
summary: input.outcome.summary
|
|
455
|
+
}, now);
|
|
456
|
+
}
|
|
325
457
|
store.saveWorkItem(input.taskId, updateWorkItemExecutionGroup(item, grouped, now));
|
|
326
458
|
}
|
|
327
459
|
}
|
|
@@ -643,6 +775,12 @@ function matchesSessionFence(sessions, input) {
|
|
|
643
775
|
&& inFlight.receiptId === input.receiptId);
|
|
644
776
|
}
|
|
645
777
|
function matchesLaunchFence(store, sessions, input) {
|
|
778
|
+
// A resumed Run shares the native Provider Session whose process environment
|
|
779
|
+
// was created for an earlier Run. Once that exact native Session has passed
|
|
780
|
+
// matchesSessionFence, its immutable per-launch environment is not a second
|
|
781
|
+
// Run identity. Opaque hosts still require the launch fence below.
|
|
782
|
+
if (input.nativeSessionId !== undefined)
|
|
783
|
+
return true;
|
|
646
784
|
if (input.launchId === undefined)
|
|
647
785
|
return true;
|
|
648
786
|
const session = sessions?.sessions[input.agentId];
|
|
@@ -664,7 +802,9 @@ function settleLaunchReservation(store, sessions, input) {
|
|
|
664
802
|
const mailbox = store.getWorkMailbox(target);
|
|
665
803
|
const reservation = mailbox?.processing;
|
|
666
804
|
const session = sessions?.sessions[input.agentId];
|
|
667
|
-
const launchId = input.
|
|
805
|
+
const launchId = input.nativeSessionId === undefined
|
|
806
|
+
? input.launchId ?? session?.launchId
|
|
807
|
+
: session?.launchId;
|
|
668
808
|
if (launchId === undefined || !isRuntimeLaunchReservation(reservation, launchId))
|
|
669
809
|
return;
|
|
670
810
|
const settled = completeProcessing(mailbox, reservation.batchId);
|