agent-tempo 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/CLAUDE.md +253 -219
  2. package/LICENSE +21 -21
  3. package/README.md +293 -289
  4. package/assets/icon-dark.svg +9 -9
  5. package/assets/icon.svg +9 -9
  6. package/assets/logo-dark.svg +11 -11
  7. package/assets/logo-light.svg +11 -11
  8. package/dashboard/README.md +91 -91
  9. package/dashboard/dist/assets/{index-D6Xyje_n.js → index-jmYe6rmS.js} +2 -2
  10. package/dashboard/dist/assets/index-jmYe6rmS.js.map +1 -0
  11. package/dashboard/dist/index.html +20 -20
  12. package/dashboard/package.json +47 -47
  13. package/dist/activities/outbox.d.ts +30 -1
  14. package/dist/activities/outbox.js +96 -3
  15. package/dist/adapters/base.js +5 -0
  16. package/dist/adapters/copilot/adapter.js +12 -1
  17. package/dist/adapters/index.d.ts +1 -1
  18. package/dist/adapters/index.js +7 -0
  19. package/dist/adapters/pi/adapter.d.ts +2 -0
  20. package/dist/adapters/pi/adapter.js +43 -0
  21. package/dist/adapters/pi/index.d.ts +16 -0
  22. package/dist/adapters/pi/index.js +10 -0
  23. package/dist/cli/global-wrapper.d.ts +19 -0
  24. package/dist/cli/global-wrapper.js +169 -0
  25. package/dist/cli/help-text.js +97 -97
  26. package/dist/cli/startup.js +11 -0
  27. package/dist/cli/upgrade-command.js +81 -81
  28. package/dist/cli.js +12 -0
  29. package/dist/client/core.js +9 -2
  30. package/dist/client/interface.d.ts +6 -0
  31. package/dist/config.d.ts +79 -0
  32. package/dist/config.js +74 -0
  33. package/dist/daemon.js +37 -1
  34. package/dist/http/aggregate.d.ts +22 -1
  35. package/dist/http/aggregate.js +41 -0
  36. package/dist/http/auth.d.ts +94 -8
  37. package/dist/http/auth.js +93 -9
  38. package/dist/http/body.d.ts +4 -1
  39. package/dist/http/body.js +6 -3
  40. package/dist/http/event-bus.js +1 -0
  41. package/dist/http/event-types.d.ts +34 -2
  42. package/dist/http/event-types.js +1 -0
  43. package/dist/http/gate-audit.d.ts +12 -0
  44. package/dist/http/gate-audit.js +95 -0
  45. package/dist/http/gate-registry.d.ts +167 -0
  46. package/dist/http/gate-registry.js +163 -0
  47. package/dist/http/gate-routes.d.ts +48 -0
  48. package/dist/http/gate-routes.js +102 -0
  49. package/dist/http/ingest-registry.d.ts +30 -0
  50. package/dist/http/ingest-registry.js +108 -0
  51. package/dist/http/inner-loop-routes.d.ts +66 -0
  52. package/dist/http/inner-loop-routes.js +182 -0
  53. package/dist/http/inner-loop.d.ts +92 -0
  54. package/dist/http/inner-loop.js +155 -0
  55. package/dist/http/server.d.ts +38 -3
  56. package/dist/http/server.js +211 -6
  57. package/dist/http/snapshot.d.ts +6 -0
  58. package/dist/http/snapshot.js +6 -0
  59. package/dist/pi/cue-pump.d.ts +61 -0
  60. package/dist/pi/cue-pump.js +95 -0
  61. package/dist/pi/extension.d.ts +45 -0
  62. package/dist/pi/extension.js +407 -0
  63. package/dist/pi/gate-client.d.ts +54 -0
  64. package/dist/pi/gate-client.js +136 -0
  65. package/dist/pi/headless.d.ts +85 -0
  66. package/dist/pi/headless.js +224 -0
  67. package/dist/pi/index.d.ts +28 -0
  68. package/dist/pi/index.js +43 -0
  69. package/dist/pi/inner-loop-client.d.ts +67 -0
  70. package/dist/pi/inner-loop-client.js +164 -0
  71. package/dist/pi/inner-loop-publisher.d.ts +187 -0
  72. package/dist/pi/inner-loop-publisher.js +236 -0
  73. package/dist/pi/lazy-proxy.d.ts +37 -0
  74. package/dist/pi/lazy-proxy.js +55 -0
  75. package/dist/pi/mission-control/actions.d.ts +48 -0
  76. package/dist/pi/mission-control/actions.js +98 -0
  77. package/dist/pi/mission-control/board.d.ts +53 -0
  78. package/dist/pi/mission-control/board.js +104 -0
  79. package/dist/pi/mission-control/extension.d.ts +44 -0
  80. package/dist/pi/mission-control/extension.js +251 -0
  81. package/dist/pi/mission-control/index.d.ts +15 -0
  82. package/dist/pi/mission-control/index.js +32 -0
  83. package/dist/pi/mission-control/inner-tail.d.ts +48 -0
  84. package/dist/pi/mission-control/inner-tail.js +76 -0
  85. package/dist/pi/mission-control/pi-ui.d.ts +43 -0
  86. package/dist/pi/mission-control/pi-ui.js +10 -0
  87. package/dist/pi/mission-control/render.d.ts +6 -0
  88. package/dist/pi/mission-control/render.js +95 -0
  89. package/dist/pi/phase-driver.d.ts +74 -0
  90. package/dist/pi/phase-driver.js +122 -0
  91. package/dist/pi/pi-types.d.ts +208 -0
  92. package/dist/pi/pi-types.js +21 -0
  93. package/dist/pi/probe.d.ts +80 -0
  94. package/dist/pi/probe.js +154 -0
  95. package/dist/pi/render-tools.d.ts +17 -0
  96. package/dist/pi/render-tools.js +51 -0
  97. package/dist/pi/reset-pump.d.ts +47 -0
  98. package/dist/pi/reset-pump.js +85 -0
  99. package/dist/pi/tool-capability.d.ts +60 -0
  100. package/dist/pi/tool-capability.js +156 -0
  101. package/dist/pi/workflow-client.d.ts +158 -0
  102. package/dist/pi/workflow-client.js +289 -0
  103. package/dist/pi/zod-to-typebox.d.ts +74 -0
  104. package/dist/pi/zod-to-typebox.js +191 -0
  105. package/dist/scripts/verify-daemon-isolation-guard.js +24 -24
  106. package/dist/server-tools.d.ts +2 -0
  107. package/dist/server-tools.js +50 -46
  108. package/dist/server.js +4 -0
  109. package/dist/spawn.d.ts +55 -0
  110. package/dist/spawn.js +84 -12
  111. package/dist/tools/agent-types.d.ts +2 -2
  112. package/dist/tools/agent-types.js +22 -17
  113. package/dist/tools/attachment-info.d.ts +2 -2
  114. package/dist/tools/attachment-info.js +38 -33
  115. package/dist/tools/broadcast.d.ts +2 -2
  116. package/dist/tools/broadcast.js +69 -64
  117. package/dist/tools/cancel-stage.d.ts +2 -2
  118. package/dist/tools/cancel-stage.js +20 -15
  119. package/dist/tools/clear-state.d.ts +2 -2
  120. package/dist/tools/clear-state.js +25 -20
  121. package/dist/tools/coat-check-evict.d.ts +2 -2
  122. package/dist/tools/coat-check-evict.js +30 -25
  123. package/dist/tools/coat-check-get.d.ts +2 -2
  124. package/dist/tools/coat-check-get.js +39 -34
  125. package/dist/tools/coat-check-list.d.ts +2 -2
  126. package/dist/tools/coat-check-list.js +48 -43
  127. package/dist/tools/coat-check-put.d.ts +2 -2
  128. package/dist/tools/coat-check-put.js +41 -36
  129. package/dist/tools/cue.d.ts +2 -2
  130. package/dist/tools/cue.js +57 -52
  131. package/dist/tools/descriptor.d.ts +72 -0
  132. package/dist/tools/descriptor.js +39 -0
  133. package/dist/tools/destroy.d.ts +2 -2
  134. package/dist/tools/destroy.js +153 -148
  135. package/dist/tools/ensemble.d.ts +2 -2
  136. package/dist/tools/ensemble.js +71 -66
  137. package/dist/tools/evaluate-gate.d.ts +2 -2
  138. package/dist/tools/evaluate-gate.js +33 -27
  139. package/dist/tools/fetch-state.d.ts +2 -2
  140. package/dist/tools/fetch-state.js +43 -38
  141. package/dist/tools/gates.d.ts +2 -2
  142. package/dist/tools/gates.js +39 -34
  143. package/dist/tools/hosts.d.ts +2 -2
  144. package/dist/tools/hosts.js +25 -20
  145. package/dist/tools/listen.d.ts +2 -2
  146. package/dist/tools/listen.js +23 -18
  147. package/dist/tools/load-lineup.d.ts +2 -2
  148. package/dist/tools/load-lineup.js +324 -319
  149. package/dist/tools/migrate.d.ts +2 -2
  150. package/dist/tools/migrate.js +45 -40
  151. package/dist/tools/pause.d.ts +2 -2
  152. package/dist/tools/pause.js +34 -29
  153. package/dist/tools/play.d.ts +2 -2
  154. package/dist/tools/play.js +53 -48
  155. package/dist/tools/quality-gate.d.ts +2 -2
  156. package/dist/tools/quality-gate.js +26 -21
  157. package/dist/tools/recall.d.ts +2 -2
  158. package/dist/tools/recall.js +32 -27
  159. package/dist/tools/recruit.d.ts +2 -2
  160. package/dist/tools/recruit.js +325 -256
  161. package/dist/tools/release.d.ts +2 -2
  162. package/dist/tools/release.js +85 -80
  163. package/dist/tools/report.d.ts +2 -2
  164. package/dist/tools/report.js +28 -23
  165. package/dist/tools/reset.d.ts +3 -0
  166. package/dist/tools/reset.js +51 -0
  167. package/dist/tools/restart.d.ts +2 -2
  168. package/dist/tools/restart.js +51 -46
  169. package/dist/tools/restore.d.ts +2 -2
  170. package/dist/tools/restore.js +76 -71
  171. package/dist/tools/save-lineup.d.ts +2 -2
  172. package/dist/tools/save-lineup.js +32 -27
  173. package/dist/tools/save-state.d.ts +2 -2
  174. package/dist/tools/save-state.js +43 -38
  175. package/dist/tools/schedule.d.ts +2 -2
  176. package/dist/tools/schedule.js +133 -128
  177. package/dist/tools/schedules.d.ts +2 -2
  178. package/dist/tools/schedules.js +41 -36
  179. package/dist/tools/set-ensemble-description.d.ts +2 -2
  180. package/dist/tools/set-ensemble-description.js +26 -21
  181. package/dist/tools/set-name.d.ts +2 -2
  182. package/dist/tools/set-name.js +38 -33
  183. package/dist/tools/set-part.d.ts +2 -2
  184. package/dist/tools/set-part.js +20 -15
  185. package/dist/tools/shutdown.d.ts +2 -2
  186. package/dist/tools/shutdown.js +39 -34
  187. package/dist/tools/stage.d.ts +2 -2
  188. package/dist/tools/stage.js +28 -23
  189. package/dist/tools/stages.d.ts +2 -2
  190. package/dist/tools/stages.js +36 -31
  191. package/dist/tools/unschedule.d.ts +2 -2
  192. package/dist/tools/unschedule.js +30 -25
  193. package/dist/tools/who-am-i.d.ts +2 -2
  194. package/dist/tools/who-am-i.js +36 -31
  195. package/dist/tools/worktree.d.ts +2 -2
  196. package/dist/tools/worktree.js +134 -129
  197. package/dist/tui/index.js +6 -6
  198. package/dist/types.d.ts +47 -2
  199. package/dist/types.js +1 -1
  200. package/dist/utils/default-part.js +1 -0
  201. package/dist/utils/grpc-shutdown-guard.d.ts +52 -0
  202. package/dist/utils/grpc-shutdown-guard.js +88 -0
  203. package/dist/utils/sdk-probe.d.ts +23 -0
  204. package/dist/utils/sdk-probe.js +46 -7
  205. package/dist/worker.d.ts +3 -1
  206. package/dist/worker.js +6 -2
  207. package/dist/workflows/session.js +70 -2
  208. package/dist/workflows/signals.d.ts +32 -2
  209. package/dist/workflows/signals.js +25 -2
  210. package/examples/agents/tempo-composer.md +56 -56
  211. package/examples/agents/tempo-conductor.md +117 -117
  212. package/examples/agents/tempo-critic.md +73 -73
  213. package/examples/agents/tempo-improv.md +74 -74
  214. package/examples/agents/tempo-liner.md +75 -75
  215. package/examples/agents/tempo-roadie.md +61 -61
  216. package/examples/agents/tempo-soloist.md +71 -71
  217. package/examples/agents/tempo-tuner.md +94 -94
  218. package/examples/ensembles/tempo-big-band.yaml +146 -146
  219. package/examples/ensembles/tempo-dev-team.yaml +58 -58
  220. package/examples/ensembles/tempo-headless-jam.yaml +77 -77
  221. package/examples/ensembles/tempo-jam-session.yaml +41 -41
  222. package/examples/ensembles/tempo-mock-jam.yaml +79 -79
  223. package/examples/ensembles/tempo-review-squad.yaml +32 -32
  224. package/package.json +176 -173
  225. package/packaging/launchd/com.agent.tempo.plist +46 -46
  226. package/packaging/systemd/agent-tempo.service +32 -32
  227. package/packaging/windows/install-task.ps1 +71 -71
  228. package/scenarios/conductor-recruit-mock.yaml +33 -33
  229. package/scenarios/echo-roundtrip.yaml +15 -15
  230. package/scenarios/multi-player-handoff.yaml +38 -38
  231. package/scenarios/recruit-cascade.yaml +38 -38
  232. package/scenarios/two-player-conversation.yaml +33 -33
  233. package/workflow-bundle.js +97 -6
  234. package/dashboard/dist/assets/index-D6Xyje_n.js.map +0 -1
  235. package/dist/activities/claude-stop.d.ts +0 -21
  236. package/dist/activities/claude-stop.js +0 -94
  237. package/dist/channel.d.ts +0 -3
  238. package/dist/channel.js +0 -48
  239. package/dist/copilot-bridge.d.ts +0 -22
  240. package/dist/copilot-bridge.js +0 -565
  241. package/dist/scripts/258-spotcheck.js +0 -303
  242. package/dist/tools/detach.d.ts +0 -4
  243. package/dist/tools/detach.js +0 -45
  244. package/dist/tools/encore.d.ts +0 -4
  245. package/dist/tools/encore.js +0 -31
  246. package/dist/tools/helpers.d.ts +0 -21
  247. package/dist/tools/helpers.js +0 -25
  248. package/dist/tools/pause-ensemble.d.ts +0 -4
  249. package/dist/tools/pause-ensemble.js +0 -58
  250. package/dist/tools/resume-ensemble.d.ts +0 -4
  251. package/dist/tools/resume-ensemble.js +0 -79
  252. package/dist/tools/stop.d.ts +0 -4
  253. package/dist/tools/stop.js +0 -29
  254. package/dist/tui/client.d.ts +0 -6
  255. package/dist/tui/client.js +0 -9
  256. package/dist/tui/components/ActivityLog.d.ts +0 -16
  257. package/dist/tui/components/ActivityLog.js +0 -36
  258. package/dist/tui/components/CommandOverlay.d.ts +0 -15
  259. package/dist/tui/components/CommandOverlay.js +0 -34
  260. package/dist/tui/components/ConductorChat.d.ts +0 -16
  261. package/dist/tui/components/ConductorChat.js +0 -32
  262. package/dist/tui/components/EnsembleListView.d.ts +0 -14
  263. package/dist/tui/components/EnsembleListView.js +0 -32
  264. package/dist/tui/components/EnsemblePanel.d.ts +0 -12
  265. package/dist/tui/components/EnsemblePanel.js +0 -40
  266. package/dist/tui/components/InputBar.d.ts +0 -13
  267. package/dist/tui/components/InputBar.js +0 -58
  268. package/dist/tui/components/ScheduleOverlay.d.ts +0 -13
  269. package/dist/tui/components/ScheduleOverlay.js +0 -113
  270. package/dist/tui/components/TopBar.d.ts +0 -12
  271. package/dist/tui/components/TopBar.js +0 -15
  272. package/dist/tui/core-api.d.ts +0 -26
  273. package/dist/tui/core-api.js +0 -67
  274. package/dist/tui/hooks/useEnsembleDiscovery.d.ts +0 -3
  275. package/dist/tui/hooks/useEnsembleDiscovery.js +0 -30
  276. package/dist/tui/hooks/useMaestroPoller.d.ts +0 -3
  277. package/dist/tui/hooks/useMaestroPoller.js +0 -36
  278. package/dist/tui/hooks/useSendCommand.d.ts +0 -7
  279. package/dist/tui/hooks/useSendCommand.js +0 -29
  280. package/dist/utils/bg-preflight.d.ts +0 -25
  281. package/dist/utils/bg-preflight.js +0 -154
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InnerLoopRegistry = exports.InnerSubscription = exports.INNER_SUB_QUEUE_MAX = void 0;
4
+ /** Per-subscriber bounded queue depth before drop-oldest engages. */
5
+ exports.INNER_SUB_QUEUE_MAX = 256;
6
+ /**
7
+ * One connected `/inner` SSE subscriber. Async-iterable: the SSE handler does
8
+ * `for await (const frame of sub) { write(frame) }`. Bounded queue with
9
+ * drop-oldest; a `compacted{dropped,sinceTs}` marker is injected before the next
10
+ * real frame whenever drops have occurred since the last delivery.
11
+ *
12
+ * No ring / replay / seq — this is an ephemeral best-effort tail (MD-F): a
13
+ * disconnect loses in-flight deltas, by design.
14
+ */
15
+ class InnerSubscription {
16
+ now;
17
+ queue = [];
18
+ dropped = 0;
19
+ droppedSinceTs = 0;
20
+ waiter = null;
21
+ closed = false;
22
+ constructor(now = Date.now) {
23
+ this.now = now;
24
+ }
25
+ /** Enqueue a frame, dropping the oldest if the queue is full. Source → sub. */
26
+ push(frame) {
27
+ if (this.closed)
28
+ return;
29
+ if (this.queue.length >= exports.INNER_SUB_QUEUE_MAX) {
30
+ this.queue.shift(); // drop oldest
31
+ if (this.dropped === 0)
32
+ this.droppedSinceTs = this.now();
33
+ this.dropped++;
34
+ }
35
+ this.queue.push(frame);
36
+ // A full queue means no waiter is parked (a waiter only parks on an empty
37
+ // queue), so a drop never races a pending take — drain just wakes a waiter
38
+ // when one exists (queue was empty, now has one item).
39
+ if (this.waiter) {
40
+ const next = this.take();
41
+ if (next) {
42
+ const w = this.waiter;
43
+ this.waiter = null;
44
+ w({ value: next, done: false });
45
+ }
46
+ }
47
+ }
48
+ /** Pull the next deliverable, injecting a `compacted` marker first if drops are pending. */
49
+ take() {
50
+ if (this.dropped > 0) {
51
+ const marker = { type: 'compacted', dropped: this.dropped, sinceTs: this.droppedSinceTs };
52
+ this.dropped = 0;
53
+ return marker;
54
+ }
55
+ return this.queue.shift() ?? null;
56
+ }
57
+ next() {
58
+ if (this.closed)
59
+ return Promise.resolve({ value: undefined, done: true });
60
+ const item = this.take();
61
+ if (item)
62
+ return Promise.resolve({ value: item, done: false });
63
+ return new Promise((resolve) => { this.waiter = resolve; });
64
+ }
65
+ /** Terminate the stream — wakes a parked consumer with `done: true`. Idempotent. */
66
+ close() {
67
+ if (this.closed)
68
+ return;
69
+ this.closed = true;
70
+ if (this.waiter) {
71
+ const w = this.waiter;
72
+ this.waiter = null;
73
+ w({ value: undefined, done: true });
74
+ }
75
+ }
76
+ return() {
77
+ this.close();
78
+ return Promise.resolve({ value: undefined, done: true });
79
+ }
80
+ [Symbol.asyncIterator]() {
81
+ return this;
82
+ }
83
+ }
84
+ exports.InnerSubscription = InnerSubscription;
85
+ /**
86
+ * Per-daemon registry of inner-loop subscribers, keyed by the player's fixed
87
+ * session `workflowId`. Implements eng's {@link InnerLoopSink} interface
88
+ * (`publish` + `subscriberCount`) so the publisher's thin client and this sink
89
+ * share one contract.
90
+ */
91
+ class InnerLoopRegistry {
92
+ now;
93
+ subs = new Map();
94
+ constructor(now = Date.now) {
95
+ this.now = now;
96
+ }
97
+ /** Open a new SSE subscription for a player. Caller drains it, then `unsubscribe`. */
98
+ subscribe(workflowId) {
99
+ const sub = new InnerSubscription(this.now);
100
+ let set = this.subs.get(workflowId);
101
+ if (!set) {
102
+ set = new Set();
103
+ this.subs.set(workflowId, set);
104
+ }
105
+ set.add(sub);
106
+ return sub;
107
+ }
108
+ /** Remove + close one subscription (on SSE disconnect). Prunes the empty player set. */
109
+ unsubscribe(workflowId, sub) {
110
+ const set = this.subs.get(workflowId);
111
+ if (!set)
112
+ return;
113
+ set.delete(sub);
114
+ sub.close();
115
+ if (set.size === 0)
116
+ this.subs.delete(workflowId);
117
+ }
118
+ /** Fan a frame out to every live subscriber for the player. Source → subs. */
119
+ publish(workflowId, frame) {
120
+ const set = this.subs.get(workflowId);
121
+ if (!set)
122
+ return;
123
+ for (const sub of set)
124
+ sub.push(frame);
125
+ }
126
+ /** Live subscriber count — the publisher's presence gate reads this (via `/inner/presence`). */
127
+ subscriberCount(workflowId) {
128
+ return this.subs.get(workflowId)?.size ?? 0;
129
+ }
130
+ /** Close every subscriber for a player (player gone → streams end with `:closed`). */
131
+ closePlayer(workflowId) {
132
+ const set = this.subs.get(workflowId);
133
+ if (!set)
134
+ return;
135
+ for (const sub of set)
136
+ sub.close();
137
+ this.subs.delete(workflowId);
138
+ }
139
+ /** Total live inner-tail subscribers across all players (diagnostics). */
140
+ totalSubscriberCount() {
141
+ let n = 0;
142
+ for (const set of this.subs.values())
143
+ n += set.size;
144
+ return n;
145
+ }
146
+ /** Close everything (daemon shutdown). */
147
+ close() {
148
+ for (const set of this.subs.values()) {
149
+ for (const sub of set)
150
+ sub.close();
151
+ }
152
+ this.subs.clear();
153
+ }
154
+ }
155
+ exports.InnerLoopRegistry = InnerLoopRegistry;
@@ -18,6 +18,9 @@
18
18
  import * as http from 'http';
