pi-amq 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/LICENSE +21 -0
  3. package/README.md +92 -0
  4. package/THIRD_PARTY_NOTICES.md +33 -0
  5. package/dist/extension/guidance.d.ts +25 -0
  6. package/dist/extension/guidance.d.ts.map +1 -0
  7. package/dist/extension/guidance.js +121 -0
  8. package/dist/extension/guidance.js.map +1 -0
  9. package/dist/extension/guide.d.ts +30 -0
  10. package/dist/extension/guide.d.ts.map +1 -0
  11. package/dist/extension/guide.js +128 -0
  12. package/dist/extension/guide.js.map +1 -0
  13. package/dist/extension/index.d.ts +4 -0
  14. package/dist/extension/index.d.ts.map +1 -0
  15. package/dist/extension/index.js +6 -0
  16. package/dist/extension/index.js.map +1 -0
  17. package/dist/extension/runtime.d.ts +48 -0
  18. package/dist/extension/runtime.d.ts.map +1 -0
  19. package/dist/extension/runtime.js +1095 -0
  20. package/dist/extension/runtime.js.map +1 -0
  21. package/dist/extension/tools.d.ts +12 -0
  22. package/dist/extension/tools.d.ts.map +1 -0
  23. package/dist/extension/tools.js +198 -0
  24. package/dist/extension/tools.js.map +1 -0
  25. package/dist/extension/worker-runtime.d.ts +34 -0
  26. package/dist/extension/worker-runtime.d.ts.map +1 -0
  27. package/dist/extension/worker-runtime.js +974 -0
  28. package/dist/extension/worker-runtime.js.map +1 -0
  29. package/dist/extension/worker-tools.d.ts +140 -0
  30. package/dist/extension/worker-tools.d.ts.map +1 -0
  31. package/dist/extension/worker-tools.js +217 -0
  32. package/dist/extension/worker-tools.js.map +1 -0
  33. package/dist/integration/confirmed-bind-onboarding.d.ts +44 -0
  34. package/dist/integration/confirmed-bind-onboarding.d.ts.map +1 -0
  35. package/dist/integration/confirmed-bind-onboarding.js +199 -0
  36. package/dist/integration/confirmed-bind-onboarding.js.map +1 -0
  37. package/dist/integration/fleet-view.d.ts +66 -0
  38. package/dist/integration/fleet-view.d.ts.map +1 -0
  39. package/dist/integration/fleet-view.js +238 -0
  40. package/dist/integration/fleet-view.js.map +1 -0
  41. package/dist/integration/fresh-session-dispatcher.d.ts +66 -0
  42. package/dist/integration/fresh-session-dispatcher.d.ts.map +1 -0
  43. package/dist/integration/fresh-session-dispatcher.js +103 -0
  44. package/dist/integration/fresh-session-dispatcher.js.map +1 -0
  45. package/dist/integration/launch-preview.d.ts +31 -0
  46. package/dist/integration/launch-preview.d.ts.map +1 -0
  47. package/dist/integration/launch-preview.js +325 -0
  48. package/dist/integration/launch-preview.js.map +1 -0
  49. package/dist/integration/transport-port.d.ts +39 -0
  50. package/dist/integration/transport-port.d.ts.map +1 -0
  51. package/dist/integration/transport-port.js +2 -0
  52. package/dist/integration/transport-port.js.map +1 -0
  53. package/dist/integration/worker-completion-notifier.d.ts +22 -0
  54. package/dist/integration/worker-completion-notifier.d.ts.map +1 -0
  55. package/dist/integration/worker-completion-notifier.js +81 -0
  56. package/dist/integration/worker-completion-notifier.js.map +1 -0
  57. package/dist/transport/amq-cli-adapter.d.ts +41 -0
  58. package/dist/transport/amq-cli-adapter.d.ts.map +1 -0
  59. package/dist/transport/amq-cli-adapter.js +291 -0
  60. package/dist/transport/amq-cli-adapter.js.map +1 -0
  61. package/dist/transport/amq-message.d.ts +47 -0
  62. package/dist/transport/amq-message.d.ts.map +1 -0
  63. package/dist/transport/amq-message.js +173 -0
  64. package/dist/transport/amq-message.js.map +1 -0
  65. package/dist/transport/amq-operations.d.ts +105 -0
  66. package/dist/transport/amq-operations.d.ts.map +1 -0
  67. package/dist/transport/amq-operations.js +320 -0
  68. package/dist/transport/amq-operations.js.map +1 -0
  69. package/dist/transport/atomic-json.d.ts +18 -0
  70. package/dist/transport/atomic-json.d.ts.map +1 -0
  71. package/dist/transport/atomic-json.js +90 -0
  72. package/dist/transport/atomic-json.js.map +1 -0
  73. package/dist/transport/binding-resolver.d.ts +46 -0
  74. package/dist/transport/binding-resolver.d.ts.map +1 -0
  75. package/dist/transport/binding-resolver.js +389 -0
  76. package/dist/transport/binding-resolver.js.map +1 -0
  77. package/dist/transport/bridge-journal.d.ts +100 -0
  78. package/dist/transport/bridge-journal.d.ts.map +1 -0
  79. package/dist/transport/bridge-journal.js +459 -0
  80. package/dist/transport/bridge-journal.js.map +1 -0
  81. package/dist/transport/dlq-retry-audit.d.ts +28 -0
  82. package/dist/transport/dlq-retry-audit.d.ts.map +1 -0
  83. package/dist/transport/dlq-retry-audit.js +66 -0
  84. package/dist/transport/dlq-retry-audit.js.map +1 -0
  85. package/dist/transport/inbox-pump.d.ts +81 -0
  86. package/dist/transport/inbox-pump.d.ts.map +1 -0
  87. package/dist/transport/inbox-pump.js +558 -0
  88. package/dist/transport/inbox-pump.js.map +1 -0
  89. package/dist/transport/index.d.ts +12 -0
  90. package/dist/transport/index.d.ts.map +1 -0
  91. package/dist/transport/index.js +12 -0
  92. package/dist/transport/index.js.map +1 -0
  93. package/dist/transport/presence-projector.d.ts +29 -0
  94. package/dist/transport/presence-projector.d.ts.map +1 -0
  95. package/dist/transport/presence-projector.js +73 -0
  96. package/dist/transport/presence-projector.js.map +1 -0
  97. package/dist/transport/priority-delivery-scheduler.d.ts +85 -0
  98. package/dist/transport/priority-delivery-scheduler.d.ts.map +1 -0
  99. package/dist/transport/priority-delivery-scheduler.js +210 -0
  100. package/dist/transport/priority-delivery-scheduler.js.map +1 -0
  101. package/dist/transport/receiver-lease.d.ts +102 -0
  102. package/dist/transport/receiver-lease.d.ts.map +1 -0
  103. package/dist/transport/receiver-lease.js +778 -0
  104. package/dist/transport/receiver-lease.js.map +1 -0
  105. package/dist/transport/runtime-status.d.ts +48 -0
  106. package/dist/transport/runtime-status.d.ts.map +1 -0
  107. package/dist/transport/runtime-status.js +71 -0
  108. package/dist/transport/runtime-status.js.map +1 -0
  109. package/dist/workflow/atomic-storage.d.ts +12 -0
  110. package/dist/workflow/atomic-storage.d.ts.map +1 -0
  111. package/dist/workflow/atomic-storage.js +104 -0
  112. package/dist/workflow/atomic-storage.js.map +1 -0
  113. package/dist/workflow/budget.d.ts +18 -0
  114. package/dist/workflow/budget.d.ts.map +1 -0
  115. package/dist/workflow/budget.js +74 -0
  116. package/dist/workflow/budget.js.map +1 -0
  117. package/dist/workflow/index.d.ts +17 -0
  118. package/dist/workflow/index.d.ts.map +1 -0
  119. package/dist/workflow/index.js +17 -0
  120. package/dist/workflow/index.js.map +1 -0
  121. package/dist/workflow/lifecycle-projection.d.ts +18 -0
  122. package/dist/workflow/lifecycle-projection.d.ts.map +1 -0
  123. package/dist/workflow/lifecycle-projection.js +22 -0
  124. package/dist/workflow/lifecycle-projection.js.map +1 -0
  125. package/dist/workflow/model-fallback.d.ts +2 -0
  126. package/dist/workflow/model-fallback.d.ts.map +1 -0
  127. package/dist/workflow/model-fallback.js +17 -0
  128. package/dist/workflow/model-fallback.js.map +1 -0
  129. package/dist/workflow/pi-session-fork.d.ts +18 -0
  130. package/dist/workflow/pi-session-fork.d.ts.map +1 -0
  131. package/dist/workflow/pi-session-fork.js +80 -0
  132. package/dist/workflow/pi-session-fork.js.map +1 -0
  133. package/dist/workflow/run-artifacts.d.ts +5 -0
  134. package/dist/workflow/run-artifacts.d.ts.map +1 -0
  135. package/dist/workflow/run-artifacts.js +25 -0
  136. package/dist/workflow/run-artifacts.js.map +1 -0
  137. package/dist/workflow/run-cleanup.d.ts +70 -0
  138. package/dist/workflow/run-cleanup.d.ts.map +1 -0
  139. package/dist/workflow/run-cleanup.js +222 -0
  140. package/dist/workflow/run-cleanup.js.map +1 -0
  141. package/dist/workflow/run-gate.d.ts +43 -0
  142. package/dist/workflow/run-gate.d.ts.map +1 -0
  143. package/dist/workflow/run-gate.js +70 -0
  144. package/dist/workflow/run-gate.js.map +1 -0
  145. package/dist/workflow/run-store.d.ts +208 -0
  146. package/dist/workflow/run-store.d.ts.map +1 -0
  147. package/dist/workflow/run-store.js +2965 -0
  148. package/dist/workflow/run-store.js.map +1 -0
  149. package/dist/workflow/spawn-confirmation.d.ts +44 -0
  150. package/dist/workflow/spawn-confirmation.d.ts.map +1 -0
  151. package/dist/workflow/spawn-confirmation.js +228 -0
  152. package/dist/workflow/spawn-confirmation.js.map +1 -0
  153. package/dist/workflow/system-process-inspector.d.ts +9 -0
  154. package/dist/workflow/system-process-inspector.d.ts.map +1 -0
  155. package/dist/workflow/system-process-inspector.js +129 -0
  156. package/dist/workflow/system-process-inspector.js.map +1 -0
  157. package/dist/workflow/worker-child-extension.d.ts +3 -0
  158. package/dist/workflow/worker-child-extension.d.ts.map +1 -0
  159. package/dist/workflow/worker-child-extension.js +182 -0
  160. package/dist/workflow/worker-child-extension.js.map +1 -0
  161. package/dist/workflow/worker-gate.d.ts +74 -0
  162. package/dist/workflow/worker-gate.d.ts.map +1 -0
  163. package/dist/workflow/worker-gate.js +165 -0
  164. package/dist/workflow/worker-gate.js.map +1 -0
  165. package/dist/workflow/worker-launch.d.ts +65 -0
  166. package/dist/workflow/worker-launch.d.ts.map +1 -0
  167. package/dist/workflow/worker-launch.js +338 -0
  168. package/dist/workflow/worker-launch.js.map +1 -0
  169. package/dist/workflow/worker-model-catalog.d.ts +36 -0
  170. package/dist/workflow/worker-model-catalog.d.ts.map +1 -0
  171. package/dist/workflow/worker-model-catalog.js +105 -0
  172. package/dist/workflow/worker-model-catalog.js.map +1 -0
  173. package/dist/workflow/worker-process-supervisor.d.ts +110 -0
  174. package/dist/workflow/worker-process-supervisor.d.ts.map +1 -0
  175. package/dist/workflow/worker-process-supervisor.js +1188 -0
  176. package/dist/workflow/worker-process-supervisor.js.map +1 -0
  177. package/dist/workflow/worker-prompt-capsule.d.ts +32 -0
  178. package/dist/workflow/worker-prompt-capsule.d.ts.map +1 -0
  179. package/dist/workflow/worker-prompt-capsule.js +52 -0
  180. package/dist/workflow/worker-prompt-capsule.js.map +1 -0
  181. package/dist/workflow/worker-role-presets.d.ts +38 -0
  182. package/dist/workflow/worker-role-presets.d.ts.map +1 -0
  183. package/dist/workflow/worker-role-presets.js +201 -0
  184. package/dist/workflow/worker-role-presets.js.map +1 -0
  185. package/dist/workflow/workflow-doctor.d.ts +57 -0
  186. package/dist/workflow/workflow-doctor.d.ts.map +1 -0
  187. package/dist/workflow/workflow-doctor.js +289 -0
  188. package/dist/workflow/workflow-doctor.js.map +1 -0
  189. package/dist/workflow/workflow-lease.d.ts +125 -0
  190. package/dist/workflow/workflow-lease.d.ts.map +1 -0
  191. package/dist/workflow/workflow-lease.js +689 -0
  192. package/dist/workflow/workflow-lease.js.map +1 -0
  193. package/dist/workflow/workflow-types.d.ts +335 -0
  194. package/dist/workflow/workflow-types.d.ts.map +1 -0
  195. package/dist/workflow/workflow-types.js +11 -0
  196. package/dist/workflow/workflow-types.js.map +1 -0
  197. package/dist/workflow/workspace-paths.d.ts +4 -0
  198. package/dist/workflow/workspace-paths.d.ts.map +1 -0
  199. package/dist/workflow/workspace-paths.js +22 -0
  200. package/dist/workflow/workspace-paths.js.map +1 -0
  201. package/docs/conformance.md +116 -0
  202. package/docs/demo-transcript.md +34 -0
  203. package/docs/design.md +1481 -0
  204. package/docs/operator-guide.md +100 -0
  205. package/extension/guidance.ts +162 -0
  206. package/extension/guide.ts +155 -0
  207. package/extension/index.ts +8 -0
  208. package/extension/runtime.ts +1224 -0
  209. package/extension/tools.ts +221 -0
  210. package/extension/worker-runtime.ts +1150 -0
  211. package/extension/worker-tools.ts +276 -0
  212. package/integration/confirmed-bind-onboarding.ts +296 -0
  213. package/integration/fleet-view.ts +286 -0
  214. package/integration/fresh-session-dispatcher.ts +166 -0
  215. package/integration/launch-preview.ts +396 -0
  216. package/integration/transport-port.ts +29 -0
  217. package/integration/worker-completion-notifier.ts +98 -0
  218. package/package.json +55 -0
  219. package/roles/oracle.md +9 -0
  220. package/roles/researcher.md +8 -0
  221. package/roles/reviewer.md +9 -0
  222. package/roles/scout.md +8 -0
  223. package/roles/worker.md +10 -0
  224. package/scripts/demo-two-terminal.sh +59 -0
  225. package/skills/coordination-patterns/SKILL.md +41 -0
  226. package/skills/coordination-patterns/references/supervised-worker-fan-out.md +12 -0
  227. package/skills/coordination-patterns/references/today-mail-team.md +36 -0
  228. package/transport/amq-cli-adapter.ts +381 -0
  229. package/transport/amq-message.ts +218 -0
  230. package/transport/amq-operations.ts +482 -0
  231. package/transport/atomic-json.ts +115 -0
  232. package/transport/binding-resolver.ts +565 -0
  233. package/transport/bridge-journal.ts +588 -0
  234. package/transport/dlq-retry-audit.ts +97 -0
  235. package/transport/inbox-pump.ts +729 -0
  236. package/transport/index.ts +11 -0
  237. package/transport/presence-projector.ts +104 -0
  238. package/transport/priority-delivery-scheduler.ts +328 -0
  239. package/transport/receiver-lease.ts +945 -0
  240. package/transport/runtime-status.ts +112 -0
  241. package/workflow/.gitkeep +1 -0
  242. package/workflow/atomic-storage.ts +120 -0
  243. package/workflow/budget.ts +90 -0
  244. package/workflow/index.ts +16 -0
  245. package/workflow/lifecycle-projection.ts +37 -0
  246. package/workflow/model-fallback.ts +17 -0
  247. package/workflow/pi-session-fork.ts +96 -0
  248. package/workflow/run-artifacts.ts +36 -0
  249. package/workflow/run-cleanup.ts +315 -0
  250. package/workflow/run-gate.ts +122 -0
  251. package/workflow/run-store.ts +3890 -0
  252. package/workflow/spawn-confirmation.ts +305 -0
  253. package/workflow/system-process-inspector.ts +142 -0
  254. package/workflow/worker-child-extension.ts +193 -0
  255. package/workflow/worker-gate.ts +238 -0
  256. package/workflow/worker-launch.ts +485 -0
  257. package/workflow/worker-model-catalog.ts +165 -0
  258. package/workflow/worker-process-supervisor.ts +1504 -0
  259. package/workflow/worker-prompt-capsule.ts +81 -0
  260. package/workflow/worker-role-presets.ts +241 -0
  261. package/workflow/workflow-doctor.ts +400 -0
  262. package/workflow/workflow-lease.ts +905 -0
  263. package/workflow/workflow-types.ts +367 -0
  264. package/workflow/workspace-paths.ts +32 -0
