@phyzess/pappra 0.0.0-stage → 0.1.0-beta.2

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 (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -2
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/assets/integrations/skills/pappra/SKILL.md +68 -0
  5. package/assets/integrations/skills/pappra/references/hosts.md +30 -0
  6. package/assets/integrations/skills/pappra/references/lifecycle.md +37 -0
  7. package/assets/integrations/skills/pappra/references/operations.md +14 -0
  8. package/assets/manifest.json +4 -0
  9. package/dist/app/collaboration.d.ts +170 -0
  10. package/dist/app/collaboration.js +1650 -0
  11. package/dist/app/doctor.d.ts +9 -0
  12. package/dist/app/doctor.js +1030 -0
  13. package/dist/app/init.d.ts +143 -0
  14. package/dist/app/init.js +1111 -0
  15. package/dist/cli/commands/code.d.ts +16 -0
  16. package/dist/cli/commands/code.js +127 -0
  17. package/dist/cli/commands/collaboration.d.ts +13 -0
  18. package/dist/cli/commands/collaboration.js +186 -0
  19. package/dist/cli/commands/continuity.d.ts +29 -0
  20. package/dist/cli/commands/continuity.js +133 -0
  21. package/dist/cli/commands/docs.d.ts +11 -0
  22. package/dist/cli/commands/docs.js +133 -0
  23. package/dist/cli/commands/doctor.d.ts +14 -0
  24. package/dist/cli/commands/doctor.js +57 -0
  25. package/dist/cli/commands/host.d.ts +13 -0
  26. package/dist/cli/commands/host.js +125 -0
  27. package/dist/cli/commands/init.d.ts +51 -0
  28. package/dist/cli/commands/init.js +668 -0
  29. package/dist/cli/main.d.ts +2 -0
  30. package/dist/cli/main.js +10 -0
  31. package/dist/cli/output.d.ts +4 -0
  32. package/dist/cli/output.js +1 -0
  33. package/dist/cli/run.d.ts +2 -0
  34. package/dist/cli/run.js +597 -0
  35. package/dist/code/budget.d.ts +14 -0
  36. package/dist/code/budget.js +39 -0
  37. package/dist/code/exec.d.ts +23 -0
  38. package/dist/code/exec.js +133 -0
  39. package/dist/code/identity.d.ts +13 -0
  40. package/dist/code/identity.js +214 -0
  41. package/dist/code/index.d.ts +6 -0
  42. package/dist/code/index.js +3 -0
  43. package/dist/code/providers/codegraph.d.ts +59 -0
  44. package/dist/code/providers/codegraph.js +567 -0
  45. package/dist/code/registration.d.ts +9 -0
  46. package/dist/code/registration.js +293 -0
  47. package/dist/code/service.d.ts +53 -0
  48. package/dist/code/service.js +633 -0
  49. package/dist/code/text.d.ts +3 -0
  50. package/dist/code/text.js +202 -0
  51. package/dist/collab/admission.d.ts +450 -0
  52. package/dist/collab/admission.js +797 -0
  53. package/dist/collab/canonical.d.ts +26 -0
  54. package/dist/collab/canonical.js +136 -0
  55. package/dist/collab/contracts.d.ts +73 -0
  56. package/dist/collab/contracts.js +76 -0
  57. package/dist/collab/envelope.d.ts +52 -0
  58. package/dist/collab/envelope.js +241 -0
  59. package/dist/collab/fingerprint.d.ts +7 -0
  60. package/dist/collab/fingerprint.js +58 -0
  61. package/dist/collab/handoff.d.ts +350 -0
  62. package/dist/collab/handoff.js +513 -0
  63. package/dist/collab/index.d.ts +9 -0
  64. package/dist/collab/index.js +8 -0
  65. package/dist/collab/lifecycle.d.ts +449 -0
  66. package/dist/collab/lifecycle.js +534 -0
  67. package/dist/collab/messages.d.ts +148 -0
  68. package/dist/collab/messages.js +583 -0
  69. package/dist/collab/notify.d.ts +47 -0
  70. package/dist/collab/notify.js +140 -0
  71. package/dist/collab/preflight.d.ts +31 -0
  72. package/dist/collab/preflight.js +271 -0
  73. package/dist/collab/resources.d.ts +119 -0
  74. package/dist/collab/resources.js +451 -0
  75. package/dist/collab/store.d.ts +143 -0
  76. package/dist/collab/store.js +606 -0
  77. package/dist/collab/types.d.ts +354 -0
  78. package/dist/collab/types.js +1 -0
  79. package/dist/collab/validation.d.ts +10 -0
  80. package/dist/collab/validation.js +303 -0
  81. package/dist/continuity/context.d.ts +24 -0
  82. package/dist/continuity/context.js +179 -0
  83. package/dist/continuity/index.d.ts +3 -0
  84. package/dist/continuity/index.js +1 -0
  85. package/dist/continuity/paths.d.ts +37 -0
  86. package/dist/continuity/paths.js +258 -0
  87. package/dist/continuity/service.d.ts +59 -0
  88. package/dist/continuity/service.js +1258 -0
  89. package/dist/hosts/claude/native.d.ts +79 -0
  90. package/dist/hosts/claude/native.js +853 -0
  91. package/dist/hosts/codex/native.d.ts +345 -0
  92. package/dist/hosts/codex/native.js +1528 -0
  93. package/dist/hosts/codex/protocol.d.ts +51 -0
  94. package/dist/hosts/codex/protocol.js +42 -0
  95. package/dist/hosts/detect.d.ts +28 -0
  96. package/dist/hosts/detect.js +73 -0
  97. package/dist/hosts/index.d.ts +10 -0
  98. package/dist/hosts/index.js +10 -0
  99. package/dist/hosts/integration/bundle.d.ts +16 -0
  100. package/dist/hosts/integration/bundle.js +81 -0
  101. package/dist/hosts/integration/hook-entry.d.ts +1 -0
  102. package/dist/hosts/integration/hook-entry.js +68 -0
  103. package/dist/hosts/integration/hook.d.ts +32 -0
  104. package/dist/hosts/integration/hook.js +781 -0
  105. package/dist/hosts/integration/pi-extension-runtime.d.ts +18 -0
  106. package/dist/hosts/integration/pi-extension-runtime.js +89 -0
  107. package/dist/hosts/integration/pi-template.d.ts +3 -0
  108. package/dist/hosts/integration/pi-template.js +5 -0
  109. package/dist/hosts/integration/service.d.ts +41 -0
  110. package/dist/hosts/integration/service.js +1381 -0
  111. package/dist/hosts/integration/support.d.ts +36 -0
  112. package/dist/hosts/integration/support.js +41 -0
  113. package/dist/hosts/integration/templates.d.ts +22 -0
  114. package/dist/hosts/integration/templates.js +70 -0
  115. package/dist/hosts/pi/protocol.d.ts +22 -0
  116. package/dist/hosts/pi/protocol.js +28 -0
  117. package/dist/hosts/pi/query-result.d.ts +22 -0
  118. package/dist/hosts/pi/query-result.js +710 -0
  119. package/dist/hosts/pi/rpc.d.ts +937 -0
  120. package/dist/hosts/pi/rpc.js +1621 -0
  121. package/dist/hosts/pilot.d.ts +485 -0
  122. package/dist/hosts/pilot.js +593 -0
  123. package/dist/index.d.ts +5 -0
  124. package/dist/index.js +4 -0
  125. package/dist/infra/fs.d.ts +27 -0
  126. package/dist/infra/fs.js +130 -0
  127. package/dist/infra/git.d.ts +60 -0
  128. package/dist/infra/git.js +358 -0
  129. package/dist/infra/owned-process-group.d.ts +173 -0
  130. package/dist/infra/owned-process-group.js +571 -0
  131. package/dist/install/apply.d.ts +65 -0
  132. package/dist/install/apply.js +663 -0
  133. package/dist/install/index.d.ts +5 -0
  134. package/dist/install/index.js +5 -0
  135. package/dist/install/ownership.d.ts +85 -0
  136. package/dist/install/ownership.js +430 -0
  137. package/dist/install/plan.d.ts +39 -0
  138. package/dist/install/plan.js +97 -0
  139. package/dist/install/rollback.d.ts +18 -0
  140. package/dist/install/rollback.js +544 -0
  141. package/dist/install/runtime.d.ts +76 -0
  142. package/dist/install/runtime.js +1145 -0
  143. package/dist/knowledge/catalog.d.ts +47 -0
  144. package/dist/knowledge/catalog.js +308 -0
  145. package/dist/knowledge/context.d.ts +28 -0
  146. package/dist/knowledge/context.js +61 -0
  147. package/dist/knowledge/index.d.ts +3 -0
  148. package/dist/knowledge/index.js +1 -0
  149. package/dist/knowledge/journal.d.ts +16 -0
  150. package/dist/knowledge/journal.js +109 -0
  151. package/dist/knowledge/lifecycle.d.ts +8 -0
  152. package/dist/knowledge/lifecycle.js +1001 -0
  153. package/dist/knowledge/paths.d.ts +37 -0
  154. package/dist/knowledge/paths.js +236 -0
  155. package/dist/knowledge/refs.d.ts +36 -0
  156. package/dist/knowledge/refs.js +267 -0
  157. package/dist/knowledge/service.d.ts +21 -0
  158. package/dist/knowledge/service.js +393 -0
  159. package/dist/knowledge/types.d.ts +171 -0
  160. package/dist/knowledge/types.js +1 -0
  161. package/dist/knowledge/write.d.ts +8 -0
  162. package/dist/knowledge/write.js +1223 -0
  163. package/dist/package-info.d.ts +6 -0
  164. package/dist/package-info.js +19 -0
  165. package/dist/schemas/code.d.ts +377 -0
  166. package/dist/schemas/code.js +145 -0
  167. package/dist/schemas/collaboration.d.ts +115 -0
  168. package/dist/schemas/collaboration.js +106 -0
  169. package/dist/schemas/continuity.d.ts +315 -0
  170. package/dist/schemas/continuity.js +199 -0
  171. package/dist/schemas/diagnostics.d.ts +28 -0
  172. package/dist/schemas/diagnostics.js +27 -0
  173. package/dist/schemas/doctor.d.ts +157 -0
  174. package/dist/schemas/doctor.js +75 -0
  175. package/dist/schemas/document.d.ts +644 -0
  176. package/dist/schemas/document.js +296 -0
  177. package/dist/schemas/errors.d.ts +177 -0
  178. package/dist/schemas/errors.js +176 -0
  179. package/dist/schemas/evidence.d.ts +24 -0
  180. package/dist/schemas/evidence.js +16 -0
  181. package/dist/schemas/host.d.ts +436 -0
  182. package/dist/schemas/host.js +201 -0
  183. package/dist/schemas/index.d.ts +15 -0
  184. package/dist/schemas/index.js +15 -0
  185. package/dist/schemas/init.d.ts +121 -0
  186. package/dist/schemas/init.js +94 -0
  187. package/dist/schemas/install.d.ts +213 -0
  188. package/dist/schemas/install.js +139 -0
  189. package/dist/schemas/machine.d.ts +43 -0
  190. package/dist/schemas/machine.js +96 -0
  191. package/dist/schemas/outcome.d.ts +23 -0
  192. package/dist/schemas/outcome.js +17 -0
  193. package/dist/schemas/paths.d.ts +19 -0
  194. package/dist/schemas/paths.js +63 -0
  195. package/dist/schemas/workspace.d.ts +120 -0
  196. package/dist/schemas/workspace.js +201 -0
  197. package/dist/workspace/config-file.d.ts +36 -0
  198. package/dist/workspace/config-file.js +237 -0
  199. package/dist/workspace/config.d.ts +28 -0
  200. package/dist/workspace/config.js +79 -0
  201. package/dist/workspace/discover.d.ts +34 -0
  202. package/dist/workspace/discover.js +195 -0
  203. package/dist/workspace/documents.d.ts +21 -0
  204. package/dist/workspace/documents.js +395 -0
  205. package/dist/workspace/ignore.d.ts +10 -0
  206. package/dist/workspace/ignore.js +48 -0
  207. package/dist/workspace/index.d.ts +7 -0
  208. package/dist/workspace/index.js +3 -0
  209. package/dist/workspace/repositories.d.ts +24 -0
  210. package/dist/workspace/repositories.js +411 -0
  211. package/dist/workspace/resolve.d.ts +24 -0
  212. package/dist/workspace/resolve.js +268 -0
  213. package/dist/workspace/types.d.ts +71 -0
  214. package/dist/workspace/types.js +1 -0
  215. package/dist/workspace/validation.d.ts +18 -0
  216. package/dist/workspace/validation.js +197 -0
  217. package/package.json +105 -4
  218. package/skills/pappra/SKILL.md +68 -0
  219. package/skills/pappra/references/hosts.md +30 -0
  220. package/skills/pappra/references/lifecycle.md +37 -0
  221. package/skills/pappra/references/operations.md +14 -0
@@ -0,0 +1,583 @@
1
+ import { contentDigest, registerMessage, validateEnvelope, } from './envelope.js';
2
+ import { bindNative, checkOwnerEpoch, findAttempt, openUncertainty, recordResult, resolveUncertainty, transitionAttempt, upsertAssignment, writeResultEvidence, } from './lifecycle.js';
3
+ import { beginHandoff } from './handoff.js';
4
+ import { nowIso, transact } from './store.js';
5
+ // Applying a registered message to the record. Registration is atomic and separate;
6
+ // this step is a transaction. A message never restarts native work and never releases
7
+ // resources on its own.
8
+ // An ANSWER to a clarification is a reply, not a dispatch: it must already correlate with
9
+ // a stored BLOCK, so it carries no owner_epoch of its own.
10
+ const DISPATCH_TYPES = new Set(['ASSIGN', 'HANDOFF', 'CANCEL', 'CANCELLED']);
11
+ function alreadyApplied(record, message) {
12
+ const previous = (record.messages ?? []).find((item) => item.message_id === message.message_id);
13
+ return previous ?? null;
14
+ }
15
+ function recordMessage(record, message, state, detail, clock) {
16
+ record.messages = record.messages ?? [];
17
+ record.messages.push({
18
+ message_id: message.message_id,
19
+ type: message.type,
20
+ subtask_id: message.subtask_id ?? null,
21
+ attempt: message.attempt ?? null,
22
+ from: message.from,
23
+ to: message.to ?? null,
24
+ reply_to: message.reply_to ?? null,
25
+ owner_epoch: message.owner_epoch ?? null,
26
+ content_digest: contentDigest(message),
27
+ at: message.created_at ?? nowIso(clock),
28
+ applied_at: nowIso(clock),
29
+ state,
30
+ detail,
31
+ body_kind: message.body?.kind ?? null,
32
+ });
33
+ }
34
+ function findMessage(record, messageId) {
35
+ return ((record.messages ?? []).find((item) => item.message_id === messageId) ??
36
+ null);
37
+ }
38
+ /**
39
+ * Pure-ish apply function for `transact`: returns the outcome object or `{ok:false,...}`.
40
+ */
41
+ export function applyMessage(record, message, { clock = Date.now(), nativeDir: _nativeDirectory = null, } = {}) {
42
+ validateEnvelope(message);
43
+ const previous = alreadyApplied(record, message);
44
+ if (previous) {
45
+ if (previous.content_digest !== contentDigest(message)) {
46
+ return {
47
+ ok: false,
48
+ reason: 'content_conflict',
49
+ message_id: message.message_id,
50
+ };
51
+ }
52
+ return {
53
+ ok: false,
54
+ reason: 'duplicate_applied',
55
+ message_id: message.message_id,
56
+ state: previous.state,
57
+ };
58
+ }
59
+ if (message.task_id !== record.task_id)
60
+ return {
61
+ ok: false,
62
+ reason: 'task_mismatch',
63
+ expected: record.task_id,
64
+ };
65
+ if (DISPATCH_TYPES.has(message.type)) {
66
+ if (record.dispatch_frozen && message.type === 'ASSIGN') {
67
+ return {
68
+ ok: false,
69
+ reason: 'dispatch_frozen',
70
+ detail: 'coordinator handoff is in progress',
71
+ };
72
+ }
73
+ if (message.owner_epoch === null || message.owner_epoch === undefined) {
74
+ return {
75
+ ok: false,
76
+ reason: 'epoch_required',
77
+ detail: `${message.type} must carry the owner_epoch it acts under`,
78
+ };
79
+ }
80
+ const epoch = checkOwnerEpoch(record, message.owner_epoch);
81
+ if (!epoch.ok) {
82
+ openUncertainty(record, {
83
+ kind: 'stale_epoch',
84
+ subtaskId: message.subtask_id ?? null,
85
+ attempt: message.attempt ?? null,
86
+ detail: `${message.type} carried epoch ${message.owner_epoch}, current is ${record.coordinator?.owner_epoch ?? null}`,
87
+ clock,
88
+ });
89
+ // The refusal is recorded so an old owner's attempt to dispatch is visible later.
90
+ return {
91
+ ok: false,
92
+ reason: 'stale_epoch',
93
+ expected: epoch.expected,
94
+ actual: epoch.actual,
95
+ commit: true,
96
+ record,
97
+ op: 'stale-epoch',
98
+ summary: { type: message.type },
99
+ result: {
100
+ ok: false,
101
+ reason: 'stale_epoch',
102
+ expected: epoch.expected,
103
+ actual: epoch.actual,
104
+ },
105
+ };
106
+ }
107
+ }
108
+ switch (message.type) {
109
+ case 'ASSIGN': {
110
+ const outcome = upsertAssignment(record, {
111
+ message,
112
+ assignmentRef: message.body.assignment_ref ?? null,
113
+ assignmentDigest: message.body.assignment_digest ?? null,
114
+ inputRevision: message.body.input_revision ?? null,
115
+ clock,
116
+ });
117
+ if (!outcome.ok)
118
+ return outcome;
119
+ recordMessage(record, message, 'applied', 'attempt registered', clock);
120
+ openUncertainty(record, {
121
+ kind: 'registered_not_started',
122
+ subtaskId: message.subtask_id,
123
+ attempt: message.attempt,
124
+ detail: 'assignment registered; no native launch has been confirmed yet',
125
+ resolutionHint: 'bind the native run id, or record that nothing started',
126
+ clock,
127
+ });
128
+ return {
129
+ record,
130
+ op: 'assign',
131
+ summary: { subtask_id: message.subtask_id, attempt: message.attempt },
132
+ result: outcome,
133
+ };
134
+ }
135
+ case 'ACK': {
136
+ const referenced = message.reply_to
137
+ ? findMessage(record, message.reply_to)
138
+ : null;
139
+ const subtaskId = message.subtask_id ?? referenced?.subtask_id ?? null;
140
+ const attempt = message.attempt ?? referenced?.attempt ?? null;
141
+ if (!subtaskId || !attempt) {
142
+ recordMessage(record, message, 'recorded', 'acknowledgement without a resolvable assignment', clock);
143
+ return {
144
+ record,
145
+ op: 'ack',
146
+ summary: { correlated: false },
147
+ result: { ok: true, correlated: false },
148
+ };
149
+ }
150
+ const target = findAttempt(record, subtaskId, attempt);
151
+ if (!target)
152
+ return {
153
+ ok: false,
154
+ reason: 'unknown_attempt',
155
+ subtask_id: subtaskId,
156
+ attempt,
157
+ };
158
+ const transition = target.state === 'assigned'
159
+ ? transitionAttempt(record, {
160
+ subtaskId,
161
+ attempt,
162
+ to: 'accepted',
163
+ actor: message.from.agent_id,
164
+ reason: `ack ${message.message_id}`,
165
+ evidence: message.reply_to,
166
+ clock,
167
+ })
168
+ : {
169
+ ok: false,
170
+ reason: 'not_awaiting_ack',
171
+ state: target.state,
172
+ };
173
+ recordMessage(record, message, 'applied', `acknowledgement for ${subtaskId}#${attempt}`, clock);
174
+ return {
175
+ record,
176
+ op: 'ack',
177
+ summary: { subtask_id: subtaskId, attempt },
178
+ result: transition,
179
+ };
180
+ }
181
+ case 'ANSWER': {
182
+ const referenced = message.reply_to
183
+ ? findMessage(record, message.reply_to)
184
+ : null;
185
+ if (referenced?.type === 'BLOCK' && referenced.body_kind === 'approval') {
186
+ return {
187
+ ok: false,
188
+ reason: 'approval_not_auto_answerable',
189
+ detail: 'an approval block is answered by the user, not by another agent',
190
+ };
191
+ }
192
+ const subtaskId = message.subtask_id ?? referenced?.subtask_id ?? null;
193
+ const attempt = message.attempt ?? referenced?.attempt ?? null;
194
+ const target = subtaskId && attempt ? findAttempt(record, subtaskId, attempt) : null;
195
+ const transition = target &&
196
+ target.state === 'needs_input' &&
197
+ target.awaiting === 'clarification'
198
+ ? transitionAttempt(record, {
199
+ subtaskId,
200
+ attempt,
201
+ to: 'running',
202
+ actor: message.from.agent_id,
203
+ reason: `clarification answered by ${message.message_id}`,
204
+ evidence: message.reply_to,
205
+ clock,
206
+ })
207
+ : {
208
+ ok: false,
209
+ reason: 'no_open_clarification',
210
+ state: target?.state ?? null,
211
+ };
212
+ recordMessage(record, message, 'applied', 'clarification answer', clock);
213
+ return {
214
+ record,
215
+ op: 'answer',
216
+ summary: { subtask_id: subtaskId, attempt },
217
+ result: transition,
218
+ };
219
+ }
220
+ case 'REPORT': {
221
+ if (!message.subtask_id || !message.attempt || !message.body.status)
222
+ throw new Error('Invalid report identity or status');
223
+ const reportTarget = findAttempt(record, message.subtask_id, message.attempt);
224
+ if (!reportTarget)
225
+ return { ok: false, reason: 'unknown_attempt' };
226
+ const owner = reportTarget.assignee ?? {};
227
+ const senderIsOwner = owner.agent_id === message.from.agent_id &&
228
+ owner.session_id === message.from.session_id;
229
+ if (!senderIsOwner) {
230
+ // A previous owner's receipt is evidence, never a state change for the current owner.
231
+ const stale = {
232
+ schema: 'harness-collab/result/1',
233
+ subtask_id: message.subtask_id,
234
+ attempt: message.attempt,
235
+ status: message.body.status,
236
+ message_id: message.message_id,
237
+ input_revision: message.body.input_revision ?? null,
238
+ outputs: message.body.outputs ?? [],
239
+ native_ref: message.body.native_ref ?? null,
240
+ recorded_at: nowIso(clock),
241
+ evidence_ref: message.body.evidence_ref ?? null,
242
+ classification: 'stale_sender',
243
+ reason: `report sent by ${message.from.agent_id}/${message.from.session_id}, current owner is ${owner.agent_id ?? 'unknown'}/${owner.session_id ?? 'unknown'}`,
244
+ };
245
+ record.results = record.results ?? [];
246
+ record.results.push(stale);
247
+ openUncertainty(record, {
248
+ kind: 'late_result',
249
+ subtaskId: message.subtask_id,
250
+ attempt: message.attempt,
251
+ detail: stale.reason,
252
+ evidenceRef: message.body.evidence_ref ?? null,
253
+ resolutionHint: 'confirm which instance still owns the execution before using this result',
254
+ clock,
255
+ });
256
+ recordMessage(record, message, 'applied', 'report classified stale_sender', clock);
257
+ return {
258
+ record,
259
+ op: 'report',
260
+ summary: {
261
+ subtask_id: message.subtask_id,
262
+ attempt: message.attempt,
263
+ classification: 'stale_sender',
264
+ },
265
+ result: {
266
+ ok: true,
267
+ classification: 'stale_sender',
268
+ entry: stale,
269
+ },
270
+ };
271
+ }
272
+ const outcome = recordResult(record, {
273
+ subtaskId: message.subtask_id,
274
+ attempt: message.attempt,
275
+ status: message.body.status,
276
+ messageId: message.message_id,
277
+ inputRevision: message.body.input_revision ?? null,
278
+ outputs: message.body.outputs ?? [],
279
+ nativeRef: message.body.native_ref ?? null,
280
+ evidenceRef: message.body.evidence_ref ?? null,
281
+ clock,
282
+ });
283
+ if (!outcome.ok)
284
+ return outcome;
285
+ recordMessage(record, message, 'applied', `report classified ${outcome.classification}`, clock);
286
+ return {
287
+ record,
288
+ op: 'report',
289
+ summary: {
290
+ subtask_id: message.subtask_id,
291
+ attempt: message.attempt,
292
+ classification: outcome.classification,
293
+ },
294
+ result: outcome,
295
+ };
296
+ }
297
+ case 'STATUS_QUERY':
298
+ case 'STATUS': {
299
+ recordMessage(record, message, 'recorded', 'status exchange; no task state change', clock);
300
+ return {
301
+ record,
302
+ op: 'status',
303
+ summary: { type: message.type },
304
+ result: {
305
+ ok: true,
306
+ state_revision: record.state_revision,
307
+ attempts: (record.attempts ?? []).map((item) => ({
308
+ subtask_id: item.subtask_id,
309
+ attempt: item.attempt,
310
+ state: item.state,
311
+ native: item.native?.observation ?? 'unknown',
312
+ })),
313
+ },
314
+ };
315
+ }
316
+ case 'BLOCK': {
317
+ const subtaskId = message.subtask_id ?? null;
318
+ const attempt = message.attempt ?? null;
319
+ const kind = message.body.kind;
320
+ if (!kind)
321
+ throw new Error('Invalid block kind');
322
+ if (!subtaskId || !attempt)
323
+ return { ok: false, reason: 'block_requires_attempt' };
324
+ const target = findAttempt(record, subtaskId, attempt);
325
+ if (!target)
326
+ return { ok: false, reason: 'unknown_attempt' };
327
+ target.awaiting = kind;
328
+ const transition = transitionAttempt(record, {
329
+ subtaskId,
330
+ attempt,
331
+ to: 'needs_input',
332
+ actor: message.from.agent_id,
333
+ reason: `blocked: ${kind}`,
334
+ evidence: message.body.evidence_ref ?? null,
335
+ clock,
336
+ });
337
+ if (!transition.ok && transition.reason !== 'already_in_state') {
338
+ delete target.awaiting;
339
+ return transition;
340
+ }
341
+ recordMessage(record, message, 'applied', `block (${kind})`, clock);
342
+ return {
343
+ record,
344
+ op: 'block',
345
+ summary: { subtask_id: subtaskId, attempt, kind },
346
+ result: {
347
+ ok: true,
348
+ awaiting: kind,
349
+ human_required: kind === 'approval',
350
+ },
351
+ };
352
+ }
353
+ case 'CANCEL': {
354
+ const subtaskId = message.subtask_id;
355
+ const attempt = message.attempt;
356
+ const target = findAttempt(record, subtaskId, attempt);
357
+ if (!target)
358
+ return { ok: false, reason: 'unknown_attempt' };
359
+ const at = nowIso(clock);
360
+ target.cancel_requested_at = at;
361
+ // The request must use the same predicate as the confirmed cancellation: agent AND
362
+ // session, plus the recorded subtask/attempt. Marking another session's claim would
363
+ // block its later release with a permanent cancel_unconfirmed.
364
+ const cancelHolder = target.assignee ?? {};
365
+ const marked = [];
366
+ for (const claim of record.claims ?? []) {
367
+ if (claim.released_at)
368
+ continue;
369
+ if (claim.holder?.agent_id !== cancelHolder.agent_id)
370
+ continue;
371
+ if (!cancelHolder.session_id ||
372
+ claim.holder?.session_id !== cancelHolder.session_id)
373
+ continue;
374
+ if (claim.subtask_id && claim.subtask_id !== subtaskId)
375
+ continue;
376
+ if (claim.attempt !== null &&
377
+ claim.attempt !== undefined &&
378
+ claim.attempt !== attempt)
379
+ continue;
380
+ claim.cancel_pending = true;
381
+ marked.push(claim.claim_id);
382
+ }
383
+ openUncertainty(record, {
384
+ kind: 'cancel_unconfirmed',
385
+ subtaskId,
386
+ attempt,
387
+ detail: 'cancellation was requested; the native process state is not confirmed',
388
+ resolutionHint: 'query the native runner; release resources only after a confirmed stop',
389
+ clock,
390
+ });
391
+ recordMessage(record, message, 'applied', 'cancellation requested', clock);
392
+ return {
393
+ record,
394
+ op: 'cancel-request',
395
+ summary: { subtask_id: subtaskId, attempt },
396
+ result: {
397
+ ok: true,
398
+ cancel_pending: true,
399
+ resources_released: false,
400
+ marked_claims: marked,
401
+ },
402
+ };
403
+ }
404
+ case 'CANCELLED': {
405
+ const subtaskId = message.subtask_id;
406
+ const attempt = message.attempt;
407
+ const target = findAttempt(record, subtaskId, attempt);
408
+ if (!target)
409
+ return { ok: false, reason: 'unknown_attempt' };
410
+ if (!message.body.stop_evidence) {
411
+ return {
412
+ ok: false,
413
+ reason: 'stop_evidence_required',
414
+ detail: 'a cancellation is only recorded as final with evidence that the process stopped',
415
+ };
416
+ }
417
+ const transition = transitionAttempt(record, {
418
+ subtaskId,
419
+ attempt,
420
+ to: 'cancelled',
421
+ actor: message.from.agent_id,
422
+ reason: `cancellation confirmed by ${message.message_id}`,
423
+ evidence: message.body.stop_evidence,
424
+ clock,
425
+ });
426
+ if (!transition.ok && transition.reason !== 'already_in_state')
427
+ return transition;
428
+ // Only the claims this execution actually holds, matched on the full holder identity
429
+ // (agent AND session) and, when recorded, on the same subtask and attempt. A
430
+ // same-named agent in another session keeps its resources.
431
+ const holder = target.assignee ?? {};
432
+ const released = [];
433
+ for (const claim of record.claims ?? []) {
434
+ if (claim.released_at)
435
+ continue;
436
+ if (claim.holder?.agent_id !== holder.agent_id)
437
+ continue;
438
+ if (!holder.session_id ||
439
+ claim.holder?.session_id !== holder.session_id)
440
+ continue;
441
+ if (claim.subtask_id && claim.subtask_id !== subtaskId)
442
+ continue;
443
+ if (claim.attempt !== null &&
444
+ claim.attempt !== undefined &&
445
+ claim.attempt !== attempt)
446
+ continue;
447
+ claim.cancel_pending = false;
448
+ claim.released_at = nowIso(clock);
449
+ claim.released_by = message.from.agent_id;
450
+ claim.release_reason = `cancelled by ${message.message_id}`;
451
+ released.push(claim.claim_id);
452
+ }
453
+ const untouched = (record.claims ?? [])
454
+ .filter((item) => !item.released_at &&
455
+ item.holder?.agent_id === holder.agent_id &&
456
+ !released.includes(item.claim_id))
457
+ .map((item) => item.claim_id);
458
+ for (const entry of record.uncertainty ?? []) {
459
+ if (!entry.resolved_at &&
460
+ entry.kind === 'cancel_unconfirmed' &&
461
+ entry.subtask_id === subtaskId &&
462
+ entry.attempt === attempt) {
463
+ resolveUncertainty(record, {
464
+ uncertaintyId: entry.uncertainty_id,
465
+ resolution: 'stop confirmed by the cancellation report',
466
+ resolutionEvidence: message.body.stop_evidence,
467
+ actor: message.from.agent_id,
468
+ clock,
469
+ });
470
+ }
471
+ }
472
+ recordMessage(record, message, 'applied', 'cancellation confirmed; resources released', clock);
473
+ return {
474
+ record,
475
+ op: 'cancelled',
476
+ summary: { subtask_id: subtaskId, attempt },
477
+ result: {
478
+ ok: true,
479
+ resources_released: true,
480
+ released_claims: released,
481
+ claims_left_untouched: untouched,
482
+ },
483
+ };
484
+ }
485
+ case 'HANDOFF': {
486
+ if (!message.body.handoff_id || message.owner_epoch === null)
487
+ throw new Error('Invalid handoff identity or epoch');
488
+ if (message.body.phase !== 'request') {
489
+ recordMessage(record, message, 'recorded', `handoff notification (${message.body.phase})`, clock);
490
+ return {
491
+ record,
492
+ op: 'handoff-note',
493
+ summary: { phase: message.body.phase },
494
+ result: {
495
+ ok: true,
496
+ note: 'notification only; it does not move ownership',
497
+ },
498
+ };
499
+ }
500
+ const outcome = beginHandoff(record, {
501
+ handoffId: message.body.handoff_id,
502
+ kind: message.body.kind ?? 'writer',
503
+ subtaskId: message.subtask_id,
504
+ attempt: message.attempt ?? null,
505
+ from: message.from,
506
+ to: message.to,
507
+ resources: message.body.resources ?? [],
508
+ ownerEpoch: message.owner_epoch,
509
+ reason: message.body.reason ?? null,
510
+ clock,
511
+ });
512
+ if (!outcome.ok)
513
+ return outcome;
514
+ recordMessage(record, message, 'applied', 'handoff requested', clock);
515
+ return {
516
+ record,
517
+ op: 'handoff-request',
518
+ summary: { handoff_id: message.body.handoff_id },
519
+ result: outcome,
520
+ };
521
+ }
522
+ default:
523
+ return {
524
+ ok: false,
525
+ reason: 'unsupported_message_type',
526
+ type: message.type,
527
+ };
528
+ }
529
+ }
530
+ /**
531
+ * Register and apply one message. Returns the registration result plus the applied
532
+ * outcome; a duplicate registration produces no second side effect.
533
+ */
534
+ export function submitMessage(taskRoot, message, { clock = Date.now(), expectRevision, waitMs = 5000, } = {}) {
535
+ const registered = registerMessage(taskRoot, message, { clock });
536
+ if (registered.status === 'content_conflict') {
537
+ return {
538
+ ok: false,
539
+ reason: 'content_conflict',
540
+ registration: registered,
541
+ };
542
+ }
543
+ const outcome = transact(taskRoot, {
544
+ actor: message.from?.agent_id ?? 'unattributed',
545
+ purpose: `message:${message.type}`,
546
+ expectRevision,
547
+ waitMs,
548
+ clock,
549
+ apply: (record) => applyMessage(record, message, { clock, nativeDir: taskRoot }),
550
+ });
551
+ if (!outcome.ok && outcome.reason === 'duplicate_applied') {
552
+ return {
553
+ ok: true,
554
+ registration: registered,
555
+ duplicate: true,
556
+ applied: outcome,
557
+ };
558
+ }
559
+ if (!outcome.ok)
560
+ return {
561
+ ok: false,
562
+ reason: outcome.reason,
563
+ registration: registered,
564
+ applied: outcome,
565
+ recorded: outcome.committed === true,
566
+ };
567
+ const result = outcome.result ?? {};
568
+ if (result.entry) {
569
+ try {
570
+ writeResultEvidence(taskRoot, result.entry);
571
+ }
572
+ catch {
573
+ // Evidence retention failure is reported through doctor, not silently ignored here.
574
+ }
575
+ }
576
+ return {
577
+ ok: true,
578
+ registration: registered,
579
+ state_revision: outcome.state_revision,
580
+ applied: outcome,
581
+ };
582
+ }
583
+ export { bindNative };
@@ -0,0 +1,47 @@
1
+ import type { TaskRecord, Notification } from './types.js';
2
+ export declare const DEFAULT_LIMITS: Readonly<{
3
+ per_key: 3;
4
+ window_ms: 60000;
5
+ max_depth: 2;
6
+ max_entries: 200;
7
+ }>;
8
+ type Limits = {
9
+ [Key in keyof typeof DEFAULT_LIMITS]?: number;
10
+ };
11
+ export declare const NOTIFICATION_KINDS: readonly string[];
12
+ export declare function validateNotification(notification: unknown): Notification;
13
+ /** Stable identity of a notification: everything that decides whether it is the same one. */
14
+ export declare function notificationKey(notification: Notification): string;
15
+ /**
16
+ * Decide whether to deliver. Returns `{admit, reason, human_required}`. An approval
17
+ * request is recorded but never answerable by another agent, and no decision here
18
+ * triggers a send, a retry or a model wake.
19
+ */
20
+ export declare function admitNotification(record: TaskRecord, notification: Notification, { clock, limits }?: {
21
+ clock?: number;
22
+ limits?: Limits;
23
+ }): {
24
+ admit: boolean;
25
+ reason: string;
26
+ human_required: boolean;
27
+ key: string;
28
+ delivered_in_window?: never;
29
+ } | {
30
+ admit: boolean;
31
+ reason: string;
32
+ delivered_in_window: number;
33
+ key: string;
34
+ human_required?: never;
35
+ } | {
36
+ delivered_in_window?: never;
37
+ admit: boolean;
38
+ reason: null;
39
+ human_required: boolean;
40
+ key: string;
41
+ };
42
+ /** Keep the notification log bounded; the newest entries are the ones that matter. */
43
+ export declare function trimNotificationLog(record: TaskRecord, { limits }?: {
44
+ clock?: number;
45
+ limits?: Limits;
46
+ }): number;
47
+ export {};