@zq-silk/yui 2.5.0 → 2.7.1

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 (104) hide show
  1. package/README.md +23 -0
  2. package/dist/cli/commandCatalog.js +31 -4
  3. package/dist/commands/globalRoleCommands.js +22 -2
  4. package/dist/commands/inboxCommands.js +352 -0
  5. package/dist/commands/roleSkillValidation.js +2 -0
  6. package/dist/commands/taskCommands.js +40 -7
  7. package/dist/commands/taskContextCommand.js +3 -2
  8. package/dist/context/currentSkills.js +48 -0
  9. package/dist/context/roleSessionContext.js +13 -16
  10. package/dist/context/runContextPack.js +2 -0
  11. package/dist/context/runInputContract.js +1 -1
  12. package/dist/context/sessionBootstrapManifest.js +27 -4
  13. package/dist/context/skillCatalog.js +2 -3
  14. package/dist/context/taskContext.js +9 -4
  15. package/dist/controlPlaneCli.js +18 -1
  16. package/dist/controller/controller.js +3 -2
  17. package/dist/controller/fileSchedulerStoreAdapter.js +19 -4
  18. package/dist/controller/globalInputDelivery.js +73 -15
  19. package/dist/controller/globalRuntimeAttention.js +1 -1
  20. package/dist/controller/operatorNotification.js +6 -1
  21. package/dist/controller/providerRetryDelivery.js +3 -5
  22. package/dist/controller/runtime.js +2 -2
  23. package/dist/doctor/doctor.js +25 -2
  24. package/dist/executor/effectiveLaunch.js +2 -2
  25. package/dist/executor/fileRoleLaunchPlanner.js +4 -6
  26. package/dist/interaction/notificationPresentation.js +2 -2
  27. package/dist/interaction/operatorPresentation.js +1 -1
  28. package/dist/kernel/builtinCapabilities.js +2 -1
  29. package/dist/message/globalInputBacklog.js +69 -0
  30. package/dist/message/globalMessageQuery.js +1 -0
  31. package/dist/message/inboxReceipt.js +32 -0
  32. package/dist/message/message.js +6 -1
  33. package/dist/message/messageContinuation.js +34 -13
  34. package/dist/message/taskInboxOwnership.js +21 -0
  35. package/dist/nativeAgent/session/backends.js +1 -1
  36. package/dist/observability/runtimeIdentity.js +1 -1
  37. package/dist/resources/sqliteResourceRegistry.js +1 -1
  38. package/dist/runtime/agentHost.js +105 -16
  39. package/dist/runtime/builtinAgentErrorMappers.js +9 -5
  40. package/dist/runtime/providerRetry.js +38 -0
  41. package/dist/runtime/taskSessions.js +240 -0
  42. package/dist/scheduler/operatorEvent.js +49 -0
  43. package/dist/scheduler/operatorInputNotificationProcessor.js +5 -4
  44. package/dist/scheduler/taskExecutionProjection.js +78 -6
  45. package/dist/scheduler/taskWake.js +5 -0
  46. package/dist/sqlite/driver.js +108 -0
  47. package/dist/storage/sqliteSchema.js +11 -1
  48. package/dist/storage/sqliteStore.js +60 -1
  49. package/dist/storage/storageSchema.js +1 -1
  50. package/dist/storage/storageVersions.js +1 -1
  51. package/dist/storage/storeRpc.js +1 -0
  52. package/dist/storage/upgrade/upgradeOrchestrator.js +1 -1
  53. package/dist/task/completionReadiness.js +11 -4
  54. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  55. package/dist/web/assets/assetManifest.js +2 -0
  56. package/dist/web/assets/client/app/taskView.js +1 -0
  57. package/dist/web/assets/client/domain/records.js +70 -2
  58. package/dist/web/assets/client/domain/runs.js +3 -2
  59. package/dist/web/assets/client/domain/vocab.js +1 -1
  60. package/dist/web/assets/client/i18n/en.js +17 -1
  61. package/dist/web/assets/client/i18n/zh-CN.js +17 -1
  62. package/dist/web/assets/client/lib/api.js +4 -1
  63. package/dist/web/assets/client/lib/theme.js +31 -10
  64. package/dist/web/assets/client/views/task/actions.js +38 -21
  65. package/dist/web/assets/client/views/task/observation.js +12 -3
  66. package/dist/web/assets/client/views/task/overview.js +7 -40
  67. package/dist/web/assets/client/views/task/runtime.js +71 -34
  68. package/dist/web/assets/shared/icons.js +2 -0
  69. package/dist/web/assets/shell/index.js +1 -0
  70. package/dist/web/assets/shell/settings.js +1 -0
  71. package/dist/web/assets/styles/views/task.js +14 -0
  72. package/dist/web/webServer.js +12 -0
  73. package/dist/web/webSessions.js +2 -165
  74. package/dist/web/webSnapshot.js +9 -5
  75. package/dist/web/webTaskActivity.js +54 -0
  76. package/dist/web/webTaskSurface.js +1 -0
  77. package/docs/managed-turn-and-session-runtime.md +81 -5
  78. package/docs/managed-turn-and-session-runtime.zh-CN.md +51 -3
  79. package/docs/observability/README.md +25 -1
  80. package/docs/observability/README.zh-CN.md +18 -1
  81. package/docs/provider-retry.md +11 -0
  82. package/docs/release-workflow.md +74 -2
  83. package/docs/release-workflow.zh-CN.md +56 -1
  84. package/docs/storage-baseline.md +17 -7
  85. package/docs/storage-baseline.zh-CN.md +11 -5
  86. package/docs/task-delivery.md +17 -8
  87. package/docs/task-delivery.zh-CN.md +11 -5
  88. package/docs/testing/verification-levels.md +22 -4
  89. package/docs/testing/verification-levels.zh-CN.md +8 -1
  90. package/i18n/README.zh-CN.md +19 -0
  91. package/package.json +5 -2
  92. package/skills/yui-leader/SKILL.md +28 -11
  93. package/skills/yui-leader/VERSION +1 -0
  94. package/skills/yui-operator/SKILL.md +10 -2
  95. package/skills/yui-operator/VERSION +1 -0
  96. package/skills/yui-operator/references/task-delivery.md +2 -0
  97. package/skills/yui-reviewer/SKILL.md +8 -1
  98. package/skills/yui-reviewer/VERSION +1 -0
  99. package/skills/yui-runtime/SKILL.md +94 -11
  100. package/skills/yui-runtime/VERSION +1 -0
  101. package/skills/yui-runtime/references/publication.md +16 -5
  102. package/skills/yui-runtime/references/recovery.md +9 -0
  103. package/skills/yui-worker/SKILL.md +8 -1
  104. package/skills/yui-worker/VERSION +1 -0
