@zq-silk/yui 0.6.16 → 0.7.0

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 (69) hide show
  1. package/dist/cli/commandCatalog.js +3 -7
  2. package/dist/cli.js +12 -33
  3. package/dist/commands/executionAuditCommands.js +19 -0
  4. package/dist/commands/globalRoleCommands.js +70 -0
  5. package/dist/commands/taskActor.js +3 -2
  6. package/dist/commands/taskCommands.js +160 -41
  7. package/dist/commands/taskContextCommand.js +1 -1
  8. package/dist/commands/taskInputCommands.js +3 -2
  9. package/dist/commands/taskRoleRuntimeStatus.js +3 -3
  10. package/dist/context/contextSnapshot.js +228 -0
  11. package/dist/context/roleSessionContext.js +3 -1
  12. package/dist/context/runContextContract.js +162 -0
  13. package/dist/context/runContextPack.js +322 -0
  14. package/dist/context/sessionBootstrapManifest.js +81 -0
  15. package/dist/context/sessionProtocolIdentity.js +23 -0
  16. package/dist/controller/agentRuntimeObserver.js +6 -1
  17. package/dist/controller/controller.js +4 -3
  18. package/dist/controller/fileSchedulerStoreAdapter.js +446 -145
  19. package/dist/controller/jobControl.js +2 -1
  20. package/dist/controller/runtime.js +83 -0
  21. package/dist/controller/runtimeHookRunFence.js +6 -2
  22. package/dist/controller/sessionOwnerReconciliation.js +5 -0
  23. package/dist/executor/agentAdapter.js +7 -2
  24. package/dist/executor/agentExecutor.js +23 -0
  25. package/dist/executor/effectiveLaunch.js +24 -0
  26. package/dist/executor/executorRegistry.js +7 -1
  27. package/dist/executor/fileRoleLaunchPlanner.js +73 -27
  28. package/dist/lifecycle/exactRunTerminalization.js +2 -3
  29. package/dist/lifecycle/providerErrorClass.js +8 -3
  30. package/dist/observability/executionAudit.js +87 -2
  31. package/dist/repository/taskWorkspacePreparer.js +2 -2
  32. package/dist/run/agentRun.js +101 -16
  33. package/dist/run/providerRetry.js +167 -56
  34. package/dist/run/providerRetryConfig.js +5 -1
  35. package/dist/run/runControlRequest.js +50 -0
  36. package/dist/runtime/agentDriver.js +47 -0
  37. package/dist/runtime/agentHost.js +327 -0
  38. package/dist/runtime/builtinAgentDrivers.js +23 -1
  39. package/dist/runtime/builtinTranscriptObserver.js +4 -0
  40. package/dist/runtime/builtinTranscriptUsage.js +2 -0
  41. package/dist/runtime/exactControlPlane.js +2 -2
  42. package/dist/runtime/globalProcessExitStore.js +38 -0
  43. package/dist/runtime/launchBroker.js +95 -0
  44. package/dist/runtime/processExitObservation.js +60 -0
  45. package/dist/runtime/runtimeBinding.js +6 -0
  46. package/dist/runtime/runtimeObservation.js +27 -6
  47. package/dist/runtime/runtimeProjection.js +6 -3
  48. package/dist/runtime/runtimeStopReceipt.js +42 -0
  49. package/dist/runtime/sessionTerminationGuard.js +13 -0
  50. package/dist/runtime/tmuxAdapters.js +203 -220
  51. package/dist/scheduler/activeRoleRunDelivery.js +24 -3
  52. package/dist/scheduler/leaderWakeupProcessor.js +18 -60
  53. package/dist/scheduler/roleRunLiveness.js +61 -27
  54. package/dist/storage/migration/productionRegistry.js +264 -0
  55. package/dist/storage/sqliteSchema.js +23 -2
  56. package/dist/storage/sqliteStore.js +39 -2
  57. package/dist/storage/taskStore.js +54 -5
  58. package/dist/storage/upgrade/recordVersions.js +3 -1
  59. package/dist/storage/upgrade/sqliteStateMigration.js +10 -0
  60. package/dist/task/taskRecordReference.js +1 -0
  61. package/dist/tmux/tmuxManager.js +15 -4
  62. package/dist/web/assets/client/components.js +1 -1
  63. package/package.json +1 -1
  64. package/skills/yui-leader/SKILL.md +10 -5
  65. package/skills/yui-operator/SKILL.md +4 -0
  66. package/skills/yui-reviewer/SKILL.md +4 -0
  67. package/skills/yui-runtime/SKILL.md +61 -0
  68. package/skills/yui-worker/SKILL.md +82 -218
  69. package/dist/executor/managedClaudeRunner.js +0 -121
@@ -27,7 +27,7 @@ export const SQLITE_LAYOUT_VERSION = 7;
27
27
  /** The aggregate version of the normalized SQLite schema. */
28
28
  export const SQLITE_AGGREGATE_VERSION = 1;
29
29
  /** The current schema migration version. */
30
- export const SQLITE_SCHEMA_VERSION = 16;
30
+ export const SQLITE_SCHEMA_VERSION = 17;
31
31
  /** Telemetry retention bounds (§4.4). Open question 3 in §11; defaults from the design. */
32
32
  export const TELEMETRY_KEEP_PER_GENERATION = 200;
33
33
  export const TELEMETRY_RUN_CAP = 50_000;
@@ -911,6 +911,25 @@ CREATE TABLE IF NOT EXISTS task_wakes (
911
911
  );
912
912
  CREATE INDEX IF NOT EXISTS idx_task_wakes_seq ON task_wakes(task_id, seq);
