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,286 @@
1
+ /*
2
+ * SPDX-License-Identifier: MIT
3
+ * Structural interaction pattern adapted from ZiChuanLan/pi-subagents-lite,
4
+ * itself forked from @tintinweb/pi-subagents v0.14.2.
5
+ * Copyright (c) 2026 tintinweb
6
+ * Copyright (c) 2026 lan
7
+ * Modified for pi-amq: render-only authoritative projection; no AgentManager.
8
+ */
9
+
10
+ export type FleetLifecycle = "idle" | "busy" | "paused" | "offline" | "ambiguous";
11
+
12
+ export interface FleetAgentRecord {
13
+ readonly id: string;
14
+ readonly handle: string;
15
+ readonly role: string;
16
+ readonly model: string;
17
+ readonly state: FleetLifecycle;
18
+ readonly spend: number | null;
19
+ readonly lastActivity: string;
20
+ readonly mode: "main" | "rpc" | "json";
21
+ readonly generation?: string;
22
+ readonly conversation?: readonly string[];
23
+ }
24
+
25
+ export interface FleetProjectionSource {
26
+ snapshot(): readonly FleetAgentRecord[];
27
+ subscribe(listener: () => void): () => void;
28
+ }
29
+
30
+ export class MutableFleetProjectionSource implements FleetProjectionSource {
31
+ #records: readonly FleetAgentRecord[];
32
+ readonly #listeners = new Set<() => void>();
33
+
34
+ constructor(initial: readonly FleetAgentRecord[]) {
35
+ this.#records = [...initial];
36
+ }
37
+
38
+ snapshot(): readonly FleetAgentRecord[] {
39
+ return this.#records.map((record) => ({ ...record }));
40
+ }
41
+
42
+ update(records: readonly FleetAgentRecord[]): void {
43
+ this.#records = [...records];
44
+ for (const listener of this.#listeners) listener();
45
+ }
46
+
47
+ subscribe(listener: () => void): () => void {
48
+ this.#listeners.add(listener);
49
+ return () => this.#listeners.delete(listener);
50
+ }
51
+ }
52
+
53
+ export class FleetListModel {
54
+ readonly #source: FleetProjectionSource;
55
+ #selected = 0;
56
+ #open = false;
57
+ #invalidations = 0;
58
+ readonly #unsubscribe: () => void;
59
+
60
+ constructor(source: FleetProjectionSource, invalidate: () => void) {
61
+ this.#source = source;
62
+ this.#unsubscribe = source.subscribe(() => {
63
+ const last = Math.max(0, this.#source.snapshot().length - 1);
64
+ this.#selected = Math.min(this.#selected, last);
65
+ this.#invalidations += 1;
66
+ invalidate();
67
+ });
68
+ }
69
+
70
+ get invalidations(): number {
71
+ return this.#invalidations;
72
+ }
73
+
74
+ get selected(): FleetAgentRecord | undefined {
75
+ return this.#source.snapshot()[this.#selected];
76
+ }
77
+
78
+ get open(): boolean {
79
+ return this.#open;
80
+ }
81
+
82
+ focus(): void {
83
+ this.#open = true;
84
+ }
85
+
86
+ close(): void {
87
+ this.#open = false;
88
+ }
89
+
90
+ move(delta: -1 | 1): void {
91
+ const count = this.#source.snapshot().length;
92
+ if (count === 0) return;
93
+ this.#selected = Math.max(0, Math.min(count - 1, this.#selected + delta));
94
+ }
95
+
96
+ render(): string[] {
97
+ const records = this.#source.snapshot();
98
+ if (records.length === 0) return ["Fleet: no active sessions"];
99
+ return [
100
+ "Fleet",
101
+ ...records.map((record, index) => {
102
+ const marker = this.#open && index === this.#selected ? ">" : " ";
103
+ const spend = record.spend === null ? "-" : `$${record.spend.toFixed(4)}`;
104
+ return `${marker} ${record.handle}/${record.role} ${record.model} ${record.state} ${spend} ${record.lastActivity}`;
105
+ }),
106
+ ];
107
+ }
108
+
109
+ dispose(): void {
110
+ this.#unsubscribe();
111
+ }
112
+ }
113
+
114
+ export type FleetInputAction =
115
+ | { readonly consume: false }
116
+ | { readonly consume: true; readonly action: "focus" | "move" | "detail" | "close" };
117
+
118
+ export function handleFleetInput(options: {
119
+ readonly data: string;
120
+ readonly editorText: string;
121
+ readonly list: FleetListModel;
122
+ }): FleetInputAction {
123
+ if (options.editorText !== "") return { consume: false };
124
+ const { data, list } = options;
125
+ if (!list.open && isDown(data)) {
126
+ list.focus();
127
+ return { consume: true, action: "focus" };
128
+ }
129
+ if (!list.open) return { consume: false };
130
+ if (isDown(data)) {
131
+ list.move(1);
132
+ return { consume: true, action: "move" };
133
+ }
134
+ if (isUp(data)) {
135
+ list.move(-1);
136
+ return { consume: true, action: "move" };
137
+ }
138
+ if (data === "\r" || data === "\n") return { consume: true, action: "detail" };
139
+ if (data === "\u001b") {
140
+ list.close();
141
+ return { consume: true, action: "close" };
142
+ }
143
+ return { consume: false };
144
+ }
145
+
146
+ export function renderFleetDetail(record: FleetAgentRecord): string[] {
147
+ return [
148
+ `${record.handle}/${record.role} (${record.state})`,
149
+ `model=${record.model} activity=${record.lastActivity}`,
150
+ ...(record.mode === "json" ? ["one-shot worker, cannot steer"] : []),
151
+ "",
152
+ ...(record.conversation ?? ["No conversation projection available."]),
153
+ ];
154
+ }
155
+
156
+ export function canSteerFleetRecord(
157
+ record: FleetAgentRecord,
158
+ currentGeneration: string | undefined,
159
+ ): boolean {
160
+ return record.mode === "rpc"
161
+ && record.generation !== undefined
162
+ && record.generation === currentGeneration;
163
+ }
164
+
165
+ export type FleetControlStatus = "idle" | "composing" | "requested" | "observed" | "indeterminate";
166
+
167
+ export class FleetDetailController {
168
+ readonly #recordId: string;
169
+ readonly #generation: string | undefined;
170
+ readonly #resolve: () => FleetAgentRecord | undefined;
171
+ readonly #steer: (recordId: string, generation: string, message: string) => Promise<void>;
172
+ readonly #cancel: (recordId: string, generation: string) => Promise<void>;
173
+ #composer = "";
174
+ #status: FleetControlStatus = "idle";
175
+ #diagnostic = "";
176
+ #xArmed = false;
177
+
178
+ constructor(options: {
179
+ readonly record: FleetAgentRecord;
180
+ readonly resolve: () => FleetAgentRecord | undefined;
181
+ readonly steer: (recordId: string, generation: string, message: string) => Promise<void>;
182
+ readonly cancel: (recordId: string, generation: string) => Promise<void>;
183
+ }) {
184
+ this.#recordId = options.record.id;
185
+ this.#generation = options.record.generation;
186
+ this.#resolve = options.resolve;
187
+ this.#steer = options.steer;
188
+ this.#cancel = options.cancel;
189
+ }
190
+
191
+ get status(): FleetControlStatus {
192
+ return this.#status;
193
+ }
194
+
195
+ render(): string[] {
196
+ const record = this.#resolve();
197
+ if (!record) return ["Worker projection disappeared.", "control=indeterminate"];
198
+ const controls = record.mode === "rpc"
199
+ ? ["Enter: compose steer · x,x: stop · Esc: close"]
200
+ : ["one-shot worker, cannot steer", "x,x: stop · Esc: close"];
201
+ return [
202
+ ...renderFleetDetail(record),
203
+ "",
204
+ ...controls,
205
+ ...(this.#status === "composing" ? [`steer> ${this.#composer}`] : []),
206
+ ...(this.#status === "idle" ? [] : [`control=${this.#status}${this.#diagnostic ? `: ${this.#diagnostic}` : ""}`]),
207
+ ];
208
+ }
209
+
210
+ handleInput(data: string): { readonly close: boolean; readonly consumed: boolean } {
211
+ if (data === "\u001b") {
212
+ if (this.#status === "composing") {
213
+ this.#composer = "";
214
+ this.#status = "idle";
215
+ return { close: false, consumed: true };
216
+ }
217
+ return { close: true, consumed: true };
218
+ }
219
+ if (this.#status === "composing") {
220
+ if (data === "\r" || data === "\n") {
221
+ const message = this.#composer.trim();
222
+ this.#composer = "";
223
+ if (message) void this.#request("steer", message);
224
+ else this.#status = "idle";
225
+ return { close: false, consumed: true };
226
+ }
227
+ if (data === "\u007f") this.#composer = this.#composer.slice(0, -1);
228
+ else if (!data.startsWith("\u001b")) this.#composer += data;
229
+ return { close: false, consumed: true };
230
+ }
231
+ if (data === "\r" || data === "\n") {
232
+ const record = this.#resolve();
233
+ if (record?.mode === "rpc" && this.#isGenerationCurrent(record)) {
234
+ this.#status = "composing";
235
+ } else {
236
+ this.#status = "indeterminate";
237
+ this.#diagnostic = record?.mode === "json"
238
+ ? "one-shot worker, cannot steer"
239
+ : "worker generation changed";
240
+ }
241
+ return { close: false, consumed: true };
242
+ }
243
+ if (data === "x") {
244
+ if (this.#xArmed) {
245
+ this.#xArmed = false;
246
+ void this.#request("cancel");
247
+ } else {
248
+ this.#xArmed = true;
249
+ }
250
+ return { close: false, consumed: true };
251
+ }
252
+ this.#xArmed = false;
253
+ return { close: false, consumed: false };
254
+ }
255
+
256
+ #isGenerationCurrent(record: FleetAgentRecord): record is FleetAgentRecord & { generation: string } {
257
+ return this.#generation !== undefined && record.generation === this.#generation;
258
+ }
259
+
260
+ async #request(kind: "steer" | "cancel", message?: string): Promise<void> {
261
+ const record = this.#resolve();
262
+ if (!record || !this.#isGenerationCurrent(record)) {
263
+ this.#status = "indeterminate";
264
+ this.#diagnostic = "worker generation changed";
265
+ return;
266
+ }
267
+ this.#status = "requested";
268
+ this.#diagnostic = kind;
269
+ try {
270
+ if (kind === "steer") await this.#steer(this.#recordId, record.generation, message!);
271
+ else await this.#cancel(this.#recordId, record.generation);
272
+ this.#status = "observed";
273
+ } catch (error) {
274
+ this.#status = "indeterminate";
275
+ this.#diagnostic = error instanceof Error ? error.message : String(error);
276
+ }
277
+ }
278
+ }
279
+
280
+ function isDown(data: string): boolean {
281
+ return data === "\u001b[B";
282
+ }
283
+
284
+ function isUp(data: string): boolean {
285
+ return data === "\u001b[A";
286
+ }
@@ -0,0 +1,166 @@
1
+ import type { AmqMessage } from "../transport/amq-message.js";
2
+
3
+ export interface FreshSessionProjectionEvidence {
4
+ readonly messageId: string;
5
+ readonly sessionId: string;
6
+ readonly diskVisible: boolean;
7
+ }
8
+
9
+ export interface FreshSessionCapabilityProbe {
10
+ probe(): Promise<
11
+ | { readonly capable: true }
12
+ | { readonly capable: false; readonly reason: string; readonly dependencies: readonly string[] }
13
+ >;
14
+ dispatch(
15
+ message: AmqMessage,
16
+ signal?: AbortSignal,
17
+ canReplaceSession?: (signal?: AbortSignal) => Promise<boolean>,
18
+ ): Promise<FreshSessionProjectionEvidence>;
19
+ }
20
+
21
+ export interface FreshSessionReplacementPort {
22
+ waitForIdle(signal?: AbortSignal): Promise<void>;
23
+ replace(
24
+ message: AmqMessage,
25
+ signal?: AbortSignal,
26
+ ): Promise<{
27
+ readonly cancelled: boolean;
28
+ readonly projection?: FreshSessionProjectionEvidence;
29
+ }>;
30
+ }
31
+
32
+ export class FreshSessionInterlockError extends Error {
33
+ constructor() {
34
+ super("worker_runtime_active");
35
+ this.name = "FreshSessionInterlockError";
36
+ }
37
+ }
38
+
39
+ /** Full capable path; a real adapter must bind replace() to replacement-context-only session control. */
40
+ export class CommandContextFreshSessionCapability implements FreshSessionCapabilityProbe {
41
+ readonly #port: FreshSessionReplacementPort;
42
+
43
+ constructor(port: FreshSessionReplacementPort) {
44
+ this.#port = port;
45
+ }
46
+
47
+ async probe(): Promise<{ readonly capable: true }> {
48
+ return { capable: true };
49
+ }
50
+
51
+ async dispatch(
52
+ message: AmqMessage,
53
+ signal?: AbortSignal,
54
+ canReplaceSession: (signal?: AbortSignal) => Promise<boolean> = async () => true,
55
+ ): Promise<FreshSessionProjectionEvidence> {
56
+ if (!await canReplaceSession(signal)) throw new FreshSessionInterlockError();
57
+ await this.#port.waitForIdle(signal);
58
+ throwIfAborted(signal);
59
+ if (!await canReplaceSession(signal)) throw new FreshSessionInterlockError();
60
+ const replaced = await this.#port.replace(message, signal);
61
+ if (replaced.cancelled) throw new Error("Fresh-session replacement was cancelled");
62
+ // diskVisible may legitimately be false here: pi persists a fresh session's file lazily
63
+ // (only once its first assistant entry arrives), so an in-memory-only projection is a
64
+ // valid deferred state, not a failure. The journal keeps the record pending with its
65
+ // target session set until reconciliation later observes the disk-visible projection.
66
+ if (!replaced.projection || replaced.projection.messageId !== message.header.id) {
67
+ throw new Error("Fresh-session replacement lacks a matching AMQ projection");
68
+ }
69
+ return replaced.projection;
70
+ }
71
+ }
72
+
73
+ export interface FreshSessionPolicy {
74
+ readonly trustedProject: boolean;
75
+ readonly allowlisted: (sender: string, project: string) => boolean;
76
+ readonly confirm?: (message: AmqMessage, signal?: AbortSignal) => Promise<boolean>;
77
+ readonly canReplaceSession: (signal?: AbortSignal) => Promise<boolean>;
78
+ readonly notifyOnce?: (message: string) => void;
79
+ }
80
+
81
+ export type FreshSessionDecision =
82
+ | { readonly kind: "ordinary" }
83
+ | { readonly kind: "refused"; readonly reason: "not-authorized" | "worker-runtime-active" }
84
+ | { readonly kind: "pending"; readonly reason: string; readonly action: "/amq ingest" }
85
+ | { readonly kind: "dispatched"; readonly projection: FreshSessionProjectionEvidence };
86
+
87
+ /** Serializes privileged session replacement behind explicit policy and capability ports. */
88
+ export class FreshSessionDispatcher {
89
+ readonly #capability: FreshSessionCapabilityProbe;
90
+ readonly #policy: FreshSessionPolicy;
91
+ #tail: Promise<void> = Promise.resolve();
92
+ #notifiedIncapable = false;
93
+
94
+ constructor(options: {
95
+ readonly capability: FreshSessionCapabilityProbe;
96
+ readonly policy: FreshSessionPolicy;
97
+ }) {
98
+ this.#capability = options.capability;
99
+ this.#policy = options.policy;
100
+ }
101
+
102
+ dispatch(message: AmqMessage, signal?: AbortSignal): Promise<FreshSessionDecision> {
103
+ const task = this.#tail.then(() => this.#dispatch(message, signal));
104
+ this.#tail = task.then(() => undefined, () => undefined);
105
+ return task;
106
+ }
107
+
108
+ async #dispatch(message: AmqMessage, signal?: AbortSignal): Promise<FreshSessionDecision> {
109
+ if (!requestsFreshSession(message)) return { kind: "ordinary" };
110
+ throwIfAborted(signal);
111
+
112
+ const allowlisted = this.#policy.trustedProject
113
+ && this.#policy.allowlisted(message.header.from, message.header.fromProject);
114
+ const confirmed = allowlisted
115
+ ? true
116
+ : this.#policy.confirm
117
+ ? await this.#policy.confirm(message, signal)
118
+ : false;
119
+ if (!confirmed) return { kind: "refused", reason: "not-authorized" };
120
+ if (!await this.#policy.canReplaceSession(signal)) {
121
+ return { kind: "refused", reason: "worker-runtime-active" };
122
+ }
123
+
124
+ const capability = await this.#capability.probe();
125
+ if (!capability.capable) {
126
+ const reason = `${capability.reason}; requires ${capability.dependencies.join(" and ")}`;
127
+ if (!this.#notifiedIncapable) {
128
+ this.#notifiedIncapable = true;
129
+ this.#policy.notifyOnce?.(`Fresh-session delivery is pending: ${reason}. Use /amq ingest.`);
130
+ }
131
+ return { kind: "pending", reason, action: "/amq ingest" };
132
+ }
133
+
134
+ try {
135
+ const projection = await this.#capability.dispatch(
136
+ message,
137
+ signal,
138
+ this.#policy.canReplaceSession,
139
+ );
140
+ if (projection.messageId !== message.header.id) {
141
+ throw new Error("Fresh-session capability returned an unrelated projection evidence");
142
+ }
143
+ return { kind: "dispatched", projection };
144
+ } catch (error) {
145
+ if (error instanceof FreshSessionInterlockError) {
146
+ return { kind: "refused", reason: "worker-runtime-active" };
147
+ }
148
+ throw error;
149
+ }
150
+ }
151
+ }
152
+
153
+ export function requestsFreshSession(message: AmqMessage): boolean {
154
+ const piAmq = message.header.context.pi_amq;
155
+ return Boolean(
156
+ piAmq
157
+ && typeof piAmq === "object"
158
+ && !Array.isArray(piAmq)
159
+ && (piAmq as Record<string, unknown>).delivery === "fresh-session",
160
+ );
161
+ }
162
+
163
+ function throwIfAborted(signal: AbortSignal | undefined): void {
164
+ if (!signal?.aborted) return;
165
+ throw signal.reason instanceof Error ? signal.reason : new Error("Fresh-session dispatch was cancelled");
166
+ }