@@ -1,16 +1,18 @@
1
- import { sendAgentHostRunControl, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime/agentHost.js";
1
+ import { sendAgentHostRunControl, reconcileAgentHostTurn, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime/agentHost.js";
2
2
  import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
3
3
  import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
4
4
  import { markGlobalRoleMessageNotDelivered } from "../message/message.js";
5
5
  import { providerRetryPending } from "../runtime/providerRetry.js";
6
6
  import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
7
7
  import { globalMessagePresentation } from "../interaction/notificationPresentation.js";
8
+ import { recordGlobalRuntimeAttention } from "./globalRuntimeAttention.js";
8
9
  /** Messages are the only queue. Host's atomic begin arbitrates native writers. */
9
- export async function deliverGlobalInputs(home, store, ensure, onError) {
10
+ export async function deliverGlobalInputs(home, store, ensure, onError, options = {}) {
10
11
  for (const role of store.listGlobalRoles()) {
11
12
  try {
12
- let pending = store.listGlobalRoleMessages(role.name).filter(message => message.delivery === undefined && message.notDelivered === undefined
13
- && (message.inputControl?.action === "queue" || message.interruptThen !== undefined));
13
+ if (store.queryGlobalRoleMessages(role.name, { handling: true, limit: 0 }).count > 0)
14
+ continue;
15
+ let pending = store.queryGlobalRoleMessages(role.name, { pending: true, limit: 64 }).messages;
14
16
  if (pending.length === 0 || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name }))))
15
17
  continue;
16
18
  let sessions = store.getGlobalRoleSessionSet(role.name);
@@ -20,15 +22,68 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
20
22
  continue;
21
23
  if (sessions?.providerBinding?.retry !== undefined && !providerRetryPending(sessions.providerBinding)) {
22
24
  store.transaction(tx => settleGlobalRetryInput(tx, role.name, tx.getGlobalRoleSessionSet(role.name)?.providerBinding, new Date()));
23
- pending = store.listGlobalRoleMessages(role.name).filter(message => message.delivery === undefined && message.notDelivered === undefined
24
- && (message.inputControl?.action === "queue" || message.interruptThen !== undefined));
25
+ pending = store.queryGlobalRoleMessages(role.name, { pending: true, limit: 64 }).messages;
25
26
  if (pending.length === 0)
26
27
  continue;
27
28
  }
28
29
  if (providerRetryPending(sessions?.providerBinding))
29
30
  continue;
30
- const current = sessions?.providerBinding?.run;
31
- const obsolete = pending.filter(message => {
31
+ let current = sessions?.providerBinding?.run;
32
+ const bindingBefore = sessions?.providerBinding;
33
+ if (options.reconcile && current != null
34
+ && ["submitting", "accepted", "delivery-unknown"].includes(current.status)
35
+ && bindingBefore?.authority.owner === "controller" && bindingBefore.authority.holderId !== undefined) {
36
+ const ownedTurn = current;
37
+ // Only ask the original Host to republish evidence it already holds.
38
+ // Busy, unknown, unavailable and older Hosts never authorize replay,
39
+ // Session creation, native reads through another client, or timeout settlement.
40
+ let observed;
41
+ try {
42
+ observed = await reconcileAgentHostTurn({
43
+ home, scope: "global", roleName: role.name,
44
+ control: { protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "reconcile-turn",
45
+ nativeSessionId: currentProviderConversation(bindingBefore).conversationId,
46
+ attemptId: current.attemptId, nativeTurnId: current.nativeTurnId,
47
+ authority: { epoch: bindingBefore.authority.epoch, owner: "controller",
48
+ holderId: bindingBefore.authority.holderId } }
49
+ });
50
+ if (observed.outcome === "rejected" || ["idle", "failed", "exited"].includes(observed.snapshot.state)
51
+ && observed.reconciliation !== "terminal-republished") {
52
+ throw new Error(observed.failure?.detail ?? observed.snapshot.detail ?? "Host cannot reconcile this exact terminal.");
53
+ }
54
+ }
55
+ catch (error) {
56
+ store.transaction(tx => recordGlobalRuntimeAttention(tx, role.name, `terminal-reconcile:${currentProviderConversation(bindingBefore).conversationId}:${ownedTurn.attemptId}`, { attemptId: ownedTurn.attemptId, nativeTurnId: ownedTurn.nativeTurnId,
57
+ nativeSessionId: currentProviderConversation(bindingBefore).conversationId,
58
+ reason: "host-terminal-unobservable",
59
+ cause: observed === undefined ? "host-control-unavailable"
60
+ : observed.outcome === "rejected" ? "host-control-rejected" : "exact-terminal-not-retained",
61
+ hostState: observed?.snapshot.state ?? null,
62
+ endpointImplementation: observed?.snapshot.endpointImplementation ?? null,
63
+ eventDelivery: observed?.snapshot.eventDelivery ?? null,
64
+ // Endpoint generation is not a loaded Host/package version.
65
+ inputKind: ownedTurn.attemptId.startsWith("direct:") ? "direct" : "managed",
66
+ detail: error instanceof Error ? error.message : String(error),
67
+ recovery: "Inspect the original Host, Session, input and raw Inbox. Controller restart preserves live Hosts; it does not upgrade their loaded observation code. Restore the supported observation path, or explicitly stop/replace with exact native quiescence evidence. Missing eventDelivery means unreported, not healthy. Do not resend unknown input.",
68
+ confirmed: ownedTurn.status, unknown: "Native terminal and remaining writer ownership." }, options.now ?? new Date()));
69
+ continue;
70
+ }
71
+ sessions = store.getGlobalRoleSessionSet(role.name);
72
+ current = sessions?.providerBinding?.run;
73
+ if (sessions?.sessions[sessions.activeAgentId]?.status !== "active"
74
+ || providerRetryPending(sessions?.providerBinding)
75
+ || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name }))))
76
+ continue;
77
+ pending = store.queryGlobalRoleMessages(role.name, { pending: true, limit: 64 }).messages;
78
+ if (pending.length === 0)
79
+ continue;
80
+ }
81
+ // Interrupt-then reserves the next legal slot even behind a large queue.
82
+ const reservedPage = store.queryGlobalRoleMessages(role.name, {
83
+ pending: true, interruptThen: true, limit: 64
84
+ });
85
+ const reserved = reservedPage.messages;
86
+ const obsolete = reserved.filter(message => {
32
87
  const claim = message.interruptThen;
33
88
  return claim !== undefined && (sessions?.activeAgentId !== claim.targetAgentId
34
89
  || sessions.sessions[claim.targetAgentId]?.nativeSessionId !== claim.targetNativeSessionId
@@ -37,15 +92,18 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
37
92
  });
38
93
  for (const message of obsolete)
39
94
  store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(message, "native-session-or-authority-changed", new Date()));
