@zq-silk/yui 0.13.3 → 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 +70 -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
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
2
|
+
import { terminalizeTaskRoleRunSession } from "../executor/agentExecutor.js";
|
|
3
|
+
import { recordExecutionLaneResult } from "../execution/executionGroup.js";
|
|
4
|
+
import { usageError } from "../errors/cliError.js";
|
|
5
|
+
import { requestDurableJobCancel } from "../job/durableJob.js";
|
|
6
|
+
import { finishReviewRound, updateReviewExecutionGroup } from "../review/reviewRound.js";
|
|
7
|
+
import { updateRoleStatus } from "../role/role.js";
|
|
8
|
+
import { failAgentRun } from "../run/agentRun.js";
|
|
9
|
+
import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
|
|
10
|
+
import { startTaskExecution, stopTaskExecution } from "../task/task.js";
|
|
11
|
+
import { updateWorkItemExecutionGroup, workItemExecutionGroupById } from "../workItem/workItem.js";
|
|
12
|
+
import { taskActor } from "./taskActor.js";
|
|
13
|
+
export function parseTaskExecutionStopRequest(args) {
|
|
14
|
+
const taskId = args[0];
|
|
15
|
+
if (taskId === undefined || taskId.startsWith("--")) {
|
|
16
|
+
throw usageError("Task execution stop usage: yui task execution stop <task> --force --reason <text>.");
|
|
17
|
+
}
|
|
18
|
+
let force = false;
|
|
19
|
+
let reason;
|
|
20
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
21
|
+
const argument = args[index];
|
|
22
|
+
if (argument === "--force" && !force) {
|
|
23
|
+
force = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (argument === "--reason" && reason === undefined) {
|
|
27
|
+
reason = args[index + 1];
|
|
28
|
+
index += 1;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
throw usageError(`Unknown or duplicated Task execution stop option: ${String(argument)}.`);
|
|
32
|
+
}
|
|
33
|
+
if (!force)
|
|
34
|
+
throw usageError("Task execution stop requires --force.");
|
|
35
|
+
const normalizedReason = reason?.trim();
|
|
36
|
+
if (normalizedReason === undefined
|
|
37
|
+
|| normalizedReason.length === 0
|
|
38
|
+
|| normalizedReason.startsWith("--")) {
|
|
39
|
+
throw usageError("Task execution stop requires --reason <text>.");
|
|
40
|
+
}
|
|
41
|
+
return { taskId, reason: normalizedReason };
|
|
42
|
+
}
|
|
43
|
+
export function parseTaskExecutionStartRequest(args) {
|
|
44
|
+
if (args.length !== 1 || args[0].startsWith("--")) {
|
|
45
|
+
throw usageError("Task execution start usage: yui task execution start <task>.");
|
|
46
|
+
}
|
|
47
|
+
return args[0];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Fence execution before touching the physical runtime. Progress records and
|
|
51
|
+
* workspaces remain intact; only current attempts and delivery claims end.
|
|
52
|
+
*/
|
|
53
|
+
export function stopTaskExecutionCommand(request, store, options = {}) {
|
|
54
|
+
const now = options.now?.() ?? new Date();
|
|
55
|
+
const actor = requireOperatorOrUser(options.environment, request.taskId);
|
|
56
|
+
return store.transaction((tx) => {
|
|
57
|
+
const task = tx.getTask(request.taskId);
|
|
58
|
+
if (task === null)
|
|
59
|
+
throw usageError(`Task not found: ${request.taskId}.`);
|
|
60
|
+
if (task.status !== "active") {
|
|
61
|
+
throw usageError(`Only an active Task can be stopped: ${task.id}.`);
|
|
62
|
+
}
|
|
63
|
+
const changed = task.executionGate.state !== "stopped";
|
|
64
|
+
tx.saveTask(stopTaskExecution(task, now));
|
|
65
|
+
const activeRuns = tx.listAgentRuns(task.id).filter((run) => run.status === "active");
|
|
66
|
+
const activeJobs = tx.listDurableJobs(task.id)
|
|
67
|
+
.filter((job) => job.status === "queued" || job.status === "running");
|
|
68
|
+
for (const job of activeJobs) {
|
|
69
|
+
tx.saveDurableJob(task.id, requestDurableJobCancel(job, now));
|
|
70
|
+
}
|
|
71
|
+
failExecutionAttempts(tx, task.id, activeRuns, request.reason, now);
|
|
72
|
+
for (const run of activeRuns) {
|
|
73
|
+
tx.saveAgentRun(failAgentRun(run, `Task execution stopped: ${request.reason}`, now));
|
|
74
|
+
if (run.executionGroupId !== undefined && run.executionLaneId !== undefined) {
|
|
75
|
+
tx.clearActiveExecutionLaneRun(task.id, run.executionGroupId, run.executionLaneId);
|
|
76
|
+
}
|
|
77
|
+
tx.clearActiveAgentRun(task.id, run.roleName);
|
|
78
|
+
}
|
|
79
|
+
const roleNames = taskRuntimeRoleNames(tx, task.id, activeRuns);
|
|
80
|
+
for (const roleName of roleNames) {
|
|
81
|
+
tx.clearActiveAgentRun(task.id, roleName);
|
|
82
|
+
const sessions = tx.getTaskRoleSessionSet(task.id, roleName);
|
|
83
|
+
if (sessions !== null && sessions.inFlight !== null) {
|
|
84
|
+
tx.saveTaskRoleSessionSet(terminalizeCurrentRun(sessions, now));
|
|
85
|
+
}
|
|
86
|
+
const role = tx.getRole(task.id, roleName);
|
|
87
|
+
if (role !== null && role.status !== "idle") {
|
|
88
|
+
tx.saveRole(task.id, updateRoleStatus(role, "idle", now));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// A stop is allowed to discard stale pointer projections even when their
|
|
92
|
+
// historical Runs are already terminal.
|
|
93
|
+
for (const run of tx.listAgentRuns(task.id)) {
|
|
94
|
+
if (run.executionGroupId !== undefined && run.executionLaneId !== undefined) {
|
|
95
|
+
tx.clearActiveExecutionLaneRun(task.id, run.executionGroupId, run.executionLaneId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const mailbox of tx.listWorkMailboxes()) {
|
|
99
|
+
if ("taskId" in mailbox.target && mailbox.target.taskId === task.id) {
|
|
100
|
+
tx.removeWorkMailbox(mailbox.target);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
tx.clearLeaderFailure(task.id);
|
|
104
|
+
tx.saveEvent(task.id, createTaskEvent(tx.nextEventId(task.id), task.id, "task.execution-stopped", {
|
|
105
|
+
by: actor,
|
|
106
|
+
reason: request.reason,
|
|
107
|
+
terminatedRuns: String(activeRuns.length)
|
|
108
|
+
}, now));
|
|
109
|
+
return {
|
|
110
|
+
taskId: task.id,
|
|
111
|
+
changed,
|
|
112
|
+
roleNames,
|
|
113
|
+
terminatedRunIds: activeRuns.map(({ id }) => id),
|
|
114
|
+
cancelledJobIds: activeJobs.map(({ id }) => id),
|
|
115
|
+
output: changed
|
|
116
|
+
? `Stopped Task execution: ${task.id}. Progress was preserved; `
|
|
117
|
+
+ `${activeRuns.length} active attempt(s) were terminated and `
|
|
118
|
+
+ `${activeJobs.length} DurableJob(s) were cancelled.`
|
|
119
|
+
: `Task execution is already stopped: ${task.id}. Runtime cleanup will be verified.`
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** Enable execution only after the caller has proven all old physical writers absent. */
|
|
124
|
+
export function startTaskExecutionCommand(taskId, store, options = {}) {
|
|
125
|
+
const now = options.now?.() ?? new Date();
|
|
126
|
+
const actor = requireOperatorOrUser(options.environment, taskId);
|
|
127
|
+
return store.transaction((tx) => {
|
|
128
|
+
const task = tx.getTask(taskId);
|
|
129
|
+
if (task === null)
|
|
130
|
+
throw usageError(`Task not found: ${taskId}.`);
|
|
131
|
+
if (task.status !== "active") {
|
|
132
|
+
throw usageError(`Only an active Task can be started: ${task.id}.`);
|
|
133
|
+
}
|
|
134
|
+
if (task.executionGate.state === "enabled") {
|
|
135
|
+
return { taskId: task.id, changed: false, output: `Task execution is already enabled: ${task.id}.` };
|
|
136
|
+
}
|
|
137
|
+
if (tx.listAgentRuns(task.id).some((run) => run.status === "active")
|
|
138
|
+
|| tx.listDurableJobs(task.id).some((job) => job.status === "queued" || job.status === "running")) {
|
|
139
|
+
throw usageError(`Task still has an active execution attempt: ${task.id}.`);
|
|
140
|
+
}
|
|
141
|
+
for (const mailbox of tx.listWorkMailboxes()) {
|
|
142
|
+
if ("taskId" in mailbox.target && mailbox.target.taskId === task.id) {
|
|
143
|
+
tx.removeWorkMailbox(mailbox.target);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
tx.clearLeaderFailure(task.id);
|
|
147
|
+
tx.saveTask(startTaskExecution(task, now));
|
|
148
|
+
queueLeaderWakeup(tx, task.id, "execution-started", now);
|
|
149
|
+
tx.saveEvent(task.id, createTaskEvent(tx.nextEventId(task.id), task.id, "task.execution-started", { by: actor }, now));
|
|
150
|
+
return {
|
|
151
|
+
taskId: task.id,
|
|
152
|
+
changed: true,
|
|
153
|
+
output: `Started Task execution: ${task.id}. The Leader will continue from durable progress.`
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/** Remove cleanup-generated delivery records after physical release is proven. */
|
|
158
|
+
export function finalizeStoppedTaskExecution(taskId, store) {
|
|
159
|
+
store.transaction((tx) => {
|
|
160
|
+
const task = tx.getTask(taskId);
|
|
161
|
+
if (task === null)
|
|
162
|
+
throw usageError(`Task not found: ${taskId}.`);
|
|
163
|
+
if (task.executionGate.state !== "stopped") {
|
|
164
|
+
throw usageError(`Task execution is not stopped: ${taskId}.`);
|
|
165
|
+
}
|
|
166
|
+
for (const mailbox of tx.listWorkMailboxes()) {
|
|
167
|
+
if ("taskId" in mailbox.target && mailbox.target.taskId === taskId) {
|
|
168
|
+
tx.removeWorkMailbox(mailbox.target);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
tx.clearLeaderFailure(taskId);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function requireOperatorOrUser(environment, taskId) {
|
|
175
|
+
const actor = taskActor(environment, taskId);
|
|
176
|
+
if (actor === "leader") {
|
|
177
|
+
throw usageError("Task execution stop/start requires the global Operator or a human user.");
|
|
178
|
+
}
|
|
179
|
+
return actor;
|
|
180
|
+
}
|
|
181
|
+
function terminalizeCurrentRun(sessions, now) {
|
|
182
|
+
const inFlight = sessions.inFlight;
|
|
183
|
+
return terminalizeTaskRoleRunSession(sessions, {
|
|
184
|
+
agentId: inFlight.agentId,
|
|
185
|
+
runId: inFlight.runId,
|
|
186
|
+
receiptId: inFlight.receiptId
|
|
187
|
+
}, now);
|
|
188
|
+
}
|
|
189
|
+
function taskRuntimeRoleNames(store, taskId, runs) {
|
|
190
|
+
const names = new Set(store.listRoles(taskId).map(({ name }) => name));
|
|
191
|
+
for (const sessions of store.listRoleSessionSets(taskId))
|
|
192
|
+
names.add(sessions.owner.roleName);
|
|
193
|
+
for (const run of runs)
|
|
194
|
+
names.add(run.roleName);
|
|
195
|
+
for (const owner of store.listSessionOwners()) {
|
|
196
|
+
if (owner.owner.scope === "task" && owner.owner.taskId === taskId) {
|
|
197
|
+
names.add(owner.owner.roleName);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return [...names].sort();
|
|
201
|
+
}
|
|
202
|
+
function failExecutionAttempts(store, taskId, runs, reason, now) {
|
|
203
|
+
const summary = `Task execution stopped: ${reason}`;
|
|
204
|
+
const workItems = new Map(store.listWorkItems(taskId).map((item) => [item.id, item]));
|
|
205
|
+
const reviewRounds = new Map(store.listReviewRounds(taskId).map((round) => [round.id, round]));
|
|
206
|
+
const affectedReviewRoundIds = new Set();
|
|
207
|
+
for (const run of runs) {
|
|
208
|
+
if (run.executionGroupId === undefined || run.executionLaneId === undefined)
|
|
209
|
+
continue;
|
|
210
|
+
if (run.workItemId !== undefined) {
|
|
211
|
+
const item = workItems.get(run.workItemId);
|
|
212
|
+
const group = item === undefined
|
|
213
|
+
? undefined
|
|
214
|
+
: workItemExecutionGroupById(item, run.executionGroupId);
|
|
215
|
+
const lane = group?.lanes.find(({ id }) => id === run.executionLaneId);
|
|
216
|
+
if (item !== undefined && group !== undefined && lane !== undefined
|
|
217
|
+
&& item.currentExecutionGroupId === group.id
|
|
218
|
+
&& !["yielded", "completed", "failed", "skipped"].includes(lane.status)) {
|
|
219
|
+
const updated = updateWorkItemExecutionGroup(item, recordExecutionLaneResult(group, lane.id, { summary }, "failed", now), now);
|
|
220
|
+
workItems.set(item.id, updated);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (run.reviewRoundId !== undefined) {
|
|
224
|
+
affectedReviewRoundIds.add(run.reviewRoundId);
|
|
225
|
+
const round = reviewRounds.get(run.reviewRoundId);
|
|
226
|
+
const group = round?.executionGroup?.id === run.executionGroupId
|
|
227
|
+
? round.executionGroup
|
|
228
|
+
: undefined;
|
|
229
|
+
const lane = group?.lanes.find(({ id }) => id === run.executionLaneId);
|
|
230
|
+
if (round !== undefined && group !== undefined && lane !== undefined
|
|
231
|
+
&& !["yielded", "completed", "failed", "skipped"].includes(lane.status)) {
|
|
232
|
+
reviewRounds.set(round.id, updateReviewExecutionGroup(round, recordExecutionLaneResult(group, lane.id, { summary }, "failed", now)));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
for (const item of workItems.values()) {
|
|
237
|
+
const original = store.getWorkItem(item.taskId, item.id);
|
|
238
|
+
if (original !== null && original.revision !== item.revision) {
|
|
239
|
+
store.saveWorkItem(item.taskId, item);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
for (const reviewRoundId of affectedReviewRoundIds) {
|
|
243
|
+
const round = reviewRounds.get(reviewRoundId);
|
|
244
|
+
const original = store.getReviewRound(round.taskId, round.id);
|
|
245
|
+
if (original === null)
|
|
246
|
+
continue;
|
|
247
|
+
const changed = JSON.stringify(original.executionGroup) !== JSON.stringify(round.executionGroup);
|
|
248
|
+
const terminal = round.status === "pending" || round.status === "running"
|
|
249
|
+
? finishReviewRound(round, "failed", summary, now)
|
|
250
|
+
: round;
|
|
251
|
+
if (changed || terminal !== round)
|
|
252
|
+
store.saveReviewRound(round.taskId, terminal);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -5,7 +5,6 @@ import { extractReviewFindings, planRepairWave } from "../task/repairWave.js";
|
|
|
5
5
|
import { projectTaskOrchestration } from "../observability/orchestrationMetrics.js";
|
|
6
6
|
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
7
7
|
import { buildTaskExecutionProjection } from "../scheduler/taskExecutionProjection.js";
|
|
8
|
-
import { projectExecutionLaneRunRecoveries } from "../run/recoveryProjection.js";
|
|
9
8
|
import { projectReviewDecision } from "../review/reviewDecision.js";
|
|
10
9
|
/**
|
|
11
10
|
* Issue 07 (Leader convergence): read-only `yui task next-action <task>`.
|
|
@@ -42,8 +41,7 @@ export function runTaskNextActionCommand(args, store, currentTaskReviewCandidate
|
|
|
42
41
|
const actionFacts = {
|
|
43
42
|
...facts,
|
|
44
43
|
currentTaskReviewCandidate,
|
|
45
|
-
executionGroups: execution.executionGroups
|
|
46
|
-
runRecoveries: projectExecutionLaneRunRecoveries(reader, taskId, execution.executionGroups)
|
|
44
|
+
executionGroups: execution.executionGroups
|
|
47
45
|
};
|
|
48
46
|
const action = projectNextAction(actionFacts);
|
|
49
47
|
const repairWave = repairWaveFor(action, actionFacts);
|
|
@@ -123,14 +123,21 @@ function buildTaskOverviewEntry(task, store, now, runtimeHealthPolicy) {
|
|
|
123
123
|
now,
|
|
124
124
|
runtimeHealthPolicy
|
|
125
125
|
});
|
|
126
|
-
const next = execution.
|
|
126
|
+
const next = execution.status === "stopped"
|
|
127
127
|
? {
|
|
128
128
|
action: execution.action,
|
|
129
129
|
owner: execution.owner,
|
|
130
130
|
kind: "execution",
|
|
131
131
|
summary: execution.summary
|
|
132
132
|
}
|
|
133
|
-
:
|
|
133
|
+
: execution.action === "recover-execution"
|
|
134
|
+
? {
|
|
135
|
+
action: execution.action,
|
|
136
|
+
owner: execution.owner,
|
|
137
|
+
kind: "execution",
|
|
138
|
+
summary: execution.summary
|
|
139
|
+
}
|
|
140
|
+
: legacyNext;
|
|
134
141
|
return {
|
|
135
142
|
...task,
|
|
136
143
|
brief,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
2
|
import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
|
|
3
|
-
import { hasRuntimeCleanupObligation,
|
|
3
|
+
import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
4
4
|
import { isRoleRunStalled, latestStallProgressAt } from "../scheduler/roleRunStall.js";
|
|
5
5
|
import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
6
6
|
import { classifyRuntimeHealth, projectRuntimeMailbox, projectRuntimeObservation, projectRuntimeTaskEvents, runtimeDisplayStatus } from "../runtime/runtimeProjection.js";
|
|
@@ -9,7 +9,6 @@ import { resolveRuntimeHealth } from "../config/yuiConfig.js";
|
|
|
9
9
|
import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
10
10
|
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
11
11
|
import { projectSessionTokenMetrics, resolveSessionTokenIdentity } from "../runtime/sessionTokenMetrics.js";
|
|
12
|
-
import { freshConversationLaunchBlockers, projectConversationSwitch } from "../runtime/conversationSwitch.js";
|
|
13
12
|
export function inspectTaskRoleRuntimeStatuses(taskId, roles, store, panes, now = new Date()) {
|
|
14
13
|
const taskOpenInputRequestCount = store.listInputRequests(taskId)
|
|
15
14
|
.filter((request) => request.status === "open").length;
|
|
@@ -94,15 +93,9 @@ export function renderTaskRoleRuntimeStatus(status) {
|
|
|
94
93
|
? `needs-attention (${status.stall.kind ?? "workflow-not-progressing"}; no workflow progress since ${status.stall.progressAt ?? "unknown"})`
|
|
95
94
|
: "none"}`,
|
|
96
95
|
` Native session ${nativeSession}`,
|
|
97
|
-
` Session switch ${status.conversationSwitch === null
|
|
98
|
-
? "none"
|
|
99
|
-
: `${status.conversationSwitch.status} (${status.conversationSwitch.requestId}; ${status.conversationSwitch.reason})`}`,
|
|
100
96
|
` Agent runtime ${runtime}`,
|
|
101
97
|
` Session tokens ${sessionTokens}`,
|
|
102
98
|
` Runtime cleanup ${status.runtimeCleanupPending ? "pending" : "none"}`,
|
|
103
|
-
` Fresh launch ${status.freshLaunchAllowed
|
|
104
|
-
? "allowed"
|
|
105
|
-
: `blocked (${status.freshLaunchBlockers.join(", ")})`}`,
|
|
106
99
|
` tmux pane ${tmux}`,
|
|
107
100
|
` Workspace ${status.workspace.managed ? status.workspace.root : status.workspace.path}`,
|
|
108
101
|
...workspaceDetails
|
|
@@ -127,26 +120,12 @@ export function taskRoleNativeSessionLabel(status) {
|
|
|
127
120
|
return status.nativeSession?.status ?? "unbound";
|
|
128
121
|
}
|
|
129
122
|
export function inspectTaskRoleSessionRecovery(taskId, roleName, store) {
|
|
130
|
-
const sessions = store.getTaskRoleSessionSet(taskId, roleName);
|
|
131
123
|
const target = runtimeLifecycleTarget({ scope: "task", taskId, roleName });
|
|
132
124
|
const runtimeMailbox = store.getWorkMailbox(target);
|
|
133
|
-
const roleMailbox = store.getWorkMailbox({ kind: "role", taskId, roleName });
|
|
134
|
-
const switchBlockers = freshConversationLaunchBlockers({
|
|
135
|
-
sessions,
|
|
136
|
-
events: store.listEvents(taskId),
|
|
137
|
-
mailbox: roleMailbox,
|
|
138
|
-
roleName
|
|
139
|
-
});
|
|
140
|
-
const freshLaunchBlockers = [
|
|
141
|
-
...switchBlockers,
|
|
142
|
-
...(hasRuntimeLifecycleWork(runtimeMailbox) ? ["runtime-lifecycle-busy"] : [])
|
|
143
|
-
];
|
|
144
125
|
return {
|
|
145
126
|
taskId,
|
|
146
127
|
roleName,
|
|
147
|
-
runtimeCleanupPending: hasRuntimeCleanupObligation(runtimeMailbox)
|
|
148
|
-
freshLaunchAllowed: freshLaunchBlockers.length === 0,
|
|
149
|
-
freshLaunchBlockers
|
|
128
|
+
runtimeCleanupPending: hasRuntimeCleanupObligation(runtimeMailbox)
|
|
150
129
|
};
|
|
151
130
|
}
|
|
152
131
|
export function taskRoleOpenInputLabel(status) {
|
|
@@ -203,7 +182,6 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
|
|
|
203
182
|
? { managed: false, path: role.workspace }
|
|
204
183
|
: { ...managedWorkspace, managed: true };
|
|
205
184
|
const events = store.listEvents(taskId);
|
|
206
|
-
const conversationSwitch = projectConversationSwitch(events, role.name, sessions);
|
|
207
185
|
const sessionTokens = projectSessionTokenMetrics(events, resolveSessionTokenIdentity(nativeSession === null
|
|
208
186
|
? null
|
|
209
187
|
: { taskId, roleName: role.name, ...nativeSession }));
|
|
@@ -243,7 +221,6 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
|
|
|
243
221
|
tmux,
|
|
244
222
|
workspace,
|
|
245
223
|
sessionTokens,
|
|
246
|
-
conversationSwitch,
|
|
247
224
|
runtime,
|
|
248
225
|
stall
|
|
249
226
|
};
|
|
@@ -242,11 +242,13 @@ export class AgentRuntimeObserver {
|
|
|
242
242
|
// FileTaskStore remains a development/compatibility fallback. The normal
|
|
243
243
|
// Controller store is SQLite and must discover only its indexed hot set.
|
|
244
244
|
const activeTasks = indexedTaskIds === undefined
|
|
245
|
-
? this.store.listTasks().filter((task) => task.status === "active")
|
|
245
|
+
? this.store.listTasks().filter((task) => (task.status === "active" && task.executionGate.state === "enabled"))
|
|
246
246
|
: [...new Set(indexedTaskIds)]
|
|
247
247
|
.sort(numericCompare)
|
|
248
248
|
.map((taskId) => this.store.getTask(taskId))
|
|
249
|
-
.filter((task) => (task !== null
|
|
249
|
+
.filter((task) => (task !== null
|
|
250
|
+
&& task.status === "active"
|
|
251
|
+
&& task.executionGate.state === "enabled"));
|
|
250
252
|
for (const task of activeTasks) {
|
|
251
253
|
// A Task still incurs one O(E) event projection. Group those observations
|
|
252
254
|
// by Run and sort each group once so every active Run can reuse the same
|
|
@@ -427,10 +427,12 @@ export class FileTaskWorkflowRuntime {
|
|
|
427
427
|
this.clientOptions = clientOptions;
|
|
428
428
|
}
|
|
429
429
|
notifyStateChanged(taskId) {
|
|
430
|
-
this.notifyMailboxChanged({ kind: "task", taskId });
|
|
430
|
+
void this.notifyMailboxChanged({ kind: "task", taskId });
|
|
431
431
|
}
|
|
432
432
|
notifyMailboxChanged(target) {
|
|
433
|
-
|
|
433
|
+
const pending = callFileTaskController(this.home, "scheduler.signal", { key: controllerMailboxKey(target) }, this.clientOptions).then(() => { });
|
|
434
|
+
void pending.catch(this.clientOptions.onError ?? (() => { }));
|
|
435
|
+
return pending;
|
|
434
436
|
}
|
|
435
437
|
reconcileTask(taskId) {
|
|
436
438
|
void this.#prepareAndScan(taskId).catch(this.clientOptions.onError ?? (() => { }));
|
|
@@ -469,12 +471,52 @@ export class FileTaskWorkflowRuntime {
|
|
|
469
471
|
}
|
|
470
472
|
}
|
|
471
473
|
}
|
|
474
|
+
/** Wait until every cancellation requested by Task execution stop is physically settled. */
|
|
475
|
+
async stopTaskDurableJobs(taskId) {
|
|
476
|
+
const deadline = Date.now() + LIFECYCLE_REQUEST_TIMEOUT_MS;
|
|
477
|
+
for (;;) {
|
|
478
|
+
const active = this.store.listActiveDurableJobs()
|
|
479
|
+
.filter((job) => job.taskId === taskId);
|
|
480
|
+
if (active.length === 0)
|
|
481
|
+
return;
|
|
482
|
+
await callFileTaskController(this.home, "scheduler.scan", {}, {
|
|
483
|
+
...this.clientOptions,
|
|
484
|
+
requestTimeoutMs: LIFECYCLE_REQUEST_TIMEOUT_MS
|
|
485
|
+
});
|
|
486
|
+
const remaining = this.store.listActiveDurableJobs()
|
|
487
|
+
.filter((job) => job.taskId === taskId);
|
|
488
|
+
if (remaining.length === 0)
|
|
489
|
+
return;
|
|
490
|
+
if (Date.now() >= deadline) {
|
|
491
|
+
throw new Error(`DurableJobs did not stop: ${remaining.map(({ id }) => `${taskId}/${id}`).join(", ")}.`);
|
|
492
|
+
}
|
|
493
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
472
496
|
/**
|
|
473
497
|
* Issue 03 archive postcondition. Re-verifies physical absence after the
|
|
474
498
|
* runtime stop and blocks archive while any owned Provider root is still
|
|
475
499
|
* live, preserving the owner records for Operator recovery.
|
|
476
500
|
*/
|
|
477
501
|
async assertTaskPhysicalResourcesReleased(taskId) {
|
|
502
|
+
const activeJobs = this.store.listActiveDurableJobs()
|
|
503
|
+
.filter((job) => job.taskId === taskId);
|
|
504
|
+
if (activeJobs.length > 0) {
|
|
505
|
+
throw new WorkspaceCleanupBlockedError("physical-resource-live", `task:${taskId}`, true, `Task physical resources are not released: ${activeJobs.length} DurableJob(s) are still active: `
|
|
506
|
+
+ activeJobs.map(({ id }) => id).join(", "));
|
|
507
|
+
}
|
|
508
|
+
const liveSessions = this.store.listRoleSessionSets(taskId).flatMap((sessions) => (Object.values(sessions.sessions)
|
|
509
|
+
.filter((session) => session.status !== "stopped" && session.status !== "broken")
|
|
510
|
+
.map((session) => `${sessions.owner.roleName}/${session.agentId}/${session.status}`)));
|
|
511
|
+
if (liveSessions.length > 0) {
|
|
512
|
+
throw new WorkspaceCleanupBlockedError("physical-resource-live", `task:${taskId}`, true, `Task physical resources are not released: current Role Session state is still live: `
|
|
513
|
+
+ liveSessions.join(", "));
|
|
514
|
+
}
|
|
515
|
+
const livePanes = this.tmux.inspectTaskRolePanes(taskId).filter((pane) => !pane.dead);
|
|
516
|
+
if (livePanes.length > 0) {
|
|
517
|
+
throw new WorkspaceCleanupBlockedError("physical-resource-live", `task:${taskId}`, true, `Task physical resources are not released: ${livePanes.length} tmux Role pane(s) are still live: `
|
|
518
|
+
+ livePanes.map(({ roleName, pid }) => `${roleName} (pid ${pid ?? "?"})`).join(", "));
|
|
519
|
+
}
|
|
478
520
|
const reconciliation = new SessionOwnerReconciliation({
|
|
479
521
|
home: this.home,
|
|
480
522
|
store: this.store,
|
|
@@ -596,6 +638,7 @@ export class FileTaskWorkflowRuntime {
|
|
|
596
638
|
const task = this.store.getTask(taskId);
|
|
597
639
|
if (task !== null
|
|
598
640
|
&& task.status === "active"
|
|
641
|
+
&& task.executionGate.state === "enabled"
|
|
599
642
|
&& this.workspacePreparer !== undefined) {
|
|
600
643
|
await this.workspacePreparer.prepareTaskWorkspace(taskId);
|
|
601
644
|
}
|
|
@@ -658,7 +658,7 @@ async function prepareActiveWorkspaces(store, workspace, selection, maintenanceF
|
|
|
658
658
|
if (selection.blockedTaskIds?.has(taskId))
|
|
659
659
|
return [];
|
|
660
660
|
const task = store.getTask(taskId);
|
|
661
|
-
return task?.status === "active" ? [task] : [];
|
|
661
|
+
return task?.status === "active" && task.executionGate.state === "enabled" ? [task] : [];
|
|
662
662
|
});
|
|
663
663
|
const failed = new Set();
|
|
664
664
|
const ready = new Set();
|
|
@@ -1383,7 +1383,8 @@ export class FileTaskController {
|
|
|
1383
1383
|
#scheduleTaskPassRetry(scope) {
|
|
1384
1384
|
if (this.#stopped || this.#pendingFull)
|
|
1385
1385
|
return;
|
|
1386
|
-
|
|
1386
|
+
const task = this.store.getTask(scope.taskId);
|
|
1387
|
+
if (task?.status !== "active" || task.executionGate.state !== "enabled") {
|
|
1387
1388
|
this.#clearTaskPassRetry(scope.taskId);
|
|
1388
1389
|
return;
|
|
1389
1390
|
}
|
|
@@ -1410,7 +1411,9 @@ export class FileTaskController {
|
|
|
1410
1411
|
if (this.#stopped || this.#pendingFull)
|
|
1411
1412
|
return;
|
|
1412
1413
|
try {
|
|
1413
|
-
|
|
1414
|
+
const currentTask = this.store.getTask(scope.taskId);
|
|
1415
|
+
if (currentTask?.status !== "active"
|
|
1416
|
+
|| currentTask.executionGate.state !== "enabled") {
|
|
1414
1417
|
this.#clearTaskPassRetry(scope.taskId);
|
|
1415
1418
|
return;
|
|
1416
1419
|
}
|