@zq-silk/yui 0.15.8 → 0.15.11

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 (151) hide show
  1. package/ARCHITECTURE.md +2 -0
  2. package/ARCHITECTURE.zh-CN.md +151 -0
  3. package/README.md +211 -14
  4. package/dist/agent/launchEnvironment.js +7 -0
  5. package/dist/artifacts/artifactCapability.js +74 -0
  6. package/dist/artifacts/artifactCommitLock.js +249 -0
  7. package/dist/artifacts/artifactPaths.js +151 -0
  8. package/dist/artifacts/gitArtifactRef.js +146 -0
  9. package/dist/artifacts/managedGit.js +332 -0
  10. package/dist/artifacts/taskArtifactRepository.js +277 -0
  11. package/dist/cli/commandCatalog.js +40 -16
  12. package/dist/cli/interactionPolicy.js +3 -3
  13. package/dist/cli/updateOrchestrator.js +24 -1
  14. package/dist/cli/updatePorts.js +7 -3
  15. package/dist/cli/upgradeCommand.js +42 -2
  16. package/dist/cli.js +403 -93
  17. package/dist/commands/globalRoleCommands.js +314 -4
  18. package/dist/commands/operatorCommands.js +33 -2
  19. package/dist/commands/projectCommands.js +6 -7
  20. package/dist/commands/releaseCommands.js +18 -0
  21. package/dist/commands/taskActivationCommands.js +22 -0
  22. package/dist/commands/taskActor.js +25 -0
  23. package/dist/commands/taskCommands.js +846 -155
  24. package/dist/commands/taskIntegrationCommands.js +16 -38
  25. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  26. package/dist/commands/taskRemoteDeliveryCommand.js +6 -6
  27. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  28. package/dist/context/runContextPack.js +28 -16
  29. package/dist/context/taskContext.js +64 -5
  30. package/dist/controller/agentHostObservation.js +155 -0
  31. package/dist/controller/clientRuntime.js +17 -2
  32. package/dist/controller/controller.js +11 -2
  33. package/dist/controller/fileSchedulerStoreAdapter.js +446 -13
  34. package/dist/controller/globalInputDelivery.js +119 -0
  35. package/dist/controller/jobControl.js +6 -2
  36. package/dist/controller/resourceInventory.js +14 -4
  37. package/dist/controller/resourceInventoryLinux.js +2 -6
  38. package/dist/controller/runtime.js +81 -6
  39. package/dist/controller/runtimeEventInbox.js +32 -3
  40. package/dist/controller/runtimeEventProcessor.js +26 -6
  41. package/dist/controller/runtimeHookRunFence.js +75 -19
  42. package/dist/controller/structuredProviderObservation.js +133 -70
  43. package/dist/coordination/workMailboxQueue.js +5 -0
  44. package/dist/execution/workItemExecutionProjection.js +1 -1
  45. package/dist/executor/agentExecutor.js +64 -4
  46. package/dist/executor/executorRegistry.js +3 -0
  47. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  48. package/dist/integration/deliveryObligation.js +2 -1
  49. package/dist/integration/gitIntegrationService.js +312 -382
  50. package/dist/integration/integrationAttempt.js +30 -4
  51. package/dist/integration/integrationQueueService.js +7 -7
  52. package/dist/integration/integrationSourceApplication.js +323 -0
  53. package/dist/kernel/builtinCapabilities.js +32 -24
  54. package/dist/message/globalInterrupt.js +33 -0
  55. package/dist/message/inputControlResolution.js +106 -0
  56. package/dist/message/message.js +423 -0
  57. package/dist/message/messageContinuation.js +126 -3
  58. package/dist/message/taskInterrupt.js +34 -0
  59. package/dist/observability/orchestrationMetrics.js +1 -1
  60. package/dist/plugins/pluginService.js +11 -3
  61. package/dist/release/releaseHandover.js +22 -0
  62. package/dist/release/releaseWorkflowPorts.js +15 -7
  63. package/dist/repository/gitWorkspace.js +72 -15
  64. package/dist/repository/taskWorkspaceCoordinator.js +134 -0
  65. package/dist/repository/taskWorkspacePreparer.js +120 -49
  66. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  67. package/dist/resources/projectResource.js +0 -48
  68. package/dist/resources/projectResourceService.js +3 -81
  69. package/dist/resources/resourceDiscovery.js +3 -2
  70. package/dist/runtime/agentHost.js +152 -72
  71. package/dist/runtime/agentHostCompatibility.js +127 -0
  72. package/dist/runtime/agentHostProtocol.js +53 -0
  73. package/dist/runtime/executionEnvironment.js +0 -19
  74. package/dist/runtime/launchBroker.js +6 -0
  75. package/dist/runtime/sessionReconciliation.js +4 -4
  76. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  77. package/dist/runtime/tmuxAdapters.js +5 -3
  78. package/dist/scheduler/operatorEvent.js +4 -0
  79. package/dist/scheduler/taskExecutionProjection.js +12 -1
  80. package/dist/scheduler/wakeReason.js +7 -1
  81. package/dist/scheduler/wakeupQueue.js +2 -0
  82. package/dist/setup/setupCommand.js +29 -16
  83. package/dist/storage/homeLayout.js +130 -0
  84. package/dist/storage/migrations/artifactsToGit.js +338 -0
  85. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  86. package/dist/storage/migrations/integrationContinuation.js +104 -0
  87. package/dist/storage/migrations/submitIntent.js +126 -0
  88. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  89. package/dist/storage/sqliteSchema.js +173 -7
  90. package/dist/storage/sqliteStore.js +41 -22
  91. package/dist/storage/storageVersions.js +1 -1
  92. package/dist/storage/storeRpc.js +2 -1
  93. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  94. package/dist/task/archiveDiagnostics.js +128 -0
  95. package/dist/task/nextAction.js +44 -11
  96. package/dist/task/taskActivation.js +26 -0
  97. package/dist/task/taskActivationService.js +85 -69
  98. package/dist/task/taskSubmission.js +236 -0
  99. package/dist/web/assets/client/app.js +58 -2
  100. package/dist/web/assets/client/components.js +1 -0
  101. package/dist/web/assets/client/i18n.js +6 -0
  102. package/dist/web/assets/client/taskSurface.js +202 -7
  103. package/dist/web/assets/client/view.js +7 -4
  104. package/dist/web/assets/shell.js +23 -0
  105. package/dist/web/assets/styles/layout.js +1 -1
  106. package/dist/web/assets/styles/widgets.js +12 -0
  107. package/dist/web/webServer.js +135 -4
  108. package/dist/web/webSnapshot.js +4 -3
  109. package/dist/web/webTaskSurface.js +225 -8
  110. package/dist/workItem/workItem.js +14 -10
  111. package/dist/workspace/workItemChangeSetManager.js +18 -2
  112. package/docs/agent-result-consumption.md +2 -0
  113. package/docs/agent-result-consumption.zh-CN.md +81 -0
  114. package/docs/agent-runtime-drivers.md +2 -0
  115. package/docs/agent-runtime-drivers.zh-CN.md +77 -0
  116. package/docs/architecture/README.md +44 -32
  117. package/docs/architecture/README.zh-CN.md +43 -0
  118. package/docs/architecture/capabilities-and-resources.md +118 -79
  119. package/docs/architecture/capabilities-and-resources.zh-CN.md +83 -0
  120. package/docs/managed-turn-and-session-runtime.md +2 -0
  121. package/docs/managed-turn-and-session-runtime.zh-CN.md +180 -0
  122. package/docs/observability/README.md +2 -0
  123. package/docs/observability/README.zh-CN.md +71 -0
  124. package/docs/plugin-sdk.md +320 -217
  125. package/docs/plugin-sdk.zh-CN.md +293 -0
  126. package/docs/provider-runtime.md +2 -0
  127. package/docs/provider-runtime.zh-CN.md +132 -0
  128. package/docs/release-workflow.md +41 -0
  129. package/docs/release-workflow.zh-CN.md +266 -0
  130. package/docs/roles-and-configuration.md +2 -0
  131. package/docs/roles-and-configuration.zh-CN.md +96 -0
  132. package/docs/sqlite-control-plane-design.md +225 -1
  133. package/docs/sqlite-control-plane-design.zh-CN.md +62 -0
  134. package/docs/task-dag-semantics.md +80 -57
  135. package/docs/task-dag-semantics.zh-CN.md +59 -0
  136. package/docs/task-delivery.md +2 -0
  137. package/docs/task-delivery.zh-CN.md +82 -0
  138. package/docs/task-local-identity.md +2 -0
  139. package/docs/task-local-identity.zh-CN.md +58 -0
  140. package/docs/testing/verification-levels.md +26 -0
  141. package/docs/testing/verification-levels.zh-CN.md +80 -0
  142. package/i18n/README.zh-CN.md +199 -10
  143. package/package.json +2 -1
  144. package/skills/yui-leader/SKILL.md +88 -331
  145. package/skills/yui-leader/references/execution.md +405 -0
  146. package/skills/yui-leader/references/integration.md +52 -2
  147. package/skills/yui-leader/references/planning.md +109 -0
  148. package/skills/yui-leader/references/task-plugins.md +8 -4
  149. package/skills/yui-operator/SKILL.md +22 -4
  150. package/skills/yui-runtime/SKILL.md +27 -0
  151. package/skills/yui-runtime/references/publication.md +20 -0
