@zq-silk/yui 2.6.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 (72) hide show
  1. package/README.md +23 -0
  2. package/dist/cli/commandCatalog.js +28 -2
  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 +32 -4
  7. package/dist/context/currentSkills.js +48 -0
  8. package/dist/context/roleSessionContext.js +13 -16
  9. package/dist/context/runContextPack.js +2 -0
  10. package/dist/context/runInputContract.js +1 -1
  11. package/dist/context/sessionBootstrapManifest.js +27 -4
  12. package/dist/context/skillCatalog.js +2 -3
  13. package/dist/context/taskContext.js +2 -0
  14. package/dist/controlPlaneCli.js +18 -1
  15. package/dist/controller/controller.js +3 -2
  16. package/dist/controller/fileSchedulerStoreAdapter.js +19 -4
  17. package/dist/controller/globalInputDelivery.js +73 -15
  18. package/dist/controller/globalRuntimeAttention.js +1 -1
  19. package/dist/controller/operatorNotification.js +6 -1
  20. package/dist/controller/providerRetryDelivery.js +3 -5
  21. package/dist/controller/runtime.js +2 -2
  22. package/dist/doctor/doctor.js +25 -2
  23. package/dist/executor/effectiveLaunch.js +2 -2
  24. package/dist/executor/fileRoleLaunchPlanner.js +4 -6
  25. package/dist/interaction/notificationPresentation.js +2 -2
  26. package/dist/interaction/operatorPresentation.js +1 -1
  27. package/dist/message/globalInputBacklog.js +69 -0
  28. package/dist/message/globalMessageQuery.js +1 -0
  29. package/dist/message/inboxReceipt.js +32 -0
  30. package/dist/message/message.js +6 -1
  31. package/dist/message/messageContinuation.js +34 -13
  32. package/dist/message/taskInboxOwnership.js +21 -0
  33. package/dist/nativeAgent/session/backends.js +1 -1
  34. package/dist/observability/runtimeIdentity.js +1 -1
  35. package/dist/resources/sqliteResourceRegistry.js +1 -1
  36. package/dist/runtime/agentHost.js +105 -16
  37. package/dist/scheduler/operatorEvent.js +49 -0
  38. package/dist/scheduler/operatorInputNotificationProcessor.js +5 -4
  39. package/dist/scheduler/taskWake.js +5 -0
  40. package/dist/sqlite/driver.js +108 -0
  41. package/dist/storage/sqliteSchema.js +11 -1
  42. package/dist/storage/sqliteStore.js +60 -1
  43. package/dist/storage/storageSchema.js +1 -1
  44. package/dist/storage/storageVersions.js +1 -1
  45. package/dist/storage/storeRpc.js +1 -0
  46. package/dist/storage/upgrade/upgradeOrchestrator.js +1 -1
  47. package/dist/task/completionReadiness.js +11 -4
  48. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  49. package/docs/managed-turn-and-session-runtime.md +81 -5
  50. package/docs/managed-turn-and-session-runtime.zh-CN.md +51 -3
  51. package/docs/release-workflow.md +47 -2
  52. package/docs/release-workflow.zh-CN.md +34 -1
  53. package/docs/storage-baseline.md +17 -7
  54. package/docs/storage-baseline.zh-CN.md +11 -5
  55. package/docs/task-delivery.md +17 -8
  56. package/docs/task-delivery.zh-CN.md +11 -5
  57. package/docs/testing/verification-levels.md +22 -4
  58. package/docs/testing/verification-levels.zh-CN.md +8 -1
  59. package/i18n/README.zh-CN.md +19 -0
  60. package/package.json +5 -2
  61. package/skills/yui-leader/SKILL.md +28 -11
  62. package/skills/yui-leader/VERSION +1 -0
  63. package/skills/yui-operator/SKILL.md +10 -2
  64. package/skills/yui-operator/VERSION +1 -0
  65. package/skills/yui-operator/references/task-delivery.md +2 -0
  66. package/skills/yui-reviewer/SKILL.md +8 -1
  67. package/skills/yui-reviewer/VERSION +1 -0
  68. package/skills/yui-runtime/SKILL.md +94 -11
  69. package/skills/yui-runtime/VERSION +1 -0
  70. package/skills/yui-runtime/references/publication.md +16 -5
  71. package/skills/yui-worker/SKILL.md +8 -1
  72. package/skills/yui-worker/VERSION +1 -0
