@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
@@ -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,
@@ -259,15 +259,10 @@ export class FileRoleLaunchPlanner {
259
259
  const existingSession = owner.scope === "task"
260
260
  ? this.store.getTaskRoleSessionSet(owner.taskId, role.name)?.sessions[input.agentId]
261
261
  : this.store.getGlobalRoleSessionSet(role.name)?.sessions[input.agentId];
262
- // A resume carries the Session's recorded generation forward unchanged. This
263
- // planner runs in the Controller, which after an upgrade is already the new
264
- // code, so it cannot speak for the still-running Agent Host that owns the
265
- // Session: rejecting here would end live Sessions on Controller upgrade,
266
- // while rewriting to current code would silently move a Session onto code it
267
- // never started on. The Host that actually executes the implementation makes
268
- // the decision — a reused live Host accepts its own generation, and a newly
269
- // started Host running different code fails closed
270
- // (`agentEndpointOwnership.pin` -> `requireBuiltinAgentEndpointImplementation`).
262
+ // Retain original launch provenance. The Host reports its actually loaded
263
+ // build separately and checks adapter/native identity, not build equality.
264
+ // A compatible new Host can resume this exact Session after safe release;
265
+ // this planner never silently replaces a live Host or native conversation.
271
266
  const endpointImplementation = input.mode === "resume"
272
267
  ? validateAgentEndpointImplementation(existingSession.endpointImplementation)
273
268
  : builtinAgentEndpointImplementation(binding.adapterId);
@@ -337,12 +332,10 @@ export class FileRoleLaunchPlanner {
337
332
  Object.assign(launchEnvironment, runtimeIsolation === undefined
338
333
  ? {}
339
334
  : 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 });
335
+ // Frozen packages describe historical materialization, not Session identity
336
+ // or current instruction availability. Never require or reconstruct them
337
+ // when reconnecting a valid old Session.
338
+ const baseSessionContext = compileRoleSessionContext(this.home, launchRole, owner, sessionPolicy);
346
339
  const bootstrap = materializeSessionBootstrap({
347
340
  yuiHome: this.home,
348
341
  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
  };
@@ -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,62 @@
1
+ import { requireKnownFields, requireText, requireTimestamp } from "../domain/validation.js";
2
+ import { isDeepStrictEqual } from "node:util";
3
+ /** Semantic handling and transport delivery are independent facts. Projections
4
+ * must not infer handling from delivery, a native terminal, or an empty queue. */
5
+ export function inboxHandlingState(receipt) {
6
+ return receipt?.ack !== undefined ? "handled" : receipt !== undefined ? "claimed-unacknowledged" : "unclaimed";
7
+ }
8
+ export function isGlobalInboxMessage(message) {
9
+ return message.inputControl !== undefined || message.interruptThen !== undefined;
10
+ }
11
+ /** Unhandled own-role input, including accepted and unknown delivery. This is
12
+ * NOT the scheduler's deliverable queue; selecting it never authorizes replay. */
13
+ export function pendingGlobalInboxMessages(messages) {
14
+ return messages.filter(message => isGlobalInboxMessage(message) && inboxHandlingState(message.inbox) !== "handled");
15
+ }
16
+ /** Original message evidence only. A missing receipt is not proof of
17
+ * non-submission; admission additionally inspects the exact native attempt. */
18
+ export function globalInboxDeliveryState(message) {
19
+ return message.control?.outcome ?? (message.delivery?.via === "provider" ? "accepted"
20
+ : message.delivery === undefined ? "unobserved" : "delivery-unknown");
21
+ }
22
+ export function validateInboxReceipt(receipt) {
23
+ if (receipt === undefined)
24
+ return;
25
+ requireKnownFields(receipt, ["claim", "ack"], "Inbox receipt");
26
+ for (const [kind, entry] of Object.entries(receipt)) {
27
+ requireKnownFields(entry, kind === "claim" ? ["requestId", "actor", "at"]
28
+ : ["requestId", "actor", "at", "reason"], "Inbox action");
29
+ requireText(entry.requestId, "Inbox request id");
30
+ requireTimestamp(entry.at, "Inbox timestamp");
31
+ const actor = entry.actor;
32
+ requireKnownFields(actor, ["scope", "taskId", "roleName", "agentId", "nativeSessionId", "runId"], "Inbox actor");
33
+ if (!["task", "global"].includes(actor.scope)
34
+ || (actor.scope === "task") !== (actor.taskId !== undefined))
35
+ throw new Error("Inbox actor scope is invalid.");
36
+ for (const value of Object.values(actor))
37
+ requireText(value, "Inbox actor identity");
38
+ if (kind === "ack")
39
+ requireText(entry.reason, "Inbox disposition");
40
+ }
41
+ if (!receipt.claim)
42
+ throw new Error("Inbox receipt requires its original claim.");
43
+ }
44
+ /** A stale automatic writer cannot erase an active claim between its read and
45
+ * Host admission. The original claim and any ack are immutable audit facts. */
46
+ export function assertInboxReceiptPreserved(previous, next) {
47
+ if (previous !== undefined && (!isDeepStrictEqual(previous.claim, next?.claim)
48
+ || previous.ack !== undefined && !isDeepStrictEqual(previous.ack, next?.ack))) {
49
+ throw new Error("Inbox handling ownership changed; reread the original instead of overwriting its receipt.");
50
+ }
51
+ }
52
+ /** A receipt cannot silently acknowledge different content under the same ID.
53
+ * Operational delivery/Assignment fields may evolve with their existing
54
+ * guards, but handled originals must be submitted as new Messages to change.
55
+ */
56
+ export function assertInboxOriginalPreserved(previous, next) {
57
+ assertInboxReceiptPreserved(previous?.inbox, next.inbox);
58
+ if (previous?.inbox !== undefined && (previous.body !== next.body || previous.kind !== next.kind
59
+ || !isDeepStrictEqual(previous.author, next.author) || !isDeepStrictEqual(previous.inputControl, next.inputControl))) {
60
+ throw new Error("Inbox receipt binds the exact original Message; submit changed content as a new Message.");
61
+ }
62
+ }
@@ -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,43 @@
1
+ import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
2
+ /** Pin mutable originals by their existing event identity, not wall-clock time.
3
+ * Both automatic notification and active consumption use the same fixed refs. */
4
+ export function pinMessageRevisions(store, taskId, refs) {
5
+ const pinned = [...refs];
6
+ for (const ref of refs) {
7
+ if (ref.type !== "message" || ref.taskId !== taskId)
8
+ continue;
9
+ const message = store.listMessages(taskId).find(message => message.id === ref.id);
10
+ const revision = message === undefined ? undefined : latestMessageUpdate(store, message);
11
+ if (revision !== undefined && !pinned.some(ref => ref.type === "event" && ref.id === revision.id)) {
12
+ pinned.push({ type: "event", taskId, id: revision.id });
13
+ }
14
+ }
15
+ return pinned;
16
+ }
17
+ export function latestMessageUpdate(store, message) {
18
+ return store.listEvents(message.taskId).filter(event => event.type === "message.updated" && event.payload.messageId === message.id).at(-1);
19
+ }
20
+ /** Leader steer has no Assignment recipient; ordinary Task records are not
21
+ * private inbox messages. Queued Leader inputs remain fixed wake windows. */
22
+ export function isLeaderSteer(message) {
23
+ return message.recipient === undefined && message.inputControl?.action === "steer";
24
+ }
25
+ /** Derive ownership from original receipts, never from disposable mailboxes. */
26
+ export function taskMessageInboxReceipt(store, message) {
27
+ return message.inbox ?? messageRevisionWakes(store, message).find(wake => wake.inbox !== undefined)?.inbox;
28
+ }
29
+ /** Delivery suppresses repeat notification of that revision, but remains
30
+ * distinct from handling. The original fixed window stays in the own inbox. */
31
+ export function deliveredMessageWake(store, message) {
32
+ return messageRevisionWakes(store, message).find(wake => wake.status === "consumed");
33
+ }
34
+ function messageRevisionWakes(store, message) {
35
+ const revision = latestMessageUpdate(store, message);
36
+ return store.listTaskWakes(message.taskId).filter(wake => (revision === undefined || wake.refs?.some(ref => ref.type === "event" && ref.taskId === message.taskId && ref.id === revision.id))
37
+ && wake.refs?.some(ref => ref.type === "message" && ref.taskId === message.taskId && ref.id === message.id));
38
+ }
39
+ export function unfinishedLeaderInbox(store, taskId) {
40
+ return store.listTaskWakes(taskId).some(wake => wake.inbox !== undefined && wake.inbox.ack === undefined)
41
+ || operationalTaskRecords(store.listMessages(taskId), store.listEvents(taskId), "message").some(message => isLeaderSteer(message)
42
+ && message.inbox !== undefined && message.inbox.ack === undefined);
43
+ }