40
- pending = pending.filter(message => !obsolete.includes(message));
95
+ const obsoleteIds = new Set(obsolete.map(message => message.id));
96
+ pending = pending.filter(message => !obsoleteIds.has(message.id));
41
97
  if (pending.length === 0)
42
98
  continue;
99
+ if (reservedPage.count > reserved.length && obsolete.length === reserved.length)
100
+ continue;
43
101
  if (current != null && ["submitting", "accepted", "delivery-unknown"].includes(current.status))
44
102
  continue;
45
103
  const matchesTarget = (message) => message.deliveryTarget === undefined
46
104
  || sessions?.activeAgentId === message.deliveryTarget.agentId
47
105
  && sessions.sessions[message.deliveryTarget.agentId]?.nativeSessionId === message.deliveryTarget.nativeSessionId;
48
- const claims = pending.filter(message => message.interruptThen !== undefined);
106
+ const claims = reserved.filter(message => !obsolete.includes(message));
49
107
  let next;
50
108
  if (claims.length > 0) {
51
109
  next = claims.find(message => {
@@ -76,7 +134,7 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
76
134
  return false;
77
135
  });
78
136
  }
79
- if (next === undefined)
137
+ if (next === undefined || next.inbox !== undefined)
80
138
  continue;
81
139
  const attemptId = `global-input:${role.name}/${next.id}`;
82
140
  if (current?.attemptId === attemptId)
@@ -96,8 +154,8 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
96
154
  if (binding == null || binding.authority.owner !== "controller")
97
155
  continue;
98
156
  const nativeSessionId = currentProviderConversation(binding).conversationId;
99
- const persisted = store.listGlobalRoleMessages(role.name).find(message => message.id === next.id);
100
- if (persisted === undefined || JSON.stringify(persisted) !== JSON.stringify(next))
157
+ const persisted = store.queryGlobalRoleMessages(role.name, { id: next.id, limit: 1 }).messages[0];
158
+ if (persisted === undefined || persisted.inbox !== undefined || JSON.stringify(persisted) !== JSON.stringify(next))
101
159
  continue;
102
160
  const claim = next.interruptThen;
103
161
  if (!matchesTarget(next) || claim !== undefined
@@ -125,8 +183,8 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
125
183
  });