@@ -0,0 +1,106 @@
1
+ import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
2
+ /**
3
+ * Turn statuses that mean a Turn record is present and not yet terminal. This is
4
+ * only "there is something here", not "it is safe to target": `submitting` is a
5
+ * call still in flight and `delivery-unknown` is an outbound whose acceptance was
6
+ * never proven. Both are separated from `accepted` below (decision-3 §3/§8) so an
7
+ * unconfirmed delivery is reported as DELIVERY_UNKNOWN, never silently steered or
8
+ * cancelled as if it were a clean active Turn.
9
+ */
10
+ const PRESENT_TURN_STATUSES = new Set(["submitting", "accepted", "delivery-unknown"]);
11
+ /**
12
+ * The scope-generic core. It takes an already-fetched Session set and the owner
13
+ * decoration for a resolved target, so Task and Global callers share the exact
14
+ * same capability, present-Turn, authority, and target-match logic.
15
+ *
16
+ * @param decorate Adds the owner-specific fields to a resolved target. For a
17
+ * Task Role it attaches the taskId; for a Global Role it attaches nothing.
18
+ */
19
+ function resolveInputControl(sessions, kind, expectedTarget, roleName) {
20
+ const active = sessions?.sessions[sessions.activeAgentId];
21
+ // Capability is read from the declared Driver of the current active Session,
22
+ // never hardcoded by Agent name (decision-3 §7). When there is no active
23
+ // Session at all there is also no current Turn to target.
24
+ if (sessions == null || active === undefined || active.status !== "active") {
25
+ return { outcome: "no-active-turn", code: "NO_ACTIVE_TURN",
26
+ detail: "No active native Session holds a current Turn for this Role." };
27
+ }
28
+ const capabilities = builtinAgentDriverRegistry().requireByAdapterId(active.adapterId).capabilities;
29
+ if (kind === "steer" && capabilities.input.steer !== "fenced") {
30
+ return { outcome: "unsupported", code: "STEER_UNSUPPORTED",
31
+ detail: `Agent plan '${active.adapterId}' cannot steer its current Turn; `
32
+ + "the input is saved for an explicit interrupt --then-message or a queue." };
33
+ }
34
+ if (kind === "interrupt" && capabilities.control.interruptDelivery !== "native") {
35
+ return { outcome: "unsupported", code: "INTERRUPT_UNSUPPORTED",
36
+ detail: `Agent plan '${active.adapterId}' has no native interrupt; Yui will not `
37
+ + "stop the owned process, kill, restart, or detach it." };
38
+ }
39
+ // A Global Role now carries the same optional providerBinding as a Task Role;
40
+ // both read it the same way. Its absence is "no current Turn", never an error.
41
+ const binding = "providerBinding" in sessions ? sessions.providerBinding ?? null : null;
42
+ const turn = binding?.run ?? null;
43
+ if (binding === null || turn === null || !PRESENT_TURN_STATUSES.has(turn.status)) {
44
+ return { outcome: "no-active-turn", code: "NO_ACTIVE_TURN",
45
+ detail: "The Provider is not running a Turn now; there is nothing to steer or interrupt." };
46
+ }
47
+ // A human writer fence (an interactive takeover) owns the Turn; the Controller
48
+ // must not steer or cancel across it.
49
+ if (binding.authority.owner !== "controller") {
50
+ return { outcome: "target-changed", code: "TARGET_CHANGED",
51
+ detail: `Provider authority is ${binding.authority.owner}-held; release the takeover before a managed control.` };
52
+ }
53
+ const observedId = turn.nativeTurnId ?? turn.attemptId;
54
+ if (expectedTarget !== observedId
55
+ && !(turn.nativeTurnId !== undefined && expectedTarget === turn.attemptId)) {
56
+ return { outcome: "target-changed", code: "TARGET_CHANGED",
57
+ detail: `The current Turn is ${observedId}, not ${expectedTarget}; re-read the Session before retrying.` };
58
+ }
59
+ // The right Turn is confirmed; now report its own delivery state. decision-3
60
+ // §3/§8, message-3 #7: an original Turn still in flight (`submitting`) or whose
61
+ // acceptance was never proven (`delivery-unknown`) is not a safe target.
62
+ // Steering it would push a second input onto an unconfirmed one; cancelling it
63
+ // would guess a stop against an attempt that may not have landed. Report
64
+ // DELIVERY_UNKNOWN so the caller confirms the original attempt first — never a
65
+ // silent downgrade to another action (§10).
66
+ if (turn.status !== "accepted") {
67
+ return { outcome: "delivery-unknown", code: "DELIVERY_UNKNOWN",
68
+ detail: `The current Turn's own delivery is ${turn.status}; confirm the original attempt `
69
+ + "before steering or interrupting. Do not reissue under a new requestId or a different action." };
70
+ }
71
+ if (kind === "steer" && turn.nativeTurnId === undefined) {
72
+ return { outcome: "delivery-unknown", code: "DELIVERY_UNKNOWN",
73
+ detail: "The Provider has not supplied an exact native Turn id for steering." };
74
+ }
75
+ return { outcome: "ready", target: {
76
+ roleName, agentId: active.agentId, adapterId: active.adapterId,
77
+ nativeSessionId: active.nativeSessionId,
78
+ ...(turn.nativeTurnId === undefined ? {} : { nativeTurnId: turn.nativeTurnId }),
79
+ attemptId: turn.attemptId,
80
+ authority: { epoch: binding.authority.epoch, owner: "controller",
81
+ holderId: binding.authority.holderId ?? "controller" }
82
+ } };
83
+ }
84
+ /**
85
+ * Resolve a live steer/interrupt against a Task Role's current native Turn.
86
+ *
87
+ * @param expectedTarget The Leader's exact expectation for the current Turn,
88
+ * as observed (its nativeTurnId, or its attemptId when no native id exists).
89
+ * A steer/interrupt that no longer matches is TARGET_CHANGED, never retargeted.
90
+ */
91
+ export function resolveTaskInputControl(store, taskId, roleName, kind, expectedTarget) {
92
+ const resolution = resolveInputControl(store.getTaskRoleSessionSet(taskId, roleName), kind, expectedTarget, roleName);
93
+ // A Task target carries its taskId so the live edge addresses the exact Host.
94
+ return resolution.outcome === "ready"
95
+ ? { outcome: "ready", target: { taskId, ...resolution.target } }
96
+ : resolution;
97
+ }
98
+ /**
99
+ * Resolve a live steer/interrupt against a Global Role's current native Turn
100
+ * (decision-3 §6/§9). Identical contract to the Task resolver, reading the
101
+ * Global Role's own Session set; the resolved target carries no taskId because a
102
+ * Global Role has no owning Task and never fabricates one.
103
+ */
104
+ export function resolveGlobalInputControl(store, roleName, kind, expectedTarget) {
105
+ return resolveInputControl(store.getGlobalRoleSessionSet(roleName), kind, expectedTarget, roleName);
106
+ }
@@ -1,5 +1,26 @@
1
1
  import { validateTaskRecordReference } from "../task/taskRecordReference.js";
