@zq-silk/yui 0.13.4 → 0.13.6

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 (54) hide show
  1. package/ARCHITECTURE.md +13 -13
  2. package/README.md +19 -20
  3. package/dist/cli/commandCatalog.js +22 -21
  4. package/dist/cli/interactionPolicy.js +0 -14
  5. package/dist/cli.js +42 -0
  6. package/dist/commands/executionAuditCommands.js +1 -1
  7. package/dist/commands/taskCommands.js +60 -326
  8. package/dist/commands/taskContextCommand.js +3 -14
  9. package/dist/commands/taskExecutionCommands.js +254 -0
  10. package/dist/commands/taskNextActionCommand.js +1 -3
  11. package/dist/commands/taskOverviewCommand.js +9 -2
  12. package/dist/commands/taskRoleRuntimeStatus.js +2 -25
  13. package/dist/controller/agentRuntimeObserver.js +4 -2
  14. package/dist/controller/clientRuntime.js +45 -2
  15. package/dist/controller/controller.js +6 -3
  16. package/dist/controller/fileSchedulerStoreAdapter.js +59 -188
  17. package/dist/controller/jobControl.js +3 -2
  18. package/dist/controller/runtime.js +6 -33
  19. package/dist/controller/runtimeEventProcessor.js +8 -4
  20. package/dist/controller/runtimeHookRunFence.js +4 -10
  21. package/dist/execution/executionHealth.js +8 -16
  22. package/dist/executor/agentAdapter.js +3 -8
  23. package/dist/executor/agentExecutor.js +13 -14
  24. package/dist/executor/fileRoleLaunchPlanner.js +10 -26
  25. package/dist/lifecycle/exactRunTerminalization.js +24 -322
  26. package/dist/repository/taskWorkspaceCoordinator.js +0 -9
  27. package/dist/runtime/agentHost.js +22 -83
  28. package/dist/runtime/builtinAgentDrivers.js +1 -1
  29. package/dist/runtime/exactControlPlane.js +15 -9
  30. package/dist/runtime/launchBroker.js +1 -11
  31. package/dist/runtime/providerContinuationReconciliationService.js +1 -1
  32. package/dist/runtime/providerRecoveryDecision.js +1 -1
  33. package/dist/runtime/providerRuntimeIdentity.js +25 -15
  34. package/dist/runtime/structuredProviderHost.js +0 -57
  35. package/dist/scheduler/activeRoleRunDelivery.js +1 -19
  36. package/dist/scheduler/leaderWakeupProcessor.js +12 -63
  37. package/dist/scheduler/ports.js +3 -2
  38. package/dist/scheduler/roleRunLiveness.js +4 -1
  39. package/dist/scheduler/roleRunStall.js +0 -2
  40. package/dist/scheduler/taskExecutionProjection.js +18 -1
  41. package/dist/scheduler/wakeupQueue.js +2 -1
  42. package/dist/storage/migration/productionRegistry.js +65 -0
  43. package/dist/storage/sqliteStore.js +10 -2
  44. package/dist/storage/taskStore.js +11 -3
  45. package/dist/task/completionReadiness.js +0 -67
  46. package/dist/task/nextAction.js +16 -32
  47. package/dist/task/task.js +38 -3
  48. package/dist/web/assets/client/i18n.js +0 -4
  49. package/dist/web/assets/client/view.js +0 -18
  50. package/dist/web/webSnapshot.js +7 -13
  51. package/i18n/README.zh-CN.md +4 -4
  52. package/package.json +1 -1
  53. package/dist/run/recoveryProjection.js +0 -252
  54. package/dist/runtime/conversationSwitch.js +0 -277
@@ -25,13 +25,14 @@ export function selectedActiveSchedulerTasks(store, selection) {
25
25
  const indexedTaskIds = store.listActiveTaskIds?.();
26
26
  if (indexedTaskIds === undefined) {
27
27
  return store.listTasks().filter((task) => (task.status === "active"
28
+ && task.executionGate.state === "enabled"
28
29
  && !selection?.blockedTaskIds?.has(task.id)));
29
30
  }
30
31
  return [...indexedTaskIds].flatMap((taskId) => {
31
32
  if (selection?.blockedTaskIds?.has(taskId))
32
33
  return [];
33
34
  const task = store.getTask(taskId);
34
- return task?.status === "active" ? [task] : [];
35
+ return task?.status === "active" && task.executionGate.state === "enabled" ? [task] : [];
35
36
  });
36
37
  }