126
184
  if (result.outcome === "rejected") {
127
185
  store.transaction(tx => {
128
- const latest = tx.listGlobalRoleMessages(role.name).find(message => message.id === next.id);
129
- if (latest === undefined || latest.delivery !== undefined || latest.notDelivered !== undefined)
186
+ const latest = tx.queryGlobalRoleMessages(role.name, { id: next.id, limit: 1 }).messages[0];
187
+ if (latest === undefined || latest.inbox !== undefined || latest.delivery !== undefined || latest.notDelivered !== undefined)
130
188
  return;
131
189
  const turn = tx.getGlobalRoleSessionSet(role.name)?.providerBinding?.run;
132
190
  if (providerRetryPending(tx.getGlobalRoleSessionSet(role.name)?.providerBinding))
@@ -19,7 +19,7 @@ export function recordGlobalRuntimeAttention(store, roleName, identity, facts, n
19
19
  // diagnostic readable; never create a Role or launch a substitute Agent.
20
20
  const recipient = store.getGlobalRole("operator") === null ? roleName : "operator";
21
21
  const requestId = `runtime-attention:${createHash("sha256").update(`${roleName}\0${identity}`).digest("hex")}`;
22
- if (store.listGlobalRoleMessages(recipient).some(message => message.inputControl?.requestId === requestId))
22
+ if (store.queryGlobalRoleMessages(recipient, { requestId, limit: 1 }).messages.length > 0)
23
23
  return;
24
24
  const message = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), recipient, [
25
25
  `Global Role ${roleName} needs attention. No recovery or new authority is implied.`,
@@ -8,7 +8,7 @@ import { createGlobalRoleMessage } from "../message/message.js";
8
8
  export function queueOperatorNotification(store, input, now = new Date()) {
9
9
  return store.transaction(tx => {
10
10
  const requestId = `operator-notice:${encodeURIComponent(input.receiptId)}`;
11
- const previous = tx.listGlobalRoleMessages("operator").find(entry => (entry.inputControl ?? entry.interruptThen?.reusedInput)?.requestId === requestId);
11
+ const previous = tx.queryGlobalRoleMessages("operator", { requestId, limit: 1 }).messages[0];
12
12
  const mailbox = tx.getWorkMailbox({ kind: "operator" });
13
13
  const claimed = mailbox?.processing?.batchId === input.batchId;
14
14
  if (previous !== undefined) {
@@ -18,6 +18,11 @@ export function queueOperatorNotification(store, input, now = new Date()) {
18
18
  }
19
19
  if (!claimed)
20
20
  throw new Error(`Operator notification claim changed: ${input.batchId}`);
21
+ // At most one unsent engineering notice. New facts coalesce in the existing
22
+ // mailbox while busy, without rewriting originals or overtaking user input.
23
+ if (tx.queryGlobalRoleMessages("operator", { pending: true, operatorNotice: true, limit: 1 }).count > 0) {
24
+ return { status: "deferred" };
25
+ }
21
26
  const sessions = tx.getGlobalRoleSessionSet("operator");
22
27
  const session = sessions?.sessions[sessions.activeAgentId];
23
28
  if (session?.status !== "active" || sessions?.providerBinding == null)
@@ -1,6 +1,6 @@
1
1
  import { performance } from "node:perf_hooks";
2
2
  import { runTaskCommand } from "../commands/taskCommands.js";
3
- import { enqueueWork } from "../coordination/workMailboxQueue.js";
3
+ import { routeRoleEvent } from "../scheduler/operatorEvent.js";
4
4
  import { createTaskEvent } from "../event/taskEvent.js";
5
5
  import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
6
6
  import { redactAgentErrorText } from "../runtime/agentError.js";
@@ -57,8 +57,7 @@ export function createProviderRetryHooks(home, store, ports = {}) {
57
57
  roleName: owner.roleName, chainId: binding.retry.chainId, reason
58
58
  }, new Date(at));
59
59
  store.saveEvent(owner.taskId, event);
60
- enqueueWork(store, owner.roleName === "leader" ? { kind: "operator" }
61
- : { kind: "role", taskId: owner.taskId, roleName: "leader" }, "provider-retry-needs-attention", new Date(at), [{ type: "event", taskId: owner.taskId, id: event.id }]);
60
+ routeRoleEvent(store, event, owner.roleName, "provider-retry-needs-attention", new Date(at));
62
61
  }
63
62
  });
64
63
  };
@@ -142,8 +141,7 @@ export function createProviderRetryHooks(home, store, ports = {}) {
142
141
  roleName: owner.roleName, attemptId: retry.currentAttemptId, nativeSessionId: retry.nativeSessionId, detail
143
142
  }, new Date(at));
144
143
  store.saveEvent(owner.taskId, event);
145
- enqueueWork(store, owner.roleName === "leader" ? { kind: "operator" }
146
- : { kind: "role", taskId: owner.taskId, roleName: "leader" }, "provider-retry-needs-attention", new Date(at), [{ type: "event", taskId: owner.taskId, id: event.id }]);
144
+ routeRoleEvent(store, event, owner.roleName, "provider-retry-needs-attention", new Date(at));
147
145
  }
148
146
  });
149
147
  }
@@ -560,9 +560,9 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
560
560
  }
561
561
  }),
562
562
  jobSupervisor,
563
- globalInputDelivery: () => deliverGlobalInputs(home, store, async (roleName) => {
563
+ globalInputDelivery: (deliveryOptions) => deliverGlobalInputs(home, store, async (roleName) => {
564
564
  await lifecycleDispatcher("runtime.ensure-role-session", { scope: "global", roleName });
565
- }, options.onError ?? (() => undefined)),
565
+ }, options.onError ?? (() => undefined), deliveryOptions),
566
566
  jobControl,
567
567
  capabilityDispatcher: createCapabilityDispatcher(kernel.capabilities),
568
568
  ...(continuationReconciler === undefined ? {} : { continuationReconciler }),
@@ -1,6 +1,6 @@
1
1
  import { accessSync, constants, existsSync, lstatSync, realpathSync } from "node:fs";
2
2
  import { isAbsolute, join } from "node:path";
3
- import Database from "better-sqlite3";
3
+ import Database, { selectSqliteDriver } from "../sqlite/driver.js";
4
4
  import { inspectPty } from "./ptyProbe.js";
5
5
  import { gitVersionSupport } from "../repository/gitCompatibility.js";
6
6
  import { resolveExecutable } from "../external/pinnedCommandRunner.js";
@@ -31,6 +31,7 @@ import { CommandExecutionError } from "../tmux/commandExecutor.js";
31
31
  * and never block an update.
32
32
  */