2
2
  export const TASK_MESSAGE_KINDS = ["user", "operator", "role-result", "system"];
3
+ /**
4
+ * How a user/operator submission asks its Task to react (task-32 Requirement A).
5
+ *
6
+ * - `record`: save only. No Leader wake, no planning, no activation.
7
+ * - `discuss`: save and route to planning (the default when a submission omits
8
+ * an intent, so an old client keeps its existing Leader-waking behaviour).
9
+ * - `develop`: save the requirement and its activation intent; an unplanned
10
+ * Draft records an activation request and queues only activation processing.
11
+ *
12
+ * The intent is never inferred from body text and is only meaningful on a
13
+ * user/operator submission: a role-result or system Message can never carry one,
14
+ * so an internal Agent report cannot acquire develop authority.
15
+ */
16
+ export const TASK_SUBMISSION_INTENTS = ["record", "discuss", "develop"];
17
+ /** The two durable input actions an authorized caller can choose (decision-3).
18
+ * `interrupt` is a control operation, not a persisted Message action, so it is
19
+ * absent here. */
20
+ export const TASK_MESSAGE_INPUT_ACTIONS = ["queue", "steer"];
21
+ /** The recordable outcomes of a steer's one live control attempt (message-5 gap
22
+ * D). `not-submitted` is not here: it is the absence of a record, not a value. */
23
+ export const TASK_MESSAGE_INPUT_CONTROL_OUTCOMES = ["pending", "accepted", "rejected", "delivery-unknown"];
3
24
  export function createTaskMessage(id, taskId, body, kind, author, now, context = {}) {
4
25
  validateKindAndAuthor(kind, author);
5
26
  const message = {
@@ -12,6 +33,44 @@ export function createTaskMessage(id, taskId, body, kind, author, now, context =
12
33
  ...(context.wakePolicy === undefined
13
34
  ? {}
14
35
  : { wakePolicy: context.wakePolicy }),
36
+ ...(context.intent === undefined
37
+ ? {}
38
+ : { intent: context.intent }),
39
+ ...(context.submissionKey === undefined
40
+ ? {}
41
+ : { submissionKey: requireText(context.submissionKey, "Message submission key") }),
42
+ ...(context.inputControl === undefined
43
+ ? {}
44
+ : { inputControl: {
45
+ action: context.inputControl.action,
46
+ requestId: requireSafeIdentity(context.inputControl.requestId, "Message input requestId"),
47
+ ...(context.inputControl.expectedTarget === undefined
48
+ ? {} : { expectedTarget: requireText(context.inputControl.expectedTarget, "Message input expectedTarget") })
49
+ } }),
50
+ ...(context.interruptThen === undefined
51
+ ? {}
52
+ : { interruptThen: {
53
+ requestId: requireSafeIdentity(context.interruptThen.requestId, "Message interrupt requestId"),
54
+ targetAttemptId: requireText(context.interruptThen.targetAttemptId, "Message interrupt targetAttemptId"),
55
+ targetRoleName: context.interruptThen.targetRoleName,
56
+ targetAgentId: context.interruptThen.targetAgentId,
57
+ targetAdapterId: context.interruptThen.targetAdapterId,
58
+ targetNativeSessionId: context.interruptThen.targetNativeSessionId,
59
+ targetAuthorityEpoch: context.interruptThen.targetAuthorityEpoch,
60
+ targetAuthorityHolderId: context.interruptThen.targetAuthorityHolderId,
61
+ ...(context.interruptThen.targetNativeTurnId === undefined ? {} : {
62
+ targetNativeTurnId: context.interruptThen.targetNativeTurnId
63
+ }),
64
+ ...(context.interruptThen.notDeliveredReason === undefined ? {} : {
65
+ notDeliveredReason: context.interruptThen.notDeliveredReason
66
+ }),
67
+ ...(context.interruptThen.targetRunId === undefined
68
+ ? {} : { targetRunId: context.interruptThen.targetRunId }),
69
+ ...(context.interruptThen.reusedInput === undefined ? {} : { reusedInput: {
70
+ action: context.interruptThen.reusedInput.action,
71
+ requestId: requireSafeIdentity(context.interruptThen.reusedInput.requestId, "Reused input requestId")
72
+ } })
73
+ } }),
15
74
  ...(context.runId === undefined
16
75
  ? {}
17
76
  : { runId: requireSafeIdentity(context.runId, "Message AgentRun id") }),
@@ -29,6 +88,69 @@ export function createTaskMessage(id, taskId, body, kind, author, now, context =
29
88
  export function taskMessageAuthorLabel(author) {
30
89
  return author.type === "role" ? author.roleName : author.type;
31
90
  }
91
+ /**
92
+ * The four visible states a later reader distinguishes for a steer input
93
+ * (message-5 gap D): the absence of any live attempt is `not-submitted`, and a
94
+ * recorded attempt reports its own observed outcome. A non-steer Message has no
95
+ * control state and returns `undefined`. This is the single reader other CLI
96
+ * invocations, the Web surface, and the Leader's Context read all use, so no
97
+ * consumer reconstructs the state from raw fields.
98
+ */
99
+ export function taskMessageInputControlState(message) {
100
+ const wasSteer = message.inputControl?.action === "steer"
101
+ || message.interruptThen?.reusedInput?.action === "steer";
102
+ if (!wasSteer)
103
+ return undefined;
104
+ return message.control?.outcome ?? "not-submitted";
105
+ }
106
+ /**
107
+ * Record the observed disposition of a steer's one live control attempt as an
108
+ * independent, idempotent, monotonic control op (decision-3 §3/§8, message-5 gap
109
+ * D). The op is keyed by the exact `receiptId` fence and the steer's own
110
+ * `requestId`; a record naming a different Message's receipt or a different
111
+ * input identity is refused, so an outcome can never be folded onto the wrong
112
+ * Message or fork a second op. Terminals never rewind to `pending` and never
113
+ * overwrite a different terminal — a repeated or out-of-order live/Host record
114
+ * is absorbed, never a conflicting second write. The frozen business
115
+ * {@link TaskMessageInputControl} intent is left untouched.
116
+ */
117
+ export function recordTaskMessageControlOutcome(message, record) {
118
+ const inputRequestId = message.inputControl?.requestId ?? message.interruptThen?.reusedInput?.requestId;
119
+ const wasSteer = message.inputControl?.action === "steer"
120
+ || message.interruptThen?.reusedInput?.action === "steer";
121
+ if (!wasSteer || inputRequestId === undefined) {
122
+ throw new Error("Only a steer input Message records a live control outcome.");
123
+ }
124
+ if (record.requestId !== inputRequestId) {
125
+ throw new Error("Control outcome requestId must match the steer input identity.");
126
+ }
127
+ const existing = message.control;
128
+ if (existing !== undefined && existing.receiptId !== record.receiptId) {
129
+ throw new Error("Control outcome receiptId cannot name another control op.");
130
+ }
131
+ // Monotonic freeze (decision-3 §8, message-5 gap D). Once any terminal is
132
+ // proven it is final: a later `pending` never rewinds it and a different
133
+ // terminal never overwrites it — the first proven terminal wins. While still
134
+ // `pending`, an identical repeat is idempotent. Each frozen/absorbed case
135
+ // returns the Message unchanged so a fold can persist without a second write.
136
+ if (existing !== undefined) {
137
+ if (existing.outcome === "accepted" || existing.outcome === "rejected")
138
+ return message;
139
+ if (record.outcome === "pending" || record.outcome === existing.outcome)
140
+ return message;
141
+ }
142
+ const updated = {
143
+ ...message,
144
+ control: {
145
+ requestId: requireSafeIdentity(record.requestId, "Message control requestId"),
146
+ receiptId: requireText(record.receiptId, "Message control receiptId"),
147
+ outcome: record.outcome,
148
+ observedAt: record.observedAt.toISOString()
149
+ }
150
+ };
151
+ validateTaskMessage(updated);
152
+ return updated;
153
+ }
32
154
  /** A role-result reference never duplicates the execution's report body. */
33
155
  export function expandTaskMessageResult(message, getRun) {
34
156
  if (message.resultRef === undefined)
@@ -57,6 +179,24 @@ export function updateDraftTaskMessage(message, update) {
57
179
  validateTaskMessage(updated);
58
180
  return updated;
59
181
  }
182
+ /**
183
+ * Attach the §2.3 receipt a keyed submission earned, in the same transaction that
184
+ * saved and routed the Message. The receipt is a write-once fact: it is only set
185
+ * on a keyed Message that has none yet, so a replay (which never re-routes) can
186
+ * never overwrite the disposition the original submission recorded.
187
+ */
188
+ export function withSubmissionReceipt(message, receipt) {
189
+ validateTaskMessage(message);
190
+ if (message.submissionKey === undefined) {
191
+ throw new Error(`A submission receipt requires a submission key: ${message.id}.`);
192
+ }
193
+ if (message.submissionReceipt !== undefined) {
194
+ throw new Error(`Submission receipt is already recorded: ${message.id}.`);
195
+ }
196
+ const updated = { ...message, submissionReceipt: receipt };
197
+ validateTaskMessage(updated);
198
+ return updated;
199
+ }
60
200
  export function validateTaskMessage(message) {
61
201
  if (message.schemaVersion !== 3)
62
202
  throw new Error("Task Message must use schemaVersion 3.");
@@ -74,6 +214,90 @@ export function validateTaskMessage(message) {
74
214
  && message.kind !== "operator") {
75
215
  throw new Error("Message wakePolicy is only valid for user/operator messages.");
76
216
  }
217
+ if (message.intent !== undefined
218
+ && !TASK_SUBMISSION_INTENTS.includes(message.intent)) {
219
+ throw new Error(`Message intent is invalid: ${String(message.intent)}.`);
220
+ }
221
+ if (message.intent !== undefined
222
+ && message.kind !== "user"
223
+ && message.kind !== "operator") {
224
+ throw new Error("Message intent is only valid for user/operator messages.");
225
+ }
226
+ if (message.submissionKey !== undefined) {
227
+ requireText(message.submissionKey, "Message submission key");
228
+ if (message.kind !== "user" && message.kind !== "operator") {
229
+ throw new Error("Message submission key is only valid for user/operator messages.");
230
+ }
231
+ }
232
+ if (message.submissionReceipt !== undefined && message.submissionKey === undefined) {
233
+ throw new Error("Message submission receipt requires a submission key.");
234
+ }
235
+ if (message.inputControl !== undefined) {
236
+ if (!TASK_MESSAGE_INPUT_ACTIONS.includes(message.inputControl.action)) {
237
+ throw new Error(`Message input action is invalid: ${String(message.inputControl.action)}.`);
238
+ }
239
+ requireSafeIdentity(message.inputControl.requestId, "Message input requestId");
240
+ if (message.inputControl.expectedTarget !== undefined) {
241
+ requireText(message.inputControl.expectedTarget, "Message input expectedTarget");
242
+ if (message.inputControl.action !== "steer") {
243
+ throw new Error("Only a steer input binds an expectedTarget.");
244
+ }
245
+ }
246
+ }
247
+ if (message.interruptThen !== undefined) {
248
+ requireSafeIdentity(message.interruptThen.requestId, "Message interrupt requestId");
249
+ requireText(message.interruptThen.targetAttemptId, "Message interrupt targetAttemptId");
250
+ requireSafeIdentity(message.interruptThen.targetRoleName, "Message interrupt target Role");
251
+ requireSafeIdentity(message.interruptThen.targetAgentId, "Message interrupt target Agent");
252
+ requireSafeIdentity(message.interruptThen.targetAdapterId, "Message interrupt target Adapter");
253
+ requireText(message.interruptThen.targetNativeSessionId, "Message interrupt target Session");
254
+ requireText(message.interruptThen.targetAuthorityHolderId, "Message interrupt authority holder");
255
+ if (message.interruptThen.notDeliveredReason !== undefined) {
256
+ requireText(message.interruptThen.notDeliveredReason, "Message interrupt nondelivery reason");
257
+ }
258
+ if (!Number.isSafeInteger(message.interruptThen.targetAuthorityEpoch)
259
+ || message.interruptThen.targetAuthorityEpoch < 1)
260
+ throw new Error("Invalid interrupt authority epoch.");
261
+ if (message.interruptThen.targetRunId !== undefined) {
262
+ validateTaskRecordReference({ taskId: message.taskId, localId: message.interruptThen.targetRunId }, "run");
263
+ }
264
+ if (message.interruptThen.reusedInput !== undefined) {
265
+ if (!TASK_MESSAGE_INPUT_ACTIONS.includes(message.interruptThen.reusedInput.action)) {
266
+ throw new Error(`Reused input action is invalid: ${String(message.interruptThen.reusedInput.action)}.`);
267
+ }
268
+ requireSafeIdentity(message.interruptThen.reusedInput.requestId, "Reused input requestId");
269
+ }
270
+ // A then-handoff is an explicit deliverable Message, never an auto-queued
271
+ // steer: the two intents are mutually exclusive by construction. The prior
272
+ // steer identity, when one existed, is preserved as reusedInput provenance
273
+ // rather than left live on inputControl.
274
+ if (message.inputControl?.action === "steer") {
275
+ throw new Error("An interrupt-then handoff cannot also be a steer input.");
276
+ }
277
+ }
278
+ if (message.control !== undefined) {
279
+ requireSafeIdentity(message.control.requestId, "Message control requestId");
280
+ requireText(message.control.receiptId, "Message control receiptId");
281
+ if (!TASK_MESSAGE_INPUT_CONTROL_OUTCOMES.includes(message.control.outcome)) {
282
+ throw new Error(`Message control outcome is invalid: ${String(message.control.outcome)}.`);
283
+ }
284
+ if (typeof message.control.observedAt !== "string" || Number.isNaN(Date.parse(message.control.observedAt))) {
285
+ throw new Error("Message control observedAt is invalid.");
286
+ }
287
+ // A recorded control outcome is the disposition of a live steer attempt; it
288
+ // only exists for a steer input, or for the reused-steer provenance a
289
+ // handoff preserved. It is never fabricated on a plain queue or a Message
290
+ // that never carried a steer (decision-3 §3, message-5 gap D).
291
+ const inputRequestId = message.inputControl?.requestId ?? message.interruptThen?.reusedInput?.requestId;
292
+ const wasSteer = message.inputControl?.action === "steer"
293
+ || message.interruptThen?.reusedInput?.action === "steer";
294
+ if (!wasSteer || inputRequestId === undefined) {
295
+ throw new Error("Only a steer input Message records a live control outcome.");
296
+ }
297
+ if (message.control.requestId !== inputRequestId) {
298
+ throw new Error("Message control requestId must match the steer input identity.");
299
+ }
300
+ }
77
301
  if (message.runId !== undefined)
78
302
  requireSafeIdentity(message.runId, "Message AgentRun id");
79
303
  if (message.recipient !== undefined) {
@@ -130,6 +354,205 @@ export function validateTaskMessage(message) {
130
354
  throw new Error("Message createdAt is invalid.");
131
355
  }
132
356
  }
357
+ /**
358
+ * The author kinds a Global Role input can carry. A Global Role has no Task
359
+ * Assignment, so a `role-result` (which references a Task AgentRun) is not one
360
+ * of them; user/operator/system inputs are (decision-3 §9/§11).
361
+ */
362
+ export const GLOBAL_ROLE_MESSAGE_KINDS = ["user", "operator", "system", "agent"];
363
+ export function createGlobalRoleMessage(id, roleName, body, kind, author, now, context = {}) {
364
+ validateGlobalKindAndAuthor(kind, author);
365
+ const message = {
366
+ schemaVersion: 1,
367
+ id: validateGlobalRoleMessageId(id),
368
+ roleName: requireSafeIdentity(roleName, "Global message Role name"),
369
+ kind,
370
+ author: { type: author.type },
371
+ body: requireBody(body),
372
+ ...(context.inputControl === undefined
373
+ ? {}
374
+ : { inputControl: {
375
+ action: context.inputControl.action,
376
+ requestId: requireSafeIdentity(context.inputControl.requestId, "Message input requestId"),
377
+ ...(context.inputControl.expectedTarget === undefined
378
+ ? {} : { expectedTarget: requireText(context.inputControl.expectedTarget, "Message input expectedTarget") })
379
+ } }),
380
+ createdAt: now.toISOString()
381
+ };
382
+ validateGlobalRoleMessage(message);
383
+ return message;
384
+ }
385
+ export function validateGlobalRoleMessage(message) {
386
+ if (message.schemaVersion !== 1)
387
+ throw new Error("Global Role Message must use schemaVersion 1.");
388
+ validateGlobalRoleMessageId(message.id);
389
+ requireSafeIdentity(message.roleName, "Global message Role name");
390
+ requireBody(message.body);
391
+ validateGlobalKindAndAuthor(message.kind, message.author);
392
+ if (message.deliveryTarget !== undefined) {
393
+ requireText(message.deliveryTarget.agentId, "Global input Agent");
394
+ requireText(message.deliveryTarget.nativeSessionId, "Global input Session");
395
+ }
396
+ if (message.control !== undefined) {
397
+ requireSafeIdentity(message.control.requestId, "Global input requestId");
398
+ requireText(message.control.receiptId, "Global input receiptId");
399
+ if (!TASK_MESSAGE_INPUT_CONTROL_OUTCOMES.includes(message.control.outcome)
400
+ || !Number.isFinite(Date.parse(message.control.observedAt)))
401
+ throw new Error("Global input disposition is invalid.");
402
+ }
403
+ if (message.inputControl !== undefined) {
404
+ if (!TASK_MESSAGE_INPUT_ACTIONS.includes(message.inputControl.action)) {
405
+ throw new Error(`Message input action is invalid: ${String(message.inputControl.action)}.`);
406
+ }
407
+ requireSafeIdentity(message.inputControl.requestId, "Message input requestId");
408
+ if (message.inputControl.expectedTarget !== undefined) {
409
+ requireText(message.inputControl.expectedTarget, "Message input expectedTarget");
410
+ if (message.inputControl.action !== "steer") {
411
+ throw new Error("Only a steer input binds an expectedTarget.");
412
+ }
413
+ }
414
+ }
415
+ if (message.delivery !== undefined) {
416
+ if (message.delivery.via !== "provider" && message.delivery.via !== "transport") {
417
+ throw new Error(`Global message delivery via is invalid: ${String(message.delivery.via)}.`);
418
+ }
419
+ if (typeof message.delivery.deliveredAt !== "string"
420
+ || Number.isNaN(Date.parse(message.delivery.deliveredAt))) {
421
+ throw new Error("Global message delivery deliveredAt is invalid.");
422
+ }
423
+ // Receipt evidence is shared by queue, explicit then and native steer.
424
+ if (message.inputControl === undefined && message.interruptThen === undefined) {
425
+ throw new Error("Only a durable Global input records Provider delivery.");
426
+ }
427
+ }
428
+ if (message.interruptThen !== undefined) {
429
+ requireSafeIdentity(message.interruptThen.requestId, "Global message interrupt requestId");
430
+ requireText(message.interruptThen.targetAttemptId, "Global message interrupt targetAttemptId");
431
+ requireText(message.interruptThen.targetNativeSessionId, "Global interrupt target Session");
432
+ requireText(message.interruptThen.targetAgentId, "Global interrupt target Agent");
433
+ requireText(message.interruptThen.targetAuthorityHolderId, "Global interrupt writer");
434
+ if (!Number.isSafeInteger(message.interruptThen.targetAuthorityEpoch)
435
+ || message.interruptThen.targetAuthorityEpoch < 1)
436
+ throw new Error("Global interrupt epoch is invalid.");
437
+ if (message.interruptThen.targetNativeTurnId !== undefined) {
438
+ requireText(message.interruptThen.targetNativeTurnId, "Global message interrupt targetNativeTurnId");
439
+ }
440
+ if (message.interruptThen.reusedInput !== undefined) {
441
+ if (!TASK_MESSAGE_INPUT_ACTIONS.includes(message.interruptThen.reusedInput.action)) {
442
+ throw new Error(`Reused input action is invalid: ${String(message.interruptThen.reusedInput.action)}.`);
443
+ }
444
+ requireSafeIdentity(message.interruptThen.reusedInput.requestId, "Reused input requestId");
445
+ }
446
+ }
447
+ if (message.notDelivered !== undefined) {
448
+ requireText(message.notDelivered.reason, "Global message nondelivery reason");
449
+ if (typeof message.notDelivered.at !== "string"
450
+ || Number.isNaN(Date.parse(message.notDelivered.at))) {
451
+ throw new Error("Global message nondelivery timestamp is invalid.");
452
+ }
453
+ // A delivered Message is a settled positive fact; a not-delivered fact is its
454
+ // visible negative twin. The two are mutually exclusive on one Message.
455
+ if (message.delivery !== undefined) {
456
+ throw new Error("A Global message cannot be both delivered and not-delivered.");
457
+ }
458
+ if (message.inputControl?.action !== "queue" && message.interruptThen === undefined) {
459
+ throw new Error("Only a queued or interrupt-then Global message records a nondelivery.");
460
+ }
461
+ }
462
+ if (typeof message.createdAt !== "string" || Number.isNaN(Date.parse(message.createdAt))) {
463
+ throw new Error("Message createdAt is invalid.");
464
+ }
465
+ }
466
+ /** A Global message local id is `global-message-<n>`; it carries no Task id. */
467
+ export function validateGlobalRoleMessageId(localId) {
468
+ const normalized = requireSafeIdentity(localId, "Global message id");
469
+ if (!/^global-message-[1-9]\d*$/.test(normalized)) {
470
+ throw new Error(`Global message id is invalid: ${localId}.`);
471
+ }
472
+ return normalized;
473
+ }
474
+ /**
475
+ * Return a copy of a queued Global Message marked delivered at its next legal
476
+ * opportunity — the Role's own authorized Context read (decision-3 §9). Only a
477
+ * `queue` input may be consumed this way and only once: a Message that already
478
+ * carries a delivery is returned unchanged, so a repeated self-read is
479
+ * idempotent and never re-delivers. A steer/interrupt Message, which targets the
480
+ * exact current Turn rather than a queued next opportunity, is never delivered
481
+ * here.
482
+ */
483
+ export function markGlobalRoleMessageDelivered(message, now, via = "provider") {
484
+ if (message.inputControl === undefined && message.interruptThen === undefined) {
485
+ throw new Error("Only a durable Global input is delivered by a Provider.");
486
+ }
487
+ if (message.delivery?.via === "provider" || message.delivery?.via === via)
488
+ return message;
489
+ const delivered = {
490
+ ...message,
491
+ ...(message.control === undefined ? {} : { control: {
492
+ ...message.control, outcome: via === "provider" ? "accepted" : "pending", observedAt: now.toISOString()
493
+ } }),
494
+ delivery: { deliveredAt: now.toISOString(), via }
495
+ };
496
+ validateGlobalRoleMessage(delivered);
497
+ return delivered;
498
+ }
499
+ /**
500
+ * Return a copy of an already-saved durable Global Message that claims the single
501
+ * interrupt-then continuation of an exact interrupted native Turn (decision-3
502
+ * §4). The original input identity, when the Message carried one, is preserved as
503
+ * `reusedInput` provenance rather than erased, so a replay of that requestId
504
+ * keeps resolving to this same Message and never creates a second input.
505
+ */
506
+ export function claimGlobalRoleMessageInterruptThen(message, claim) {
507
+ const { inputControl, notDelivered: _priorNondelivery, ...rest } = message;
508
+ const claimed = {
509
+ ...rest,
510
+ interruptThen: {
511
+ requestId: requireSafeIdentity(claim.requestId, "Global message interrupt requestId"),
512
+ targetAttemptId: requireText(claim.targetAttemptId, "Global message interrupt targetAttemptId"),
513
+ targetNativeSessionId: claim.targetNativeSessionId,
514
+ targetAgentId: claim.targetAgentId,
515
+ targetAuthorityEpoch: claim.targetAuthorityEpoch,
516
+ targetAuthorityHolderId: claim.targetAuthorityHolderId,
517
+ ...(claim.targetNativeTurnId === undefined
518
+ ? {}
519
+ : { targetNativeTurnId: requireText(claim.targetNativeTurnId, "Global message interrupt targetNativeTurnId") }),
520
+ ...(inputControl === undefined ? {} : { reusedInput: inputControl })
521
+ }
522
+ };
523
+ validateGlobalRoleMessage(claimed);
524
+ return claimed;
525
+ }
526
+ /**
527
+ * Return a copy of a durable Global Message marked visibly not-delivered
528
+ * (decision-3 §5/§10), the Global twin of the Task {@link markNotDelivered}. An
529
+ * ordinary queue entry or a claimed interrupt-then handoff whose target can never
530
+ * prove a safe boundary fails here and stops holding the Role's pending set,
531
+ * rather than silently wedging it. It is idempotent for the same reason — a
532
+ * Message already carrying this exact reason is returned unchanged — and a
533
+ * delivered Message is never overwritten with a nondelivery.
534
+ */
535
+ export function markGlobalRoleMessageNotDelivered(message, reason, now) {
536
+ if (message.delivery !== undefined) {
537
+ throw new Error("A delivered Global message cannot be marked not-delivered.");
538
+ }
539
+ if (message.notDelivered?.reason === reason)
540
+ return message;
541
+ const marked = {
542
+ ...message,
543
+ notDelivered: { reason: requireText(reason, "Global message nondelivery reason"), at: now.toISOString() }
544
+ };
545
+ validateGlobalRoleMessage(marked);
546
+ return marked;
547
+ }
548
+ function validateGlobalKindAndAuthor(kind, author) {
549
+ if (!GLOBAL_ROLE_MESSAGE_KINDS.includes(kind)) {
550
+ throw new Error(`Global message kind is invalid: ${String(kind)}.`);
551
+ }
552
+ if (author?.type !== kind) {
553
+ throw new Error(`Global message kind ${kind} requires a ${kind} author.`);
554
+ }
555
+ }
133
556
  function validateKindAndAuthor(kind, author) {
134
557
  if (!TASK_MESSAGE_KINDS.includes(kind))
135
558
  throw new Error(`Message kind is invalid: ${String(kind)}.`);