@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
|
@@ -773,7 +773,7 @@ export class SqliteTaskStore {
|
|
|
773
773
|
if (found === undefined)
|
|
774
774
|
throw new StorageRecordError(`Task Project not found: ${binding.projectId}`);
|
|
775
775
|
}
|
|
776
|
-
const isActive = task.status === "active" ? 1 : 0;
|
|
776
|
+
const isActive = task.status === "active" && task.executionGate.state === "enabled" ? 1 : 0;
|
|
777
777
|
// The catalog projection is inserted first because task_records FKs it.
|
|
778
778
|
this.#db.prepare(`INSERT INTO tasks_catalog (task_id, status, lifecycle, is_active, created_at, updated_at)
|
|
779
779
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
@@ -807,6 +807,7 @@ export class SqliteTaskStore {
|
|
|
807
807
|
task: {
|
|
808
808
|
id: task.id,
|
|
809
809
|
status: task.status,
|
|
810
|
+
executionGate: task.executionGate,
|
|
810
811
|
projectBindings: task.projectBindings,
|
|
811
812
|
type: task.type
|
|
812
813
|
},
|
|
@@ -835,7 +836,6 @@ export class SqliteTaskStore {
|
|
|
835
836
|
return {
|
|
836
837
|
...base,
|
|
837
838
|
agentRuns: operationalTaskRecords(this.listAgentRuns(taskId), this.listEvents(taskId), "agent-run"),
|
|
838
|
-
roleSessionSets: this.listRoleSessionSets(taskId),
|
|
839
839
|
managedWorkspaces: this.#sortById(this.#listPayload("managed_workspaces", "task_id = ?", [taskId]), (workspace) => managedWorkspaceKey(workspace.owner)),
|
|
840
840
|
durableJobs: this.#sortById(this.#listPayload("durable_jobs", "task_id = ?", [taskId]), (job) => job.id),
|
|
841
841
|
integrationQueueEntries: this.#sortById(this.#listPayload("integration_queue", "task_id = ?", [taskId]), (entry) => entry.id),
|
|
@@ -1754,6 +1754,10 @@ export class SqliteTaskStore {
|
|
|
1754
1754
|
return;
|
|
1755
1755
|
}
|
|
1756
1756
|
this.transaction((store) => {
|
|
1757
|
+
const task = store.getTask(run.taskId);
|
|
1758
|
+
if (task === null || task.status !== "active" || task.executionGate.state !== "enabled") {
|
|
1759
|
+
throw new StorageRecordError(`Task execution is not enabled: ${run.taskId}.`);
|
|
1760
|
+
}
|
|
1757
1761
|
this.#assertActiveRunForWrite(run);
|
|
1758
1762
|
const current = store.getActiveAgentRun(run.taskId, run.roleName);
|
|
1759
1763
|
if (current !== null && current.id !== run.id) {
|
|
@@ -1796,6 +1800,10 @@ export class SqliteTaskStore {
|
|
|
1796
1800
|
throw new StorageRecordError(`Active execution-lane run requires group and lane ids: ${run.id}`);
|
|
1797
1801
|
}
|
|
1798
1802
|
this.transaction((store) => {
|
|
1803
|
+
const task = store.getTask(run.taskId);
|
|
1804
|
+
if (task === null || task.status !== "active" || task.executionGate.state !== "enabled") {
|
|
1805
|
+
throw new StorageRecordError(`Task execution is not enabled: ${run.taskId}.`);
|
|
1806
|
+
}
|
|
1799
1807
|
const key = executionLaneActiveRunKey(run.executionGroupId, run.executionLaneId);
|
|
1800
1808
|
this.#assertActiveRunForWrite(run, {
|
|
1801
1809
|
executionGroupId: run.executionGroupId,
|
|
@@ -61,7 +61,7 @@ export const CURRENT_PROJECT_SCHEMA_VERSION = 5;
|
|
|
61
61
|
export const CURRENT_AGENT_PROFILE_SCHEMA_VERSION = 2;
|
|
62
62
|
export const CURRENT_GLOBAL_ROLE_SCHEMA_VERSION = 3;
|
|
63
63
|
export const CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION = 3;
|
|
64
|
-
export const CURRENT_TASK_SCHEMA_VERSION =
|
|
64
|
+
export const CURRENT_TASK_SCHEMA_VERSION = 6;
|
|
65
65
|
export const CURRENT_TASK_BRIEF_SCHEMA_VERSION = 2;
|
|
66
66
|
export const CURRENT_CONTEXT_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
67
67
|
export const CURRENT_TASK_ROLE_SCHEMA_VERSION = 3;
|
|
@@ -431,7 +431,7 @@ export class FileTaskStore {
|
|
|
431
431
|
// The file rollback backend has no catalog index, so it filters the loaded
|
|
432
432
|
// aggregate. Layout-7 Controller hot paths use SQLite's bounded selector.
|
|
433
433
|
return this.listTasks()
|
|
434
|
-
.filter((task) => task.status === "active")
|
|
434
|
+
.filter((task) => task.status === "active" && task.executionGate.state === "enabled")
|
|
435
435
|
.map((task) => task.id);
|
|
436
436
|
}
|
|
437
437
|
getStateRevision() { return this.#state().revision; }
|
|
@@ -446,6 +446,7 @@ export class FileTaskStore {
|
|
|
446
446
|
task: {
|
|
447
447
|
id: aggregate.task.id,
|
|
448
448
|
status: aggregate.task.status,
|
|
449
|
+
executionGate: aggregate.task.executionGate,
|
|
449
450
|
projectBindings: aggregate.task.projectBindings,
|
|
450
451
|
type: aggregate.task.type
|
|
451
452
|
},
|
|
@@ -485,7 +486,6 @@ export class FileTaskStore {
|
|
|
485
486
|
return {
|
|
486
487
|
...base,
|
|
487
488
|
agentRuns: operationalTaskRecords(this.listAgentRuns(taskId), values(aggregate.events, "id"), "agent-run"),
|
|
488
|
-
roleSessionSets: this.listRoleSessionSets(taskId),
|
|
489
489
|
managedWorkspaces: values(aggregate.managedWorkspaces, (workspace) => managedWorkspaceKey(workspace.owner)),
|
|
490
490
|
durableJobs: values(aggregate.durableJobs, "id"),
|
|
491
491
|
integrationQueueEntries: values(aggregate.integrationQueue, "id"),
|
|
@@ -1116,6 +1116,10 @@ export class FileTaskStore {
|
|
|
1116
1116
|
if (run.status !== "active")
|
|
1117
1117
|
throw new StorageRecordError(`Active Agent run must have active status: ${run.id}`);
|
|
1118
1118
|
this.transaction((store) => {
|
|
1119
|
+
const task = store.getTask(run.taskId);
|
|
1120
|
+
if (task === null || task.status !== "active" || task.executionGate.state !== "enabled") {
|
|
1121
|
+
throw new StorageRecordError(`Task execution is not enabled: ${run.taskId}.`);
|
|
1122
|
+
}
|
|
1119
1123
|
const current = store.getActiveAgentRun(run.taskId, run.roleName);
|
|
1120
1124
|
if (current !== null && current.id !== run.id) {
|
|
1121
1125
|
throw new StorageRecordError(`Role already has an active Agent run: ${run.taskId}/${run.roleName}`);
|
|
@@ -1177,6 +1181,10 @@ export class FileTaskStore {
|
|
|
1177
1181
|
throw new StorageRecordError(`Lane active Agent run requires execution lineage: ${run.id}`);
|
|
1178
1182
|
}
|
|
1179
1183
|
this.transaction((store) => {
|
|
1184
|
+
const task = store.getTask(run.taskId);
|
|
1185
|
+
if (task === null || task.status !== "active" || task.executionGate.state !== "enabled") {
|
|
1186
|
+
throw new StorageRecordError(`Task execution is not enabled: ${run.taskId}.`);
|
|
1187
|
+
}
|
|
1180
1188
|
const key = executionLaneActiveRunKey(run.executionGroupId, run.executionLaneId);
|
|
1181
1189
|
const current = store.getActiveExecutionLaneRun(run.taskId, run.executionGroupId, run.executionLaneId);
|
|
1182
1190
|
if (current !== null && current.id !== run.id) {
|
|
@@ -5,7 +5,6 @@ import { isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
|
|
|
5
5
|
import { reviewFindingLedgerWriteFailedFromEvents } from "../review/reviewFindingLedger.js";
|
|
6
6
|
import { resolveRecordedTaskFinalReviewContract } from "../review/taskFinalReviewContractRebind.js";
|
|
7
7
|
import { sameTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
|
|
8
|
-
import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
9
8
|
const ACTIVE_JOB_STATUSES = new Set([
|
|
10
9
|
"queued",
|
|
11
10
|
"running",
|
|
@@ -160,19 +159,6 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
160
159
|
fix: `settle integration queue entry ${entry.id} (continue or supersede)`
|
|
161
160
|
});
|
|
162
161
|
}
|
|
163
|
-
// Provider continuations that may still write the Workspace.
|
|
164
|
-
for (const continuation of blockingProviderContinuations(facts.events)) {
|
|
165
|
-
if (!providerContinuationBlocksCompletion(continuation, facts))
|
|
166
|
-
continue;
|
|
167
|
-
const identity = continuation.identity;
|
|
168
|
-
blockers.push({
|
|
169
|
-
code: "blocking-provider-continuation",
|
|
170
|
-
ref: ref("provider-continuation", identity.continuationId),
|
|
171
|
-
reason: `Provider continuation ${identity.continuationId} (run ${continuation.runId}) `
|
|
172
|
-
+ "may still write the Workspace or has an identity conflict.",
|
|
173
|
-
fix: `wait for or recover the Provider turn on run ${continuation.runId}`
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
162
|
// Terminal child workspaces are cleanup advisories: Task completion is the
|
|
177
163
|
// semantic delivery boundary, while archive remains the fail-closed resource
|
|
178
164
|
// reclamation boundary. Missing/non-terminal ownership stays conservative.
|
|
@@ -183,18 +169,6 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
183
169
|
if (disposition?.kind === "advisory")
|
|
184
170
|
advisories.push(disposition.value);
|
|
185
171
|
}
|
|
186
|
-
// Active non-Leader Runs must finish. The Leader's own Run is terminalized
|
|
187
|
-
// by the completion transaction itself, so it is not a readiness blocker.
|
|
188
|
-
for (const run of facts.activeRuns) {
|
|
189
|
-
if (run.roleName === "leader")
|
|
190
|
-
continue;
|
|
191
|
-
blockers.push({
|
|
192
|
-
code: "active-run",
|
|
193
|
-
ref: ref("agent-run", run.id),
|
|
194
|
-
reason: `Role ${run.roleName} has an active Run ${run.id}.`,
|
|
195
|
-
fix: `wait for Run ${run.id} to yield or fail`
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
172
|
// Review finding ledger gate (enforce mode only).
|
|
199
173
|
// The transactional completion path runs this gate after final-review
|
|
200
174
|
// preparation: a pending/running Task-final Review is expected to resolve
|
|
@@ -248,47 +222,6 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
248
222
|
advisories: sortedAdvisories
|
|
249
223
|
};
|
|
250
224
|
}
|
|
251
|
-
/**
|
|
252
|
-
* A terminal Run releases only its completion blocker, never its immutable
|
|
253
|
-
* continuation audit. Missing or inconsistent ownership remains ambiguous and
|
|
254
|
-
* therefore fail-closed. A live exact native Session can still deliver or
|
|
255
|
-
* write for a terminal Run, so it retains the blocker until that Session is
|
|
256
|
-
* stopped, broken, or replaced by another Conversation identity.
|
|
257
|
-
*/
|
|
258
|
-
function providerContinuationBlocksCompletion(continuation, facts) {
|
|
259
|
-
if (continuation.identityConflict)
|
|
260
|
-
return true;
|
|
261
|
-
const ownerRun = facts.agentRuns.find(({ id }) => id === continuation.runId);
|
|
262
|
-
if (ownerRun === undefined
|
|
263
|
-
|| ownerRun.taskId !== continuation.taskId
|
|
264
|
-
|| ownerRun.roleName !== continuation.roleName
|
|
265
|
-
|| ownerRun.effective.agentId !== continuation.identity.accountScope) {
|
|
266
|
-
return true;
|
|
267
|
-
}
|
|
268
|
-
if (ownerRun.status !== "failed" && ownerRun.status !== "yielded")
|
|
269
|
-
return true;
|
|
270
|
-
const sessions = facts.roleSessionSets.find(({ owner }) => (owner.taskId === continuation.taskId && owner.roleName === continuation.roleName));
|
|
271
|
-
const session = sessions?.sessions[continuation.identity.accountScope];
|
|
272
|
-
if (session !== undefined
|
|
273
|
-
&& session.status !== "stopped"
|
|
274
|
-
&& session.status !== "broken"
|
|
275
|
-
&& session.nativeSessionId === continuation.identity.conversationId) {
|
|
276
|
-
return true;
|
|
277
|
-
}
|
|
278
|
-
const binding = sessions?.providerBinding;
|
|
279
|
-
if (binding === undefined || binding === null
|
|
280
|
-
|| binding.providerNamespace !== continuation.identity.providerNamespace
|
|
281
|
-
|| binding.accountScope !== continuation.identity.accountScope) {
|
|
282
|
-
return false;
|
|
283
|
-
}
|
|
284
|
-
const conversationIsCurrent = binding.conversations.some((conversation) => (conversation.conversationId === continuation.identity.conversationId
|
|
285
|
-
&& conversation.epoch === binding.currentConversationEpoch
|
|
286
|
-
&& conversation.status === "current"));
|
|
287
|
-
const activationIsLive = binding.activations.some((activation) => (activation.activationId === continuation.identity.activationId
|
|
288
|
-
&& activation.conversationId === continuation.identity.conversationId
|
|
289
|
-
&& activation.status === "active"));
|
|
290
|
-
return conversationIsCurrent && activationIsLive;
|
|
291
|
-
}
|
|
292
225
|
function workspaceCompletionDisposition(facts, taskId, workspace) {
|
|
293
226
|
const owner = workspace.owner;
|
|
294
227
|
switch (owner.type) {
|
package/dist/task/nextAction.js
CHANGED
|
@@ -22,6 +22,17 @@ export function projectNextAction(facts) {
|
|
|
22
22
|
]
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
|
+
if (task.status === "active" && task.executionGate.state === "stopped") {
|
|
26
|
+
return buildAction(facts, {
|
|
27
|
+
kind: "start-task-execution",
|
|
28
|
+
reason: `Task ${task.id} execution is stopped; durable progress is preserved.`,
|
|
29
|
+
refs: [ref("task", task.id)],
|
|
30
|
+
preconditions: [
|
|
31
|
+
{ fact: "Task execution is stopped", satisfied: true, ref: ref("task", task.id) }
|
|
32
|
+
],
|
|
33
|
+
recommendedCommand: `yui task execution start ${task.id}`
|
|
34
|
+
});
|
|
35
|
+
}
|
|
25
36
|
const openInput = facts.openInputRequests[0];
|
|
26
37
|
if (openInput !== undefined) {
|
|
27
38
|
return buildAction(facts, {
|
|
@@ -597,42 +608,15 @@ function buildExecutionLaneRecoveryAction(facts, lane) {
|
|
|
597
608
|
ref("execution-lane", lane.laneId),
|
|
598
609
|
ref("agent-run", lane.runId)
|
|
599
610
|
];
|
|
600
|
-
if (lane.recovery === "retry-new-agent-run") {
|
|
601
|
-
return buildAction(facts, {
|
|
602
|
-
kind: "recover-execution-lane",
|
|
603
|
-
reason: `Execution Lane ${lane.laneId} is durably failed; retry only exact Run ${lane.runId} and retain sibling results.`,
|
|
604
|
-
refs,
|
|
605
|
-
preconditions: [
|
|
606
|
-
{ fact: "Execution Lane is failed and unresolved", satisfied: true, ref: refs[1] },
|
|
607
|
-
{ fact: "Exact failed AgentRun is retained", satisfied: true, ref: refs[2] }
|
|
608
|
-
],
|
|
609
|
-
recommendedCommand: `yui task run retry ${facts.task.id}/${lane.runId}`
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
const action = lane.recovery === "diagnose" ? "diagnose" : "terminate";
|
|
613
|
-
const recovery = facts.runRecoveries?.find(({ runId }) => runId === lane.runId);
|
|
614
|
-
const plan = recovery?.actions.find((candidate) => candidate.action === action);
|
|
615
611
|
return buildAction(facts, {
|
|
616
|
-
kind: "
|
|
617
|
-
reason: lane.
|
|
618
|
-
? `Execution Lane ${lane.laneId} needs bounded diagnostics for exact Run ${lane.runId}.`
|
|
619
|
-
: `Execution Lane ${lane.laneId} has confirmed death evidence; terminate exact Run ${lane.runId}.`,
|
|
612
|
+
kind: "retry-execution-lane",
|
|
613
|
+
reason: `Execution Lane ${lane.laneId} is durably failed; retry only exact Run ${lane.runId} and retain sibling results.`,
|
|
620
614
|
refs,
|
|
621
615
|
preconditions: [
|
|
622
|
-
{ fact:
|
|
623
|
-
{
|
|
624
|
-
fact: `Exact Run exposes a current ${action} recovery plan`,
|
|
625
|
-
satisfied: plan !== undefined,
|
|
626
|
-
ref: refs[2]
|
|
627
|
-
}
|
|
616
|
+
{ fact: "Execution Lane is failed and unresolved", satisfied: true, ref: refs[1] },
|
|
617
|
+
{ fact: "Exact failed AgentRun is retained", satisfied: true, ref: refs[2] }
|
|
628
618
|
],
|
|
629
|
-
|
|
630
|
-
...(plan !== undefined && recovery?.judgmentRequired === undefined
|
|
631
|
-
? {}
|
|
632
|
-
: {
|
|
633
|
-
judgmentRequired: recovery?.judgmentRequired
|
|
634
|
-
?? `Inspect yui task run show ${facts.task.id}/${lane.runId}; its exact recovery fence is unavailable.`
|
|
635
|
-
})
|
|
619
|
+
recommendedCommand: `yui task run retry ${facts.task.id}/${lane.runId}`
|
|
636
620
|
});
|
|
637
621
|
}
|
|
638
622
|
function exhaustedExplorationReason(item, executionGroups) {
|
package/dist/task/task.js
CHANGED
|
@@ -2,11 +2,12 @@ import { validateTaskWorkspaceIdentity } from "../repository/taskWorkspaceIdenti
|
|
|
2
2
|
export function createTask(id, title, now, metadata = {}) {
|
|
3
3
|
const timestamp = now.toISOString();
|
|
4
4
|
return {
|
|
5
|
-
schemaVersion:
|
|
5
|
+
schemaVersion: 6,
|
|
6
6
|
id: requireSafeIdentity(id, "Task id"),
|
|
7
7
|
title: requireText(title, "Task title"),
|
|
8
8
|
...cloneMetadata(metadata),
|
|
9
9
|
status: "draft",
|
|
10
|
+
executionGate: { state: "enabled" },
|
|
10
11
|
createdAt: timestamp,
|
|
11
12
|
updatedAt: timestamp
|
|
12
13
|
};
|
|
@@ -169,9 +170,38 @@ export function updateTaskWorkspace(task, cwd, now) {
|
|
|
169
170
|
export function isTaskArchived(task) {
|
|
170
171
|
return task.status === "archived";
|
|
171
172
|
}
|
|
173
|
+
export function isTaskExecutionEnabled(task) {
|
|
174
|
+
return task.executionGate.state === "enabled";
|
|
175
|
+
}
|
|
176
|
+
export function stopTaskExecution(task, now) {
|
|
177
|
+
validateTask(task);
|
|
178
|
+
if (task.status !== "active") {
|
|
179
|
+
throw new Error(`Only an active Task can be stopped: ${task.id}.`);
|
|
180
|
+
}
|
|
181
|
+
if (task.executionGate.state === "stopped")
|
|
182
|
+
return task;
|
|
183
|
+
return validateTask({
|
|
184
|
+
...task,
|
|
185
|
+
executionGate: { state: "stopped" },
|
|
186
|
+
updatedAt: now.toISOString()
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
export function startTaskExecution(task, now) {
|
|
190
|
+
validateTask(task);
|
|
191
|
+
if (task.status !== "active") {
|
|
192
|
+
throw new Error(`Only an active Task can be started: ${task.id}.`);
|
|
193
|
+
}
|
|
194
|
+
if (task.executionGate.state === "enabled")
|
|
195
|
+
return task;
|
|
196
|
+
return validateTask({
|
|
197
|
+
...task,
|
|
198
|
+
executionGate: { state: "enabled" },
|
|
199
|
+
updatedAt: now.toISOString()
|
|
200
|
+
});
|
|
201
|
+
}
|
|
172
202
|
export function validateTask(task) {
|
|
173
|
-
if (task.schemaVersion !==
|
|
174
|
-
throw new Error("Task must use schemaVersion
|
|
203
|
+
if (task.schemaVersion !== 6)
|
|
204
|
+
throw new Error("Task must use schemaVersion 6.");
|
|
175
205
|
requireSafeIdentity(task.id, "Task id");
|
|
176
206
|
requireText(task.title, "Task title");
|
|
177
207
|
if (task.type !== undefined)
|
|
@@ -179,6 +209,11 @@ export function validateTask(task) {
|
|
|
179
209
|
if (!["draft", "active", "completed", "retired", "archived"].includes(task.status)) {
|
|
180
210
|
throw new Error(`Task status is invalid: ${String(task.status)}.`);
|
|
181
211
|
}
|
|
212
|
+
if (task.executionGate === null
|
|
213
|
+
|| typeof task.executionGate !== "object"
|
|
214
|
+
|| !["enabled", "stopped"].includes(task.executionGate.state)) {
|
|
215
|
+
throw new Error(`Task execution state is invalid: ${String(task.executionGate?.state)}.`);
|
|
216
|
+
}
|
|
182
217
|
requireTimestamp(task.createdAt, "Task createdAt");
|
|
183
218
|
requireTimestamp(task.updatedAt, "Task updatedAt");
|
|
184
219
|
if (Date.parse(task.updatedAt) < Date.parse(task.createdAt)) {
|
|
@@ -86,8 +86,6 @@ const messages = {
|
|
|
86
86
|
"detail.focus": "Current focus",
|
|
87
87
|
"detail.runtimeHealth": "Runtime health",
|
|
88
88
|
"detail.lastProgress": "Last semantic progress",
|
|
89
|
-
"detail.recoveryFence": "Canonical recovery fence (Yui durable)",
|
|
90
|
-
"detail.recoveryProviderObserved": "Provider observation (evidence only)",
|
|
91
89
|
"input.freeText": "Type your answer",
|
|
92
90
|
"input.answered": "Input answered.",
|
|
93
91
|
"input.new": "A new input needs your attention.",
|
|
@@ -424,8 +422,6 @@ const messages = {
|
|
|
424
422
|
"detail.focus": "当前重点",
|
|
425
423
|
"detail.runtimeHealth": "运行健康",
|
|
426
424
|
"detail.lastProgress": "最近语义进展",
|
|
427
|
-
"detail.recoveryFence": "规范恢复 fence(Yui 持久)",
|
|
428
|
-
"detail.recoveryProviderObserved": "Provider 观测(仅证据)",
|
|
429
425
|
"input.freeText": "输入回答",
|
|
430
426
|
"input.answered": "输入已回答。",
|
|
431
427
|
"input.new": "有新的输入请求需要处理。",
|
|
@@ -345,24 +345,6 @@ export function renderTaskDetail(detail, data, t, locale, actions) {
|
|
|
345
345
|
node("p", "record-copy", (run.kind || "workflow-not-progressing") + " · " + (run.classification || "truly-stalled")),
|
|
346
346
|
node("small", "", t("detail.lastProgress") + " · " + formatDateTime(run.progressAt, locale))
|
|
347
347
|
);
|
|
348
|
-
// Issue 08: the same canonical recovery projection as the CLI. The
|
|
349
|
-
// durable fence and the Provider observation stay clearly separated;
|
|
350
|
-
// the exact recovery actions live in "yui task run show".
|
|
351
|
-
if (run.recovery) {
|
|
352
|
-
if (run.recovery.canonicalProgressAt) {
|
|
353
|
-
card.append(node("small", "",
|
|
354
|
-
t("detail.recoveryFence") + " · " + formatDateTime(run.recovery.canonicalProgressAt, locale)));
|
|
355
|
-
}
|
|
356
|
-
if (run.recovery.provider && run.recovery.provider.observedAt) {
|
|
357
|
-
card.append(node("small", "",
|
|
358
|
-
t("detail.recoveryProviderObserved") + " · " + run.recovery.provider.observationKind
|
|
359
|
-
+ " " + formatDateTime(run.recovery.provider.observedAt, locale)));
|
|
360
|
-
}
|
|
361
|
-
if (run.recovery.recoverable) {
|
|
362
|
-
card.append(node("small", "record-copy",
|
|
363
|
-
"yui task run show " + task.id + "/" + run.runId));
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
348
|
runtimeHealthBody.append(card);
|
|
367
349
|
});
|
|
368
350
|
scaffold.append(anchorSection(
|
package/dist/web/webSnapshot.js
CHANGED
|
@@ -2,7 +2,6 @@ import { isRoleRunStalled, latestRunDurableProgressAt } from "../scheduler/roleR
|
|
|
2
2
|
import { buildTaskExecutionProjection } from "../scheduler/taskExecutionProjection.js";
|
|
3
3
|
import { summarizeExecutionGroup } from "../execution/executionGroup.js";
|
|
4
4
|
import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
|
|
5
|
-
import { projectRunRecovery, readRunRecoveryFacts } from "../run/recoveryProjection.js";
|
|
6
5
|
import { classifyRuntimeHealth, projectRuntimeTaskEvents } from "../runtime/runtimeProjection.js";
|
|
7
6
|
import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
8
7
|
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
@@ -73,18 +72,13 @@ export function buildWebTaskDetail(store, taskId, now = new Date()) {
|
|
|
73
72
|
const events = reader.listEvents?.(taskId) ?? [];
|
|
74
73
|
const needsAttentionRuns = runs
|
|
75
74
|
.filter((run) => run.status === "active" && isRoleRunStalled(events, run.id))
|
|
76
|
-
.map((run) => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
classification: latestStallField(events, run.id, "classification") ?? "truly-stalled",
|
|
84
|
-
// Issue 08: the same canonical recovery projection the CLI exposes.
|
|
85
|
-
...(facts === null ? {} : { recovery: projectRunRecovery(facts) })
|
|
86
|
-
};
|
|
87
|
-
});
|
|
75
|
+
.map((run) => ({
|
|
76
|
+
runId: run.id,
|
|
77
|
+
roleName: run.roleName,
|
|
78
|
+
progressAt: latestStallProgress(events, run.id),
|
|
79
|
+
kind: latestStallField(events, run.id, "kind") ?? "workflow-not-progressing",
|
|
80
|
+
classification: latestStallField(events, run.id, "classification") ?? "truly-stalled"
|
|
81
|
+
}));
|
|
88
82
|
const activeRuns = new Map(runs
|
|
89
83
|
.filter((run) => run.status === "active")
|
|
90
84
|
.map((run) => [run.roleName, run]));
|
package/package.json
CHANGED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
|
|
3
|
-
import { actionableExecutionLaneRecoveries } from "../execution/executionHealth.js";
|
|
4
|
-
import { runOwnsBlockingProviderContinuation } from "../runtime/runtimeContinuationProjection.js";
|
|
5
|
-
import { runHasActiveRuntimeOperations } from "../runtime/runtimeObservation.js";
|
|
6
|
-
export const RUN_RECOVERY_ACTIONS = [
|
|
7
|
-
"diagnose",
|
|
8
|
-
"retry",
|
|
9
|
-
"terminate"
|
|
10
|
-
];
|
|
11
|
-
/**
|
|
12
|
-
* Reads every durable record the recovery projection needs. Returns null
|
|
13
|
-
* only when the Run itself is absent.
|
|
14
|
-
*/
|
|
15
|
-
export function readRunRecoveryFacts(store, taskId, runId) {
|
|
16
|
-
const run = store.getAgentRun(taskId, runId);
|
|
17
|
-
if (run === null || run.taskId !== taskId)
|
|
18
|
-
return null;
|
|
19
|
-
const task = store.getTask(taskId);
|
|
20
|
-
const sessionSet = store.getTaskRoleSessionSet(taskId, run.roleName);
|
|
21
|
-
const events = store.listEvents(taskId);
|
|
22
|
-
const roleMailbox = store.getWorkMailbox?.({
|
|
23
|
-
kind: "role",
|
|
24
|
-
taskId,
|
|
25
|
-
roleName: run.roleName
|
|
26
|
-
}) ?? null;
|
|
27
|
-
const progress = latestRunDurableProgressAt(store, taskId, run.roleName, runId);
|
|
28
|
-
return {
|
|
29
|
-
run,
|
|
30
|
-
task: task === null ? null : { id: task.id, status: task.status },
|
|
31
|
-
sessionSet,
|
|
32
|
-
inputDeliveryUnsettled: roleMailbox?.inputDelivery != null,
|
|
33
|
-
blockingProviderContinuation: runOwnsBlockingProviderContinuation(events, {
|
|
34
|
-
taskId,
|
|
35
|
-
roleName: run.roleName,
|
|
36
|
-
runId: run.id,
|
|
37
|
-
agentId: run.effective.agentId
|
|
38
|
-
}),
|
|
39
|
-
activeRuntimeOperation: runHasActiveRuntimeOperations(events, {
|
|
40
|
-
taskId,
|
|
41
|
-
roleName: run.roleName,
|
|
42
|
-
runId: run.id,
|
|
43
|
-
agentId: run.effective.agentId
|
|
44
|
-
}),
|
|
45
|
-
progress,
|
|
46
|
-
latestProviderObservation: latestRunProviderObservation(events, runId)
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Resolve the exact live-Run recovery plans referenced by Lane health. Failed
|
|
51
|
-
* terminal Lanes use `task run retry` directly and therefore need no live-Run
|
|
52
|
-
* recovery projection here.
|
|
53
|
-
*/
|
|
54
|
-
export function projectExecutionLaneRunRecoveries(store, taskId, groups) {
|
|
55
|
-
const runIds = new Set(actionableExecutionLaneRecoveries(groups).flatMap((lane) => (lane.runId === undefined || lane.recovery === "retry-new-agent-run"
|
|
56
|
-
? []
|
|
57
|
-
: [lane.runId])));
|
|
58
|
-
return [...runIds].flatMap((runId) => {
|
|
59
|
-
const facts = readRunRecoveryFacts(store, taskId, runId);
|
|
60
|
-
return facts === null ? [] : [projectRunRecovery(facts)];
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Latest Provider observation for a Run. Provider timestamps are evidence:
|
|
65
|
-
* they explain why a stale fence was supplied but never authorize recovery.
|
|
66
|
-
*/
|
|
67
|
-
function latestRunProviderObservation(events, runId) {
|
|
68
|
-
let latest = null;
|
|
69
|
-
for (const event of events) {
|
|
70
|
-
if (event.type !== "runtime.observation")
|
|
71
|
-
continue;
|
|
72
|
-
if (event.payload.runId !== runId)
|
|
73
|
-
continue;
|
|
74
|
-
const kind = typeof event.payload.kind === "string" ? event.payload.kind : "unknown";
|
|
75
|
-
const receivedAt = typeof event.payload.receivedAt === "string"
|
|
76
|
-
&& Number.isFinite(Date.parse(event.payload.receivedAt))
|
|
77
|
-
? event.payload.receivedAt
|
|
78
|
-
: event.createdAt;
|
|
79
|
-
const at = Date.parse(receivedAt);
|
|
80
|
-
if (latest === null || at > latest.at) {
|
|
81
|
-
latest = { kind, receivedAt, at };
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return latest === null ? null : { kind: latest.kind, receivedAt: latest.receivedAt };
|
|
85
|
-
}
|
|
86
|
-
export function projectRunRecovery(facts) {
|
|
87
|
-
const { run, task, sessionSet, progress } = facts;
|
|
88
|
-
const session = activeSession(facts);
|
|
89
|
-
const canonicalProgressAt = progress?.progressAt ?? null;
|
|
90
|
-
const accepted = run.deliveredAt !== undefined;
|
|
91
|
-
const acceptanceOptions = accepted
|
|
92
|
-
? ["accepted", "ambiguous"]
|
|
93
|
-
: ["rejected", "ambiguous"];
|
|
94
|
-
const blocked = recoveryBlocker(facts, session, canonicalProgressAt);
|
|
95
|
-
const sessionTerminal = session?.status === "stopped" || session?.status === "broken";
|
|
96
|
-
const providerTerminal = sessionSet?.providerBinding?.turn?.status === "failed"
|
|
97
|
-
|| sessionSet?.providerBinding?.turn?.status === "cancelled"
|
|
98
|
-
|| sessionSet?.providerBinding?.turn?.status === "rejected";
|
|
99
|
-
const supportedActions = RUN_RECOVERY_ACTIONS.filter((action) => ((action !== "retry" || !sessionTerminal)
|
|
100
|
-
&& (action !== "terminate" || sessionTerminal || providerTerminal)));
|
|
101
|
-
const actions = blocked === null
|
|
102
|
-
? supportedActions.map((action) => buildActionPlan(facts, action, session, canonicalProgressAt))
|
|
103
|
-
: [];
|
|
104
|
-
const judgmentRequired = blocked === null && actions.some((plan) => plan.argv.includes(PROVIDER_ACCEPTANCE_PLACEHOLDER))
|
|
105
|
-
? "Provider acceptance is not durably determined for every action; pass --provider-acceptance explicitly."
|
|
106
|
-
: undefined;
|
|
107
|
-
return {
|
|
108
|
-
taskId: run.taskId,
|
|
109
|
-
runId: run.id,
|
|
110
|
-
roleName: run.roleName,
|
|
111
|
-
runStatus: run.status,
|
|
112
|
-
recoverable: blocked === null,
|
|
113
|
-
canonicalProgressAt,
|
|
114
|
-
...(progress?.evidence === undefined ? {} : { canonicalProgressEvidence: progress.evidence }),
|
|
115
|
-
provider: {
|
|
116
|
-
acceptedAt: run.deliveredAt ?? null,
|
|
117
|
-
observedAt: facts.latestProviderObservation?.receivedAt ?? null,
|
|
118
|
-
observationKind: facts.latestProviderObservation?.kind ?? null
|
|
119
|
-
},
|
|
120
|
-
providerAcceptance: {
|
|
121
|
-
accepted,
|
|
122
|
-
options: acceptanceOptions
|
|
123
|
-
},
|
|
124
|
-
session: session === null ? null : {
|
|
125
|
-
status: session.status,
|
|
126
|
-
...(session.nativeSessionId === undefined ? {} : { nativeSessionId: session.nativeSessionId }),
|
|
127
|
-
...(session.launchId === undefined ? {} : { launchId: session.launchId })
|
|
128
|
-
},
|
|
129
|
-
actions,
|
|
130
|
-
...(judgmentRequired === undefined ? {} : { judgmentRequired }),
|
|
131
|
-
...(blocked === null ? {} : { reason: blocked })
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
const PROVIDER_ACCEPTANCE_PLACEHOLDER = "<accepted|rejected|ambiguous>";
|
|
135
|
-
function activeSession(facts) {
|
|
136
|
-
const sessions = facts.sessionSet;
|
|
137
|
-
if (sessions === null)
|
|
138
|
-
return null;
|
|
139
|
-
const session = sessions.sessions[sessions.activeAgentId];
|
|
140
|
-
if (session === undefined)
|
|
141
|
-
return null;
|
|
142
|
-
if (session.agentId !== facts.run.effective.agentId)
|
|
143
|
-
return null;
|
|
144
|
-
if (session.adapterId !== facts.run.effective.adapterId)
|
|
145
|
-
return null;
|
|
146
|
-
return session;
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Mirrors the fail-closed checks of `recoverExactAgentRun` that are visible
|
|
150
|
-
* from durable records. A non-null result means recovery cannot currently be
|
|
151
|
-
* applied; the canonical fence is still projected for diagnosis.
|
|
152
|
-
*/
|
|
153
|
-
function recoveryBlocker(facts, session, canonicalProgressAt) {
|
|
154
|
-
const { run, task } = facts;
|
|
155
|
-
if (task === null)
|
|
156
|
-
return "task-missing";
|
|
157
|
-
if (task.status !== "active")
|
|
158
|
-
return "task-terminal";
|
|
159
|
-
if (run.status !== "active")
|
|
160
|
-
return "run-terminal";
|
|
161
|
-
if (canonicalProgressAt === null)
|
|
162
|
-
return "progress-unavailable";
|
|
163
|
-
if (session === null)
|
|
164
|
-
return "session-missing";
|
|
165
|
-
if (facts.inputDeliveryUnsettled)
|
|
166
|
-
return "provider-input-delivery-unsettled";
|
|
167
|
-
if (facts.blockingProviderContinuation)
|
|
168
|
-
return "provider-continuation-writer-owned";
|
|
169
|
-
if (facts.activeRuntimeOperation)
|
|
170
|
-
return "provider-operation-active";
|
|
171
|
-
const binding = facts.sessionSet?.providerBinding;
|
|
172
|
-
if (run.deliveredAt !== undefined
|
|
173
|
-
&& (binding === null || binding?.turn === null))
|
|
174
|
-
return "provider-turn-state-missing";
|
|
175
|
-
if (binding !== null && binding !== undefined) {
|
|
176
|
-
if (["submitting", "accepted", "running", "delivery-unknown"].includes(binding.turn?.status ?? ""))
|
|
177
|
-
return "provider-turn-unsettled";
|
|
178
|
-
if (binding.authority.owner === "human" || binding.authority.owner === "unknown") {
|
|
179
|
-
return "provider-writer-authority-unavailable";
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
function buildActionPlan(facts, action, session, canonicalProgressAt) {
|
|
185
|
-
const { run } = facts;
|
|
186
|
-
const acceptance = actionAcceptance(facts, action);
|
|
187
|
-
const argv = [
|
|
188
|
-
"task",
|
|
189
|
-
"run",
|
|
190
|
-
"recover",
|
|
191
|
-
`${run.taskId}/${run.id}`,
|
|
192
|
-
"--action",
|
|
193
|
-
action,
|
|
194
|
-
"--expected-progress-at",
|
|
195
|
-
canonicalProgressAt,
|
|
196
|
-
"--provider-acceptance",
|
|
197
|
-
acceptance,
|
|
198
|
-
"--reason",
|
|
199
|
-
"<text>",
|
|
200
|
-
"--agent-id",
|
|
201
|
-
run.effective.agentId,
|
|
202
|
-
"--adapter-id",
|
|
203
|
-
run.effective.adapterId,
|
|
204
|
-
...(session.nativeSessionId === undefined
|
|
205
|
-
? []
|
|
206
|
-
: ["--native-session-id", session.nativeSessionId]),
|
|
207
|
-
...(session.launchId === undefined
|
|
208
|
-
? []
|
|
209
|
-
: ["--launch-id", session.launchId])
|
|
210
|
-
];
|
|
211
|
-
const command = `yui ${argv
|
|
212
|
-
.map((part) => (part === "<text>" ? '"<text>"' : part))
|
|
213
|
-
.join(" ")}`;
|
|
214
|
-
const fingerprintSource = [
|
|
215
|
-
run.id,
|
|
216
|
-
action,
|
|
217
|
-
canonicalProgressAt,
|
|
218
|
-
run.effective.agentId,
|
|
219
|
-
run.effective.adapterId,
|
|
220
|
-
session.nativeSessionId ?? "",
|
|
221
|
-
session.launchId ?? ""
|
|
222
|
-
].join("|");
|
|
223
|
-
return {
|
|
224
|
-
action,
|
|
225
|
-
reason: ACTION_REASONS[action],
|
|
226
|
-
expectedProgressAt: canonicalProgressAt,
|
|
227
|
-
agentId: run.effective.agentId,
|
|
228
|
-
adapterId: run.effective.adapterId,
|
|
229
|
-
...(session.nativeSessionId === undefined
|
|
230
|
-
? {}
|
|
231
|
-
: { nativeSessionId: session.nativeSessionId }),
|
|
232
|
-
...(session.launchId === undefined ? {} : { launchId: session.launchId }),
|
|
233
|
-
command,
|
|
234
|
-
argv,
|
|
235
|
-
fingerprint: createHash("sha256").update(fingerprintSource).digest("hex")
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* The acceptance value for the copy-paste command. When exactly one value is
|
|
240
|
-
* durably valid it is filled in (the durable record, not a guess); otherwise
|
|
241
|
-
* the Leader must choose and the command carries an explicit placeholder.
|
|
242
|
-
*/
|
|
243
|
-
function actionAcceptance(facts, action) {
|
|
244
|
-
if (action === "diagnose")
|
|
245
|
-
return PROVIDER_ACCEPTANCE_PLACEHOLDER;
|
|
246
|
-
return facts.run.deliveredAt === undefined ? "rejected" : "accepted";
|
|
247
|
-
}
|
|
248
|
-
const ACTION_REASONS = {
|
|
249
|
-
diagnose: "Collect bounded diagnostics before any state-changing recovery.",
|
|
250
|
-
retry: "Request another provider turn on the same native Session when the failure is transient.",
|
|
251
|
-
terminate: "Fail the Run explicitly when recovery is not viable."
|
|
252
|
-
};
|