@zq-silk/yui 2.6.0 → 2.8.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 (126) hide show
  1. package/README.md +23 -0
  2. package/dist/cli/commandCatalog.js +34 -7
  3. package/dist/cli/updateCommand.js +3 -0
  4. package/dist/commands/configCommands.js +1 -0
  5. package/dist/commands/globalRoleCommands.js +62 -35
  6. package/dist/commands/inboxCommands.js +372 -0
  7. package/dist/commands/roleSkillValidation.js +2 -0
  8. package/dist/commands/taskCommands.js +39 -7
  9. package/dist/commands/taskRoleRuntimeStatus.js +2 -0
  10. package/dist/context/currentSkills.js +48 -0
  11. package/dist/context/roleSessionContext.js +13 -16
  12. package/dist/context/runContextPack.js +50 -3
  13. package/dist/context/runInputContract.js +1 -1
  14. package/dist/context/sessionBootstrapManifest.js +27 -4
  15. package/dist/context/skillCatalog.js +2 -3
  16. package/dist/context/taskContext.js +38 -12
  17. package/dist/controlPlaneCli.js +58 -7
  18. package/dist/controller/controller.js +8 -2
  19. package/dist/controller/fileSchedulerStoreAdapter.js +26 -6
  20. package/dist/controller/globalInputDelivery.js +86 -16
  21. package/dist/controller/globalRuntimeAttention.js +136 -6
  22. package/dist/controller/operatorNotification.js +6 -1
  23. package/dist/controller/providerRetryDelivery.js +3 -5
  24. package/dist/controller/runtime.js +6 -2
  25. package/dist/controller/structuredProviderObservation.js +51 -15
  26. package/dist/controller/taskHostTerminalRecovery.js +33 -0
  27. package/dist/doctor/doctor.js +25 -2
  28. package/dist/executor/effectiveLaunch.js +2 -2
  29. package/dist/executor/fileRoleLaunchPlanner.js +8 -15
  30. package/dist/interaction/notificationPresentation.js +2 -2
  31. package/dist/interaction/operatorPresentation.js +1 -1
  32. package/dist/message/globalInputBacklog.js +69 -0
  33. package/dist/message/globalMessageQuery.js +1 -0
  34. package/dist/message/inboxReceipt.js +62 -0
  35. package/dist/message/message.js +6 -1
  36. package/dist/message/messageContinuation.js +34 -13
  37. package/dist/message/taskInboxOwnership.js +43 -0
  38. package/dist/models/modelManagement.js +469 -0
  39. package/dist/models/modelManagementConfig.js +46 -0
  40. package/dist/models/modelManagementFiles.js +90 -0
  41. package/dist/models/modelManagementHttp.js +90 -0
  42. package/dist/models/modelManagementTypes.js +1 -0
  43. package/dist/nativeAgent/session/backends.js +1 -1
  44. package/dist/observability/runtimeIdentity.js +1 -1
  45. package/dist/output/boundedRead.js +43 -14
  46. package/dist/resources/sqliteResourceRegistry.js +1 -1
  47. package/dist/runtime/agentEndpointIdentity.js +8 -4
  48. package/dist/runtime/agentEndpointOwnership.js +5 -6
  49. package/dist/runtime/agentHost.js +165 -24
  50. package/dist/runtime/agentHostImplementation.js +18 -0
  51. package/dist/runtime/managedCaller.js +25 -1
  52. package/dist/runtime/managedIdentity.js +1 -1
  53. package/dist/runtime/providerRetry.js +19 -1
  54. package/dist/runtime/sessionCaller.js +53 -0
  55. package/dist/scheduler/operatorEvent.js +49 -0
  56. package/dist/scheduler/operatorInputNotificationProcessor.js +5 -4
  57. package/dist/scheduler/taskWake.js +5 -0
  58. package/dist/sqlite/driver.js +108 -0
  59. package/dist/storage/contextRecords.js +28 -0
  60. package/dist/storage/sqliteSchema.js +21 -1
  61. package/dist/storage/sqliteStore.js +64 -1
  62. package/dist/storage/storageSchema.js +1 -1
  63. package/dist/storage/storageVersions.js +1 -1
  64. package/dist/storage/storeRpc.js +1 -0
  65. package/dist/storage/taskStore.js +4 -0
  66. package/dist/storage/upgrade/upgradeOrchestrator.js +1 -1
  67. package/dist/task/completionReadiness.js +13 -6
  68. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  69. package/dist/web/assets/assetManifest.js +28 -2
  70. package/dist/web/assets/client/app/manage.js +6 -0
  71. package/dist/web/assets/client/app/models.js +20 -0
  72. package/dist/web/assets/client/app/settings.js +41 -9
  73. package/dist/web/assets/client/i18n/en.js +180 -2
  74. package/dist/web/assets/client/i18n/zh-CN.js +178 -2
  75. package/dist/web/assets/client/index.js +8 -0
  76. package/dist/web/assets/client/layout/manageShell.js +94 -0
  77. package/dist/web/assets/client/lib/api.js +13 -1
  78. package/dist/web/assets/client/lib/i18n.js +5 -3
  79. package/dist/web/assets/client/views/dock/conversation.js +231 -80
  80. package/dist/web/assets/client/views/models.js +777 -0
  81. package/dist/web/assets/client/views/skills.js +22 -15
  82. package/dist/web/assets/shared/icons.js +2 -0
  83. package/dist/web/assets/shared/manageNav.js +13 -0
  84. package/dist/web/assets/shell/manage.js +54 -0
  85. package/dist/web/assets/shell/models.js +7 -0
  86. package/dist/web/assets/shell/settings.js +1 -35
  87. package/dist/web/assets/shell/settingsContent.js +30 -0
  88. package/dist/web/assets/shell/sidebar.js +1 -0
  89. package/dist/web/assets/styles/index.js +6 -0
  90. package/dist/web/assets/styles/views/conversation.js +133 -0
  91. package/dist/web/assets/styles/views/manage.js +71 -0
  92. package/dist/web/assets/styles/views/models.js +129 -0
  93. package/dist/web/assets/styles/views/settings.js +28 -14
  94. package/dist/web/assets/styles/views/sidebar.js +1 -0
  95. package/dist/web/webServer.js +59 -9
  96. package/docs/cli-information-contract.md +42 -11
  97. package/docs/cli-information-contract.zh-CN.md +28 -7
  98. package/docs/managed-turn-and-session-runtime.md +127 -5
  99. package/docs/managed-turn-and-session-runtime.zh-CN.md +83 -3
  100. package/docs/release-workflow.md +105 -9
  101. package/docs/release-workflow.zh-CN.md +73 -5
  102. package/docs/storage-baseline.md +22 -7
  103. package/docs/storage-baseline.zh-CN.md +14 -5
  104. package/docs/task-delivery.md +17 -8
  105. package/docs/task-delivery.zh-CN.md +11 -5
  106. package/docs/testing/review-evaluation.md +148 -0
  107. package/docs/testing/review-evaluation.zh-CN.md +117 -0
  108. package/docs/testing/verification-levels.md +26 -4
  109. package/docs/testing/verification-levels.zh-CN.md +11 -1
  110. package/i18n/README.zh-CN.md +19 -0
  111. package/package.json +5 -2
  112. package/skills/yui-leader/SKILL.md +28 -11
  113. package/skills/yui-leader/VERSION +1 -0
  114. package/skills/yui-leader/references/execution.md +29 -0
  115. package/skills/yui-leader/references/replicated-execution.md +7 -0
  116. package/skills/yui-operator/SKILL.md +15 -3
  117. package/skills/yui-operator/VERSION +1 -0
  118. package/skills/yui-operator/references/task-delivery.md +2 -0
  119. package/skills/yui-reviewer/SKILL.md +43 -1
  120. package/skills/yui-reviewer/VERSION +1 -0
  121. package/skills/yui-runtime/SKILL.md +144 -15
  122. package/skills/yui-runtime/VERSION +1 -0
  123. package/skills/yui-runtime/references/publication.md +16 -5
  124. package/skills/yui-runtime/references/recovery.md +24 -5
  125. package/skills/yui-worker/SKILL.md +8 -1
  126. package/skills/yui-worker/VERSION +1 -0
