@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.
Files changed (48) hide show
  1. package/README.md +8 -8
  2. package/dist/cli/commandCatalog.js +22 -21
  3. package/dist/cli/interactionPolicy.js +0 -14
  4. package/dist/cli.js +42 -0
  5. package/dist/commands/executionAuditCommands.js +1 -1
  6. package/dist/commands/taskCommands.js +60 -326
  7. package/dist/commands/taskContextCommand.js +3 -14
  8. package/dist/commands/taskExecutionCommands.js +254 -0
  9. package/dist/commands/taskNextActionCommand.js +1 -3
  10. package/dist/commands/taskOverviewCommand.js +9 -2
  11. package/dist/commands/taskRoleRuntimeStatus.js +2 -25
  12. package/dist/controller/agentRuntimeObserver.js +4 -2
  13. package/dist/controller/clientRuntime.js +45 -2
  14. package/dist/controller/controller.js +6 -3
  15. package/dist/controller/fileSchedulerStoreAdapter.js +59 -188
  16. package/dist/controller/jobControl.js +3 -2
  17. package/dist/controller/runtime.js +6 -33
  18. package/dist/controller/runtimeEventProcessor.js +8 -4
  19. package/dist/controller/runtimeHookRunFence.js +4 -10
  20. package/dist/execution/executionHealth.js +8 -16
  21. package/dist/executor/agentExecutor.js +13 -14
  22. package/dist/executor/fileRoleLaunchPlanner.js +9 -16
  23. package/dist/lifecycle/exactRunTerminalization.js +24 -322
  24. package/dist/repository/taskWorkspaceCoordinator.js +0 -9
  25. package/dist/runtime/agentHost.js +20 -80
  26. package/dist/runtime/exactControlPlane.js +15 -9
  27. package/dist/runtime/providerContinuationReconciliationService.js +1 -1
  28. package/dist/runtime/providerRecoveryDecision.js +1 -1
  29. package/dist/runtime/providerRuntimeIdentity.js +25 -15
  30. package/dist/scheduler/activeRoleRunDelivery.js +1 -19
  31. package/dist/scheduler/leaderWakeupProcessor.js +12 -63
  32. package/dist/scheduler/ports.js +3 -2
  33. package/dist/scheduler/roleRunLiveness.js +4 -1
  34. package/dist/scheduler/roleRunStall.js +0 -2
  35. package/dist/scheduler/taskExecutionProjection.js +18 -1
  36. package/dist/scheduler/wakeupQueue.js +2 -1
  37. package/dist/storage/migration/productionRegistry.js +65 -0
  38. package/dist/storage/sqliteStore.js +10 -2
  39. package/dist/storage/taskStore.js +11 -3
  40. package/dist/task/completionReadiness.js +0 -67
  41. package/dist/task/nextAction.js +16 -32
  42. package/dist/task/task.js +38 -3
  43. package/dist/web/assets/client/i18n.js +0 -4
  44. package/dist/web/assets/client/view.js +0 -18
  45. package/dist/web/webSnapshot.js +7 -13
  46. package/package.json +1 -1
  47. package/dist/run/recoveryProjection.js +0 -252
  48. 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 = 5;
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) {
@@ -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: "recover-execution-lane",
617
- reason: lane.recovery === "diagnose"
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: `Lane recovery is ${lane.recovery}`, satisfied: true, ref: refs[1] },
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
- ...(plan === undefined ? {} : { recommendedCommand: plan.command }),
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,
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 !== 5)
174
- throw new Error("Task must use schemaVersion 5.");
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(
@@ -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
- const facts = readRunRecoveryFacts(reader, taskId, run.id);
78
- return {
79
- runId: run.id,
80
- roleName: run.roleName,
81
- progressAt: latestStallProgress(events, run.id),
82
- kind: latestStallField(events, run.id, "kind") ?? "workflow-not-progressing",
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.13.4",
3
+ "version": "0.13.5",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -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
- };