37
38
  const taskIds = selection.taskIds;
@@ -39,7 +40,7 @@ export function selectedActiveSchedulerTasks(store, selection) {
39
40
  if (selection.blockedTaskIds?.has(taskId))
40
41
  return [];
41
42
  const task = store.getTask(taskId);
42
- return task?.status === "active" ? [task] : [];
43
+ return task?.status === "active" && task.executionGate.state === "enabled" ? [task] : [];
43
44
  });
44
45
  }
45
46
  /** Resolves either every Role in a selected Task or only explicit Role keys. */
@@ -196,7 +196,10 @@ function exactBatchInventory(batch, candidates) {
196
196
  return { statuses, resources, hostExits };
197
197
  }
198
198
  function isResourceCandidate(task, run, now) {
199
- if (task.status !== "active" || run.status !== "active" || run.deliveredAt === undefined) {
199
+ if (task.status !== "active"
200
+ || task.executionGate.state !== "enabled"
201
+ || run.status !== "active"
202
+ || run.deliveredAt === undefined) {
200
203
  return false;
201
204
  }
202
205
  const deliveredAt = Date.parse(run.deliveredAt);
@@ -14,8 +14,6 @@ export const RUN_STALLED_EVENT = "run.stalled";
14
14
  export const RUN_RECOVERED_EVENT = "run.recovered";
15
15
  export const RUN_DIAGNOSTIC_FINISHED_EVENT = "runtime.diagnostic-finished";
16
16
  /** Structured, non-Message recovery evidence written by an explicit Leader. */
17
- export const RUN_RECOVERY_REQUESTED_EVENT = "run.recovery-requested";
18
- export const RUN_RECOVERY_APPLIED_EVENT = "run.recovery-applied";
19
17
  /** Workflow-semantic events that count for the durable progress clock. */
20
18
  const ACTIVITY_EVENT_TYPES = new Set([
21
19
  RUN_PROGRESS_EVENT,
@@ -132,12 +132,29 @@ export function projectTaskExecution(facts) {
132
132
  ...(run.executionGroupId === undefined ? {} : { executionGroupId: run.executionGroupId }),
133
133
  ...(run.executionLaneId === undefined ? {} : { executionLaneId: run.executionLaneId })
134
134
  }));
135
- const monitoring = task.status === "completed"
135
+ const monitoring = task.executionGate.state === "stopped"
136
+ || task.status === "completed"
136
137
  || task.status === "retired"
137
138
  || task.status === "archived"
138
139
  ? "stopped"
139
140
  : "active";
140
141
  if (monitoring === "stopped") {
142
+ if (task.executionGate.state === "stopped" && task.status === "active") {
143
+ return render({
144
+ task,
145
+ status: "stopped",
146
+ owner: "operator",
147
+ action: "start-execution",
148
+ summary: `Task ${task.id} execution is stopped; durable progress is preserved.`,
149
+ reason: "execution-stopped",
150
+ monitoring,
151
+ failClosed: false,
152
+ activeRuns: activeRunViews,
153
+ attention: [],
154
+ blockers: [],
155
+ pendingWakeup
156
+ });
157
+ }
141
158
  const stoppedStatus = task.status;
142
159
  return render({
143
160
  task,
@@ -11,7 +11,8 @@ export function queueLeaderWakeup(store, taskId, reason, now) {
11
11
  export function queueLeaderWakeupAfterYield(store, task, run, now) {
12
12
  if (run.taskId !== task.id)
13
13
  throw new Error(`AgentRun belongs to another Task: ${run.taskId}.`);
14
- if (task.status !== "active" || run.roleName === "leader")
14
+ if (task.status !== "active" || task.executionGate.state !== "enabled" || run.roleName === "leader") {
15
15
  return null;
16
+ }
16
17
  return queueLeaderWakeup(store, task.id, wakeReason("role-result"), now);
17
18
  }
@@ -24,6 +24,8 @@ const TASK_FROM_VERSION = 3;
24
24
  const TASK_TO_VERSION = 4;
25
25
  const TASK_INTENT_FROM_VERSION = 4;
26
26
  const TASK_INTENT_TO_VERSION = 5;
27
+ const TASK_EXECUTION_GATE_FROM_VERSION = 5;
28
+ const TASK_EXECUTION_GATE_TO_VERSION = 6;
27
29
  const WORK_ITEM_FROM_VERSION = 6;
28
30
  const WORK_ITEM_TO_VERSION = 7;
29
31
  const WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION = 7;
@@ -161,6 +163,7 @@ export function createProductionStorageRegistry() {
161
163
  .registerCompatible(projectLifecycleStep())
162
164
  .registerOfflineMigration(taskWorkspaceIdentityStep())
163
165
  .registerOfflineMigration(taskIntentStep())
166
+ .registerOfflineMigration(taskExecutionGateStep())
164
167
  .registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_FROM_VERSION, WORK_ITEM_TO_VERSION, "workItems"))
165
168
  .registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION, WORK_ITEM_GIT_SNAPSHOT_TO_VERSION, "workItems"))
166
169
  .registerOfflineMigration(workItemExecutionGroupHistoryStep())
@@ -3375,6 +3378,68 @@ function migrateTaskV4ToV5(snapshot) {
3375
3378
  state: { ...snapshot.state, tasks: nextTasks }
3376
3379
  };
3377
3380
  }
3381
+ /** Task v6 separates semantic lifecycle from the current execution admission gate. */
3382
+ function taskExecutionGateStep() {
3383
+ return {
3384
+ axis: "record",
3385
+ recordKind: "task",
3386
+ fromVersion: TASK_EXECUTION_GATE_FROM_VERSION,
3387
+ toVersion: TASK_EXECUTION_GATE_TO_VERSION,
3388
+ preconditions: requireTaskV5Family,
3389
+ transform: migrateTaskV5ToV6,
3390
+ declaredEffects: []
3391
+ };
3392
+ }
3393
+ function requireTaskV5Family(snapshot) {
3394
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3395
+ if (manifestVersions.task !== TASK_EXECUTION_GATE_FROM_VERSION) {
3396
+ throw new Error(`Record task migration requires manifest version ${TASK_EXECUTION_GATE_FROM_VERSION}.`);
3397
+ }
3398
+ if (snapshot.state === null)
3399
+ return;
3400
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
3401
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
3402
+ const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
3403
+ if (aggregate.task === undefined)
3404
+ continue;
3405
+ const record = asObject(aggregate.task, `Task ${taskId}`);
3406
+ if (record.schemaVersion !== TASK_EXECUTION_GATE_FROM_VERSION) {
3407
+ throw new Error(`Task ${taskId} must use schemaVersion ${TASK_EXECUTION_GATE_FROM_VERSION}.`);
3408
+ }
3409
+ }
3410
+ }
3411
+ function migrateTaskV5ToV6(snapshot) {
3412
+ requireTaskV5Family(snapshot);
3413
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3414
+ const schemaManifest = {
3415
+ ...snapshot.schemaManifest,
3416
+ recordVersions: { ...manifestVersions, task: TASK_EXECUTION_GATE_TO_VERSION }
3417
+ };
3418
+ if (snapshot.state === null)
3419
+ return { schemaManifest, state: null };
3420
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
3421
+ const nextTasks = {};
3422
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
3423
+ const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
3424
+ if (aggregate.task === undefined) {
3425
+ nextTasks[taskId] = { ...aggregate };
3426
+ continue;
3427
+ }
3428
+ const task = asObject(aggregate.task, `Task ${taskId}`);
3429
+ nextTasks[taskId] = {
3430
+ ...aggregate,
3431
+ task: {
3432
+ ...task,
3433
+ schemaVersion: TASK_EXECUTION_GATE_TO_VERSION,
3434
+ executionGate: { state: "enabled" }
3435
+ }
3436
+ };
3437
+ }
3438
+ return {
3439
+ schemaManifest,
3440
+ state: { ...snapshot.state, tasks: nextTasks }
3441
+ };
3442
+ }
3378
3443
  /**
3379
3444
  * A version bump is deliverable only when the shared planner resolves the full
3380
3445
  * adjacent path. This also covers target-only record families as explicit 0->1
@@ -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]));
@@ -510,9 +510,9 @@ Task 生命周期的交互选择只展示有效来源状态:activate 只展示
510
510
 
511
511
  ## Session 与 tmux
512
512
 
513
- 受管理的 Provider 会话仍然是普通用户会话。Yui 只添加对应的 Role Skill 与 Session Manifest 指针,并通过 Provider 原生结构化协议提交 Task 工作;Yui 不接管完整对话历史。受管理输入绝不会作为终端按键、粘贴文本或启动 argv 发送。Codex 通过 `app-server proxy` 在共享 App Server daemon 上创建或恢复普通 thread;proxy 断开时 Host 会保留逻辑 Activation,重新连接并用原生 thread 历史核对 Yui 已持有的 Turn。Claude 继续使用独立的持久 stream-json 进程,并以精确回放的 user message 作为接收确认。
513
+ 受管理的 Provider 会话仍然是普通用户会话。Yui 只添加对应的 Role Skill 与 Session Manifest 指针,并通过 Provider 原生结构化协议提交 Task 工作;Yui 不接管完整对话历史。受管理输入绝不会作为终端按键、粘贴文本或启动 argv 发送。Codex 由每个存活的 Role Runtime 启动并持有独立的 `app-server` 子进程;Task execution stop 会终止 Agent Host 及其 Provider 进程组,start 会创建新的受控运行时,不再依赖共享 Codex daemon。原生 thread 历史仍由 Codex 保留,Task、WorkItem、代码与持久消息仍由 Yui 保留。Claude 继续使用独立的持久 stream-json 进程,并以精确回放的 user message 作为接收确认。
514
514
 
515
- Run、Conversation、Activation 与 Turn 是四个独立身份。Conversation 可以跨多个 Run 和客户端连接;Activation 只代表 Yui 当前的连接,而不是对 Provider thread 的独占所有权;Turn 在写入前先持久化。写入超时或结果不明确会进入 `delivery-unknown`,不会自动重发。用户在 Desktop 直接发起的 active Turn 只会让 Yui 暂时等待,不会导致 Yui Run 失败。
515
+ Run、Conversation、Activation 与 Turn 是四个独立身份。Conversation 可以跨多个 Run 和客户端连接;Activation 只代表 Yui 当前的连接,而不是对 Provider thread 的独占所有权;Turn 在写入前先持久化。写入超时或结果不明确会进入 `delivery-unknown`,不会自动重发。已经存在的原生 active Turn 只会让 Yui 暂时等待,不会导致 Yui Run 失败;存活的受管理 Role 应通过 Yui 的 view/takeover 边界进行人工控制,避免其他客户端并发写入。
516
516
 
517
517
  Task Role 使用以下显式入口:
518
518
 
@@ -524,7 +524,7 @@ yui task role takeover <task-id> <role>
524
524
  yui task role release <task-id> <role>
525
525
  ```
526
526
 
527
- Codex Role thread 可以直接在 Desktop 中打开和交互,不需要执行 `takeover`;Yui 不写入全局 Hook/config。Yui 可以幂等启动尚未运行的共享 daemon,但不会因 thread 错误停止或重启它。`view`、`takeover`、`release` 继续作为独立进程型 Provider 的显式 PTY 输入网关。Global Operator 与 global Role 继续使用原生交互式 CLI,不属于受管理 Task Provider 协议。
527
+ Codex Role thread 仍可在 Desktop 中查看,但存活的受管理 Role 应通过 `view`、`takeover`、`release` 进行人工控制;若要从其他客户端恢复该 thread,应先停止 Task execution。Yui 不写入全局 Hook/config,也不依赖或控制共享 Codex daemon。Global Operator 与 global Role 继续使用原生交互式 CLI,不属于受管理 Task Provider 协议。
528
528
 
529
529
  当新版本需要离线迁移 Home 时,应等待当前 Turn/Run 完成,然后从普通 shell
530
530
  执行 `yui session stop --all`,再重新执行 `yui update`。停止命令会先整体预检:
@@ -560,7 +560,7 @@ state、receipt 与 pane fence。Yui 不会解析 prompt glyph、进度文本、
560
560
  或其他 Agent 终端输出来推断 ready 或 success。`captureRole()` 只用于显式的人类
561
561
  transcript 查看,不具备生命周期权威。
562
562
 
563
- 稳定的 Role 上下文不会创建额外的 bootstrap Turn。Task execution Run 按角色使用通用 Leader 或 Worker Skill,review Run 则按持久 Run purpose 使用通用 Reviewer Skill;Provider 可以通过安全的追加式原生上下文通道携带 Skill,也可以在普通 Task 投递中指向它。这些都只是 Yui 自己拥有的可移植编排规则。Project Skills 始终是 Project 中正常版本化的文件,由 Agent 通过自身项目机制发现、选择并按需加载;Yui 不扫描、不解析、不复制,也不注入 Project Skills。Managed Codex 保留用户原有的 developer instructions;普通 Task 消息会携带精简的 Session Manifest 绝对路径,Manifest 再指向对应的 Yui Role Skill,供 Codex 按需读取。Role 选择的 model、effort、permission、workspace 与 shell 设置通过共享 App Server daemon,作为线程级 `thread/start` 或 `thread/resume` 配置传入。Codex 原生 config profile 不能隔离到共享 daemon 的单个 thread,因此 Managed Codex 会拒绝这个设置并提示使用 Yui Agent Profile;其他非 Yui 或交互式 Codex 会话不受影响。其他 Codex 线程继续使用原有的用户、profile、Project 和 system 配置,Yui 不修改底层配置文件。App Server 原生通知是 Managed Codex 线程的生命周期权威;Yui 不为它安装 Hook,也不占用 `notify`。交互式 Codex Session 仍可使用 Yui 的结构化 `notify` callback,Doctor 会报告最终生效的配置冲突。`skills.config` 只负责启停已发现 Skill,Yui 不会误用它。Claude 从 Yui 管理的私有 `0600` context 文件读取同一份 Yui Role Skill 内容,不再把大段或敏感文本放进 argv;重试和 resume 会复用按 purpose 区分的稳定路径。非 Operator 的 global Role 保持中性,不会注入 Task 编排 Skill。因此 Operator 会停在空白的原生 composer,用户输入仍是第一条 user message;Leader wake、Worker 和 Reviewer Run assignment 仍是邮箱投递的真实工作消息。
563
+ 稳定的 Role 上下文不会创建额外的 bootstrap Turn。Task execution Run 按角色使用通用 Leader 或 Worker Skill,review Run 则按持久 Run purpose 使用通用 Reviewer Skill;Provider 可以通过安全的追加式原生上下文通道携带 Skill,也可以在普通 Task 投递中指向它。这些都只是 Yui 自己拥有的可移植编排规则。Project Skills 始终是 Project 中正常版本化的文件,由 Agent 通过自身项目机制发现、选择并按需加载;Yui 不扫描、不解析、不复制,也不注入 Project Skills。Managed Codex 保留用户原有的 developer instructions;普通 Task 消息会携带精简的 Session Manifest 绝对路径,Manifest 再指向对应的 Yui Role Skill,供 Codex 按需读取。每个存活 Role Yui-owned App Server 进程会加载所选 Codex 原生 config profile;model、effort、permission、workspace 与 shell 设置作为线程级 `thread/start` 或 `thread/resume` 配置传入,Yui 不修改底层 Codex 配置文件。App Server 原生通知是 Managed Codex 线程的生命周期权威;Yui 不为它安装 Hook,也不占用 `notify`。交互式 Codex Session 仍可使用 Yui 的结构化 `notify` callback,Doctor 会报告最终生效的配置冲突。`skills.config` 只负责启停已发现 Skill,Yui 不会误用它。Claude 从 Yui 管理的私有 `0600` context 文件读取同一份 Yui Role Skill 内容,不再把大段或敏感文本放进 argv;重试和 resume 会复用按 purpose 区分的稳定路径。非 Operator 的 global Role 保持中性,不会注入 Task 编排 Skill。因此 Operator 会停在空白的原生 composer,用户输入仍是第一条 user message;Leader wake、Worker 和 Reviewer Run assignment 仍是邮箱投递的真实工作消息。
564
564
 
565
565
  ## Controller 与失败处理
566
566
 
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.6",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,