@@ -16,12 +16,31 @@ export function requireManagedGlobalCaller(store, env) {
16
16
  || !role || !binding || !set || !session || session.status !== "active"
17
17
  || set.activeAgentId !== role.activeAgentId || session.agentId !== binding.agentId
18
18
  || session.adapterId !== binding.adapterId || nativeSessionId !== session.nativeSessionId
19
+ || (env.YUI_WORKSPACE !== undefined && env.YUI_WORKSPACE !== session.effective.workspace.root)
19
20
  || (env.YUI_AGENT_ID !== undefined && env.YUI_AGENT_ID !== session.agentId)
20
21
  || (env.YUI_ADAPTER_ID !== undefined && env.YUI_ADAPTER_ID !== session.adapterId)) {
21
22
  throw new ManagedRuntimeDriftError("This command requires the current managed global native Session; the previous Session has no write authority.");
22
23
  }
23
24
  return { roleName: role.name, agentId: session.agentId, adapterId: session.adapterId, nativeSessionId };
24
25
  }
26
+ /** Historical self diagnostics remain readable, but never authorize inbox
27
+ * handling, configuration, another Role's records, or lifecycle mutation. */
28
+ export function requireManagedGlobalReader(store, env) {
29
+ try {
30
+ requireManagedGlobalCaller(store, env);
31
+ return;
32
+ }
33
+ catch (error) {
34
+ const set = env.YUI_ROLE === undefined ? null : store.getGlobalRoleSessionSet(env.YUI_ROLE);
35
+ const known = [...Object.values(set?.sessions ?? {}), ...Object.values(set?.history ?? {})].some(session => session.nativeSessionId === identity(session.adapterId === "codex"
36
+ ? env.CODEX_THREAD_ID ?? env.YUI_NATIVE_SESSION_ID : env.YUI_NATIVE_SESSION_ID)
37
+ && (env.YUI_AGENT_ID === undefined || env.YUI_AGENT_ID === session.agentId)
38
+ && (env.YUI_ADAPTER_ID === undefined || env.YUI_ADAPTER_ID === session.adapterId)
39
+ && (env.YUI_WORKSPACE === undefined || env.YUI_WORKSPACE === session.effective.workspace.root));
40
+ if (env.YUI_SESSION_SCOPE !== "global" || env.YUI_TASK_ID !== undefined || !known)
41
+ throw error;
42
+ }
43
+ }
25
44
  /**
26
45
  * A managed Session presented valid self-identity but is no longer the current
27
46
  * runtime of its Task Role. This is a bounded diagnosis rather than a bare
@@ -62,7 +81,12 @@ export function resolveManagedTaskCaller(store, environment) {
62
81
  const self = managedTaskSessionIdentity(environment);
63
82
  if (self === undefined)
64
83
  return undefined;
65
- return requireCurrentRuntime(store, self);
84
+ const caller = requireCurrentRuntime(store, self);
85
+ if (environment?.YUI_AGENT_ID !== undefined && environment.YUI_AGENT_ID !== caller.agentId
86
+ || environment?.YUI_ADAPTER_ID !== undefined && environment.YUI_ADAPTER_ID !== caller.adapterId) {
87
+ throw new ManagedRuntimeDriftError("Managed Task Agent/adapter hints conflict with the current native Session.");
88
+ }
89
+ return caller;
66
90
  }
67
91
  /** Same as resolveManagedTaskCaller, but requires a managed Task Session. */
68
92
  export function requireManagedTaskCaller(store, environment) {
@@ -2,5 +2,5 @@
2
2
  * silently acquire the authority of an unbound public caller. */
3
3
  export function hasManagedIdentity(environment) {
4
4
  return ["YUI_SESSION_SCOPE", "YUI_ROLE", "YUI_AGENT_ID", "YUI_NATIVE_SESSION_ID",
5
- "YUI_TASK_ID", "YUI_SESSION_MANIFEST"].some(key => environment[key] !== undefined);
5
+ "YUI_TASK_ID", "YUI_SESSION_MANIFEST", "CODEX_THREAD_ID"].some(key => environment[key] !== undefined);
6
6
  }