913
913
  `;
914
+ /** Migration 17: immutable, Task-scoped ContextSnapshot records. */
915
+ const MIGRATION_17_SQL = `
916
+ CREATE TABLE IF NOT EXISTS context_snapshots (
917
+ task_id TEXT NOT NULL,
918
+ snapshot_id TEXT NOT NULL,
919
+ scope TEXT NOT NULL CHECK (scope IN ('task','workitem','stage')),
920
+ scope_ref TEXT,
921
+ sequence INTEGER NOT NULL CHECK (sequence > 0),
922
+ digest TEXT NOT NULL CHECK (length(digest) = 64),
923
+ payload TEXT NOT NULL,
924
+ frozen_at TEXT NOT NULL,
925
+ PRIMARY KEY (task_id, snapshot_id),
926
+ FOREIGN KEY (task_id) REFERENCES tasks_catalog(task_id) ON DELETE CASCADE,
927
+ CHECK ((scope = 'task' AND scope_ref IS NULL) OR (scope <> 'task' AND scope_ref IS NOT NULL))
928
+ );
929
+
930
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_context_snapshots_scope_sequence
931
+ ON context_snapshots(task_id, scope, COALESCE(scope_ref, ''), sequence);
932
+ `;
914
933
  const MIGRATIONS = [
915
934
  { version: 1, axis: "layout", sql: MIGRATION_1_SQL },
916
935
  { version: 2, axis: "record", recordKind: "durableJob+capability-grant+release-workflow", sql: MIGRATION_2_SQL },
@@ -927,7 +946,8 @@ const MIGRATIONS = [
927
946
  { version: 13, axis: "layout", sql: MIGRATION_13_SQL },
928
947
  { version: 14, axis: "record", recordKind: "workMailbox", sql: MIGRATION_14_SQL },
929
948
  { version: 15, axis: "record", recordKind: "publicationReference", sql: MIGRATION_15_SQL },
930
- { version: 16, axis: "record", recordKind: "taskWake", sql: MIGRATION_16_SQL }
949
+ { version: 16, axis: "record", recordKind: "taskWake", sql: MIGRATION_16_SQL },
950
+ { version: 17, axis: "record", recordKind: "contextSnapshot", sql: MIGRATION_17_SQL }
931
951
  ];
932
952
  /** Current hot-path indexes whose absence would invalidate a current Home. */
933
953
  const REQUIRED_SCHEMA_INDEXES = [
@@ -1169,6 +1189,7 @@ export const SQLITE_SCHEMA_TABLES = [
1169
1189
  "role_session_sets",
1170
1190
  "work_items",
1171
1191
  "work_item_candidates",
1192
+ "context_snapshots",
1172
1193
  "agent_runs",
1173
1194
  "active_runs",
1174
1195
  "review_rounds",
@@ -39,6 +39,7 @@ import { join } from "node:path";
39
39
  import { isDeepStrictEqual } from "node:util";
40
40
  import Database from "better-sqlite3";
41
41
  import { consumePendingBatch, mailboxTargetKey, pendingLane, validateWorkMailbox } from "../coordination/workMailbox.js";
42
+ import { validateContextSnapshot } from "../context/contextSnapshot.js";
42
43
  import { compareRuntimeSessionCandidates, projectRuntimeSessionCandidate } from "../runtime/runtimeSessionCandidate.js";
43
44
  import { validateReviewFinding } from "../review/reviewFinding.js";
44
45
  import { reviewFindingLedgerMode } from "../review/reviewFindingLedger.js";
@@ -1418,6 +1419,37 @@ export class SqliteTaskStore {
1418
1419
  return Object.freeze({ retained, deleted: toDelete.length });
1419
1420
  });
1420
1421
  }
1422
+ // -- context snapshots ------------------------------------------------------
1423
+ nextContextSnapshotId(taskId) {
1424
+ return this.#nextTaskRecordId(taskId, "contextSnapshot");
1425
+ }
1426
+ getContextSnapshot(taskId, snapshotId) {
1427
+ return this.#getPayload("context_snapshots", "task_id = ? AND snapshot_id = ?", [taskId, snapshotId]);
1428
+ }
1429
+ listContextSnapshots(taskId) {
1430
+ return this.#sortById(this.#listPayload("context_snapshots", "task_id = ?", [taskId]), (snapshot) => snapshot.id);
1431
+ }
1432
+ saveContextSnapshot(snapshot) {
1433
+ const stored = validateContextSnapshot(snapshot);
1434
+ this.#requireTask(stored.taskId);
1435
+ const existing = this.getContextSnapshot(stored.taskId, stored.id);
1436
+ if (existing !== null) {
1437
+ if (!isDeepStrictEqual(existing, stored)) {
1438
+ throw new StorageRecordError(`Context Snapshot is immutable: ${stored.id}.`);
1439
+ }
1440
+ return;
1441
+ }
1442
+ const duplicate = this.#db.prepare(`SELECT snapshot_id FROM context_snapshots
1443
+ WHERE task_id = ? AND scope = ? AND COALESCE(scope_ref, '') = COALESCE(?, '') AND sequence = ?`).get(stored.taskId, stored.scope, stored.scopeRef ?? null, stored.sequence);
1444
+ if (duplicate !== undefined) {
1445
+ throw new StorageRecordError(`Context Snapshot sequence already exists: ${stored.taskId}/${stored.scope}/${stored.sequence}.`);
1446
+ }
1447
+ this.#mutate(() => {
1448
+ this.#db.prepare(`INSERT INTO context_snapshots
1449
+ (task_id, snapshot_id, scope, scope_ref, sequence, digest, payload, frozen_at)
1450
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(stored.taskId, stored.id, stored.scope, stored.scopeRef ?? null, stored.sequence, stored.digest, this.#json(stored), stored.frozenAt);
1451
+ });
1452
+ }
1421
1453
  // -- agent runs -------------------------------------------------------------