33
33
  export const STORAGE_DOCTOR_CHECK_NAMES = Object.freeze([
34
+ "sqlite driver",
34
35
  "storage schema",
35
36
  "storage compatibility",
36
37
  "storage state"
@@ -38,7 +39,7 @@ export const STORAGE_DOCTOR_CHECK_NAMES = Object.freeze([
38
39
  /**
39
40
  * Classify the storage checks into a machine-readable health verdict. This is the
40
41
  * canonical, parseable signal the update post-verify consumes (P1-3): storage is
41
- * healthy only when schema, compatibility, and state are all `ok`. Any
42
+ * healthy only when driver, schema, compatibility, and state are all `ok`. Any
42
43
  * `unsupported` (version mismatch / needs-new-version), `invalid` (corrupted /
43
44
  * unreadable), or `missing` (uninitialized) storage check is blocking — even
44
45
  * and the machine-readable command exits non-zero.
@@ -119,6 +120,7 @@ export async function getDoctorChecks(env, executor) {
119
120
  async function inspectDoctor(env, executor) {
120
121
  const home = resolveYuiHome(env);
121
122
  const homeCheck = checkHome(home);
123
+ const driverCheck = checkSqliteDriver();
122
124
  const schema = readSchema(home);
123
125
  const compatibility = inspectCompatibility(home, homeCheck, schema);
124
126
  const schemaCheck = checkSchema(schema);
@@ -135,6 +137,7 @@ async function inspectDoctor(env, executor) {
135
137
  return {
136
138
  checks: [
137
139
  homeCheck,
140
+ driverCheck,
138
141
  schemaCheck,
139
142
  compatibility.check,
140
143
  storage.check,
@@ -148,6 +151,26 @@ async function inspectDoctor(env, executor) {
148
151
  review: review.report
149
152
  };
150
153
  }
154
+ export function checkSqliteDriver() {
155
+ try {
156
+ const selected = selectSqliteDriver();
157
+ return {
158
+ name: "sqlite driver",
159
+ status: "ok",
160
+ detail: `${selected.name}@${selected.version}; SQLite ${selected.sqliteVersion}; `
161
+ + (selected.fallbackReason === undefined
162
+ ? "no fallback"
163
+ : `fallback: ${selected.fallbackReason}`)
164
+ + ". Isolated native connection probe; does not validate Home data."
165
+ };
166
+ }
167
+ catch (error) {
168
+ return {
169
+ name: "sqlite driver", status: "invalid",
170
+ detail: `Native driver probe failed (not a Home diagnosis): ${error instanceof Error ? error.message : String(error)}`
171
+ };
172
+ }
173
+ }
151
174
  /** Query the authenticated Controller endpoint without starting it. */
152
175
  export async function checkController(home, probe = callController) {
153
176
  try {
@@ -2,7 +2,7 @@ import { isDeepStrictEqual } from "node:util";
2
2
  import { validateExecutionEnvironmentSnapshot } from "../resources/projectResource.js";
3
3
  import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
4
4
  import { resolveAgentAdapter } from "./agentAdapter.js";
5
- import { compileRoleSessionContext, roleSessionKind } from "../context/roleSessionContext.js";
5
+ import { loadYuiSkillContexts, roleSessionKind, roleSkillIds } from "../context/roleSessionContext.js";
6
6
  import { validateSkillPackageRef } from "../context/skillPackage.js";
7
7
  import { isAgentAdapterId } from "../agent/adapterCatalog.js";
8
8
  import { adapterIdForExecutionComponent, isAgentExecutionComponentId, resolveAgentExecutionComponent } from "../agent/executionComponents.js";
@@ -24,7 +24,7 @@ export function resolveEffectiveLaunch(input) {
24
24
  const continuing = existing?.status === "active";
25
25
  const skillPackages = continuing ? existing.effective.skillPackages
26
26
  : home === undefined ? undefined
27
- : compileRoleSessionContext(home, input.role, "taskId" in input.role ? { scope: "task", taskId: input.role.taskId } : { scope: "global" }, { purpose: input.purpose }).skills.map(skill => skill.package);
27
+ : loadYuiSkillContexts(home, roleSkillIds(input.role, roleSessionKind(input.role, "taskId" in input.role ? { scope: "task", taskId: input.role.taskId } : { scope: "global" }, input.purpose))).map(skill => skill.package);
28
28
  return snapshotFromConfig({
29
29
  executionAuthority: input.executionAuthority ?? (input.purpose === "planning" ? "planning" : "delivery"),
30
30
  sourceDesiredRevision: input.role.launchRevision,
@@ -337,12 +337,10 @@ export class FileRoleLaunchPlanner {
337
337
  Object.assign(launchEnvironment, runtimeIsolation === undefined
338
338
  ? {}
339
339
  : taskRuntimeIsolationEnvironment(runtimeIsolation));
340
- const skillPackages = input.mode === "resume"
341
- ? existingSession?.effective.skillPackages : effective.skillPackages;
342
- if (skillPackages === undefined) {
343
- throw new Error("This launch has no frozen Skill package evidence (pre-1.5 or externally recorded Session). Create a new Session from current Role configuration; historical Skill versions cannot be reconstructed.");
344
- }
345
- const baseSessionContext = compileRoleSessionContext(this.home, launchRole, owner, { ...sessionPolicy, skillPackages });
340
+ // Frozen packages describe historical materialization, not Session identity
341
+ // or current instruction availability. Never require or reconstruct them
342
+ // when reconnecting a valid old Session.
343
+ const baseSessionContext = compileRoleSessionContext(this.home, launchRole, owner, sessionPolicy);
346
344
  const bootstrap = materializeSessionBootstrap({
347
345
  yuiHome: this.home,
348
346
  role: launchRole,
@@ -69,7 +69,7 @@ export function taskNotificationPresentation(wake, store) {
69
69
  ...(messages.length > 4 ? [`${messages.length - 4} more messages in the fixed wake.`] : []),
70
70
  `Refresh: yui task context ${wake.taskId} --json`,
71
71
  `Read inputs/results: yui task wake show ${wake.taskId} ${wake.id} --json`,
72
- "Apply yui-leader (from the current Manifest); read relevant originals in full and advance authorized work."
72
+ "Check current Skills each turn: yui session skills --json. Apply current yui-leader; read relevant originals in full and advance authorized work."
73
73
  ].join("\n");
74
74
  }
75
75
  export function globalMessagePresentation(roleName, messageId, message) {
@@ -80,6 +80,6 @@ export function globalMessagePresentation(roleName, messageId, message) {
80
80
  ...(summary === undefined ? [] : [`Author summary (message-body): ${JSON.stringify(summary)}`]),
81
81
  "Refresh Session Context using the current Manifest's loadCommand.",
82
82
  `Read original: yui role message show ${roleName} ${messageId} --json`,
83
- `Apply ${roleName === "operator" ? "yui-operator" : "yui-runtime"} from the current Manifest within existing authority.`
83
+ `The Global loadCommand checks current Skill versions each turn. Apply current ${roleName === "operator" ? "yui-operator" : "yui-runtime"} within existing authority.`
84
84
  ].join("\n");
85
85
  }
@@ -10,7 +10,7 @@ export function createOperatorBatchPresentation(batchId, items) {
10
10
  receiptId: `operator-batch:${batchId}`,
11
11
  text: [
12
12
  "[Yui updates]",
13
- "Apply yui-operator from the current Manifest: inspect these originals and present material changes or decisions.",
13
+ "Check current Skills each turn via the Global Manifest's loadCommand. Apply current yui-operator; inspect these originals and present material changes or decisions.",
14
14
  ...items.flatMap(renderItem)
15
15
  ].join("\n")
16
16
  };
@@ -47,7 +47,7 @@ const definitions = [
47
47
  name: "context.list", summary: "Discover one authorized record family with bounded summaries and exact references.",
48
48
  effect: "query", requiredPermissions: ["task:read"], source: "listTaskContext",
49
49
  inputSchema: object({ taskId: text, store: text, cursor: text, limit: { type: "integer" },
50
- status: text, after: text, workItemId: text }, ["taskId", "store"]),
50
+ view: { type: "string", enum: ["current", "all"] }, status: text, after: text, workItemId: text }, ["taskId", "store"]),
51
51
  outputSchema: { type: "object", required: ["items", "total", "complete", "nextCursor"] }
52
52
  },
53
53
  {
@@ -304,6 +304,7 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
304
304
  }, callerEnvironment(caller));
305
305
  if (name === "context.list")
306
306
  return listTaskContext(store, taskId, params.store, {
307
+ view: params.view,
307
308
  cursor: params.cursor, limit: params.limit,
308
309
  status: params.status, after: params.after,
309
310
  workItemId: params.workItemId
@@ -0,0 +1,69 @@
1
+ import { providerRetryPending } from "../runtime/providerRetry.js";
2
+ import { hasRuntimeCleanupObligation } from "../runtime/lifecycleReservation.js";
3
+ /** Bounded, derived diagnostics only; never an acknowledgement or recovery authority. */
4
+ export function globalInputBacklog(pending, sessions, now = new Date(), mailboxes = {}) {
5
+ const first = pending[0];
6
+ const run = sessions?.providerBinding?.run;
7
+ const session = sessions?.sessions[sessions.activeAgentId];
8
+ const authority = sessions?.providerBinding?.authority;
9
+ const batches = [mailboxes.attention?.pending, mailboxes.attention?.processing?.batch]
10
+ .filter(batch => batch != null);
11
+ const coalescedFactCount = batches.reduce((count, batch) => count + batch.requestCount, 0);
12
+ let reason = "ready";
13
+ let recovery = "The Controller delivers at the next legal opportunity; do not duplicate the input.";
14
+ if (first === undefined && coalescedFactCount === 0)
15
+ reason = "empty";
16
+ else if (hasRuntimeCleanupObligation(mailboxes.lifecycle ?? null)) {
17
+ reason = "lifecycle-cleanup";
18
+ recovery = "Complete the existing exact-owner stop/replacement; queued input cannot revive the Session.";
19
+ }
20
+ else if (session?.status !== "active") {
21
+ reason = session === undefined ? "session-unavailable" : "session-stopped";
22
+ recovery = "Inspect Session lifecycle. Explicit stop/replacement is not undone by background delivery.";
23
+ }
24
+ else if (authority?.owner !== "controller") {
25
+ reason = "external-or-unknown-writer";
26
+ recovery = "Resolve the current writer/authority explicitly; no background takeover is allowed.";
27
+ }
28
+ else if (providerRetryPending(sessions?.providerBinding)) {
29
+ reason = "provider-retry";
30
+ recovery = "The existing bounded Provider retry owns this input; inspect session retry before intervening.";
31
+ }
32
+ else if (run && ["submitting", "accepted", "delivery-unknown"].includes(run.status)) {
33
+ reason = run.status === "accepted" ? "in-flight-or-unobserved-terminal" : run.status;
34
+ recovery = "Age does not prove a stalled Turn. Periodic reconciliation asks only the original Host for exact retained terminal evidence. Inspect recent runtime-attention Messages if the Host is unobservable; never resend unknown input.";
35
+ }
36
+ else if (first?.inbox) {
37
+ reason = "agent-inbox-handling";
38
+ recovery = "Read the exact inbox original and prior handling claim; explicitly acknowledge after processing. No automatic replay or timeout release.";
39
+ }
40
+ else if (first?.control && first.control.outcome !== "rejected") {
41
+ reason = "message-acceptance-unresolved";
42
+ recovery = "Inspect this original Message's exact control receipt; do not replay or bypass uncertain input.";
43
+ }
44
+ else if (first?.interruptThen) {
45
+ reason = "reserved-interrupt-then";
46
+ recovery = "Only the original Session, authority and exact terminal release the reserved continuation.";
47
+ }
48
+ else if (first?.deliveryTarget && (first.deliveryTarget.agentId !== sessions?.activeAgentId
49
+ || first.deliveryTarget.nativeSessionId !== session.nativeSessionId)) {
50
+ reason = "stale-target";
51
+ recovery = "Unsubmitted stale messages are retained with a nondelivery reason; no cross-Session input copy.";
52
+ }
53
+ const oldestCreatedAt = [...pending.map(message => message.createdAt), ...batches.map(batch => batch.firstQueuedAt)]
54
+ .reduce((oldest, at) => oldest === null || at < oldest ? at : oldest, null);
55
+ return {
56
+ pendingCount: pending.length, coalescedFactCount, oldestCreatedAt,
57
+ oldestAgeMs: oldestCreatedAt === null ? null : Math.max(0, now.getTime() - Date.parse(oldestCreatedAt)),
58
+ reason, recovery,
59
+ target: session === undefined ? null : {
60
+ agentId: session.agentId, nativeSessionId: session.nativeSessionId,
61
+ authority: authority ?? null, attemptId: run?.attemptId ?? null, nativeTurnId: run?.nativeTurnId ?? null
62
+ },
63
+ messages: pending.slice(0, 8).map(message => ({
64
+ id: message.id, createdAt: message.createdAt, deliveryTarget: message.deliveryTarget ?? null,
65
+ control: message.control ?? null, interruptThen: message.interruptThen ?? null
66
+ })),
67
+ omittedMessages: Math.max(0, pending.length - 8)
68
+ };
69
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import { requireKnownFields, requireText, requireTimestamp } from "../domain/validation.js";
2
+ import { isDeepStrictEqual } from "node:util";
3
+ export function validateInboxReceipt(receipt) {
4
+ if (receipt === undefined)
5
+ return;
6
+ requireKnownFields(receipt, ["claim", "ack"], "Inbox receipt");
7
+ for (const [kind, entry] of Object.entries(receipt)) {
8
+ requireKnownFields(entry, kind === "claim" ? ["requestId", "actor", "at"]
9
+ : ["requestId", "actor", "at", "reason"], "Inbox action");
10
+ requireText(entry.requestId, "Inbox request id");
11
+ requireTimestamp(entry.at, "Inbox timestamp");
12
+ const actor = entry.actor;
13
+ requireKnownFields(actor, ["scope", "taskId", "roleName", "agentId", "nativeSessionId", "runId"], "Inbox actor");
14
+ if (!["task", "global"].includes(actor.scope)
15
+ || (actor.scope === "task") !== (actor.taskId !== undefined))
16
+ throw new Error("Inbox actor scope is invalid.");
17
+ for (const value of Object.values(actor))
18
+ requireText(value, "Inbox actor identity");
19
+ if (kind === "ack")
20
+ requireText(entry.reason, "Inbox disposition");
21
+ }
22
+ if (!receipt.claim)
23
+ throw new Error("Inbox receipt requires its original claim.");
24
+ }
25
+ /** A stale automatic writer cannot erase an active claim between its read and
26
+ * Host admission. The original claim and any ack are immutable audit facts. */
27
+ export function assertInboxReceiptPreserved(previous, next) {
28
+ if (previous !== undefined && (!isDeepStrictEqual(previous.claim, next?.claim)
29
+ || previous.ack !== undefined && !isDeepStrictEqual(previous.ack, next?.ack))) {
30
+ throw new Error("Inbox handling ownership changed; reread the original instead of overwriting its receipt.");
31
+ }
32
+ }
@@ -1,5 +1,6 @@
1
1
  import { validateTaskRecordReference } from "../task/taskRecordReference.js";
2
2
  import { requireKnownFields } from "../domain/validation.js";
3
+ import { validateInboxReceipt } from "./inboxReceipt.js";
3
4
  export const TASK_MESSAGE_KINDS = ["user", "operator", "role-result", "system"];
4
5
  /**
5
6
  * How a user/operator submission asks its Task to react (task-32 Requirement A).
@@ -207,8 +208,9 @@ export function validateTaskMessage(message) {
207
208
  throw new Error("Task Message must use schemaVersion 1.");
208
209
  requireKnownFields(message, [
209
210
  "schemaVersion", "id", "taskId", "kind", "author", "body", "intent", "submissionKey", "submissionReceipt",
210
- "inputControl", "interruptThen", "control", "runId", "resultRef", "workItemId", "recipient", "continuation", "handovers", "createdAt"
211
+ "inputControl", "interruptThen", "control", "runId", "resultRef", "workItemId", "recipient", "continuation", "handovers", "createdAt", "inbox"
211
212
  ], "Task Message");
213
+ validateInboxReceipt(message.inbox);
212
214
  validateTaskRecordReference({ taskId: message.taskId, localId: message.id }, "message");
213
215
  requireText(message.body, "Message body");
214
216
  validateKindAndAuthor(message.kind, message.author);
@@ -394,6 +396,7 @@ export function createGlobalRoleMessage(id, roleName, body, kind, author, now, c
394
396
  return message;
395
397
  }
396
398
  export function validateGlobalRoleMessage(message) {
399
+ validateInboxReceipt(message.inbox);
397
400
  if (message.schemaVersion !== 1)
398
401
  throw new Error("Global Role Message must use schemaVersion 1.");
399
402
  validateGlobalRoleMessageId(message.id);
@@ -517,6 +520,8 @@ export function markGlobalRoleMessageDelivered(message, now, via = "provider") {
517
520
  * keeps resolving to this same Message and never creates a second input.
518
521
  */
519
522
  export function claimGlobalRoleMessageInterruptThen(message, claim) {
523
+ if (message.inbox !== undefined)
524
+ throw new Error("Agent inbox handling already owns this Message; it cannot be replayed through interrupt-then.");
520
525
  const { inputControl, notDelivered: _priorNondelivery, ...rest } = message;
521
526
  const claimed = {
522
527
  ...rest,
@@ -10,6 +10,7 @@ import { validateExactRunReviewRound } from "../lifecycle/exactRunTerminalizatio
10
10
  import { sourceRunContextValue } from "../context/sourceRunContext.js";
11
11
  import { requireManagedTaskCaller } from "../runtime/managedCaller.js";
12
12
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
13
+ import { isTaskRecordRetired, operationalTaskRecords } from "../task/taskRecordRetirement.js";
13
14
  export function resolveMessageRecipient(store, taskId, roleName, scope) {
14
15
  if (scope.reviewRoundId !== undefined) {
15
16
  const round = store.getReviewRound(taskId, scope.reviewRoundId);
@@ -28,8 +29,11 @@ export function resolveMessageRecipient(store, taskId, roleName, scope) {
28
29
  throw new Error("No existing Dispatch owns this Role and work scope.");
29
30
  return { roleName, ownerRunId: previous.id, ...scope };
30
31
  }
31
- /** Bounded diagnosis, not automatic ownership repair. */
32
- export function messageContinuationBlocker(store, message) {
32
+ /** Current Assignment authority, independent of eligibility to start a new Run.
33
+ * A Reviewer still owns its frozen clarification when Task main advances. */
34
+ export function messageAssignmentBlocker(store, message) {
35
+ if (isTaskRecordRetired(store.listEvents(message.taskId), "message", message.id))
36
+ return "message-retired";
33
37
  const target = message.recipient;
34
38
  if (target?.ownerRunId === undefined)
35
39
  return "recipient-assignment-missing";
@@ -78,22 +82,29 @@ export function messageContinuationBlocker(store, message) {
78
82
  && latest.inputs[0]?.input.source.channel !== "message-continuation")
79
83
  return "assignment-changed";
80
84
  if (target.reviewRoundId !== undefined) {
81
- const round = store.getReviewRound(message.taskId, target.reviewRoundId);
82
- if (round === null)
83
- return "review-round-missing";
85
+ const validation = validateExactRunReviewRound(store, latest ?? owner, { allowTerminal: true });
86
+ if (validation.disposition !== "applied")
87
+ return validation.reason ?? "review-candidate-stale";
88
+ }
89
+ return undefined;
90
+ }
91
+ /** Starting another Run additionally requires the reviewed candidate to remain current. */
92
+ export function messageContinuationBlocker(store, message) {
93
+ const invalid = messageAssignmentBlocker(store, message);
94
+ if (invalid !== undefined)
95
+ return invalid;
96
+ const reviewRoundId = message.recipient?.reviewRoundId;
97
+ if (reviewRoundId !== undefined) {
98
+ const round = store.getReviewRound(message.taskId, reviewRoundId);
84
99
  if (round.scope === "task") {
100
+ const task = store.getTask(message.taskId);
85
101
  if (round.taskCandidate?.projects.some((project) => task.projectBindings.find((binding) => binding.projectId === project.projectId)?.currentCommit !== project.commit)) {
86
102
  return "review-candidate-stale";
87
103
  }
88
104
  }
89
- else {
90
- const item = store.getWorkItem(message.taskId, round.workItemId);
91
- if (item?.candidates.at(-1)?.id !== round.candidateId)
92
- return "review-candidate-stale";
105
+ else if (store.getWorkItem(message.taskId, round.workItemId)?.candidates.at(-1)?.id !== round.candidateId) {
106
+ return "review-candidate-stale";
93
107
  }
94
- const validation = validateExactRunReviewRound(store, latest ?? owner, { allowTerminal: true });
95
- if (validation.disposition !== "applied")
96
- return validation.reason ?? "review-candidate-stale";
97
108
  }
98
109
  return undefined;
99
110
  }
@@ -189,7 +200,11 @@ export function interruptThenTerminalState(store, message) {
189
200
  * remain the pending authority; Mailbox is only the existing scheduling hint.
190
201
  * Reserving inputs and creating the next Run is one atomic effect. */
191
202
  export function prepareMessageContinuations(store, taskId, now, roleName) {
192
- const pending = store.listMessages(taskId).filter((message) => message.recipient?.roleName === roleName && message.recipient.ownerRunId !== undefined
203
+ const messages = operationalTaskRecords(store.listMessages(taskId), store.listEvents(taskId), "message");
204
+ const claims = messages.filter(message => message.recipient?.roleName === roleName
205
+ && message.inbox !== undefined && message.inbox.ack === undefined);
206
+ const pending = messages.filter((message) => message.recipient?.roleName === roleName && message.recipient.ownerRunId !== undefined
207
+ && message.inbox === undefined
193
208
  && message.continuation?.runId === undefined
194
209
  // A steer targets the exact current native turn, never a queued next Run.
195
210
  // If its live attempt did not submit, the message stays saved for the
@@ -197,6 +212,12 @@ export function prepareMessageContinuations(store, taskId, now, roleName) {
197
212
  && message.inputControl?.action !== "steer");
198
213
  for (const message of pending) {
199
214
  const recipient = message.recipient;
215
+ // Retain ordering within the claimed Assignment, not across unrelated
216
+ // work owned by the same Role. Only ack or explicit retirement settles a
217
+ // claim's scheduling obligation; neither time nor a failed Run does.
218
+ if (claims.some(claim => claim.recipient?.workItemId === recipient.workItemId
219
+ && claim.recipient?.reviewRoundId === recipient.reviewRoundId))
220
+ continue;
200
221
  const blocker = messageContinuationBlocker(store, message);
201
222
  if (blocker !== undefined) {
202
223
  markNotDelivered(store, message, blocker);
@@ -0,0 +1,21 @@
1
+ import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
2
+ export function latestMessageUpdate(store, message) {
3
+ return store.listEvents(message.taskId).filter(event => event.type === "message.updated" && event.payload.messageId === message.id).at(-1);
4
+ }
5
+ /** Leader steer has no Assignment recipient; ordinary Task records are not
6
+ * private inbox messages. Queued Leader inputs remain fixed wake windows. */
7
+ export function isLeaderSteer(message) {
8
+ return message.recipient === undefined && message.inputControl?.action === "steer";
9
+ }
10
+ /** Derive ownership from original receipts, never from disposable mailboxes. */
11
+ export function taskMessageInboxReceipt(store, message) {
12
+ const revision = latestMessageUpdate(store, message);
13
+ return message.inbox ?? store.listTaskWakes(message.taskId).find(wake => wake.inbox !== undefined
14
+ && (revision === undefined || wake.refs?.some(ref => ref.type === "event" && ref.taskId === message.taskId && ref.id === revision.id))
15
+ && wake.refs?.some(ref => ref.type === "message" && ref.taskId === message.taskId && ref.id === message.id))?.inbox;
16
+ }
17
+ export function unfinishedLeaderInbox(store, taskId) {
18
+ return store.listTaskWakes(taskId).some(wake => wake.inbox !== undefined && wake.inbox.ack === undefined)
19
+ || operationalTaskRecords(store.listMessages(taskId), store.listEvents(taskId), "message").some(message => isLeaderSteer(message)
20
+ && message.inbox !== undefined && message.inbox.ack === undefined);
21
+ }