@@ -176,7 +176,7 @@ export function providerRetryPrompt(retry, original) {
176
176
  original
177
177
  ].join("\n");
178
178
  }
179
- /** The same presentation is consumed by CLI, Context and Web. */
179
+ /** The same presentation is consumed by explicit detail reads and Web. */
180
180
  export function providerRetryProjection(binding) {
181
181
  const retry = binding?.retry;
182
182
  if (retry === undefined)
@@ -209,6 +209,24 @@ export function providerRetryProjection(binding) {
209
209
  ...(retry.successorRunId === undefined ? {} : { successorRunId: retry.successorRunId })
210
210
  };
211
211
  }
212
+ /** Entry-point facts, never accumulated retry history or raw diagnostics.
213
+ * Exact Session retry/show and failureRef retain the original evidence. */
214
+ export function providerRetrySummary(binding) {
215
+ const view = providerRetryProjection(binding);
216
+ if (view === null)
217
+ return null;
218
+ const { history, error, currentTurn, ...facts } = view;
219
+ return {
220
+ ...facts,
221
+ error: { source: error.source, phase: error.phase, category: error.category,
222
+ code: error.code, inputDisposition: error.inputDisposition,
223
+ sessionDisposition: error.sessionDisposition, retryable: error.retryable },
224
+ currentTurn: currentTurn === null ? null : {
225
+ attemptId: currentTurn.attemptId, status: currentTurn.status, nativeTurnId: currentTurn.nativeTurnId
226
+ },
227
+ historyCount: history.length, detailOmitted: true
228
+ };
229
+ }
212
230
  export function controlProviderRetry(binding, action, at) {
213
231
  if (action === "enable")
214
232
  return { ...binding, retryDisabled: false };
@@ -0,0 +1,53 @@
1
+ import { hasManagedIdentity } from "./managedIdentity.js";
2
+ import { ManagedRuntimeDriftError, requireManagedGlobalCaller, requireManagedTaskCaller } from "./managedCaller.js";
3
+ /** Resolve location from this Home's registry, not from a launch environment.
4
+ * The returned hints are not authority: consumers still validate the current
5
+ * binding (including revocation, Assignment and captured workspace).
6
+ * Native children with their own Codex thread cannot borrow a parent's hints.
7
+ */
8
+ export function sessionCallerEnvironment(store, env) {
9
+ const native = env.CODEX_THREAD_ID ?? env.YUI_NATIVE_SESSION_ID;
10
+ if (native === undefined) {
11
+ if (hasManagedIdentity(env))
12
+ throw new ManagedRuntimeDriftError("Managed Session identity has no native session id.");
13
+ return env;
14
+ }
15
+ if (!native || native.trim() !== native || native.includes("\0")) {
16
+ throw new ManagedRuntimeDriftError("Managed native Session identity is invalid.");
17
+ }
18
+ // Reuse the existing derived active-Session inventory; no new identity index
19
+ // or scan of every Task body/history is needed.
20
+ const matches = store.listRuntimeSessionCandidates().filter(session => session.nativeSessionId === (session.adapterId === "codex" ? env.CODEX_THREAD_ID ?? env.YUI_NATIVE_SESSION_ID : env.YUI_NATIVE_SESSION_ID));
21
+ if (matches.length !== 1) {
22
+ throw new ManagedRuntimeDriftError(matches.length
23
+ ? "Native Session identity is ambiguous in this YUI_HOME; no caller authority was selected."
24
+ : "Native Session is not registered in this YUI_HOME; refusing to infer public user authority.");
25
+ }
26
+ const match = matches[0];
27
+ const set = match.owner.scope === "global" ? store.getGlobalRoleSessionSet(match.owner.roleName)
28
+ : store.getTaskRoleSessionSet(match.owner.taskId, match.owner.roleName);
29
+ const session = set?.sessions[match.agentId];
30
+ if (set === null || session === undefined || session.nativeSessionId !== match.nativeSessionId) {
31
+ throw new ManagedRuntimeDriftError("Native Session registration changed during caller resolution; read current state again.");
32
+ }
33
+ const hints = {
34
+ YUI_SESSION_SCOPE: set.owner.scope, YUI_ROLE: set.owner.roleName,
35
+ YUI_AGENT_ID: session.agentId, YUI_ADAPTER_ID: session.adapterId,
36
+ YUI_WORKSPACE: session.effective.workspace.root,
37
+ ...(set.owner.scope === "task" ? { YUI_TASK_ID: set.owner.taskId } : {})
38
+ };
39
+ for (const key of [...Object.keys(hints), "YUI_TASK_ID"]) {
40
+ if (env[key] !== undefined && env[key] !== hints[key]) {
41
+ throw new ManagedRuntimeDriftError(`Managed Session hint ${key} conflicts with this Home's native Session registration.`);
42
+ }
43
+ }
44
+ return { ...env, ...hints };
45
+ }
46
+ /** One current caller for inbox and other own-address operations. */
47
+ export function requireManagedCaller(store, env) {
48
+ const current = sessionCallerEnvironment(store, env);
49
+ if (current.YUI_SESSION_SCOPE === "global") {
50
+ return { scope: "global", ...requireManagedGlobalCaller(store, current) };
51
+ }
52
+ return { scope: "task", ...requireManagedTaskCaller(store, current) };
53
+ }
@@ -1,6 +1,55 @@
1
1
  import { enqueueWork } from "../coordination/workMailboxQueue.js";
2
2
  import { createTaskEvent } from "../event/taskEvent.js";
3
+ import { claimPending } from "../coordination/workMailbox.js";
3
4
  export const LEADER_ATTENTION_REQUIRED_EVENT = "leader.attention-required";
5
+ /** Bound each Operator pass, retaining the exact unclaimed sequence suffix. */
6
+ export function claimOperatorAttention(mailbox, options) {
7
+ const pending = mailbox.pending;
8
+ if (pending === null || pending.requestCount <= 64)
9
+ return claimPending(mailbox, options);
10
+ // Operator producers append one unique source ref per signal. Mailbox merge
11
+ // preserves insertion order for refs and keys, including the supported
12
+ // default sequence keys used by Provider retry attention. Preserve those
13
+ // keys verbatim; they are valid current data, not a migration or repair.
14
+ if (pending.requestCount !== pending.dedupeKeys.length
15
+ || pending.refs.length !== pending.requestCount
16
+ || pending.toSequence - pending.fromSequence + 1 !== pending.requestCount
17
+ || pending.dedupeKeys.some((key, index) => {
18
+ const ref = pending.refs[index];
19
+ return (ref.type !== "event" && ref.type !== "input")
20
+ || key !== `operator-${ref.type}:${ref.taskId}:${ref.id}`
21
+ && key !== `sequence:${pending.fromSequence + index}`;
22
+ })) {
23
+ throw new Error("Operator attention batch cannot be safely partitioned; inspect its source refs and sequence before retrying.");
24
+ }
25
+ const prefixKeys = pending.dedupeKeys.slice(0, 64);
26
+ const suffixKeys = pending.dedupeKeys.slice(64);
27
+ const through = pending.fromSequence + 63;
28
+ const prefix = { ...pending, toSequence: through, requestCount: 64,
29
+ dedupeKeys: prefixKeys, refs: pending.refs.slice(0, 64) };
30
+ const claimed = claimPending({ ...mailbox, pending: prefix }, {
31
+ ...options, batchId: `operator:${prefix.fromSequence}-${prefix.toSequence}`
32
+ });
33
+ return { ...claimed, pending: { ...pending, fromSequence: through + 1,
34
+ requestCount: pending.requestCount - 64, dedupeKeys: suffixKeys,
35
+ refs: pending.refs.slice(64) } };
36
+ }
37
+ /**
38
+ * Fixed source admission at the durable event write, in the same transaction.
39
+ * The existing mailbox is the incremental restart cursor: periodic scheduling
40
+ * reads only outstanding refs, never re-scans or replays historical events.
41
+ * Context, idle observations and ordinary progress are deliberately absent.
42
+ */
43
+ export function enqueueDurableOperatorFact(store, event) {
44
+ const now = new Date(event.createdAt);
45
+ if (event.type === "input.requested" && typeof event.payload.requestId === "string") {
46
+ enqueueWork(store, { kind: "operator" }, "input-requested", now, [{ type: "input", taskId: event.taskId, id: event.payload.requestId }], { source: "input-request", dedupeKey: `operator-input:${event.taskId}:${event.payload.requestId}` });
47
+ }
48
+ else if (["task.completed", "task.cancelled", "task.retired", "publication.recorded",
49
+ "publication.candidate-adopted", LEADER_ATTENTION_REQUIRED_EVENT].includes(event.type)) {
50
+ enqueueOperatorEvent(store, event, event.type.startsWith("task.") ? "task-terminal" : event.type, now);
51
+ }
52
+ }
4
53
  /** One event, one supervisor signal; the Leader wake is this mailbox's projection. */
5
54
  export function enqueueSupervisorEvent(store, event, recipient, reason, now) {
6
55
  if (store.getTask(event.taskId)?.status === "archived")
@@ -28,8 +28,9 @@ export async function processOperatorInputNotifications(store, selection, now =
28
28
  }
29
29
  if (ref.type !== "event")
30
30
  return [];
31
- const event = store.listEvents(ref.taskId).find(({ id }) => id === ref.id);
32
- return event === undefined ? [] : [{ kind: "event", event }];
31
+ const event = store.getEvent !== undefined ? store.getEvent(ref.taskId, ref.id)
32
+ : store.listEvents(ref.taskId).find(({ id }) => id === ref.id);
33
+ return event == null ? [] : [{ kind: "event", event }];
33
34
  }));