@@ -0,0 +1,48 @@
1
+ import { accessSync, constants, lstatSync, readFileSync, realpathSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ export const BUILTIN_YUI_SKILLS = new Set([
5
+ "yui-runtime", "yui-operator", "yui-leader", "yui-worker", "yui-reviewer"
6
+ ]);
7
+ export function currentSkillPath(home, id) {
8
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/u.test(id))
9
+ throw new Error(`Invalid configured Skill id: ${id}.`);
10
+ if (BUILTIN_YUI_SKILLS.has(id))
11
+ return fileURLToPath(new URL(`../../skills/${id}`, import.meta.url));
12
+ if (home === undefined)
13
+ throw new Error("YUI_HOME is required to load configured Role skills.");
14
+ return resolve(home, "skills", id);
15
+ }
16
+ /** Per-turn metadata only: never read the body, traverse resources, snapshot a
17
+ * package, or claim that the model still has instructions in its context. */
18
+ export function checkCurrentSkills(home, ids) {
19
+ return [...new Set(ids)].map(id => {
20
+ const path = currentSkillPath(home, id);
21
+ const versionPath = join(path, "VERSION");
22
+ try {
23
+ for (const file of [join(path, "SKILL.md"), versionPath]) {
24
+ if (realpathSync(file) !== resolve(file) || !lstatSync(file).isFile()) {
25
+ throw new Error(`Skill files must be regular files without symlinks: ${file}.`);
26
+ }
27
+ accessSync(file, constants.R_OK);
28
+ }
29
+ if (lstatSync(versionPath).size > 128)
30
+ throw new Error("VERSION exceeds 128 bytes.");
31
+ const version = readFileSync(versionPath, "utf8").trim();
32
+ if (!/^[1-9][0-9]{0,8}$/u.test(version))
33
+ throw new Error("VERSION must contain a positive internal integer.");
34
+ return { id, path, version, versionPath };
35
+ }
36
+ catch (error) {
37
+ throw new Error(`Current Skill ${id} is missing, unreadable or invalid at ${path}. `
38
+ + `${error instanceof Error ? error.message : String(error)} `
39
+ + "Restore the current installation/configured Skill (including VERSION) in this Home, then check again; "
40
+ + "do not replace the Session or replay input to repair Skill loading.", { cause: error });
41
+ }
42
+ });
43
+ }
44
+ export const SKILL_CHECK_COMMAND = "yui session skills --json";
45
+ export const SKILL_REUSE_RULE = "At the start of every turn, check current Skill versions and availability. "
46
+ + "Read full SKILL.md and necessary references on first entry, version change, recovery, "
47
+ + "or missing/compacted/uncertain context; reuse only unchanged full instructions available now. "
48
+ + "A version check is not a loading receipt or authority.";
@@ -3,29 +3,18 @@ import { readFileSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { snapshotSkillPackage, verifySkillPackage } from "./skillPackage.js";
6
+ import { BUILTIN_YUI_SKILLS, checkCurrentSkills } from "./currentSkills.js";
7
+ export { BUILTIN_YUI_SKILLS } from "./currentSkills.js";
6
8
  import { SYSTEM_LEADER_ROLE, SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
7
- export const BUILTIN_YUI_SKILLS = new Set([
8
- "yui-runtime",
9
- "yui-operator",
10
- "yui-leader",
11
- "yui-worker",
12
- "yui-reviewer"
13
- ]);
14
9
  /**
15
10
  * Compiles stable Role policy for an Agent-native instruction channel.
16
11
  * Dynamic wakeups and AgentRun assignments deliberately remain outside this value.
17
12
  */
18
13
  export function compileRoleSessionContext(yuiHome, role, owner, options = {}) {
19
14
  const kind = roleSessionKind(role, owner, options.purpose ?? "execution");
20
- const builtInSkillId = kind === "global" ? undefined : `yui-${kind}`;
21
- const skillIds = unique([
22
- "yui-runtime",
23
- ...(builtInSkillId === undefined ? [] : [builtInSkillId]),
24
- ...(role.skills ?? [])
25
- ]);
26
- const skills = options.skillPackages === undefined
27
- ? loadYuiSkillContexts(yuiHome, skillIds)
28
- : options.skillPackages.map(loadSkillPackageContext);
15
+ const skills = checkCurrentSkills(yuiHome, roleSkillIds(role, kind)).map(skill => ({
16
+ ...skill, content: readFileSync(join(skill.path, "SKILL.md"), "utf8").trim()
17
+ }));
29
18
  return {
30
19
  developerInstructions: renderDeveloperInstructions(kind, role, owner),
31
20
  skills,
@@ -34,6 +23,14 @@ export function compileRoleSessionContext(yuiHome, role, owner, options = {}) {
34
23
  : { managedContextFile: roleContextFile(yuiHome, role, owner, kind) })
35
24
  };
36
25
  }
26
+ export function roleSkillIds(role, kind) {
27
+ const builtInSkillId = kind === "global" ? undefined : `yui-${kind}`;
28
+ return unique([
29
+ "yui-runtime",
30
+ ...(builtInSkillId === undefined ? [] : [builtInSkillId]),
31
+ ...(role.skills ?? [])
32
+ ]);
33
+ }
37
34
  function renderDeveloperInstructions(kind, role, owner) {
38
35
  const core = renderRoleCore(kind, role, owner);
39
36
  const profile = [
@@ -344,6 +344,8 @@ export function buildRunContextPack(store, taskId, runId) {
344
344
  const preliminaryBytes = Buffer.byteLength(JSON.stringify({ ...body, digest }), "utf8");
345
345
  const pack = Object.freeze({
346
346
  ...body,
347
+ // Current loading guidance is not part of the frozen Assignment digest.
348
+ skillCheckCommand: "yui session skills --json",
347
349
  inventory: Object.freeze({
348
350
  refs: pointers.length,
349
351
  frozenBytes: preliminaryBytes,
@@ -79,7 +79,7 @@ export function serializeRunInputEnvelope(value) {
79
79
  .join(",");
80
80
  const snapshot = normalized.contextSnapshotRef;
81
81
  const lines = [
82
- "Yui AgentRun input. Follow the Session Manifest and injected Skills.",
82
+ "Yui AgentRun input. Check current Skills: yui session skills --json. Follow the Session Manifest profile and current Skills.",
83
83
  `${normalized.subject.taskId === undefined ? "" : `task=${normalized.subject.taskId} `}run=${normalized.runId} role=${normalized.roleName}`,
84
84
  `source=${normalized.source.type}/${normalized.source.channel} purpose=${normalized.purpose} subject=${subject || "global"} snapshot=${snapshot === undefined ? "none" : `${snapshot.id}@${snapshot.digest}`}`,
85
85
  normalized.deltaRefIds.length === 0
@@ -3,6 +3,7 @@ import { chmodSync, readFileSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { writeTextFileAtomically } from "../storage/durableFile.js";
5
5
  import { validateSkillPackageRef } from "./skillPackage.js";
6
+ import { checkCurrentSkills, SKILL_CHECK_COMMAND, SKILL_REUSE_RULE } from "./currentSkills.js";
6
7
  import { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
7
8
  export { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
8
9
  /** Portable read pointers accompany every managed input, including the first
@@ -20,9 +21,22 @@ export function withSessionContextPointer(text, environment) {
20
21
  /** No remembered/sent flag can prove instructions survived context compaction. */
21
22
  export function sessionContextInstructions(manifestPath, digest) {
22
23
  return `Yui Session Manifest: ${manifestPath}${digest === undefined ? "" : ` (digest ${digest})`}. `
23
- + "Apply yui-runtime and its Role Skills/profile by exact manifest paths. "
24
- + "Load on first entry, version change or missing context; reuse only same-version instructions "
25
- + "fully available now, never infer availability from prior reads.";
24
+ + `Before work on every turn run the Manifest's contextProtocol.skillCheckCommand `
25
+ + `(older Manifests: ${SKILL_CHECK_COMMAND}; Global loadCommand also checks current Skills). `
26
+ + "Use its current stable Skill paths instead of historical frozen-package paths; preserve the Manifest's Role profile. "
27
+ + SKILL_REUSE_RULE;
28
+ }
29
+ /** The caller must already pass the ordinary managed Session/CLI/Home checks.
30
+ * Old manifests remain immutable audit records, while this read resolves their
31
+ * Skill IDs against the currently executing installation and Home. */
32
+ export function checkSessionSkills(home, manifestPath) {
33
+ const manifest = readSessionBootstrapManifest(manifestPath);
34
+ return {
35
+ skills: checkCurrentSkills(home, manifest.skills.map(skill => skill.id)),
36
+ roleProfileRef: manifest.roleProfileRef,
37
+ rule: SKILL_REUSE_RULE,
38
+ historicalPackagesAreAuditOnly: true
39
+ };
26
40
  }
27
41
  /** Read back one immutable Session Manifest and verify its content digest. */
28
42
  export function readSessionBootstrapManifest(path) {
@@ -76,6 +90,10 @@ export function readSessionBootstrapManifest(path) {
76
90
  requireText(entry.id, "Session Manifest Skill id");
77
91
  requireText(entry.path, "Session Manifest Skill path");
78
92
  requireDigest(entry.digest, "Session Manifest Skill digest");
93
+ if (entry.version !== undefined) {
94
+ requireText(entry.version, "Session Manifest Skill version");
95
+ requireText(entry.versionPath, "Session Manifest Skill version path");
96
+ }
79
97
  if (entry.package !== undefined) {
80
98
  const ref = entry.package;
81
99
  validateSkillPackageRef(ref);
@@ -121,6 +139,7 @@ export function materializeSessionBootstrap(input) {
121
139
  id: skill.id,
122
140
  path: skill.path,
123
141
  digest: skill.package?.digest ?? digest(skill.content),
142
+ ...(skill.version === undefined ? {} : { version: skill.version, versionPath: skill.versionPath }),
124
143
  ...(skill.package === undefined ? {} : { package: skill.package })
125
144
  })),
126
145
  roleProfileRef: { digest: profileDigest, path: roleProfilePath },
@@ -129,11 +148,15 @@ export function materializeSessionBootstrap(input) {
129
148
  // A Global Session may move between Yui's remote TUI and Desktop.
130
149
  // Carry its read entry in the Thread-visible Manifest instead of
131
150
  // depending on the client process that happened to create it.
132
- loadCommand: renderGlobalContextCommand(input.entryPoint, home, input.role.name)
151
+ loadCommand: renderGlobalContextCommand(input.entryPoint, home, input.role.name),
152
+ // Same self-contained read remains usable from Desktop without the
153
+ // launching shell's managed environment or PATH.
154
+ skillCheckCommand: renderGlobalContextCommand(input.entryPoint, home, input.role.name)
133
155
  }
134
156
  : {
135
157
  loadCommand: `yui task run context ${quoteShellWord(`${input.owner.taskId}/<run-id>`)} --json`,
136
158
  expandCommand: `yui task run context expand ${quoteShellWord(`${input.owner.taskId}/<run-id>`)} <ref-id> --store <store> --mode full --json`,
159
+ skillCheckCommand: SKILL_CHECK_COMMAND,
137
160
  ...(input.role.name === "leader" ? {
138
161
  currentCommand: `yui task context ${quoteShellWord(input.owner.taskId)} --json`
139
162
  } : {})
@@ -1,9 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
3
3
  import { dirname, join, relative, resolve, sep } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
4
  import { verifySkillPackage } from "./skillPackage.js";
6
- import { BUILTIN_YUI_SKILLS } from "./roleSessionContext.js";
5
+ import { BUILTIN_YUI_SKILLS, currentSkillPath } from "./currentSkills.js";
7
6
  const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_-]*$/u;
8
7
  const MAX_TEXT = 256 * 1024;
9
8
  const digest = (data) => createHash("sha256").update(data).digest("hex");
@@ -13,7 +12,7 @@ export function skillSource(home, id) {
13
12
  throw new Error("Invalid Skill id.");
14
13
  const builtin = BUILTIN_YUI_SKILLS.has(id);
15
14
  return { kind: builtin ? "builtin" : "configured",
16
- path: builtin ? fileURLToPath(new URL(`../../skills/${id}`, import.meta.url)) : resolve(home, "skills", id) };
15
+ path: currentSkillPath(home, id) };
17
16
  }
18
17
  function physical(path) {
19
18
  if (realpathSync(path) !== resolve(path))
@@ -67,6 +67,8 @@ export function readTaskContext(store, taskId, environment = {}) {
67
67
  discovery: `task context list ${taskId} --store <store>`,
68
68
  commandDiscovery: {
69
69
  help: "yui help", describe: "yui --json help",
70
+ skillCheck: "yui session skills --json",
71
+ ownInbox: "yui inbox list --json",
70
72
  note: "Help and completion share the current caller's offline command view. Follow that view for operation-specific discovery; execution always revalidates authority."
71
73
  },
72
74
  detail: `task context inspect ${taskId} --store <store> --ref <refId> --digest <digest>`,
@@ -38,6 +38,7 @@ import { runDurableJobCommand } from "./commands/durableJobCommands.js";
38
38
  import { parseExecutionAuditOptions, runExecutionAuditCommand } from "./commands/executionAuditCommands.js";
39
39
  import { runGlobalRoleCommand, previewGlobalRoleAgentConfigurationMutation } from "./commands/globalRoleCommands.js";
40
40
  import { runJobCommand } from "./commands/jobCommands.js";
41
+ import { runInboxCommand } from "./commands/inboxCommands.js";
41
42
  import { applyOperatorSessionControl, runOperatorCommand } from "./commands/operatorCommands.js";
42
43
  import { previewProfileAgentConfigurationMutation, runProfileCommand } from "./commands/profileCommands.js";
43
44
  import { runProjectCommand } from "./commands/projectCommands.js";
@@ -61,7 +62,7 @@ import { runWorkflowCommandAsync } from "./commands/workflowCommands.js";
61
62
  import { FileCompletionManager } from "./completion/fileCompletionManager.js";
62
63
  import { CONFIG_DOMAINS } from "./config/configCatalog.js";
63
64
  import { resolveTmuxBin, resolveTmuxHistoryLimit } from "./config/yuiConfig.js";
64
- import { readSessionBootstrapManifest } from "./context/sessionBootstrapManifest.js";
65
+ import { checkSessionSkills, readSessionBootstrapManifest } from "./context/sessionBootstrapManifest.js";
65
66
  import { assertFileTaskControllerStorageCompatible, ensureFileTaskController, FileTaskWorkflowRuntime, refreshRunningFileTaskControllerConfiguration, refreshRunningFileTaskControllerEnvironment, restartFileTaskController, stopFileTaskController } from "./controller/clientRuntime.js";
66
67
  import { FileSchedulerStoreAdapter } from "./controller/fileSchedulerStoreAdapter.js";
67
68
  import { createControllerIntegrationJobPort } from "./controller/jobClient.js";
@@ -632,6 +633,11 @@ export async function main() {
632
633
  return;
633
634
  }
634
635
  assertTaskInvocationScope(resolved, process.env);
636
+ if (resolved[0] === "inbox") {
637
+ const result = runInboxCommand(resolved.slice(1), store, process.env);
638
+ emit(JSON.stringify(result, null, 2), false, result);
639
+ return;
640
+ }
635
641
  if (resolved[0] === "task" && resolved[1] === "role" && resolved[2] === "capabilities") {
636
642
  const query = parseTaskAgentCapabilityQuery(resolved.slice(3));
637
643
  const result = await callController(home, "task.role-capabilities", {
@@ -789,6 +795,17 @@ export async function main() {
789
795
  return;
790
796
  }
791
797
  if (resolved[0] === "session") {
798
+ if (resolved[1] === "skills") {
799
+ if (resolved.length !== 2)
800
+ throw usageError("Usage: yui session skills --json");
801
+ const path = process.env.YUI_SESSION_MANIFEST;
802
+ if (!["task", "global"].includes(process.env.YUI_SESSION_SCOPE ?? "") || path === undefined) {
803
+ throw usageError("Skill checks require the current managed Session and its Manifest.");
804
+ }
805
+ const data = checkSessionSkills(home, path);
806
+ emit(JSON.stringify(data, null, 2), false, data);
807
+ return;
808
+ }
792
809
  if (resolved[1] === "stop") {
793
810
  const options = parseSessionStopOptions(resolved.slice(2));
794
811
  const result = await runSessionStopCommand({
@@ -996,7 +996,7 @@ export class FileTaskController {
996
996
  }
997
997
  const firstRuntimeDrain = await this.#drainRuntimeEvents();
998
998
  await this.#providerRetry?.reconcile();
999
- await this.#globalInputDelivery?.();
999
+ await this.#globalInputDelivery?.({ reconcile: scope.kind === "full", now: this.#now() });
1000
1000
  for (const taskId of runtimeTaskFailureIds(firstRuntimeDrain)) {
1001
1001
  runtimeFailedTaskIds.add(taskId);
1002
1002
  }
@@ -1076,7 +1076,8 @@ export class FileTaskController {
1076
1076
  if (scope.kind === "full"
1077
1077
  || scope.keys.some((key) => key !== "operator")
1078
1078
  || runtimeAdvanced) {
1079
- this.#signalOperatorMailbox(runtimeAdvanced);
1079
+ this.#signalOperatorMailbox(runtimeAdvanced || scope.kind === "full"
1080
+ || scope.kind === "dirty" && scope.keys.some(key => parseMailboxKey(key).kind === "global-role"));
1080
1081
  }
1081
1082
  }
1082
1083
  catch (error) {
@@ -1,5 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
+ import { taskMessageInboxReceipt, unfinishedLeaderInbox } from "../message/taskInboxOwnership.js";
3
4
  import { contextSnapshotRef } from "../context/contextSnapshot.js";
4
5
  import { freezeRunContextSnapshot } from "../context/runContextPack.js";
5
6
  import { taskNotificationPresentation } from "../interaction/notificationPresentation.js";
@@ -30,6 +31,7 @@ import { standardAgentError } from "../runtime/agentError.js";
30
31
  import { recordTaskAgentError } from "./taskAgentError.js";
31
32
  import { recordGlobalRuntimeAttention, recordGlobalRuntimeFailure } from "./globalRuntimeAttention.js";
32
33
  import { queueOperatorNotification } from "./operatorNotification.js";
34
+ import { claimOperatorAttention } from "../scheduler/operatorEvent.js";
33
35
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
34
36
  import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_HOST_DETACH_REQUIRED_REASON, RUNTIME_LIFECYCLE_OWNER, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeCleanupReason, runtimeCleanupDisposition, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
35
37
  import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
@@ -882,6 +884,11 @@ export class FileSchedulerStoreAdapter {
882
884
  return [...this.store.listPendingActivationRequestTaskIds()].sort((left, right) => (left.localeCompare(right, undefined, { numeric: true })));
883
885
  }
884
886
  getTask(taskId) { return this.store.getTask(taskId); }
887
+ getEvent(taskId, eventId) {
888
+ return this.store.queryContextRecords(taskId, {
889
+ family: "task-event", ids: [eventId], limit: 1
890
+ }).records[0]?.value ?? null;
891
+ }
885
892
  getTaskWorkspace(taskId) { return this.store.getTaskWorkspace(taskId); }
886
893
  getTaskBrief(taskId) { return this.store.getTaskBrief(taskId); }
887
894
  listDecisions(taskId) { return this.store.listDecisions(taskId); }
@@ -1146,7 +1153,8 @@ export class FileSchedulerStoreAdapter {
1146
1153
  throw new AgentHostProviderSessionBusyError("The next Global input belongs to an explicit then handoff.");
1147
1154
  }
1148
1155
  if (message !== undefined) {
1149
- if (message.delivery !== undefined || message.notDelivered !== undefined
1156
+ if (store.queryGlobalRoleMessages(input.roleName, { handling: true, limit: 0 }).count > 0
1157
+ || message.inbox !== undefined || message.delivery !== undefined || message.notDelivered !== undefined
1150
1158
  || message.control !== undefined && message.control.outcome !== "rejected"
1151
1159
  || message.deliveryTarget?.agentId !== input.agentId
1152
1160
  || message.deliveryTarget?.nativeSessionId !== input.nativeSessionId) {
@@ -1567,7 +1575,7 @@ export class FileSchedulerStoreAdapter {
1567
1575
  if (mailbox.processing !== null) {
1568
1576
  return { status: "processing", processing: mailbox.processing };
1569
1577
  }
1570
- let claimed = claimPending(mailbox, {
1578
+ let claimed = (input.target.kind === "operator" ? claimOperatorAttention : claimPending)(mailbox, {
1571
1579
  batchId: input.batchId,
1572
1580
  owner: input.owner,
1573
1581
  startedAt: input.now.toISOString()
@@ -1737,7 +1745,7 @@ export class FileSchedulerStoreAdapter {
1737
1745
  clearPendingWakeup(taskId) { this.store.clearPendingWakeup(taskId); }
1738
1746
  getLeaderFailure(taskId) { return this.store.getLeaderFailure(taskId); }
1739
1747
  prepareMessageContinuations(taskId, now) {
1740
- const roles = new Set(this.store.listMessages(taskId).flatMap((message) => message.recipient?.ownerRunId !== undefined && message.continuation?.runId === undefined ? [message.recipient.roleName] : []));
1748
+ const roles = new Set(this.store.listMessages(taskId).flatMap((message) => message.inbox === undefined && message.recipient?.ownerRunId !== undefined && message.continuation?.runId === undefined ? [message.recipient.roleName] : []));
1741
1749
  for (const roleName of roles) {
1742
1750
  try {
1743
1751
  this.store.transaction((store) => prepareMessageContinuations(store, taskId, now, roleName));
@@ -1746,7 +1754,7 @@ export class FileSchedulerStoreAdapter {
1746
1754
  const reason = `preparation-failed: ${error instanceof Error ? error.message : String(error)}`.slice(0, 1000);
1747
1755
  this.store.transaction((store) => {
1748
1756
  for (const message of store.listMessages(taskId)) {
1749
- if (message.recipient?.roleName === roleName && message.recipient.ownerRunId !== undefined && message.continuation?.runId === undefined
1757
+ if (message.inbox === undefined && message.recipient?.roleName === roleName && message.recipient.ownerRunId !== undefined && message.continuation?.runId === undefined
1750
1758
  && message.continuation?.notDeliveredReason !== reason) {
1751
1759
  store.updateMessage(taskId, { ...message, continuation: { notDeliveredReason: reason } });
1752
1760
  }
@@ -1855,6 +1863,8 @@ export class FileSchedulerStoreAdapter {
1855
1863
  return { wakeId, attemptId: nextAttempt, disposition: "submit",
1856
1864
  presentation: taskNotificationPresentation(wake, store) };
1857
1865
  }
1866
+ if (unfinishedLeaderInbox(store, taskId))
1867
+ return null;
1858
1868
  if (mailbox.pending === null || (provider !== null && provider !== undefined
1859
1869
  && ["submitting", "accepted", "delivery-unknown"].includes(provider.status)))
1860
1870
  return null;
@@ -1864,6 +1874,11 @@ export class FileSchedulerStoreAdapter {
1864
1874
  const sessions = store.getTaskRoleSessionSet(taskId, "leader");
1865
1875
  const session = sessions?.sessions[sessions.activeAgentId];
1866
1876
  const obsoleteIds = new Set();
1877
+ for (const message of store.listMessages(taskId)) {
1878
+ if (pendingMessageIds.has(message.id) && taskMessageInboxReceipt(store, message) !== undefined) {
1879
+ obsoleteIds.add(message.id);
1880
+ }
1881
+ }
1867
1882
  for (const message of store.listMessages(taskId)) {
1868
1883
  if (!pendingMessageIds.has(message.id) || message.inputControl?.expectedSessionId === undefined)
1869
1884
  continue;
@@ -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 }),