19
19
  import type { TempoClient } from '../client/interface';
20
20
  import type { AggregateRunner } from './aggregate';
21
+ import type { InnerLoopRegistry } from './inner-loop';
22
+ import type { IngestTokenRegistry } from './ingest-registry';
23
+ import type { GateRegistry } from './gate-registry';
21
24
  import { type CorsConfig } from './cors';
22
25
  import { ConnectionCap } from './sse-handler';
23
26
  /** Default bind addr per SSE-PROTOCOL.md §1. */
@@ -59,10 +62,14 @@ export interface HttpServerOptions {
59
62
  */
60
63
  portFilePath?: string;
61
64
  /**
62
- * Inject the bearer token directly. Production callers pass `undefined`
63
- * so the server reads/auto-generates from `~/.agent-tempo/config.json`.
65
+ * @deprecated 3e back-compat alias for {@link readToken}. A single injected
66
+ * bearer is treated as the READ token (T1). Prefer `readToken`/`adminToken`.
64
67
  */
65
68
  httpToken?: string;
69
+ /** 3e — inject the read-tier (T1) token directly (tests). Overrides config/env. */
70
+ readToken?: string;
71
+ /** 3e — inject the admin (T1+T2+T3) token directly (tests). Overrides env. */
72
+ adminToken?: string;
66
73
  /**
67
74
  * Test seam — lets unit tests stub `process.uptime`-style readings.
68
75
  */
@@ -80,6 +87,25 @@ export interface HttpServerOptions {
80
87
  * low value in tests to exercise the 503 path.
81
88
  */
82
89
  maxSseConnections?: number;
90
+ /**
91
+ * 3c Tier-2 — the inner-loop fine-tail registry (off-wire SSE sink) and the
92
+ * ingest-token registry (source-plane auth). When both are provided the
93
+ * `/v1/players/:e/:p/inner` egress + `/inner/ingest` + `/inner/presence`
94
+ * ingress routes light up; absent → those routes 404/503. The daemon
95
+ * constructs + shares these (the outbox mints ingest tokens into the same
96
+ * `ingestTokens` instance the server validates against).
97
+ */
98
+ innerLoop?: InnerLoopRegistry;
99
+ ingestTokens?: IngestTokenRegistry;
100
+ /**
101
+ * 3d MD-G — the operator-gate registry. When provided (with `ingestTokens`)
102
+ * the `/v1/players/:e/:p/gate-arm` + `/gate-disarm` + `/gate/:requestId`
103
+ * operator routes (requireTier(3)) and the `/gate/:requestId/resolution`
104
+ * subprocess-poll route (ingest-token) light up; absent → those routes 404.
105
+ * The daemon constructs + shares this with the worker (auto-disarm on
106
+ * detach/destroy) — same singleton pattern as the inner-loop registries.
107
+ */
108
+ gate?: GateRegistry;
83
109
  }
84
110
  export interface HttpServerHandle {
85
111
  /** The actual port the server is listening on (after `.listen()` resolves). */
@@ -106,13 +132,22 @@ interface HandleContext {
106
132
  version: string;
107
133
  bindAddr: string;
108
134
  corsConfig: CorsConfig;
109
- httpToken: string | null;
135
+ /** 3e RBAC read-tier token (T1), or null. */
136
+ readToken: string | null;
137
+ /** 3e RBAC admin token (T1+T2+T3) — env-var-only, or null when unset. */
138
+ adminToken: string | null;
110
139
  startedAt: number;
111
140
  subscriberCount: () => number;
112
141
  /** Present when PR-2 streaming is wired; null on PR-1-only deployments. */
113
142
  aggregate: AggregateRunner | null;
114
143
  /** Process-wide SSE subscriber cap (§7.3). */
115
144
  sseConnectionCap: ConnectionCap;
145
+ /** 3c Tier-2 inner-loop fine-tail sink (off-wire) — null when unwired. */
146
+ innerLoop: InnerLoopRegistry | null;
147
+ /** 3c Tier-2 ingest-token registry (source-plane auth) — null when unwired. */
148
+ ingestTokens: IngestTokenRegistry | null;
149
+ /** 3d MD-G operator-gate registry — null when unwired. */
150
+ gate: GateRegistry | null;
116
151
  }
117
152
  /**
118
153
  * Top-level request dispatcher — exported for unit tests that want to
@@ -56,6 +56,8 @@ exports.handle = handle;
56
56
  const http = __importStar(require("http"));
57
57
  const config_1 = require("../config");
58
58
  const auth_1 = require("./auth");
59
+ const inner_loop_routes_1 = require("./inner-loop-routes");
60
+ const gate_routes_1 = require("./gate-routes");
59
61
  const cors_1 = require("./cors");
60
62
  const dashboard_1 = require("./dashboard");
61
63
  const dashboard_pair_1 = require("./dashboard-pair");
@@ -98,10 +100,46 @@ async function startHttpServer(opts) {
98
100
  // generation now so the daemon doesn't crash mid-request when the first
99
101
  // bearer-required call shows up.
100
102
  const bindIsLoopback = (0, auth_1.isLoopbackBindAddr)(bindAddr);
101
- const httpToken = opts.httpToken ?? (0, auth_1.loadOrGenerateHttpToken)({ bearerRequired: !bindIsLoopback });
102
- if (!bindIsLoopback && !httpToken) {
103
+ // 3e RBAC token resolution. Back-compat: a single `httpToken` option (or a
104
+ // legacy config.json `httpToken`) is adopted as the READ token (T1); the ADMIN
105
+ // token is env-var-only. Explicit `readToken`/`adminToken` options override
106
+ // (used by tests). loopback bind ⇒ no bearer required ⇒ tokens may be null.
107
+ let readToken;
108
+ let legacyMigrated = false;
109
+ if (opts.readToken !== undefined) {
110
+ readToken = opts.readToken;
111
+ }
112
+ else if (opts.httpToken !== undefined) {
113
+ // Back-compat: a single injected bearer is treated as the READ token (T1).
114
+ readToken = opts.httpToken;
115
+ }
116
+ else {
117
+ const loaded = (0, auth_1.loadReadToken)({ bearerRequired: !bindIsLoopback });
118
+ readToken = loaded.token;
119
+ legacyMigrated = loaded.legacy;
120
+ }
121
+ const adminToken = opts.adminToken ?? (0, auth_1.loadAdminToken)();
122
+ if (!bindIsLoopback && !readToken) {
103
123
  throw new Error('Bearer token required for non-loopback bind but none configured. ' +
104
- 'Set httpToken in ~/.agent-tempo/config.json or unset AGENT_TEMPO_HTTP_BIND.');
124
+ 'Set AGENT_TEMPO_HTTP_READ_TOKEN (or readToken in ~/.agent-tempo/config.json), ' +
125
+ 'or unset AGENT_TEMPO_HTTP_BIND.');
126
+ }
127
+ // 3e MD-E — one-time startup warnings (non-blocking).
128
+ //
129
+ // (1) Legacy migration: a pre-3e single `httpToken` was adopted as the READ
130
+ // token (T1) and no admin token is configured, so writes / operator gate /
131
+ // inner-tail (all Tier ≥ 2) will 503 until an admin token is set.
132
+ if (legacyMigrated && adminToken === null) {
133
+ log('NOTICE: adopted legacy config.json `httpToken` as the read-tier token. ' +
134
+ 'Writes, the operator gate, and the inner-tail are admin-only and will return ' +
135
+ '503 until you set AGENT_TEMPO_HTTP_ADMIN_TOKEN (env-var only).');
136
+ }
137
+ // (2) Plaintext-bearer exposure: binding to a non-loopback address serves the
138
+ // bearer token over cleartext HTTP. Suppressible, never blocking.
139
+ if (!bindIsLoopback && process.env[config_1.ENV.TLS_ACKNOWLEDGED] !== '1') {
140
+ log(`WARNING: binding to non-loopback ${bindAddr} serves the bearer token over ` +
141
+ 'plaintext HTTP. Terminate TLS at a reverse proxy, or tunnel via SSH/Tailscale. ' +
142
+ `Set ${config_1.ENV.TLS_ACKNOWLEDGED}=1 to acknowledge and suppress this warning.`);
105
143
  }
106
144
  const startedAt = opts.startedAtMs ?? Date.now();
107
145
  // §7.3 process-wide cap. Defaults to 100 per spec; env var override.
@@ -122,11 +160,15 @@ async function startHttpServer(opts) {
122
160
  version: opts.version,
123
161
  bindAddr,
124
162
  corsConfig,
125
- httpToken,
163
+ readToken,
164
+ adminToken,
126
165
  startedAt,
127
166
  subscriberCount,
128
167
  aggregate: opts.aggregate ?? null,
129
168
  sseConnectionCap,
169
+ innerLoop: opts.innerLoop ?? null,
170
+ ingestTokens: opts.ingestTokens ?? null,
171
+ gate: opts.gate ?? null,
130
172
  }).catch((err) => {
131
173
  log('unhandled handler error:', err instanceof Error ? err.message : err);
132
174
  if (!res.headersSent) {
@@ -233,12 +275,53 @@ async function handle(req, res, ctx) {
233
275
  if (method === 'GET' && pairConsumeMatch) {
234
276
  return (0, dashboard_pair_1.handlePairConsume)(req, res, pairConsumeMatch[1]);
235
277
  }
236
- // Authentication gate.
278
+ // 3c Tier-2 INGRESS (publisher → daemon). Matched BEFORE the outer bearer
279
+ // gate: these use their OWN source-plane auth (loopback `socket.remoteAddress`
280
+ // + `X-Ingest-Token` vs the URL workflowId), so a localhost Pi subprocess
281
+ // reaches them regardless of the daemon's bind address. Only live when the
282
+ // daemon wired the registries; else they fall through to the 404/405 path.
283
+ if (ctx.innerLoop && ctx.ingestTokens) {
284
+ const innerDeps = { innerLoop: ctx.innerLoop, ingestTokens: ctx.ingestTokens, ...(ctx.gate ? { gate: ctx.gate } : {}) };
285
+ const ingestMatch = pathname.match(/^\/v1\/players\/([^/]+)\/([^/]+)\/inner\/ingest$/);
286
+ if (ingestMatch) {
287
+ if (method !== 'POST') {
288
+ return (0, responses_1.errorResponse)(res, 405, { error: 'method-not-allowed' }, { Allow: 'POST' });
289
+ }
290
+ return (0, inner_loop_routes_1.handleInnerIngest)(req, res, innerDeps, decodeURIComponent(ingestMatch[1]), decodeURIComponent(ingestMatch[2]));
291
+ }
292
+ const presenceMatch = pathname.match(/^\/v1\/players\/([^/]+)\/([^/]+)\/inner\/presence$/);
293
+ if (presenceMatch) {
294
+ if (method !== 'GET') {
295
+ return (0, responses_1.errorResponse)(res, 405, { error: 'method-not-allowed' }, { Allow: 'GET' });
296
+ }
297
+ return (0, inner_loop_routes_1.handleInnerPresence)(req, res, innerDeps, decodeURIComponent(presenceMatch[1]), decodeURIComponent(presenceMatch[2]));
298
+ }
299
+ }
300
+ // 3d MD-G INGRESS (Pi subprocess → daemon poll). Same source-plane auth as the
301
+ // inner-loop ingest (loopback `socket.remoteAddress` + `X-Ingest-Token` vs the
302
+ // URL workflowId), matched BEFORE the bearer gate. Live only when the daemon
303
+ // wired the gate + ingest registries.
304
+ if (ctx.gate && ctx.ingestTokens) {
305
+ const gateDeps = { gate: ctx.gate, ingestTokens: ctx.ingestTokens };
306
+ const resolutionMatch = pathname.match(/^\/v1\/players\/([^/]+)\/([^/]+)\/gate\/([^/]+)\/resolution$/);
307
+ if (resolutionMatch) {
308
+ if (method !== 'GET') {
309
+ return (0, responses_1.errorResponse)(res, 405, { error: 'method-not-allowed' }, { Allow: 'GET' });
310
+ }
311
+ return (0, gate_routes_1.handleGateResolution)(req, res, gateDeps, decodeURIComponent(resolutionMatch[1]), decodeURIComponent(resolutionMatch[2]), decodeURIComponent(resolutionMatch[3]));
312
+ }
313
+ }
314
+ // Layer 2 — shared AUTHENTICATION + Origin/DNS-rebind defense (architect's
315
+ // decomposition). bearerRequired() carries the Origin-rebind logic; a request
316
+ // in bearer mode must present a token granting at LEAST a tier (read or admin).
317
+ // This is the single shared upstream pass — the per-route TIER authorization
318
+ // (Layer 3, `gateTier(N)` / inline `requireTier(3)`) refines it below: reads → T1,
319
+ // writes/pair-mint → T2 (admin), gate/inner → T3 (admin).
237
320
  const originHeader = headerString(req.headers.origin);
238
321
  const reqBearer = (0, auth_1.bearerRequired)(ctx.bindAddr, originHeader);
239
322
  if (reqBearer) {
240
323
  const provided = (0, auth_1.extractBearerToken)(headerString(req.headers.authorization));
241
- if (!provided || !ctx.httpToken || !(0, auth_1.tokensMatch)(provided, ctx.httpToken)) {
324
+ if (!provided || (0, auth_1.tierForToken)(provided, ctx) === 0) {
242
325
  writeCorsHeaders(res, originHeader, ctx, reqBearer);
243
326
  return (0, responses_1.errorResponse)(res, 401, { error: 'unauthorized' });
244
327
  }
@@ -252,6 +335,46 @@ async function handle(req, res, ctx) {
252
335
  res.setHeader('Access-Control-Allow-Origin', cors.echo);
253
336
  res.setHeader('Vary', 'Origin');
254
337
  }
338
+ // Layer 3 — per-route AUTHORIZATION (3e MD-E). The tier-guard input is
339
+ // resolved ONCE here off the shared L2 pass (bindAddr + Origin + the two
340
+ // RBAC tokens) and reused by every `gateTier(N)` call below — reads require
341
+ // T1, the write/pair-mint surface requires T2 (admin). The grandfathered T3
342
+ // gate/inner sites (3c/3d) keep their own inline `requireTier(3)` by design.
343
+ //
344
+ // This is defense-in-depth ON TOP of the L2 token-validity floor above (which
345
+ // already rejects an unrecognized bearer with 401): the explicit per-route
346
+ // guard keeps each surface protected at its declared tier even if the L2 floor
347
+ // is later relaxed, and makes the required tier self-documenting + greppable.
348
+ const tierInput = {
349
+ bindAddr: ctx.bindAddr,
350
+ originHeader,
351
+ authHeader: headerString(req.headers.authorization),
352
+ readToken: ctx.readToken,
353
+ adminToken: ctx.adminToken,
354
+ };
355
+ /**
356
+ * Write a tier-denial response, surfacing requireTier's actionable `detail`
357
+ * hint on 403 (insufficient-tier) / 503 (admin-unset) when present (3e ruling
358
+ * #3). The hint is operator guidance — e.g. "set AGENT_TEMPO_HTTP_ADMIN_TOKEN"
359
+ * — NOT a sensitive leak (security-confirmed). Shared by `gateTier` and the
360
+ * inline T3 gate/inner sites so every tier denial carries the same body shape.
361
+ */
362
+ const denyTier = (r) => {
363
+ (0, responses_1.errorResponse)(res, r.status, 'detail' in r ? { error: r.error, detail: r.detail } : { error: r.error });
364
+ };
365
+ /**
366
+ * Apply a per-route tier guard against the L2-resolved input. On failure it
367
+ * writes the 401/403/503 response and returns `false` (caller returns); on
368
+ * success returns `true`. Loopback requests short-circuit to PASS inside
369
+ * {@link requireTier} (local-trust → full tier).
370
+ */
371
+ const gateTier = (n) => {
372
+ const g = (0, auth_1.requireTier)(n, tierInput);
373
+ if (g.ok)
374
+ return true;
375
+ denyTier(g);
376
+ return false;
377
+ };
255
378
  // Write surface (PR-7a of #340) — POST `/v1/ensembles/:ensemble/<action>`
256
379
  // Match BEFORE the GET-only method gate; everything else (POST to a
257
380
  // read endpoint, GET to a write endpoint) flows into the 405 fallback
@@ -261,6 +384,10 @@ async function handle(req, res, ctx) {
261
384
  const ensemble = decodeURIComponent(writeMatch[1]);
262
385
  const action = writeMatch[2];
263
386
  if ((0, writes_1.isWriteAction)(action)) {
387
+ // L3 — the mutate surface is admin-only (Tier 2). Gate before the method
388
+ // check so an under-privileged caller can't probe the write verbs via 405.
389
+ if (!gateTier(2))
390
+ return;
264
391
  if (method !== 'POST') {
265
392
  return (0, responses_1.errorResponse)(res, 405, { error: 'method-not-allowed' }, { Allow: 'POST, OPTIONS' });
266
393
  }
@@ -274,6 +401,8 @@ async function handle(req, res, ctx) {
274
401
  // alongside the writeMatch above so it's reached before the GET-only
275
402
  // gate; the GET on the same path (list ensembles) is handled below.
276
403
  if (pathname === '/v1/ensembles' && method === 'POST') {
404
+ if (!gateTier(2))
405
+ return; // L3 — create-ensemble is a write (Tier 2).
277
406
  return (0, catalog_1.handleCreateEnsemble)(req, res, ctx.client);
278
407
  }
279
408
  // POST `/dashboard/api/pair` — mint a pairing for cross-device QR (PR-8
@@ -281,9 +410,43 @@ async function handle(req, res, ctx) {
281
410
  // proves authority before issuing a token; the token's GET-side consume
282
411
  // is the carve-out above the auth gate.
283
412
  if (method === 'POST' && pathname === '/dashboard/api/pair') {
413
+ // L3 — minting a cross-device pairing token is an admin operation (Tier 2):
414
+ // it grants a bearer-equivalent capability, so it must require the admin token.
415
+ if (!gateTier(2))
416
+ return;
284
417
  const provided = (0, auth_1.extractBearerToken)(headerString(req.headers.authorization));
285
418
  return (0, dashboard_pair_1.handlePairCreate)(req, res, provided);
286
419
  }
420
+ // 3d MD-G OPERATOR plane (operator/dashboard → daemon) — POST routes, so they
421
+ // sit BEFORE the GET-only method gate below (alongside the other POST routes).
422
+ // `requireTier(3)` — only an admin-token holder may arm/disarm or decide
423
+ // (MD-E highest tier). Live only when the daemon wired the gate registries.
424
+ if (ctx.gate && ctx.ingestTokens && method === 'POST') {
425
+ const gateDeps = { gate: ctx.gate, ingestTokens: ctx.ingestTokens };
426
+ const armMatch = pathname.match(/^\/v1\/players\/([^/]+)\/([^/]+)\/gate-(arm|disarm)$/);
427
+ const decideMatch = pathname.match(/^\/v1\/players\/([^/]+)\/([^/]+)\/gate\/([^/]+)$/);
428
+ if (armMatch || decideMatch) {
429
+ const tier = (0, auth_1.requireTier)(3, {
430
+ bindAddr: ctx.bindAddr,
431
+ originHeader,
432
+ authHeader: headerString(req.headers.authorization),
433
+ readToken: ctx.readToken,
434
+ adminToken: ctx.adminToken,
435
+ });
436
+ if (!tier.ok) {
437
+ denyTier(tier);
438
+ return;
439
+ }
440
+ if (armMatch) {
441
+ const [, e, p, verb] = armMatch;
442
+ return verb === 'arm'
443
+ ? (0, gate_routes_1.handleGateArm)(req, res, gateDeps, decodeURIComponent(e), decodeURIComponent(p))
444
+ : (0, gate_routes_1.handleGateDisarm)(req, res, gateDeps, decodeURIComponent(e), decodeURIComponent(p));
445
+ }
446
+ // decideMatch — POST /gate/:requestId { decision }
447
+ return (0, gate_routes_1.handleGateDecide)(req, res, gateDeps, decodeURIComponent(decideMatch[1]), decodeURIComponent(decideMatch[2]), decodeURIComponent(decideMatch[3]));
448
+ }
449
+ }
287
450
  // Method gate — read endpoints are GET-only. Both POST paths above
288
451
  // (PR-7a writes, PR-8 pair-mint) handle their own method matching;
289
452
  // everything else falls through here.
@@ -295,18 +458,26 @@ async function handle(req, res, ctx) {
295
458
  // `/v1/*` API uses. The pre-auth pair-token carve-out above is the
296
459
  // single exception that bootstraps cross-device pairing.
297
460
  if (pathname === '/dashboard' || pathname.startsWith('/dashboard/')) {
461
+ if (!gateTier(1))
462
+ return; // L3 — the dashboard SPA is read-tier (Tier 1).
298
463
  return (0, dashboard_1.handleDashboardStatic)(req, res, pathname);
299
464
  }
300
465
  if (pathname === '/v1/ensembles') {
466
+ if (!gateTier(1))
467
+ return; // L3 — read (Tier 1).
301
468
  return handleListEnsembles(res, ctx);
302
469
  }
303
470
  if (pathname === '/v1/hosts') {
471
+ if (!gateTier(1))
472
+ return; // L3 — read (Tier 1).
304
473
  return handleHosts(res, ctx);
305
474
  }
306
475
  // #579 — cluster-wide cross-host orphan listing for the dashboard.
307
476
  // Same bearer + CORS gate as `/v1/hosts`; optional `?ensemble=<name>`
308
477
  // narrows to one ensemble.
309
478
  if (pathname === '/v1/orphans') {
479
+ if (!gateTier(1))
480
+ return; // L3 — read (Tier 1).
310
481
  const ensembleFilter = url.searchParams.get('ensemble') ?? undefined;
311
482
  return (0, orphans_1.handleOrphans)(res, {
312
483
  client: ctx.client,
@@ -318,14 +489,20 @@ async function handle(req, res, ctx) {
318
489
  // Catalog reads (issue #400) — `listAgentTypes` / `listLineups`
319
490
  // touch local fs only, no Temporal calls; cheap to serve per-request.
320
491
  if (pathname === '/v1/agent-types') {
492
+ if (!gateTier(1))
493
+ return; // L3 — read (Tier 1).
321
494
  return (0, catalog_1.handleListAgentTypes)(res);
322
495
  }
323
496
  if (pathname === '/v1/lineups') {
497
+ if (!gateTier(1))
498
+ return; // L3 — read (Tier 1).
324
499
  return (0, catalog_1.handleListLineups)(res);
325
500
  }
326
501
  // /v1/state/:ensemble — single capture group.
327
502
  const stateMatch = pathname.match(/^\/v1\/state\/([^/]+)$/);
328
503
  if (stateMatch) {
504
+ if (!gateTier(1))
505
+ return; // L3 — read (Tier 1); covers the fixture path too.
329
506
  const ensemble = decodeURIComponent(stateMatch[1]);
330
507
  // Fixture mode (PR-3 of #340) — `?fixture=<name>` short-circuits the
331
508
  // live snapshot with canned data. Sits behind the bearer-auth gate.
@@ -340,6 +517,8 @@ async function handle(req, res, ctx) {
340
517
  // signals "feature exists, not yet wired" rather than "ensemble
341
518
  // doesn't exist".
342
519
  if (pathname === '/v1/events') {
520
+ if (!gateTier(1))
521
+ return; // L3 — read/observe stream (Tier 1).
343
522
  if (!ctx.aggregate) {
344
523
  return (0, responses_1.errorResponse)(res, 503, { error: 'streaming-not-implemented' }, { 'Retry-After': '60' });
345
524
  }
@@ -352,6 +531,8 @@ async function handle(req, res, ctx) {
352
531
  }
353
532
  const evtMatch = pathname.match(/^\/v1\/events\/([^/]+)$/);
354
533
  if (evtMatch) {
534
+ if (!gateTier(1))
535
+ return; // L3 — read/observe stream (Tier 1); covers fixture too.
355
536
  const ensemble = decodeURIComponent(evtMatch[1]);
356
537
  // Fixture mode (PR-3 of #340) — `?fixture=<name>` short-circuits both
357
538
  // the existence check and the aggregate poll loop with a canned event
@@ -378,6 +559,30 @@ async function handle(req, res, ctx) {
378
559
  cap: ctx.sseConnectionCap,
379
560
  });
380
561
  }
562
+ // 3c Tier-2 EGRESS — operator/widget inner-loop SSE fine tail. After the outer
563
+ // bearer gate (so it's already authenticated); the explicit `requireTier(3)`
564
+ // marks the tier for 3e (today the outer bearer already satisfied it — 3e
565
+ // relaxes the outer gate to a read token and this guard demands the admin
566
+ // token, no call-site change). View-agnostic: bearer-keyed, NO Origin
567
+ // requirement, plain `event:`/`data:` framing (fetch + Node-client consumable).
568
+ const innerSseMatch = pathname.match(/^\/v1\/players\/([^/]+)\/([^/]+)\/inner$/);
569
+ if (innerSseMatch) {
570
+ if (!ctx.innerLoop || !ctx.ingestTokens) {
571
+ return (0, responses_1.errorResponse)(res, 503, { error: 'streaming-not-implemented' }, { 'Retry-After': '60' });
572
+ }
573
+ const tier = (0, auth_1.requireTier)(3, {
574
+ bindAddr: ctx.bindAddr,
575
+ originHeader,
576
+ authHeader: headerString(req.headers.authorization),
577
+ readToken: ctx.readToken,
578
+ adminToken: ctx.adminToken,
579
+ });
580
+ if (!tier.ok) {
581
+ denyTier(tier);
582
+ return;
583
+ }
584
+ return (0, inner_loop_routes_1.handleInnerSse)(req, res, { innerLoop: ctx.innerLoop, ingestTokens: ctx.ingestTokens }, decodeURIComponent(innerSseMatch[1]), decodeURIComponent(innerSseMatch[2]));
585
+ }
381
586
  return (0, responses_1.errorResponse)(res, 404, { error: 'not-found' });
382
587
  }
383
588
  /** Pull a single string from a possibly-array header. */
@@ -51,6 +51,12 @@ export interface PlayerWireMeta {
51
51
  expiresAt: number | null;
52
52
  leaseMs: number | null;
53
53
  };
54
+ /** 3c Tier-1 — coarse activity (currentTool + context usage), merged onto the summary. */
55
+ coarse?: {
56
+ currentTool: string | null;
57
+ contextTokens?: number;
58
+ contextPercent?: number;
59
+ };
54
60
  }
55
61
  /**
56
62
  * Project a `MaestroPlayerInfo` into the wire-stable `PlayerSummaryV1`.
@@ -99,6 +99,12 @@ function toPlayerSummaryV1(p, wireMeta = null) {
99
99
  ...(wireMeta?.runId !== undefined ? { runId: wireMeta.runId } : {}),
100
100
  ...(wireMeta?.messaging !== undefined ? { messaging: wireMeta.messaging } : {}),
101
101
  ...(wireMeta?.lease !== undefined ? { lease: wireMeta.lease } : {}),
102
+ // 3c Tier-1 — coarse activity merged onto the summary so the aggregate
103
+ // poll/diff can emit player.activity. currentTool is always present on the
104
+ // coarse object (null = idle); context fields are conditionally included.
105
+ ...(wireMeta?.coarse?.currentTool !== undefined ? { currentTool: wireMeta.coarse.currentTool } : {}),
106
+ ...(wireMeta?.coarse?.contextTokens !== undefined ? { contextTokens: wireMeta.coarse.contextTokens } : {}),
107
+ ...(wireMeta?.coarse?.contextPercent !== undefined ? { contextPercent: wireMeta.coarse.contextPercent } : {}),
102
108
  };
103
109
  }
104
110
  /**
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Cue pump — pulls cues queued on the session workflow and injects them into
3
+ * the LIVE Pi session via `sendCustomMessage`, then acks them.
4
+ *
5
+ * Pi has no reverse-RPC into a running session from Temporal, so (like the
6
+ * existing adapters) we poll `pendingMessages` and ack via `markDelivered`.
7
+ *
8
+ * Injection follows D10 cue-delivery semantics:
9
+ * - **deliverAs** — operator cue (`msg.isMaestro`, a human steering from the
10
+ * Maestro dashboard) → `'steer'` (interrupt the in-flight turn so the
11
+ * override lands immediately); peer cue → `'followUp'` (queue behind the
12
+ * current turn rather than interrupting a peer's work).
13
+ * - **triggerTurn — always `true`.** Researcher-confirmed: Pi's `followUp`
14
+ * does NOT self-wake an idle agent, so an unconditional `triggerTurn` is
15
+ * REQUIRED to avoid #18-style silent cue loss when no human is driving. It
16
+ * is a no-op when a turn is already running (the message just queues), so we
17
+ * don't need to race-check the idle state — set it unconditionally.
18
+ *
19
+ * Adapted from Pi's `examples/extensions/file-trigger.ts`.
20
+ */
21
+ import type { Message } from '../types';
22
+ import type { PiAgentSession } from './pi-types';
23
+ /** Source of pending cues + ack — satisfied by `PiWorkflowClient`. */
24
+ export interface CueSource {
25
+ fetchPending(): Promise<Message[]>;
26
+ ackDelivered(messageIds: string[]): Promise<void>;
27
+ }
28
+ /**
29
+ * Resolves the CURRENT live Pi session at injection time. Re-acquired on every
30
+ * tick rather than captured once, so a session switch (D11) never injects into
31
+ * a stale session. Returns `null` when no session is attached.
32
+ */
33
+ export type SessionResolver = () => PiAgentSession | null;
34
+ export interface CuePumpOptions {
35
+ source: CueSource;
36
+ resolveSession: SessionResolver;
37
+ /** Poll interval (ms). */
38
+ intervalMs?: number;
39
+ }
40
+ export declare class CuePump {
41
+ private readonly source;
42
+ private readonly resolveSession;
43
+ private readonly intervalMs;
44
+ private timer;
45
+ private draining;
46
+ constructor(opts: CuePumpOptions);
47
+ start(): void;
48
+ stop(): void;
49
+ /**
50
+ * One poll cycle: fetch pending cues, inject each into the live session, ack
51
+ * the ones successfully injected. Re-entrancy guarded so a slow tick never
52
+ * overlaps the next interval.
53
+ */
54
+ tick(): Promise<void>;
55
+ /**
56
+ * Inject one cue into the live session (D10 — see file header). Operator cues
57
+ * `steer` (same-turn priority); peer cues `followUp` (queue). `triggerTurn` is
58
+ * always set: a no-op mid-turn, the required cold-idle wake otherwise.
59
+ */
60
+ private injectCue;
61
+ }