34
35
  if (attentions.length === 0) {
35
36
  store.completeWorkMailbox(targetMailbox, processing.batchId);
@@ -42,9 +43,9 @@ export async function processOperatorInputNotifications(store, selection, now =
42
43
  receiptId: presentation.receiptId,
43
44
  text: presentation.text
44
45
  }, now);
45
- if (outcome.status === "unavailable") {
46
+ if (outcome.status === "unavailable" || outcome.status === "deferred") {
46
47
  store.releaseWorkMailbox(targetMailbox, processing.batchId);
47
- return attentions.map((attention) => skipped(attention, "operator-unavailable"));
48
+ return attentions.map((attention) => skipped(attention, outcome.status === "deferred" ? "notice-pending" : "operator-unavailable"));
48
49
  }
49
50
  return attentions.map((attention) => ({
50
51
  ...attentionIdentity(attention),
@@ -1,6 +1,7 @@
1
1
  import { requireIdentity, requirePositiveInteger, requireText, requireTimestamp } from "../domain/validation.js";
2
2
  import { TASK_RECORD_ID_PREFIXES } from "../task/taskRecordReference.js";
3
3
  import { copyRef } from "../coordination/workMailbox.js";
4
+ import { validateInboxReceipt } from "../message/inboxReceipt.js";
4
5
  export const CURRENT_TASK_WAKE_SCHEMA_VERSION = 1;
5
6
  export function createTaskWake(input) {
6
7
  if (Object.hasOwn(input, "runId"))
@@ -21,6 +22,10 @@ export function createTaskWake(input) {
21
22
  return wake;
22
23
  }
23
24
  export function validateTaskWake(wake) {
25
+ validateInboxReceipt(wake.inbox);
26
+ if (wake.inboxPendingId !== undefined && (!/^pending-[a-f0-9]{64}$/.test(wake.inboxPendingId) || !wake.inbox)) {
27
+ throw new Error("An active inbox wake requires its exact pending identity and handling claim.");
28
+ }
24
29
  if (Object.hasOwn(wake, "runId"))
25
30
  throw new Error("Run-backed wakes are not current notification records.");
26
31
  if (wake.refs !== undefined) {
@@ -0,0 +1,108 @@
1
+ import { createRequire } from "node:module";
2
+ export const SQLITE_CANDIDATES = Object.freeze([
3
+ // N-API avoids node::ObjectWrap's GC cleanup abort on Node 24.19/24.20.
4
+ Object.freeze({ name: "sqlite-modern", version: "13.0.3", minimumNapi: 10 }),
5
+ Object.freeze({ name: "sqlite-compat", version: "10.1.0", minimumNapi: 0 })
6
+ ]);
7
+ /** Only errors attributable to loading this native candidate, never database errors. */
8
+ function nativeUnavailable(error, candidate) {
9
+ if (!(error instanceof Error))
10
+ return false;
11
+ const code = error.code;
12
+ const firstLine = error.message.split("\n")[0];
13
+ if (code === "MODULE_NOT_FOUND") {
14
+ if (firstLine === `Cannot find module '${candidate.name}'`)
15
+ return true;
16
+ // v13 loads its bundled prebuild or these node-gyp paths directly.
17
+ // Do not swallow an unrelated missing transitive JS dependency.
18
+ const missing = /^Cannot find module '([^']+)'$/.exec(firstLine)?.[1];
19
+ return missing !== undefined && ["Debug", "Release"].some((mode) => missing.endsWith(`/node_modules/${candidate.name}/build/${mode}/better_sqlite3.node`));
20
+ }
21
+ const tries = error.tries;
22
+ if (error.message.startsWith("Could not locate the bindings file.") && Array.isArray(tries)) {
23
+ return tries.length > 0 && tries.every((path) => typeof path === "string" && path.includes(`/node_modules/${candidate.name}/`)
24
+ && path.endsWith("/better_sqlite3.node"));
25
+ }
26
+ if (code !== "ERR_DLOPEN_FAILED")
27
+ return false;
28
+ return /NODE_MODULE_VERSION \d+/.test(error.message)
29
+ || /(?:GLIBC|GLIBCXX|CXXABI)_[\d.]+['’] not found/.test(error.message)
30
+ || /(?:cannot open shared object file|Library not loaded:|wrong ELF class:|invalid ELF header|incompatible architecture)/.test(error.message);
31
+ }
32
+ const require = createRequire(import.meta.url);
33
+ function selectionFailure(failures) {
34
+ return new AggregateError(failures, "No usable SQLite driver. " + SQLITE_CANDIDATES.map((candidate, index) => `${candidate.name}@${candidate.version}: ${failures[index] instanceof Error ? failures[index].message : String(failures[index])}`).join("; ")
35
+ + ". Reinstall with npm optional dependencies enabled (do not use --omit=optional); "
36
+ + "check Node ABI, system libraries and native build tools. Older systems may require an OS/toolchain upgrade.");
37
+ }
38
+ /**
39
+ * Selection is isolated from every real database open. No real Home operation
40
+ * is inside a fallback catch, and successful selection stays fixed per process.
41
+ * Loader injection supports deterministic native-failure tests, not user config.
42
+ */
43
+ export function createSqliteDriverSelector(load = (candidate) => require(candidate.name), napiVersion = Number(process.versions.napi ?? 0)) {
44
+ let selected;
45
+ return () => {
46
+ if (selected)
47
+ return selected;
48
+ const failures = [];
49
+ for (const candidate of SQLITE_CANDIDATES) {
50
+ // An unsupported Node-API constructor can crash outside JS exception
51
+ // handling. Exclude it before loading native code, not after probing.
52
+ if (napiVersion < candidate.minimumNapi) {
53
+ failures.push(new Error(`${candidate.name} requires Node-API ${candidate.minimumNapi}; runtime provides ${napiVersion}`));
54
+ continue;
55
+ }
56
+ let Driver;
57
+ let probe;
58
+ try {
59
+ Driver = load(candidate);
60
+ probe = new Driver(":memory:");
61
+ }
62
+ catch (error) {
63
+ if (!nativeUnavailable(error, candidate)) {
64
+ // Stop on unknown errors, but retain the modern failure when the
65
+ // compatibility candidate also fails. This does not permit retry.
66
+ if (failures.length)
67
+ throw selectionFailure([...failures, error]);
68
+ throw error;
69
+ }
70
+ failures.push(error);
71
+ continue;
72
+ }
73
+ // SQL and close failures are deliberately outside the native-load catch.
74
+ let sqliteVersion;
75
+ try {
76
+ try {
77
+ const result = probe.prepare("SELECT sqlite_version() AS version, 1 AS value").get();
78
+ if (typeof result?.version !== "string" || result.value !== 1) {
79
+ throw new Error(`SQLite probe returned an invalid result for ${candidate.name}.`);
80
+ }
81
+ sqliteVersion = result.version;
82
+ }
83
+ finally {
84
+ probe.close();
85
+ }
86
+ }
87
+ catch (error) {
88
+ if (failures.length)
89
+ throw selectionFailure([...failures, error]);
90
+ throw error;
91
+ }
92
+ selected = Object.freeze({
93
+ ...candidate, Database: Driver, sqliteVersion,
94
+ ...(failures.length ? { fallbackReason: failures[0].message } : {})
95
+ });
96
+ return selected;
97
+ }
98
+ throw selectionFailure(failures);
99
+ };
100
+ }
101
+ export const selectSqliteDriver = createSqliteDriverSelector();
102
+ // Preserve the existing constructor/type contract without loading native code
103
+ // during module import. All runtime consumers share the same selector.
104
+ const Database = function (filename, options) {
105
+ const Driver = selectSqliteDriver().Database;
106
+ return new Driver(filename, options);
107
+ };
108
+ export default Database;
@@ -58,6 +58,34 @@ export function queryContextRecords(db, taskId, query) {
58
58
  where.push(`${id} IN (SELECT value FROM json_each(@ids))`);
59
59
  params.ids = JSON.stringify(query.ids);
60
60
  }
61
+ if (query.unhandledMessages) {
62
+ if (query.family !== "task-message")
63
+ throw new Error("Unhandled selection requires Messages.");
64
+ where.push(`json_extract(r.payload, '$.inbox.ack') IS NULL`);
65
+ where.push(`NOT EXISTS (SELECT 1 FROM events e WHERE e.task_id = r.task_id
66
+ AND e.type = 'task.record-retired'
67
+ AND json_extract(e.payload, '$.payload.recordKind') = 'message'
68
+ AND json_extract(e.payload, '$.payload.recordId') = r.message_id)`);
69
+ // Match taskMessageInboxReceipt: a direct claim owns the Message, otherwise
70
+ // a wake receipt covers its exact reference and latest revision event.
71
+ where.push(`(json_extract(r.payload, '$.inbox') IS NOT NULL OR (
72
+ SELECT json_extract(w.payload, '$.inbox.ack') FROM task_wakes w
73
+ WHERE w.task_id = r.task_id AND json_extract(w.payload, '$.inbox') IS NOT NULL
74
+ AND EXISTS (SELECT 1 FROM json_each(w.payload, '$.refs') ref
75
+ WHERE json_extract(ref.value, '$.type') = 'message'
76
+ AND json_extract(ref.value, '$.taskId') = r.task_id
77
+ AND json_extract(ref.value, '$.id') = r.message_id)
78
+ AND (NOT EXISTS (SELECT 1 FROM events e WHERE e.task_id = r.task_id AND e.type = 'message.updated'
79
+ AND json_extract(e.payload, '$.payload.messageId') = r.message_id)
80
+ OR EXISTS (SELECT 1 FROM json_each(w.payload, '$.refs') update_ref
81
+ WHERE json_extract(update_ref.value, '$.type') = 'event'
82
+ AND json_extract(update_ref.value, '$.taskId') = r.task_id
83
+ AND json_extract(update_ref.value, '$.id') = (
84
+ SELECT e.event_id FROM events e WHERE e.task_id = r.task_id AND e.type = 'message.updated'
85
+ AND json_extract(e.payload, '$.payload.messageId') = r.message_id
86
+ ORDER BY CAST(substr(e.event_id, 7) AS INTEGER) DESC LIMIT 1)))
87
+ ORDER BY w.seq ASC LIMIT 1) IS NULL)`);
88
+ }
61
89
  for (const [label, statuses, not] of [
62
90
  ["statuses", query.statuses, ""],
63
91
  ["excludeStatuses", query.excludeStatuses, "NOT"]
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import Database from "better-sqlite3";
2
+ import Database from "../sqlite/driver.js";
3
3
  import { BASELINE_SCHEMA_SQL, SQLITE_SCHEMA_TABLES } from "./baselineSchema.js";
4
4
  import { migrateTaskMainWorkspaces } from "./upgrade/taskMainWorkspaceMigration.js";
5
5
  import { migrateProjectKnowledge } from "./upgrade/projectKnowledgeMigration.js";
@@ -76,6 +76,26 @@ const MINOR_UPGRADES = Object.freeze([{
76
76
  targetChecksum: CURRENT_SCHEMA_CHECKSUM,
77
77
  // Optional sourced monetary observations; historical missing costs stay unknown.
78
78
  sql: ""
79
+ }, {
80
+ fromVersion: "1.6",
81
+ toVersion: "1.7",
82
+ name: "agent-inbox-consumption",
83
+ introducedIn: "next",
84
+ sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
85
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
86
+ // Additive handling receipts on canonical Messages and wakes. Never invent
87
+ // historical processing or Provider acceptance; existing records stay intact.
88
+ sql: ""
89
+ }, {
90
+ fromVersion: "1.7",
91
+ toVersion: "1.8",
92
+ name: "model-management-connection",
93
+ introducedIn: "next",
94
+ sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
95
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
96
+ // Optional non-secret connection references. Existing Homes remain unconfigured;
97
+ // never infer local credentials or adopt an existing external service.
98
+ sql: ""
79
99
  }]);
80
100
  export function storageMinorUpgradePlan(from) {
81
101
  if (from === CURRENT_STORAGE_VERSION)
@@ -32,7 +32,7 @@
32
32
  * including writes, reads and explicit Home diagnostics. There is no weaker
33
33
  * in-process contract or deferred validation phase.
34
34
  */
35
- import Database from "better-sqlite3";
35
+ import Database from "../sqlite/driver.js";
36
36
  import { existsSync, mkdirSync } from "node:fs";
37
37
  import { join } from "node:path";
38
38
  import { isDeepStrictEqual } from "node:util";
@@ -40,8 +40,10 @@ import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
40
40
  import { validateTaskBrief } from "../brief/taskBrief.js";
41
41
  import { validateContextSnapshot } from "../context/contextSnapshot.js";
42
42
  import { consumePendingBatch, mailboxTargetKey, validateWorkMailbox } from "../coordination/workMailbox.js";
43
+ import { enqueueDurableOperatorFact } from "../scheduler/operatorEvent.js";
43
44
  import { validateDurableJob, validDurableJobTransition } from "../job/durableJob.js";
44
45
  import { validateGlobalRoleMessage } from "../message/message.js";
46
+ import { assertInboxOriginalPreserved, assertInboxReceiptPreserved } from "../message/inboxReceipt.js";
45
47
  import { validatePluginIntent, validatePluginIntentFailure } from "../plugins/pluginIntent.js";
46
48
  import { validatePluginValidation } from "../plugins/pluginPackage.js";
47
49
  import { generateHomeIdentity, validateHomeIdentity } from "../repository/homeIdentity.js";
@@ -975,10 +977,58 @@ export class SqliteTaskStore {
975
977
  listGlobalRoleMessages(roleName) {
976
978
  return this.#sortById(this.#listPayload("global_role_messages", "name = ?", [roleName]), (message) => message.id);
977
979
  }
980
+ queryGlobalRoleMessages(roleName, query) {
981
+ if (!Number.isSafeInteger(query.limit) || query.limit < 0 || query.limit > 64
982
+ || !Number.isSafeInteger(query.afterSequence ?? 0) || (query.afterSequence ?? 0) < 0) {
983
+ throw new Error("Global Message query exceeds its bounded contract.");
984
+ }
985
+ const where = ["name = @name"];
986
+ const params = { name: roleName };
987
+ if (query.handling)
988
+ where.push(`json_extract(payload, '$.inbox.claim') IS NOT NULL
989
+ AND json_extract(payload, '$.inbox.ack') IS NULL`);
990
+ if (query.pending)
991
+ where.push(`json_extract(payload, '$.delivery') IS NULL
992
+ AND json_extract(payload, '$.inbox.ack') IS NULL
993
+ AND json_extract(payload, '$.notDelivered') IS NULL
994
+ AND (json_extract(payload, '$.inputControl.action') = 'queue'
995
+ OR json_extract(payload, '$.interruptThen') IS NOT NULL)`);
996
+ if (query.interruptThen)
997
+ where.push("json_extract(payload, '$.interruptThen') IS NOT NULL");
998
+ if (query.operatorNotice)
999
+ where.push("json_extract(payload, '$.inputControl.requestId') LIKE 'operator-notice:%'");
1000
+ if (query.afterSequence !== undefined) {
1001
+ where.push("seq > @after");
1002
+ params.after = query.afterSequence;
1003
+ }
1004
+ if (query.id !== undefined) {
1005
+ where.push("message_id = @id");
1006
+ params.id = query.id;
1007
+ }
1008
+ if (query.requestId !== undefined) {
1009
+ where.push("COALESCE(json_extract(payload, '$.inputControl.requestId'), json_extract(payload, '$.interruptThen.reusedInput.requestId')) = @request");
1010
+ params.request = query.requestId;
1011
+ }
1012
+ const filter = where.join(" AND ");
1013
+ const summary = this.#db.prepare(`SELECT count(*) AS count, min(created_at) AS oldestCreatedAt
1014
+ FROM global_role_messages WHERE ${filter}`).get(params);
1015
+ const rows = this.#db.prepare(`SELECT payload FROM global_role_messages WHERE ${filter}
1016
+ ORDER BY seq ${query.descending ? "DESC" : "ASC"} LIMIT @limit`)
1017
+ .all({ ...params, limit: query.limit });
1018
+ return { ...summary, messages: rows.map(row => {
1019
+ const message = this.#parse(row.payload);
1020
+ validateGlobalRoleMessage(message);
1021
+ return message;
1022
+ }) };
1023
+ }
978
1024
  updateGlobalRoleMessage(message) {
979
1025
  validateGlobalRoleMessage(message);
980
1026
  this.#requireGlobalRole(message.roleName);
981
1027
  this.#mutate(() => {
1028
+ const existing = this.#db.prepare("SELECT payload FROM global_role_messages WHERE name = ? AND message_id = ?")
1029
+ .get(message.roleName, message.id);
1030
+ assertInboxOriginalPreserved(existing === undefined ? undefined
1031
+ : this.#parse(existing.payload), message);
982
1032
  const seq = this.#globalMessageSequence(message.id);
983
1033
  const result = this.#db.prepare(`UPDATE global_role_messages SET seq = ?, payload = ? WHERE name = ? AND message_id = ?`).run(seq, this.#recordJson("global_role_messages", message), message.roleName, message.id);
984
1034
  if (result.changes === 0) {
@@ -2001,6 +2051,10 @@ export class SqliteTaskStore {
2001
2051
  }
2002
2052
  this.#requireTask(taskId);
2003
2053
  this.#mutate(() => {
2054
+ const existing = this.#db.prepare("SELECT payload FROM messages WHERE task_id = ? AND message_id = ?")
2055
+ .get(taskId, message.id);
2056
+ assertInboxOriginalPreserved(existing === undefined ? undefined
2057
+ : this.#parse(existing.payload), message);
2004
2058
  const result = this.#db.prepare(`UPDATE messages SET payload = ? WHERE task_id = ? AND message_id = ?`).run(this.#recordJson("messages", message), taskId, message.id);
2005
2059
  if (result.changes !== 1) {
2006
2060
  throw new StorageRecordError(`Message does not exist: ${taskId}/${message.id}`);
@@ -2086,6 +2140,7 @@ export class SqliteTaskStore {
2086
2140
  // Events are terminal/semantic: retained individually, never pruned (§9).
2087
2141
  this.#db.prepare(`INSERT INTO events (task_id, event_id, type, occurred_at, payload) VALUES (?, ?, ?, ?, ?)`).run(taskId, event.id, event.type, event.createdAt, this.#recordJson("events", event));
2088
2142
  this.#observeHighWater(taskId, "event", seq);
2143
+ enqueueDurableOperatorFact(this, event);
2089
2144
  });
2090
2145
  }
2091
2146
  listEvents(taskId) {
@@ -2113,6 +2168,14 @@ export class SqliteTaskStore {
2113
2168
  validateTaskWake(wake);
2114
2169
  this.#requireTask(taskId);
2115
2170
  this.#mutate(() => {
2171
+ const existing = this.#db.prepare("SELECT payload FROM task_wakes WHERE task_id = ? AND wake_id = ?")
2172
+ .get(taskId, wake.id);
2173
+ const previous = existing === undefined ? undefined : this.#parse(existing.payload);
2174
+ if (previous !== undefined && !isDeepStrictEqual(previous.refs, wake.refs)) {
2175
+ throw new StorageRecordError("A notification window's original revision references are immutable.");
2176
+ }
2177
+ assertInboxReceiptPreserved(existing === undefined ? undefined
2178
+ : this.#parse(existing.payload).inbox, wake.inbox);
2116
2179
  const seq = this.#idSequence(wake.id, "taskWake");
2117
2180
  this.#db.prepare(`INSERT INTO task_wakes
2118
2181
  (task_id, wake_id, seq, status, from_cursor, to_cursor, reasons, payload, created_at, consumed_at)
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import Database from "better-sqlite3";
3
+ import Database from "../sqlite/driver.js";
4
4
  import { inspectSqliteSchema, storageMinorUpgradePlan, SqliteSchemaError } from "./sqliteSchema.js";
5
5
  import { compareStorageVersions, CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "./storageVersions.js";
6
6
  export { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "./storageVersions.js";
@@ -1,4 +1,4 @@
1
- export const CURRENT_STORAGE_VERSION = "1.6";
1
+ export const CURRENT_STORAGE_VERSION = "1.8";
2
2
  export const MIN_SUPPORTED_STORAGE_VERSION = "1.0";
3
3
  export const STORAGE_FORMAT = "yui-home";
4
4
  export function isStorageVersion(value) {
@@ -48,6 +48,7 @@ const READ_ONLY_STORE_METHODS = new Set([
48
48
  "getGlobalRoleSessionSet",
49
49
  "listGlobalRoleSessionSets",
50
50
  "listGlobalRoleMessages",
51
+ "queryGlobalRoleMessages",
51
52
  "listTasks",
52
53
  "getTask",
53
54
  "readNextActionFacts",
@@ -1,5 +1,6 @@
1
1
  import { lstatSync, mkdirSync, realpathSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
+ import { validateModelConnection } from "../models/modelManagementConfig.js";
3
4
  import { basename, dirname, join, resolve } from "node:path";
4
5
  import { isDeepStrictEqual } from "node:util";
5
6
  import { resolveTimeZone } from "../config/timeZone.js";
@@ -117,6 +118,7 @@ export function validateYuiConfig(config) {
117
118
  try {
118
119
  const optionalFields = [
119
120
  "defaultAgent",
121
+ "modelManagement",
120
122
  "defaultWorkspace",
121
123
  "timeZone",
122
124
  "currentTaskId",
@@ -146,6 +148,8 @@ export function validateYuiConfig(config) {
146
148
  }
147
149
  reconciliationIntervalMilliseconds(config.reconciliationIntervalSeconds);
148
150
  resolveTimeZone(config.timeZone);
151
+ if (config.modelManagement !== undefined)
152
+ validateModelConnection(config.modelManagement);
149
153
  if (config.review !== undefined)
150
154
  validateReviewConfig(config.review);
151
155
  resolveResourcesGcMode(config.resourcesGcMode);
@@ -1,6 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { copyFileSync, existsSync, mkdirSync, rmdirSync, rmSync } from "node:fs";
3
- import Database from "better-sqlite3";
3
+ import Database from "../../sqlite/driver.js";
4
4
  import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation } from "../../runtime/runtimeObservation.js";
5
5
  import { RUNTIME_PROCESS_EXIT_TASK_EVENT, validateRuntimeProcessExitObservation } from "../../runtime/processExitObservation.js";
6
6
  import { SqliteTaskStore } from "../sqliteStore.js";
@@ -1,12 +1,18 @@
1
1
  import { governingWorkItemDeliveries, integrationAttemptRequiresSettlement, workItemDeliverySettled } from "../integration/deliveryObligation.js";
2
+ import { isLeaderSteer, taskMessageInboxReceipt } from "../message/taskInboxOwnership.js";
2
3
  import { operationalTaskRecords } from "./taskRecordRetirement.js";
3
- /** Mailbox refs locate original durable user intent. No separate acknowledgement
4
- * state is introduced: finish the current native turn and let the existing
5
- * notification delivery present the new input before declaring completion. */
4
+ /** Mailbox refs locate original durable user intent. Active inbox handling
5
+ * retains the same completion fence until the exact window is acknowledged. */
6
6
  export function pendingCompletionMessages(store, taskId) {
7
7
  const mailbox = store.getWorkMailbox({ kind: "role", taskId, roleName: "leader" });
8
8
  const refs = [...(mailbox?.pending?.refs ?? [])];
9
+ for (const wake of store.listTaskWakes(taskId)) {
10
+ if (wake.inbox?.ack === undefined)
11
+ refs.push(...(wake.refs ?? []));
12
+ }
9
13
  const processing = mailbox?.processing;
14
+ if (processing?.owner.startsWith("agent-inbox:"))
15
+ refs.push(...processing.batch.refs);
10
16
  if (processing?.owner.startsWith("leader-notification:")) {
11
17
  const native = store.getTaskRoleSessionSet(taskId, "leader")?.providerBinding?.run;
12
18
  if (native?.attemptId !== processing.batchId
@@ -16,7 +22,8 @@ export function pendingCompletionMessages(store, taskId) {
16
22
  }
17
23
  const ids = new Set(refs.filter(ref => ref.type === "message" && ref.taskId === taskId).map(ref => ref.id));
18
24
  return operationalTaskRecords(store.listMessages(taskId), store.listEvents(taskId), "message")
19
- .filter(message => ids.has(message.id)
25
+ .filter(message => (ids.has(message.id) || isLeaderSteer(message) && message.inbox !== undefined)
26
+ && taskMessageInboxReceipt(store, message)?.ack === undefined
20
27
  && (message.kind === "user" || message.kind === "operator"));
21
28
  }
22
29
  const ACTIVE_JOB_STATUSES = new Set([
@@ -34,8 +41,8 @@ export function projectCompletionReadiness(facts) {
34
41
  blockers.push({
35
42
  code: "pending-user-input",
36
43
  ref: ref("message", message.id),
37
- reason: `User message ${message.id} is still awaiting Leader delivery.`,
38
- fix: `finish the current native turn, read the next notification and message ${message.id}, then reconsider completion`
44
+ reason: `User message ${message.id} is still awaiting Leader delivery or explicit handling.`,
45
+ fix: `read message ${message.id} and its inbox window; claim and acknowledge after handling, or finish this turn if delivery is still pending`
39
46
  });
40
47
  }
41
48
  // A pending/running Task-final Review must be resumed or blocked first.
@@ -1,4 +1,4 @@
1
- import Database from "better-sqlite3";
1
+ import Database from "../sqlite/driver.js";
2
2
  import { existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { CURRENT_DATABASE_FILENAME as COMMITTED_DATABASE_FILENAME } from "../storage/currentTaskStore.js";