@@ -0,0 +1,482 @@
1
+ import type { AmqCliAdapter, AmqCommandResult } from "./amq-cli-adapter.js";
2
+ import type { AmqMessage, AmqPriority } from "./amq-message.js";
3
+
4
+ const DEFAULT_LIMIT = 20;
5
+ const MAX_LIMIT = 100;
6
+ const DEFAULT_BODY_SLICE_BYTES = 8 * 1024;
7
+ const MAX_BODY_SLICE_BYTES = 24 * 1024;
8
+
9
+ export interface AmqOperationBinding {
10
+ readonly root: string;
11
+ readonly handle: string;
12
+ readonly project: string;
13
+ readonly session: string;
14
+ }
15
+
16
+ export interface AmqOperationError {
17
+ readonly operation: string;
18
+ readonly category: string;
19
+ readonly message: string;
20
+ readonly committed: boolean | "unknown";
21
+ readonly classification?: "primary-failure" | "delivery-committed-sender-audit-failed";
22
+ readonly evidence?: unknown;
23
+ readonly diagnostic?: {
24
+ readonly kind: "route" | "trace";
25
+ readonly ok: boolean;
26
+ readonly value: unknown;
27
+ };
28
+ }
29
+
30
+ type AmqOperationDiagnostic = NonNullable<AmqOperationError["diagnostic"]>;
31
+
32
+ export type AmqOperationResult<T> =
33
+ | { readonly ok: true; readonly value: T }
34
+ | { readonly ok: false; readonly error: AmqOperationError };
35
+
36
+ export interface SendInput {
37
+ readonly to: readonly string[];
38
+ readonly body: string;
39
+ readonly subject?: string;
40
+ readonly kind?: string;
41
+ readonly priority?: AmqPriority;
42
+ readonly labels?: readonly string[];
43
+ readonly context?: Readonly<Record<string, unknown>>;
44
+ readonly thread?: string;
45
+ readonly project?: string;
46
+ readonly session?: string;
47
+ readonly waitForDrained?: boolean;
48
+ readonly waitTimeout?: string;
49
+ }
50
+
51
+ export interface ReplyInput {
52
+ readonly id: string;
53
+ readonly body: string;
54
+ readonly subject?: string;
55
+ readonly kind?: string;
56
+ readonly priority?: AmqPriority;
57
+ readonly labels?: readonly string[];
58
+ readonly context?: Readonly<Record<string, unknown>>;
59
+ readonly waitForDrained?: boolean;
60
+ readonly waitTimeout?: string;
61
+ }
62
+
63
+ export interface InboxInput {
64
+ readonly box?: "new" | "cur";
65
+ readonly from?: string;
66
+ readonly kind?: string;
67
+ readonly priority?: AmqPriority;
68
+ readonly labels?: readonly string[];
69
+ readonly limit?: number;
70
+ readonly offset?: number;
71
+ }
72
+
73
+ export interface GetMessageInput {
74
+ readonly id: string;
75
+ readonly offsetBytes?: number;
76
+ readonly maximumBytes?: number;
77
+ }
78
+
79
+ export interface ReceiverClaimPort {
80
+ claimExact(id: string, signal?: AbortSignal): Promise<AmqMessage>;
81
+ }
82
+
83
+ export interface ReplyRouteLookup {
84
+ lookup(messageId: string): {
85
+ readonly to: string;
86
+ readonly project?: string;
87
+ readonly session?: string;
88
+ } | undefined;
89
+ }
90
+
91
+ /** Narrow application service for typed AMQ tools and commands. */
92
+ export class AmqOperations {
93
+ readonly #adapter: Pick<AmqCliAdapter, "runJson" | "runJsonWithBody">;
94
+ readonly #binding: AmqOperationBinding;
95
+ readonly #claimPort: ReceiverClaimPort | undefined;
96
+ readonly #replyRouteLookup: ReplyRouteLookup | undefined;
97
+
98
+ constructor(options: {
99
+ readonly adapter: Pick<AmqCliAdapter, "runJson" | "runJsonWithBody">;
100
+ readonly binding: AmqOperationBinding;
101
+ readonly claimPort?: ReceiverClaimPort;
102
+ readonly replyRouteLookup?: ReplyRouteLookup;
103
+ }) {
104
+ this.#adapter = options.adapter;
105
+ this.#binding = options.binding;
106
+ this.#claimPort = options.claimPort;
107
+ this.#replyRouteLookup = options.replyRouteLookup;
108
+ }
109
+
110
+ async send(input: SendInput, signal?: AbortSignal): Promise<AmqOperationResult<unknown>> {
111
+ if (input.to.length === 0) return invalid("send", "At least one recipient is required");
112
+ const argv = [
113
+ "send", ...this.#route(), "--to", input.to.join(","),
114
+ ...optional("--subject", input.subject),
115
+ ...optional("--kind", input.kind),
116
+ ...optional("--priority", input.priority),
117
+ ...optionalCsv("--labels", input.labels),
118
+ ...optionalJson("--context", input.context),
119
+ ...optional("--thread", input.thread),
120
+ ...optional("--project", input.project),
121
+ ...optional("--session", input.session),
122
+ ...(input.waitForDrained ? ["--wait-for", "drained", "--wait-timeout", input.waitTimeout ?? "2m"] : []),
123
+ ];
124
+ const result = await this.#adapter.runJsonWithBody(argv, input.body, signal ? { signal } : {});
125
+ const outboxFailure = senderOutboxAuditFailure(result.json);
126
+ if (result.ok && !outboxFailure) return { ok: true, value: this.#project(result.json) };
127
+ if (result.ok && outboxFailure) {
128
+ return {
129
+ ok: false,
130
+ error: {
131
+ operation: "send",
132
+ category: "sender-outbox-audit-failed",
133
+ classification: "delivery-committed-sender-audit-failed",
134
+ message: `Delivery committed, but sender outbox audit failed: ${outboxFailure.message}`,
135
+ committed: true,
136
+ evidence: {
137
+ delivery: "committed",
138
+ senderAudit: safeProjection(outboxFailure.value, 0, [this.#binding.root]),
139
+ },
140
+ },
141
+ };
142
+ }
143
+
144
+ const primary = operationError(
145
+ "send",
146
+ result,
147
+ [this.#binding.root],
148
+ result.json === undefined ? undefined : this.#project(result.json),
149
+ );
150
+ if (result.category === "timeout") {
151
+ const id = extractMessageId(result.json);
152
+ return {
153
+ ok: false,
154
+ error: {
155
+ ...primary,
156
+ ...(id ? { diagnostic: await this.#traceDiagnostic(id, signal) } : {}),
157
+ },
158
+ };
159
+ }
160
+ return {
161
+ ok: false,
162
+ error: { ...primary, diagnostic: await this.#routeDiagnostic(input.to, input, signal) },
163
+ };
164
+ }
165
+
166
+ async reply(input: ReplyInput, signal?: AbortSignal): Promise<AmqOperationResult<unknown>> {
167
+ const argv = [
168
+ "reply", ...this.#route(), "--id", input.id,
169
+ ...optional("--subject", input.subject),
170
+ ...optional("--kind", input.kind),
171
+ ...optional("--priority", input.priority),
172
+ ...optionalCsv("--labels", input.labels),
173
+ ...optionalJson("--context", input.context),
174
+ ...(input.waitForDrained ? ["--wait-for", "drained", "--wait-timeout", input.waitTimeout ?? "2m"] : []),
175
+ ];
176
+ const result = await this.#adapter.runJsonWithBody(argv, input.body, signal ? { signal } : {});
177
+ if (result.ok) return { ok: true, value: this.#project(result.json) };
178
+ const primary = operationError(
179
+ "reply",
180
+ result,
181
+ [this.#binding.root],
182
+ result.json === undefined ? undefined : this.#project(result.json),
183
+ );
184
+ if (result.category === "timeout") {
185
+ const id = extractMessageId(result.json);
186
+ return {
187
+ ok: false,
188
+ error: { ...primary, ...(id ? { diagnostic: await this.#traceDiagnostic(id, signal) } : {}) },
189
+ };
190
+ }
191
+ const route = this.#replyRouteLookup?.lookup(input.id);
192
+ return {
193
+ ok: false,
194
+ error: {
195
+ ...primary,
196
+ diagnostic: await this.#routeDiagnostic(
197
+ route ? [route.to] : [],
198
+ route ?? {},
199
+ signal,
200
+ ),
201
+ },
202
+ };
203
+ }
204
+
205
+ async inbox(input: InboxInput, signal?: AbortSignal): Promise<AmqOperationResult<unknown>> {
206
+ const limit = boundedInteger(input.limit ?? DEFAULT_LIMIT, 1, MAX_LIMIT, "limit");
207
+ const offset = boundedInteger(input.offset ?? 0, 0, Number.MAX_SAFE_INTEGER, "offset");
208
+ const argv = [
209
+ "list", ...this.#route(), input.box === "cur" ? "--cur" : "--new",
210
+ "--limit", String(limit), "--offset", String(offset),
211
+ ...optional("--from", input.from),
212
+ ...optional("--kind", input.kind),
213
+ ...optional("--priority", input.priority),
214
+ ...(input.labels ?? []).flatMap((label) => ["--label", label]),
215
+ ];
216
+ return this.#readOnly("inbox", argv, signal);
217
+ }
218
+
219
+ async getMessage(input: GetMessageInput, signal?: AbortSignal): Promise<AmqOperationResult<unknown>> {
220
+ const offset = boundedInteger(input.offsetBytes ?? 0, 0, Number.MAX_SAFE_INTEGER, "offsetBytes");
221
+ const maximum = boundedInteger(
222
+ input.maximumBytes ?? DEFAULT_BODY_SLICE_BYTES,
223
+ 1,
224
+ MAX_BODY_SLICE_BYTES,
225
+ "maximumBytes",
226
+ );
227
+ if (!this.#claimPort) {
228
+ return invalid("get_message", "An owning receiver lease is required for exact-ID retrieval");
229
+ }
230
+ let message: AmqMessage;
231
+ try {
232
+ message = await this.#claimPort.claimExact(input.id, signal);
233
+ } catch (error) {
234
+ return {
235
+ ok: false,
236
+ error: {
237
+ operation: "get_message",
238
+ category: "claim-failed",
239
+ message: redact(toError(error).message, [this.#binding.root]),
240
+ committed: "unknown",
241
+ },
242
+ };
243
+ }
244
+ const body = utf8Slice(message.body, offset, maximum);
245
+ return {
246
+ ok: true,
247
+ value: {
248
+ header: this.#project(message.header),
249
+ body: body.value,
250
+ offsetBytes: offset,
251
+ returnedBytes: Buffer.byteLength(body.value),
252
+ totalBytes: Buffer.byteLength(message.body),
253
+ truncated: body.truncated,
254
+ },
255
+ };
256
+ }
257
+
258
+ async thread(thread: string, limit = DEFAULT_LIMIT, signal?: AbortSignal): Promise<AmqOperationResult<unknown>> {
259
+ return this.#readOnly(
260
+ "thread",
261
+ ["thread", ...this.#route(), "--id", thread, "--limit", String(boundedInteger(limit, 1, MAX_LIMIT, "limit"))],
262
+ signal,
263
+ );
264
+ }
265
+
266
+ async receipts(
267
+ messageId: string,
268
+ options: { readonly wait?: boolean; readonly stage?: "drained" | "dlq"; readonly timeout?: string } = {},
269
+ signal?: AbortSignal,
270
+ ): Promise<AmqOperationResult<unknown>> {
271
+ const action = options.wait ? "wait" : "list";
272
+ return this.#readOnly(
273
+ "receipts",
274
+ [
275
+ "receipts", action, ...this.#route(), "--msg-id", messageId,
276
+ ...optional("--stage", options.stage),
277
+ ...(options.wait ? ["--timeout", options.timeout ?? "1m"] : []),
278
+ ],
279
+ signal,
280
+ );
281
+ }
282
+
283
+ async presenceList(signal?: AbortSignal): Promise<AmqOperationResult<unknown>> {
284
+ return this.#readOnly("presence_list", ["presence", "list", ...this.#route()], signal);
285
+ }
286
+
287
+ async setStatus(status: string, note?: string, signal?: AbortSignal): Promise<AmqOperationResult<unknown>> {
288
+ return this.#readOnly(
289
+ "set_status",
290
+ ["presence", "set", ...this.#route(), "--status", status, ...optional("--note", note)],
291
+ signal,
292
+ );
293
+ }
294
+
295
+ async dlqList(limit = DEFAULT_LIMIT, signal?: AbortSignal): Promise<AmqOperationResult<unknown>> {
296
+ const bound = boundedInteger(limit, 1, MAX_LIMIT, "limit");
297
+ const result = await this.#readOnly("dlq_list", ["dlq", "list", ...this.#route()], signal);
298
+ if (!result.ok || !Array.isArray(result.value)) return result;
299
+ return { ok: true, value: result.value.slice(0, bound) };
300
+ }
301
+
302
+ async dlqRead(id: string, signal?: AbortSignal): Promise<AmqOperationResult<unknown>> {
303
+ return this.#readOnly("dlq_read", ["dlq", "read", ...this.#route(), "--id", id], signal);
304
+ }
305
+
306
+ async trace(id: string, signal?: AbortSignal): Promise<AmqOperationResult<unknown>> {
307
+ return this.#readOnly("trace", ["trace", id, "--root", this.#binding.root], signal);
308
+ }
309
+
310
+ async doctor(signal?: AbortSignal): Promise<AmqOperationResult<unknown>> {
311
+ return this.#readOnly("doctor", ["doctor", "--root", this.#binding.root, "--ops"], signal);
312
+ }
313
+
314
+ async #readOnly(operation: string, argv: readonly string[], signal?: AbortSignal): Promise<AmqOperationResult<unknown>> {
315
+ const result = await this.#adapter.runJson(argv, signal ? { signal } : {});
316
+ return result.ok
317
+ ? { ok: true, value: this.#project(result.json) }
318
+ : { ok: false, error: operationError(operation, result, [this.#binding.root]) };
319
+ }
320
+
321
+ async #routeDiagnostic(
322
+ to: readonly string[],
323
+ target: { readonly project?: string; readonly session?: string },
324
+ signal?: AbortSignal,
325
+ ): Promise<AmqOperationDiagnostic> {
326
+ const result = await this.#adapter.runJson(
327
+ [
328
+ "route", "explain", ...this.#route(),
329
+ ...optional("--to", to.length > 0 ? to.join(",") : undefined),
330
+ ...optional("--project", target.project),
331
+ ...optional("--session", target.session),
332
+ ],
333
+ signal ? { signal } : {},
334
+ );
335
+ return {
336
+ kind: "route",
337
+ ok: result.ok,
338
+ value: result.ok
339
+ ? this.#project(result.json)
340
+ : operationError("route_explain", result, [this.#binding.root]),
341
+ };
342
+ }
343
+
344
+ async #traceDiagnostic(id: string, signal?: AbortSignal): Promise<AmqOperationDiagnostic> {
345
+ const result = await this.#adapter.runJson(
346
+ ["trace", id, "--root", this.#binding.root],
347
+ signal ? { signal } : {},
348
+ );
349
+ return {
350
+ kind: "trace",
351
+ ok: result.ok,
352
+ value: result.ok
353
+ ? this.#project(result.json)
354
+ : operationError("trace", result, [this.#binding.root]),
355
+ };
356
+ }
357
+
358
+ #route(): string[] {
359
+ return ["--root", this.#binding.root, "--me", this.#binding.handle];
360
+ }
361
+
362
+ #project(value: unknown): unknown {
363
+ return safeProjection(value, 0, [this.#binding.root]);
364
+ }
365
+ }
366
+
367
+ export function safeProjection(
368
+ value: unknown,
369
+ depth = 0,
370
+ redactions: readonly string[] = [],
371
+ ): unknown {
372
+ if (depth > 8) return "[projection depth exceeded]";
373
+ if (Array.isArray(value)) {
374
+ return value.slice(0, MAX_LIMIT).map((item) => safeProjection(item, depth + 1, redactions));
375
+ }
376
+ if (!value || typeof value !== "object") {
377
+ if (typeof value === "string") {
378
+ const redacted = redact(value, redactions);
379
+ return redacted.length <= 24 * 1024 ? redacted : `${redacted.slice(0, 24 * 1024)}…`;
380
+ }
381
+ return value;
382
+ }
383
+ const output: Record<string, unknown> = {};
384
+ for (const [key, child] of Object.entries(value)) {
385
+ if (/(^|_)(paths?|dirs?|directories|files?|maildirs?|roots?|outbox(?:es)?)($|_)/i.test(key)
386
+ || /(?:Paths?|Dirs?|Directories|Files?|Maildirs?|Roots?|Outbox(?:es)?)$/u.test(key)) continue;
387
+ output[key] = safeProjection(child, depth + 1, redactions);
388
+ }
389
+ return output;
390
+ }
391
+
392
+ function operationError(
393
+ operation: string,
394
+ result: AmqCommandResult<unknown>,
395
+ redactions: readonly string[] = [],
396
+ evidence?: unknown,
397
+ ): AmqOperationError {
398
+ const literal = result.stderr.trim() || result.jsonError?.message || result.processError?.message;
399
+ return {
400
+ operation,
401
+ category: result.category,
402
+ message: redact(literal || `${operation} failed (${result.category})`, redactions),
403
+ committed: result.category === "timeout"
404
+ || result.category === "process-crash"
405
+ || ((operation === "send" || operation === "reply") && result.json !== undefined)
406
+ ? "unknown"
407
+ : false,
408
+ classification: "primary-failure",
409
+ ...(evidence === undefined ? {} : { evidence }),
410
+ };
411
+ }
412
+
413
+ function senderOutboxAuditFailure(value: unknown): { readonly message: string; readonly value: unknown } | undefined {
414
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
415
+ const outbox = (value as Record<string, unknown>).outbox;
416
+ if (!outbox || typeof outbox !== "object" || Array.isArray(outbox)) return undefined;
417
+ const object = outbox as Record<string, unknown>;
418
+ const written = object.written;
419
+ const durability = object.durability;
420
+ const error = object.error;
421
+ if (written === true && durability !== "indeterminate" && (error === "" || error === undefined || error === null)) {
422
+ return undefined;
423
+ }
424
+ const message = typeof error === "string" && error.trim().length > 0
425
+ ? error.trim()
426
+ : durability === "indeterminate"
427
+ ? "outbox durability is indeterminate"
428
+ : "outbox copy was not written";
429
+ return { message, value: object };
430
+ }
431
+
432
+ function redact(value: string, redactions: readonly string[]): string {
433
+ return redactions
434
+ .filter((item) => item.length > 0)
435
+ .reduce((current, item) => current.split(item).join("<amq-root>"), value);
436
+ }
437
+
438
+ function invalid(operation: string, message: string): AmqOperationResult<never> {
439
+ return { ok: false, error: { operation, category: "invalid-request", message, committed: false } };
440
+ }
441
+
442
+ function optional(flag: string, value: string | undefined): string[] {
443
+ return value === undefined || value.length === 0 ? [] : [flag, value];
444
+ }
445
+
446
+ function optionalCsv(flag: string, value: readonly string[] | undefined): string[] {
447
+ return !value || value.length === 0 ? [] : [flag, value.join(",")];
448
+ }
449
+
450
+ function optionalJson(flag: string, value: Readonly<Record<string, unknown>> | undefined): string[] {
451
+ return value === undefined ? [] : [flag, JSON.stringify(value)];
452
+ }
453
+
454
+ function extractMessageId(value: unknown): string | undefined {
455
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
456
+ const object = value as Record<string, unknown>;
457
+ for (const candidate of [object.id, object.message_id, object.msg_id]) {
458
+ if (typeof candidate === "string" && candidate.length > 0) return candidate;
459
+ }
460
+ return undefined;
461
+ }
462
+
463
+ function boundedInteger(value: number, minimum: number, maximum: number, name: string): number {
464
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
465
+ throw new TypeError(`${name} must be an integer between ${minimum} and ${maximum}`);
466
+ }
467
+ return value;
468
+ }
469
+
470
+ function utf8Slice(value: string, offset: number, maximum: number): { readonly value: string; readonly truncated: boolean } {
471
+ const bytes = Buffer.from(value);
472
+ const end = Math.min(bytes.length, offset + maximum);
473
+ let start = Math.min(offset, bytes.length);
474
+ while (start < bytes.length && (bytes[start]! & 0xc0) === 0x80) start += 1;
475
+ let safeEnd = end;
476
+ while (safeEnd > start && safeEnd < bytes.length && (bytes[safeEnd]! & 0xc0) === 0x80) safeEnd -= 1;
477
+ return { value: bytes.subarray(start, safeEnd).toString("utf8"), truncated: safeEnd < bytes.length };
478
+ }
479
+
480
+ function toError(error: unknown): Error {
481
+ return error instanceof Error ? error : new Error(String(error));
482
+ }
@@ -0,0 +1,115 @@
1
+ import {
2
+ chmod,
3
+ link,
4
+ open,
5
+ readFile,
6
+ rename,
7
+ unlink,
8
+ } from "node:fs/promises";
9
+ import { dirname, join } from "node:path";
10
+ import { randomUUID } from "node:crypto";
11
+
12
+ export type AtomicWritePoint =
13
+ | "after-file-sync"
14
+ | "before-commit"
15
+ | "after-commit"
16
+ | "after-directory-sync";
17
+
18
+ export type AtomicFaultInjector = (
19
+ point: AtomicWritePoint,
20
+ ) => void | Promise<void>;
21
+
22
+ export interface AtomicJsonWriteOptions {
23
+ readonly createOnly?: boolean;
24
+ readonly fault?: AtomicFaultInjector;
25
+ }
26
+
27
+ /**
28
+ * Writes one private JSON record through a same-directory atomic commit.
29
+ *
30
+ * `createOnly` uses an exclusive hard-link commit so a contender can never
31
+ * replace an existing record. Replacement uses rename. In both cases the
32
+ * complete file is synced before it becomes visible and the parent directory
33
+ * is synced after the namespace mutation.
34
+ */
35
+ export async function writeAtomicJson(
36
+ path: string,
37
+ value: unknown,
38
+ options: AtomicJsonWriteOptions = {},
39
+ ): Promise<void> {
40
+ const parent = dirname(path);
41
+ const temporaryPath = join(parent, `.pi-amq-${randomUUID()}.tmp`);
42
+ const contents = `${JSON.stringify(value)}\n`;
43
+ let committed = false;
44
+
45
+ try {
46
+ const handle = await open(temporaryPath, "wx", 0o600);
47
+ try {
48
+ await handle.writeFile(contents, "utf8");
49
+ await handle.sync();
50
+ } finally {
51
+ await handle.close();
52
+ }
53
+ await chmod(temporaryPath, 0o600);
54
+ await options.fault?.("after-file-sync");
55
+ await options.fault?.("before-commit");
56
+
57
+ if (options.createOnly) {
58
+ await link(temporaryPath, path);
59
+ committed = true;
60
+ await unlink(temporaryPath);
61
+ } else {
62
+ await rename(temporaryPath, path);
63
+ committed = true;
64
+ }
65
+
66
+ await options.fault?.("after-commit");
67
+ await syncDirectory(parent);
68
+ await options.fault?.("after-directory-sync");
69
+ } catch (error) {
70
+ if (!committed) await removeIfPresent(temporaryPath);
71
+ throw error;
72
+ }
73
+ }
74
+
75
+ export async function readJson(
76
+ path: string,
77
+ ): Promise<unknown | undefined> {
78
+ let contents: string;
79
+ try {
80
+ contents = await readFile(path, "utf8");
81
+ } catch (error) {
82
+ if (isNodeError(error) && error.code === "ENOENT") return undefined;
83
+ throw error;
84
+ }
85
+ return JSON.parse(contents) as unknown;
86
+ }
87
+
88
+ export async function syncDirectory(path: string): Promise<void> {
89
+ let handle;
90
+ try {
91
+ handle = await open(path, "r");
92
+ await handle.sync();
93
+ } catch (error) {
94
+ if (!isUnsupportedDirectorySync(error)) throw error;
95
+ } finally {
96
+ await handle?.close();
97
+ }
98
+ }
99
+
100
+ async function removeIfPresent(path: string): Promise<void> {
101
+ try {
102
+ await unlink(path);
103
+ } catch (error) {
104
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
105
+ }
106
+ }
107
+
108
+ function isUnsupportedDirectorySync(error: unknown): boolean {
109
+ return isNodeError(error)
110
+ && (error.code === "EINVAL" || error.code === "ENOTSUP" || error.code === "EISDIR");
111
+ }
112
+
113
+ function isNodeError(error: unknown): error is NodeJS.ErrnoException {
114
+ return error instanceof Error && "code" in error;
115
+ }