1422
1454
  nextAgentRunId(taskId) { return this.#nextTaskRecordId(taskId, "agentRun"); }
1423
1455
  peekNextAgentRunId(taskId) { return this.#peekTaskRecordId(taskId, "agentRun"); }
@@ -1451,13 +1483,18 @@ export class SqliteTaskStore {
1451
1483
  ? ""
1452
1484
  : ` AND tc.task_id IN (${selectedTaskIds.map(() => "?").join(", ")})`;
1453
1485
  const rows = this.#db.prepare(`SELECT DISTINCT ar.task_id AS taskId, ar.run_id AS runId, ar.role_name AS roleName,
1454
- json_extract(ar.payload, '$.providerRetry.nextAttemptAt') AS nextAttemptAt
1486
+ json_extract(ar.payload, '$.providerRetry.state') AS state,
1487
+ CASE json_extract(ar.payload, '$.providerRetry.state')
1488
+ WHEN 'scheduled' THEN json_extract(ar.payload, '$.providerRetry.nextAttemptAt')
1489
+ ELSE json_extract(ar.payload, '$.providerRetry.episodeDeadlineAt')
1490
+ END AS dueAt
1455
1491
  FROM tasks_catalog tc INDEXED BY idx_tasks_active
1456
1492
  JOIN active_runs ap ON ap.task_id = tc.task_id
1457
1493
  JOIN agent_runs ar ON ar.task_id = ap.task_id AND ar.run_id = ap.run_id
1458
1494
  WHERE tc.is_active = 1
1459
1495
  AND ar.status = 'active'
1460
- AND json_extract(ar.payload, '$.providerRetry.nextAttemptAt') IS NOT NULL${taskPredicate}`).all(...(selectedTaskIds ?? []));
1496
+ AND json_extract(ar.payload, '$.providerRetry.state') IN
1497
+ ('scheduled', 'dispatching', 'awaiting-progress')${taskPredicate}`).all(...(selectedTaskIds ?? []));
1461
1498
  return rows.sort((left, right) => (numericCompare(left.taskId, right.taskId)
1462
1499
  || numericCompare(left.roleName, right.roleName)
1463
1500
  || numericCompare(left.runId, right.runId)));
@@ -9,10 +9,12 @@ import { publicationExternalKey, validatePublicationReference } from "../task/pu
9
9
  import { reconciliationIntervalMilliseconds, resolveLeaderNextActionMode, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode } from "../config/yuiConfig.js";
10
10
  import { resolveTimeZone } from "../output/timePresentation.js";
11
11
  import { mailboxBatches, consumePendingBatch, mailboxHasWork, mailboxTargetKey, pendingLane, validateWorkMailbox } from "../coordination/workMailbox.js";
12
+ import { validateContextSnapshot } from "../context/contextSnapshot.js";
12
13
  import { validateInputRequest } from "../input/inputRequest.js";
13
14
  import { validateRoleSessionSet } from "../executor/agentExecutor.js";
14
15
  import { validateTaskMessage } from "../message/message.js";
15
- import { validateAgentRun } from "../run/agentRun.js";
16
+ import { agentRunDeliveryReceiptId, validateAgentRun } from "../run/agentRun.js";
17
+ import { providerRetryWakeAt } from "../run/providerRetry.js";
16
18
  import { compareRuntimeSessionCandidates, projectRuntimeSessionCandidate } from "../runtime/runtimeSessionCandidate.js";
17
19
  import { FileSessionOwnerRegistry } from "../runtime/sessionOwnerRegistry.js";
18
20
  import { validateReviewConfig } from "../review/reviewConfig.js";
@@ -31,7 +33,7 @@ import { CURRENT_LEADER_FAILURE_SCHEMA_VERSION } from "../scheduler/leaderFailur
31
33
  import { CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION } from "../scheduler/operatorNotification.js";
32
34
  import { CURRENT_TASK_WAKE_SCHEMA_VERSION, validateTaskWake } from "../scheduler/taskWake.js";
33
35
  import { validateTask } from "../task/task.js";
34
- import { formatAgentRunReceiptId, TASK_RECORD_ID_PREFIXES, validateTaskRecordReference } from "../task/taskRecordReference.js";
36
+ import { TASK_RECORD_ID_PREFIXES, validateTaskRecordReference } from "../task/taskRecordReference.js";
35
37
  import { workItemExecutionGroupById, validateWorkItem } from "../workItem/workItem.js";
36
38
  import { isExecutionGroupTransition, validateExecutionGroup } from "../execution/executionGroup.js";
37
39
  import { managedWorkspaceKey, validateManagedWorkspace } from "../worktree/managedWorkspace.js";
@@ -60,6 +62,7 @@ export const CURRENT_GLOBAL_ROLE_SCHEMA_VERSION = 3;
60
62
  export const CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION = 3;
61
63
  export const CURRENT_TASK_SCHEMA_VERSION = 4;
62
64
  export const CURRENT_TASK_BRIEF_SCHEMA_VERSION = 2;
65
+ export const CURRENT_CONTEXT_SNAPSHOT_SCHEMA_VERSION = 1;
63
66
  export const CURRENT_TASK_ROLE_SCHEMA_VERSION = 3;
64
67
  export const CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION = 2;
65
68
  export const CURRENT_WORK_ITEM_SCHEMA_VERSION = 9;
@@ -120,7 +123,7 @@ export const CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION = 5;
120
123
  * actionability fields. All are optional, so the v6→v7 migration is a
121
124
  * version-only rewrite.
122
125
  */
123
- export const CURRENT_AGENT_RUN_SCHEMA_VERSION = 7;
126
+ export const CURRENT_AGENT_RUN_SCHEMA_VERSION = 9;
124
127
  export const CURRENT_INTEGRATION_QUEUE_SCHEMA_VERSION = 1;
125
128
  export class FileTaskStore {
126
129
  rootDir;
@@ -904,6 +907,38 @@ export class FileTaskStore {
904
907
  task.workItems[stored.id] = stored;
905
908
  });
906
909
  }
910
+ nextContextSnapshotId(taskId) {
911
+ return this.#nextTaskRecordId(taskId, "contextSnapshot");
912
+ }
913
+ getContextSnapshot(taskId, id) {
914
+ return optional(this.#state().tasks[taskId]?.contextSnapshots[id]);
915
+ }
916
+ listContextSnapshots(taskId) {
917
+ return values(this.#requireTask(taskId).contextSnapshots, "id");
918
+ }
919
+ saveContextSnapshot(snapshot) {
920
+ const stored = identified(snapshot, CURRENT_CONTEXT_SNAPSHOT_SCHEMA_VERSION, "id", snapshot.id, "Context Snapshot");
921
+ validateContextSnapshot(stored);
922
+ const aggregate = this.#requireTaskForWrite(stored.taskId);
923
+ const existing = aggregate.contextSnapshots[stored.id];
924
+ if (existing !== undefined) {
925
+ if (!isDeepStrictEqual(existing, stored)) {
926
+ throw new StorageRecordError(`Context Snapshot is immutable: ${stored.id}.`);
927
+ }
928
+ return;
929
+ }
930
+ const duplicateSequence = Object.values(aggregate.contextSnapshots).find((candidate) => (candidate.scope === stored.scope
931
+ && candidate.scopeRef === stored.scopeRef
932
+ && candidate.sequence === stored.sequence));
933
+ if (duplicateSequence !== undefined) {
934
+ throw new StorageRecordError(`Context Snapshot sequence already exists: ${stored.taskId}/${stored.scope}/${stored.sequence}.`);
935
+ }
936
+ this.#mutate((state) => {
937
+ const task = state.tasks[stored.taskId];
938
+ observeTaskRecordId(task, "contextSnapshot", stored.id);
939
+ task.contextSnapshots[stored.id] = stored;
940
+ });
941
+ }
907
942
  nextAgentRunId(taskId) {
908
943
  return this.#nextTaskRecordId(taskId, "agentRun");
909
944
  }
@@ -926,7 +961,9 @@ export class FileTaskStore {
926
961
  if (task.status !== "active")
927
962
  continue;
928
963
  for (const run of this.listAgentRuns(task.id)) {
929
- if (run.status === "active" && run.providerRetry?.nextAttemptAt !== undefined) {
964
+ if (run.status === "active"
965
+ && run.providerRetry !== undefined
966
+ && providerRetryWakeAt(run.providerRetry) !== null) {
930
967
  throw new StorageRecordError("Provider retry in place requires the SQLite backend; run `yui update` to migrate this Home.");
931
968
  }
932
969
  }
@@ -1920,6 +1957,7 @@ function emptyStoredTask(task) {
1920
1957
  roleSessionSets: {},
1921
1958
  jobCallerKeyHashes: {},
1922
1959
  workItems: {},
1960
+ contextSnapshots: {},
1923
1961
  agentRuns: {},
1924
1962
  reviewRounds: {},
1925
1963
  activeRuns: {},
@@ -1939,6 +1977,7 @@ function emptyStoredTask(task) {
1939
1977
  function emptyTaskIdHighWaterMarks() {
1940
1978
  return {
1941
1979
  workItem: 0,
1980
+ contextSnapshot: 0,
1942
1981
  agentRun: 0,
1943
1982
  reviewRound: 0,
1944
1983
  reviewFinding: 0,
@@ -2137,6 +2176,7 @@ function parseStoredTask(value, taskId) {
2137
2176
  "roleSessionSets",
2138
2177
  "jobCallerKeyHashes",
2139
2178
  "workItems",
2179
+ "contextSnapshots",
2140
2180
  "agentRuns",
2141
2181
  "reviewRounds",
2142
2182
  "activeRuns",
@@ -2226,6 +2266,14 @@ function parseStoredTask(value, taskId) {
2226
2266
  validateWorkItem(item);
2227
2267
  return item;
2228
2268
  }, "workItems");
2269
+ parseMap(aggregate.contextSnapshots, (record, key) => {
2270
+ const snapshot = identified(record, CURRENT_CONTEXT_SNAPSHOT_SCHEMA_VERSION, "id", key, "Context Snapshot");
2271
+ if (snapshot.taskId !== taskId) {
2272
+ throw new StorageRecordError(`Context Snapshot belongs to another Task: ${snapshot.taskId}.`);
2273
+ }
2274
+ validateContextSnapshot(snapshot);
2275
+ return snapshot;
2276
+ }, "contextSnapshots");
2229
2277
  parseMap(aggregate.agentRuns, (record, key) => {
2230
2278
  const run = identified(record, CURRENT_AGENT_RUN_SCHEMA_VERSION, "id", key, "Agent run");
2231
2279
  if (run.taskId !== taskId) {
@@ -2357,6 +2405,7 @@ function parseStoredTask(value, taskId) {
2357
2405
  function validateTaskIdHighWaterCoverage(aggregate, taskId) {
2358
2406
  const records = {
2359
2407
  workItem: aggregate.workItems,
2408
+ contextSnapshot: aggregate.contextSnapshots,
2360
2409
  agentRun: aggregate.agentRuns,
2361
2410
  reviewRound: aggregate.reviewRounds,
2362
2411
  // Issue 06 dbonly: review findings are SQLite-native; the file aggregate
@@ -3230,7 +3279,7 @@ function validateCanonicalTaskReferences(state, aggregate) {
3230
3279
  if (sessions.inFlight !== null) {
3231
3280
  const run = aggregate.agentRuns[sessions.inFlight.runId];
3232
3281
  if (run === undefined || run.roleName !== roleName
3233
- || sessions.inFlight.receiptId !== formatAgentRunReceiptId(taskId, run.id)) {
3282
+ || sessions.inFlight.receiptId !== agentRunDeliveryReceiptId(run)) {
3234
3283
  throw new StorageRecordError(`Task Role in-flight Run is invalid: ${taskId}/${roleName}.`);
3235
3284
  }
3236
3285
  }
@@ -16,7 +16,7 @@
16
16
  * transform in the production migration graph.
17
17
  */
18
18
  import { CURRENT_AGGREGATE_SCHEMA_VERSION, CURRENT_STORAGE_LAYOUT_VERSION } from "../storageVersions.js";
19
- import { CURRENT_AGENT_RUN_SCHEMA_VERSION, CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION, CURRENT_AGENT_PROFILE_SCHEMA_VERSION, CURRENT_CAPABILITY_GRANT_SCHEMA_VERSION, CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION, CURRENT_CHANGE_SET_SCHEMA_VERSION, CURRENT_DECISION_SCHEMA_VERSION, CURRENT_EVENT_SCHEMA_VERSION, CURRENT_GLOBAL_ROLE_SCHEMA_VERSION, CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION, CURRENT_INPUT_REQUEST_SCHEMA_VERSION, CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION, CURRENT_INTEGRATION_QUEUE_SCHEMA_VERSION, CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION, CURRENT_MESSAGE_SCHEMA_VERSION, CURRENT_MILESTONE_SCHEMA_VERSION, CURRENT_PUBLICATION_REFERENCE_SCHEMA_VERSION, CURRENT_PROJECT_SCHEMA_VERSION, CURRENT_RELEASE_WORKFLOW_SCHEMA_VERSION, CURRENT_REVIEW_ROUND_SCHEMA_VERSION, CURRENT_STORED_TASK_SCHEMA_VERSION, CURRENT_TASK_BRIEF_SCHEMA_VERSION, CURRENT_TASK_ROLE_SCHEMA_VERSION, CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION, CURRENT_TASK_SCHEMA_VERSION, CURRENT_WORK_ITEM_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION } from "../taskStore.js";
19
+ import { CURRENT_AGENT_RUN_SCHEMA_VERSION, CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION, CURRENT_AGENT_PROFILE_SCHEMA_VERSION, CURRENT_CAPABILITY_GRANT_SCHEMA_VERSION, CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION, CURRENT_CHANGE_SET_SCHEMA_VERSION, CURRENT_CONTEXT_SNAPSHOT_SCHEMA_VERSION, CURRENT_DECISION_SCHEMA_VERSION, CURRENT_EVENT_SCHEMA_VERSION, CURRENT_GLOBAL_ROLE_SCHEMA_VERSION, CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION, CURRENT_INPUT_REQUEST_SCHEMA_VERSION, CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION, CURRENT_INTEGRATION_QUEUE_SCHEMA_VERSION, CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION, CURRENT_MESSAGE_SCHEMA_VERSION, CURRENT_MILESTONE_SCHEMA_VERSION, CURRENT_PUBLICATION_REFERENCE_SCHEMA_VERSION, CURRENT_PROJECT_SCHEMA_VERSION, CURRENT_RELEASE_WORKFLOW_SCHEMA_VERSION, CURRENT_REVIEW_ROUND_SCHEMA_VERSION, CURRENT_STORED_TASK_SCHEMA_VERSION, CURRENT_TASK_BRIEF_SCHEMA_VERSION, CURRENT_TASK_ROLE_SCHEMA_VERSION, CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION, CURRENT_TASK_SCHEMA_VERSION, CURRENT_WORK_ITEM_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION } from "../taskStore.js";
20
20
  import { CURRENT_LEADER_FAILURE_SCHEMA_VERSION } from "../../scheduler/leaderFailure.js";
21
21
  import { CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION } from "../../scheduler/operatorNotification.js";
22
22
  import { CURRENT_TASK_WAKE_SCHEMA_VERSION } from "../../scheduler/taskWake.js";
@@ -44,6 +44,7 @@ const EXPECTED_DIRECT_RECORD_LOCATORS = Object.freeze({
44
44
  managedWorkspace: "state.json#/tasks/*/managedWorkspaces",
45
45
  taskRoleSessionSet: "state.json#/tasks/*/roleSessionSets",
46
46
  workItem: "state.json#/tasks/*/workItems",
47
+ contextSnapshot: "state.json#/tasks/*/contextSnapshots",
47
48
  agentRun: "state.json#/tasks/*/agentRuns",
48
49
  reviewRound: "state.json#/tasks/*/reviewRounds",
49
50
  changeSet: "state.json#/tasks/*/changeSets",
@@ -99,6 +100,7 @@ function getCurrentRecordDescriptors() {
99
100
  managedWorkspace: descriptor(CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION, "state.json#/tasks/*/managedWorkspaces"),
100
101
  taskRoleSessionSet: descriptor(CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION, "state.json#/tasks/*/roleSessionSets"),
101
102
  workItem: descriptor(CURRENT_WORK_ITEM_SCHEMA_VERSION, "state.json#/tasks/*/workItems"),
103
+ contextSnapshot: descriptor(CURRENT_CONTEXT_SNAPSHOT_SCHEMA_VERSION, "state.json#/tasks/*/contextSnapshots"),
102
104
  agentRun: descriptor(CURRENT_AGENT_RUN_SCHEMA_VERSION, "state.json#/tasks/*/agentRuns"),
103
105
  reviewRound: descriptor(CURRENT_REVIEW_ROUND_SCHEMA_VERSION, "state.json#/tasks/*/reviewRounds"),
104
106
  changeSet: descriptor(CURRENT_CHANGE_SET_SCHEMA_VERSION, "state.json#/tasks/*/changeSets"),
@@ -176,6 +176,7 @@ function asStoredTask(value) {
176
176
  managedWorkspaces: asObjectMap(record.managedWorkspaces),
177
177
  roleSessionSets: asObjectMap(record.roleSessionSets),
178
178
  workItems: asObjectMap(record.workItems),
179
+ contextSnapshots: asObjectMap(record.contextSnapshots),
179
180
  agentRuns: asObjectMap(record.agentRuns),
180
181
  reviewRounds: asObjectMap(record.reviewRounds),
181
182
  changeSets: asObjectMap(record.changeSets),
@@ -271,6 +272,9 @@ export function populateSqliteFromState(home, state, databaseFilename) {
271
272
  for (const item of Object.values(stored.workItems)) {
272
273
  store.saveWorkItem(taskId, item);
273
274
  }
275
+ for (const snapshot of Object.values(stored.contextSnapshots)) {
276
+ store.saveContextSnapshot(snapshot);
277
+ }
274
278
  for (const run of Object.values(stored.agentRuns)) {
275
279
  store.saveAgentRun(run);
276
280
  }
@@ -451,6 +455,7 @@ export function computeStateFamilyChecksums(state) {
451
455
  const workspaces = [];
452
456
  const roleSessionSets = [];
453
457
  const workItems = [];
458
+ const contextSnapshots = [];
454
459
  const agentRuns = [];
455
460
  const reviewRounds = [];
456
461
  const changeSets = [];
@@ -478,6 +483,7 @@ export function computeStateFamilyChecksums(state) {
478
483
  workspaces.push(...Object.values(stored.managedWorkspaces));
479
484
  roleSessionSets.push(...Object.values(stored.roleSessionSets));
480
485
  workItems.push(...Object.values(stored.workItems));
486
+ contextSnapshots.push(...Object.values(stored.contextSnapshots));
481
487
  agentRuns.push(...Object.values(stored.agentRuns));
482
488
  reviewRounds.push(...Object.values(stored.reviewRounds));
483
489
  changeSets.push(...Object.values(stored.changeSets));
@@ -517,6 +523,7 @@ export function computeStateFamilyChecksums(state) {
517
523
  checksums.managedWorkspace = hashRecords(workspaces);
518
524
  checksums.taskRoleSessionSet = hashRecords(roleSessionSets);
519
525
  checksums.workItem = hashRecords(workItems);
526
+ checksums.contextSnapshot = hashRecords(contextSnapshots);
520
527
  checksums.agentRun = hashRecords(agentRuns);
521
528
  checksums.reviewRound = hashRecords(reviewRounds);
522
529
  checksums.changeSet = hashRecords(changeSets);
@@ -605,6 +612,7 @@ export function computeDbFamilyChecksums(home, databaseFilename) {
605
612
  checksums.managedWorkspace = hashPayloadTable(db, "SELECT payload FROM managed_workspaces");
606
613
  checksums.taskRoleSessionSet = hashPayloadTable(db, "SELECT payload FROM role_session_sets");
607
614
  checksums.workItem = hashPayloadTable(db, "SELECT payload FROM work_items");
615
+ checksums.contextSnapshot = hashPayloadTable(db, "SELECT payload FROM context_snapshots");
608
616
  checksums.agentRun = hashPayloadTable(db, "SELECT payload FROM agent_runs");
609
617
  checksums.reviewRound = hashPayloadTable(db, "SELECT payload FROM review_rounds");
610
618
  checksums.changeSet = hashPayloadTable(db, "SELECT payload FROM change_sets");
@@ -733,6 +741,7 @@ export function readStateFromSqlite(home) {
733
741
  managedWorkspaces: {},
734
742
  roleSessionSets: {},
735
743
  workItems: {},
744
+ contextSnapshots: {},
736
745
  agentRuns: {},
737
746
  reviewRounds: {},
738
747
  changeSets: {},
@@ -760,6 +769,7 @@ export function readStateFromSqlite(home) {
760
769
  loadTaskPayloadMap(db, "managed_workspaces", tasks, "managedWorkspaces", (record) => managedWorkspaceKey(record.owner));
761
770
  loadTaskPayloadMap(db, "role_session_sets", tasks, "roleSessionSets", (record) => record.owner.roleName);
762
771
  loadTaskPayloadMap(db, "work_items", tasks, "workItems", (record) => record.id);
772
+ loadTaskPayloadMap(db, "context_snapshots", tasks, "contextSnapshots", (record) => record.id);
763
773
  loadTaskPayloadMap(db, "agent_runs", tasks, "agentRuns", (record) => record.id);
764
774
  loadTaskPayloadMap(db, "review_rounds", tasks, "reviewRounds", (record) => record.id);
765
775
  loadTaskPayloadMap(db, "change_sets", tasks, "changeSets", (record) => record.id);
@@ -1,6 +1,7 @@
1
1
  import { requireIdentity } from "../domain/validation.js";
2
2
  export const TASK_RECORD_ID_PREFIXES = {
3
3
  workItem: "work-item",
4
+ contextSnapshot: "context-snapshot",
4
5
  agentRun: "agent-run",
5
6
  reviewRound: "review-round",
6
7
  reviewFinding: "review-finding",
@@ -483,7 +483,7 @@ export class TmuxManager {
483
483
  try {
484
484
  output = this.run([
485
485
  "list-panes", "-s", "-t", this.sessionName(taskId), "-F",
486
- `#{window_name}${formatSeparator}#{pane_dead}${formatSeparator}#{pane_pid}${formatSeparator}#{pane_current_command}`
486
+ `#{window_name}${formatSeparator}#{pane_dead}${formatSeparator}#{pane_dead_status}${formatSeparator}#{pane_pid}${formatSeparator}#{pane_current_command}`
487
487
  ]);
488
488
  }
489
489
  catch (error) {
@@ -495,9 +495,10 @@ export class TmuxManager {
495
495
  if (line.length === 0)
496
496
  return [];
497
497
  const separator = line.includes(encodedSeparator) ? encodedSeparator : formatSeparator;
498
- const [roleName, deadText, pidText, currentCommand, ...extra] = line.split(separator);
498
+ const [roleName, deadText, deadStatusText, pidText, currentCommand, ...extra] = line.split(separator);
499
499
  if (roleName === undefined
500
500
  || deadText === undefined
501
+ || deadStatusText === undefined
501
502
  || pidText === undefined
502
503
  || currentCommand === undefined
503
504
  || extra.length > 0
@@ -505,11 +506,13 @@ export class TmuxManager {
505
506
  throw runtimeError(`Tmux returned an invalid Task Role pane state for ${taskId}.`);
506
507
  }
507
508
  const pid = Number(pidText);
509
+ const deadStatus = Number(deadStatusText);
508
510
  return [{
509
511
  taskId,
510
512
  roleName,
511
513
  target: this.target(taskId, roleName),
512
514
  dead: deadText === "1",
515
+ ...(Number.isSafeInteger(deadStatus) && deadStatus >= 0 ? { deadStatus } : {}),
513
516
  ...(Number.isSafeInteger(pid) && pid > 0 ? { pid } : {}),
514
517
  currentCommand
515
518
  }];
@@ -532,6 +535,7 @@ export class TmuxManager {
532
535
  "#{session_name}",
533
536
  "#{window_name}",
534
537
  "#{pane_dead}",
538
+ "#{pane_dead_status}",
535
539
  "#{pane_pid}",
536
540
  "#{pane_current_command}"
537
541
  ].join(formatSeparator)
@@ -546,10 +550,11 @@ export class TmuxManager {
546
550
  if (line.length === 0)
547
551
  return [];
548
552
  const separator = line.includes(encodedSeparator) ? encodedSeparator : formatSeparator;
549
- const [sessionName, roleName, deadText, pidText, currentCommand, ...extra] = line.split(separator);
553
+ const [sessionName, roleName, deadText, deadStatusText, pidText, currentCommand, ...extra] = line.split(separator);
550
554
  if (sessionName === undefined
551
555
  || roleName === undefined
552
556
  || deadText === undefined
557
+ || deadStatusText === undefined
553
558
  || pidText === undefined
554
559
  || currentCommand === undefined
555
560
  || extra.length > 0
@@ -563,11 +568,13 @@ export class TmuxManager {
563
568
  throw runtimeError("Tmux returned an invalid Yui Role pane identity.");
564
569
  }
565
570
  const pid = Number(pidText);
571
+ const deadStatus = Number(deadStatusText);
566
572
  return [{
567
573
  taskId,
568
574
  roleName,
569
575
  target: this.target(taskId, roleName),
570
576
  dead: deadText === "1",
577
+ ...(Number.isSafeInteger(deadStatus) && deadStatus >= 0 ? { deadStatus } : {}),
571
578
  ...(Number.isSafeInteger(pid) && pid > 0 ? { pid } : {}),
572
579
  currentCommand
573
580
  }];
@@ -585,6 +592,7 @@ export class TmuxManager {
585
592
  "#{session_name}",
586
593
  "#{window_name}",
587
594
  "#{pane_dead}",
595
+ "#{pane_dead_status}",
588
596
  "#{pane_pid}",
589
597
  "#{pane_current_command}"
590
598
  ].join(formatSeparator)
@@ -1059,10 +1067,11 @@ function parseRolePaneInventory(output, formatSeparator, encodedSeparator, sessi
1059
1067
  if (line.length === 0)
1060
1068
  return [];
1061
1069
  const separator = line.includes(encodedSeparator) ? encodedSeparator : formatSeparator;
1062
- const [sessionName, roleName, deadText, pidText, currentCommand, ...extra] = line.split(separator);
1070
+ const [sessionName, roleName, deadText, deadStatusText, pidText, currentCommand, ...extra] = line.split(separator);
1063
1071
  if (sessionName === undefined
1064
1072
  || roleName === undefined
1065
1073
  || deadText === undefined
1074
+ || deadStatusText === undefined
1066
1075
  || pidText === undefined
1067
1076
  || currentCommand === undefined
1068
1077
  || extra.length > 0
@@ -1076,11 +1085,13 @@ function parseRolePaneInventory(output, formatSeparator, encodedSeparator, sessi
1076
1085
  throw runtimeError("Tmux returned an invalid Yui Role pane identity.");
1077
1086
  }
1078
1087
  const pid = Number(pidText);
1088
+ const deadStatus = Number(deadStatusText);
1079
1089
  return [{
1080
1090
  taskId,
1081
1091
  roleName,
1082
1092
  target: target(taskId, roleName),
1083
1093
  dead: deadText === "1",
1094
+ ...(Number.isSafeInteger(deadStatus) && deadStatus >= 0 ? { deadStatus } : {}),
1084
1095
  ...(Number.isSafeInteger(pid) && pid > 0 ? { pid } : {}),
1085
1096
  currentCommand
1086
1097
  }];
@@ -509,7 +509,7 @@ export function runCard(run, t, locale) {
509
509
  idRow.append(node("time", "", formatDateTime(run.endedAt || run.updatedAt, locale)));
510
510
  card.append(idRow);
511
511
 
512
- card.append(richText(t("detail.instruction"), run.input, t, { className: "execute-io", threshold: 320 }));
512
+ card.append(richText(t("detail.instruction"), run.assignment?.directive || run.assignment?.action || "-", t, { className: "execute-io", threshold: 320 }));
513
513
  if (run.summary) {
514
514
  card.append(richText(t("detail.outcome"), run.summary, t, { className: "execute-io outcome", threshold: 320 }));
515
515
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.6.16",
3
+ "version": "0.7.0",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -5,6 +5,10 @@ description: Lead one Yui Task from the user's core outcome by reasoning from fi
5
5
 
6
6
  # Yui Leader
7
7
 
8
+ Follow `yui-runtime` first and load the exact current Run Context Pack before
9
+ making Task decisions. Recover authority from its Snapshot and deltas, never
10
+ from launch text or transcript memory.
11
+
8
12
  Own Task direction, decomposition, semantic decisions, acceptance, integration,
9
13
  and durable context. Yui has one work model: every bounded outcome is a
10
14
  WorkItem. Choose one of three execution paths for each WorkItem:
@@ -213,11 +217,11 @@ yui task wake show <task-id> <wake-id>
213
217
  ```
214
218
 
215
219
  For a fresh generation (no native history), or when the envelope indicates a
216
- major change, start with the complete Task projection, then follow its Project
217
- Policy references:
220
+ major change, start with the exact Run Context Pack loaded through
221
+ `yui-runtime`, expand only its authorized Task and Project Policy refs, then
222
+ inspect Project-owned records as needed:
218
223
 
219
224
  ```sh
220
- yui task context <task-id>
221
225
  yui project show <project>
222
226
  yui project knowledge list <project>
223
227
  ```
@@ -361,7 +365,7 @@ yui task work update <work-id> done \
361
365
  Use `failed` with recovery context when it cannot be completed. Do not mark
362
366
  work done before checking its acceptance criteria. When global review is
363
367
  enabled, `done` submits a Candidate instead of completing the WorkItem. Read
364
- `yui task context <task-id>` and follow that Candidate's snapshotted policy.
368
+ the exact Run Context Pack and follow that Candidate's snapshotted policy.
365
369
 
366
370
  ## Create a native subagent
367
371
 
@@ -488,7 +492,8 @@ delivers evidence and moves the WorkItem to Leader review; it is not acceptance.
488
492
  ## Review, retry, capture, and integrate
489
493
 
490
494
  After any Candidate is submitted, inspect its exact policy, Run result,
491
- ReviewRounds, checks, and workspace through `task context`.
495
+ ReviewRounds, checks, and workspace through the exact Run Context Pack and its
496
+ authorized expansions.
492
497
 
493
498
  - `always`: wait for the automatically requested ReviewRound to become
494
499
  terminal. Never bypass an active round.
@@ -5,6 +5,10 @@ description: Route multi-project user requests into Yui Tasks, preserve durable
5
5
 
6
6
  # Yui Operator
7
7
 
8
+ Follow `yui-runtime` for every routed managed Run. Load only the exact
9
+ authorized Context Pack; do not infer Task authority from a prompt, process, or
10
+ workspace.
11
+
8
12
  Be the task-neutral user entry point. The user should be able to discuss
9
13
  features, bugs, investigations, and questions across multiple Projects without
10
14
  managing Yui records. Route each request to the correct Project and Task; leave
@@ -5,6 +5,10 @@ description: Review the exact frozen WorkItem Candidate or Task-final Integratio
5
5
 
6
6
  # Yui Reviewer
7
7
 
8
+ Follow `yui-runtime` first and load the exact current Run Context Pack. The
9
+ ReviewRound, Candidate, workspace, and Snapshot digest returned there are the
10
+ only review scope; fail closed on any mismatch.
11
+
8
12
  Review only the exact frozen scope and ReviewRound assigned by the Leader; never
9
13
  reinterpret its scope:
10
14
 
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: yui-runtime
3
+ description: Load and use the exact authorized context for every Yui-managed Leader, Worker, Reviewer, Operator, or custom Role Run, and complete that Run through its bounded control-plane protocol.
4
+ ---
5
+
6
+ # Yui Runtime
7
+
8
+ Treat the Session Manifest and Run Bootstrap Envelope as pointers, never as the
9
+ Task brief. Do not infer Task facts from the launch command, process list,
10
+ workspace layout, native transcript, or an earlier Run.
11
+
12
+ For every managed Task Run:
13
+
14
+ 1. Read the exact Run identity from the newest Bootstrap Envelope.
15
+ 2. Before acting, load its authorized pack with the exact Session CLI:
16
+
17
+ ```sh
18
+ "$YUI_SESSION_CLI" task run context "$YUI_TASK_ID/<run-id>" --json
19
+ ```
20
+
21
+ 3. Verify that the returned Task, Run, Role, purpose, Snapshot digest, workspace,
22
+ and Adapter match the Envelope and Session Manifest. Stop and report a
23
+ context-load failure if the pack is missing, stale, unauthorized, malformed,
24
+ or mismatched. Never request an inline/full-prompt fallback.
25
+ 4. Use pack summaries and pointers first. Expand only an authorized ref when
26
+ its full value is needed:
27
+
28
+ ```sh
29
+ "$YUI_SESSION_CLI" task run context expand "$YUI_TASK_ID/<run-id>" <ref-id> --mode full --json
30
+ ```
31
+
32
+ 5. On a later wake, request only the declared delta after the last pack cursor.
33
+ If no cursor is available, reload the exact pack; do not reconstruct state
34
+ from transcript memory.
35
+
36
+ The pack's authority view and writable Project IDs are hard boundaries. A
37
+ native subagent inherits the parent Run's refs and authority; it does not gain a
38
+ new Yui actor, Run, Session, or cross-Task read permission.
39
+
40
+ For a global Operator or custom GlobalRole Session, load the stable exact view
41
+ before routing or acting:
42
+
43
+ ```sh
44
+ "$YUI_SESSION_CLI" role context "$YUI_ROLE" --json
45
+ ```
46
+
47
+ Global context grants no Task implementation workspace. Read a Task only after
48
+ the Operator has routed to its public/task-authorized context command; never
49
+ invent a Task Run identity for a GlobalRole.
50
+
51
+ Provider acceptance, Context load, and workflow completion are separate facts.
52
+ Do not treat a live pane, process output, final response, or completed Provider
53
+ Turn as a Yui yield. For a managed Task Run, use the exact current Run's
54
+ supported checkpoint/yield command as the final control-plane action, then stop
55
+ immediately. If that direct command is denied or stale, report the blocker once
56
+ and stop; do not wrap, retry, broaden permissions, or target another Run.
57
+
58
+ For a transient Provider retry Envelope, continue the failed Turn in the same
59
+ native Session. Do not replay the original Assignment or reload unrelated Task
60
+ content. Process/child replacement does not by itself authorize a new Yui
61
+ generation or native conversation.