agent-tempo 1.3.1 → 1.4.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 (199) hide show
  1. package/CLAUDE.md +39 -5
  2. package/README.md +6 -2
  3. package/dashboard/dist/assets/{index-D6Xyje_n.js → index-jmYe6rmS.js} +2 -2
  4. package/dashboard/dist/assets/index-jmYe6rmS.js.map +1 -0
  5. package/dashboard/dist/index.html +1 -1
  6. package/dashboard/package.json +1 -1
  7. package/dist/activities/outbox.d.ts +30 -1
  8. package/dist/activities/outbox.js +96 -3
  9. package/dist/adapters/base.js +5 -0
  10. package/dist/adapters/index.d.ts +1 -1
  11. package/dist/adapters/index.js +7 -0
  12. package/dist/adapters/pi/adapter.d.ts +2 -0
  13. package/dist/adapters/pi/adapter.js +43 -0
  14. package/dist/adapters/pi/index.d.ts +16 -0
  15. package/dist/adapters/pi/index.js +10 -0
  16. package/dist/client/core.js +9 -2
  17. package/dist/client/interface.d.ts +6 -0
  18. package/dist/config.d.ts +79 -0
  19. package/dist/config.js +74 -0
  20. package/dist/daemon.js +32 -1
  21. package/dist/http/aggregate.d.ts +22 -1
  22. package/dist/http/aggregate.js +41 -0
  23. package/dist/http/auth.d.ts +94 -8
  24. package/dist/http/auth.js +93 -9
  25. package/dist/http/body.d.ts +4 -1
  26. package/dist/http/body.js +6 -3
  27. package/dist/http/event-bus.js +1 -0
  28. package/dist/http/event-types.d.ts +34 -2
  29. package/dist/http/event-types.js +1 -0
  30. package/dist/http/gate-audit.d.ts +12 -0
  31. package/dist/http/gate-audit.js +95 -0
  32. package/dist/http/gate-registry.d.ts +167 -0
  33. package/dist/http/gate-registry.js +163 -0
  34. package/dist/http/gate-routes.d.ts +48 -0
  35. package/dist/http/gate-routes.js +102 -0
  36. package/dist/http/ingest-registry.d.ts +30 -0
  37. package/dist/http/ingest-registry.js +108 -0
  38. package/dist/http/inner-loop-routes.d.ts +66 -0
  39. package/dist/http/inner-loop-routes.js +182 -0
  40. package/dist/http/inner-loop.d.ts +92 -0
  41. package/dist/http/inner-loop.js +155 -0
  42. package/dist/http/server.d.ts +38 -3
  43. package/dist/http/server.js +211 -6
  44. package/dist/http/snapshot.d.ts +6 -0
  45. package/dist/http/snapshot.js +6 -0
  46. package/dist/pi/cue-pump.d.ts +61 -0
  47. package/dist/pi/cue-pump.js +95 -0
  48. package/dist/pi/extension.d.ts +45 -0
  49. package/dist/pi/extension.js +407 -0
  50. package/dist/pi/gate-client.d.ts +54 -0
  51. package/dist/pi/gate-client.js +136 -0
  52. package/dist/pi/headless.d.ts +85 -0
  53. package/dist/pi/headless.js +250 -0
  54. package/dist/pi/index.d.ts +28 -0
  55. package/dist/pi/index.js +43 -0
  56. package/dist/pi/inner-loop-client.d.ts +67 -0
  57. package/dist/pi/inner-loop-client.js +164 -0
  58. package/dist/pi/inner-loop-publisher.d.ts +187 -0
  59. package/dist/pi/inner-loop-publisher.js +236 -0
  60. package/dist/pi/lazy-proxy.d.ts +37 -0
  61. package/dist/pi/lazy-proxy.js +55 -0
  62. package/dist/pi/mission-control/actions.d.ts +48 -0
  63. package/dist/pi/mission-control/actions.js +98 -0
  64. package/dist/pi/mission-control/board.d.ts +88 -0
  65. package/dist/pi/mission-control/board.js +141 -0
  66. package/dist/pi/mission-control/extension.d.ts +51 -0
  67. package/dist/pi/mission-control/extension.js +330 -0
  68. package/dist/pi/mission-control/index.d.ts +15 -0
  69. package/dist/pi/mission-control/index.js +32 -0
  70. package/dist/pi/mission-control/inner-tail.d.ts +48 -0
  71. package/dist/pi/mission-control/inner-tail.js +76 -0
  72. package/dist/pi/mission-control/pi-ui.d.ts +43 -0
  73. package/dist/pi/mission-control/pi-ui.js +10 -0
  74. package/dist/pi/mission-control/render.d.ts +6 -0
  75. package/dist/pi/mission-control/render.js +98 -0
  76. package/dist/pi/phase-driver.d.ts +74 -0
  77. package/dist/pi/phase-driver.js +122 -0
  78. package/dist/pi/pi-types.d.ts +222 -0
  79. package/dist/pi/pi-types.js +21 -0
  80. package/dist/pi/probe.d.ts +99 -0
  81. package/dist/pi/probe.js +179 -0
  82. package/dist/pi/render-tools.d.ts +17 -0
  83. package/dist/pi/render-tools.js +56 -0
  84. package/dist/pi/reset-pump.d.ts +47 -0
  85. package/dist/pi/reset-pump.js +85 -0
  86. package/dist/pi/session-seed.d.ts +74 -0
  87. package/dist/pi/session-seed.js +103 -0
  88. package/dist/pi/tool-capability.d.ts +60 -0
  89. package/dist/pi/tool-capability.js +156 -0
  90. package/dist/pi/workflow-client.d.ts +158 -0
  91. package/dist/pi/workflow-client.js +289 -0
  92. package/dist/pi/zod-to-typebox.d.ts +74 -0
  93. package/dist/pi/zod-to-typebox.js +191 -0
  94. package/dist/server-tools.d.ts +2 -0
  95. package/dist/server-tools.js +50 -46
  96. package/dist/spawn.d.ts +55 -0
  97. package/dist/spawn.js +72 -0
  98. package/dist/tools/agent-types.d.ts +2 -2
  99. package/dist/tools/agent-types.js +22 -17
  100. package/dist/tools/attachment-info.d.ts +2 -2
  101. package/dist/tools/attachment-info.js +38 -33
  102. package/dist/tools/broadcast.d.ts +2 -2
  103. package/dist/tools/broadcast.js +69 -64
  104. package/dist/tools/cancel-stage.d.ts +2 -2
  105. package/dist/tools/cancel-stage.js +20 -15
  106. package/dist/tools/clear-state.d.ts +2 -2
  107. package/dist/tools/clear-state.js +25 -20
  108. package/dist/tools/coat-check-evict.d.ts +2 -2
  109. package/dist/tools/coat-check-evict.js +29 -24
  110. package/dist/tools/coat-check-get.d.ts +2 -2
  111. package/dist/tools/coat-check-get.js +38 -33
  112. package/dist/tools/coat-check-list.d.ts +2 -2
  113. package/dist/tools/coat-check-list.js +48 -43
  114. package/dist/tools/coat-check-put.d.ts +2 -2
  115. package/dist/tools/coat-check-put.js +38 -33
  116. package/dist/tools/cue.d.ts +2 -2
  117. package/dist/tools/cue.js +57 -52
  118. package/dist/tools/descriptor.d.ts +72 -0
  119. package/dist/tools/descriptor.js +39 -0
  120. package/dist/tools/destroy.d.ts +2 -2
  121. package/dist/tools/destroy.js +153 -148
  122. package/dist/tools/ensemble.d.ts +2 -2
  123. package/dist/tools/ensemble.js +71 -66
  124. package/dist/tools/evaluate-gate.d.ts +2 -2
  125. package/dist/tools/evaluate-gate.js +33 -27
  126. package/dist/tools/fetch-state.d.ts +2 -2
  127. package/dist/tools/fetch-state.js +42 -37
  128. package/dist/tools/gates.d.ts +2 -2
  129. package/dist/tools/gates.js +39 -34
  130. package/dist/tools/hosts.d.ts +2 -2
  131. package/dist/tools/hosts.js +25 -20
  132. package/dist/tools/listen.d.ts +2 -2
  133. package/dist/tools/listen.js +23 -18
  134. package/dist/tools/load-lineup.d.ts +2 -2
  135. package/dist/tools/load-lineup.js +324 -319
  136. package/dist/tools/migrate.d.ts +2 -2
  137. package/dist/tools/migrate.js +45 -40
  138. package/dist/tools/pause.d.ts +2 -2
  139. package/dist/tools/pause.js +34 -29
  140. package/dist/tools/play.d.ts +2 -2
  141. package/dist/tools/play.js +53 -48
  142. package/dist/tools/quality-gate.d.ts +2 -2
  143. package/dist/tools/quality-gate.js +26 -21
  144. package/dist/tools/recall.d.ts +2 -2
  145. package/dist/tools/recall.js +32 -27
  146. package/dist/tools/recruit.d.ts +2 -2
  147. package/dist/tools/recruit.js +340 -256
  148. package/dist/tools/release.d.ts +2 -2
  149. package/dist/tools/release.js +85 -80
  150. package/dist/tools/report.d.ts +2 -2
  151. package/dist/tools/report.js +28 -23
  152. package/dist/tools/reset.d.ts +3 -0
  153. package/dist/tools/reset.js +51 -0
  154. package/dist/tools/restart.d.ts +2 -2
  155. package/dist/tools/restart.js +51 -46
  156. package/dist/tools/restore.d.ts +2 -2
  157. package/dist/tools/restore.js +76 -71
  158. package/dist/tools/save-lineup.d.ts +2 -2
  159. package/dist/tools/save-lineup.js +32 -27
  160. package/dist/tools/save-state.d.ts +2 -2
  161. package/dist/tools/save-state.js +31 -26
  162. package/dist/tools/schedule.d.ts +2 -2
  163. package/dist/tools/schedule.js +133 -128
  164. package/dist/tools/schedules.d.ts +2 -2
  165. package/dist/tools/schedules.js +41 -36
  166. package/dist/tools/set-ensemble-description.d.ts +2 -2
  167. package/dist/tools/set-ensemble-description.js +26 -21
  168. package/dist/tools/set-name.d.ts +2 -2
  169. package/dist/tools/set-name.js +38 -33
  170. package/dist/tools/set-part.d.ts +2 -2
  171. package/dist/tools/set-part.js +20 -15
  172. package/dist/tools/shutdown.d.ts +2 -2
  173. package/dist/tools/shutdown.js +39 -34
  174. package/dist/tools/stage.d.ts +2 -2
  175. package/dist/tools/stage.js +28 -23
  176. package/dist/tools/stages.d.ts +2 -2
  177. package/dist/tools/stages.js +36 -31
  178. package/dist/tools/unschedule.d.ts +2 -2
  179. package/dist/tools/unschedule.js +30 -25
  180. package/dist/tools/who-am-i.d.ts +2 -2
  181. package/dist/tools/who-am-i.js +36 -31
  182. package/dist/tools/worktree.d.ts +2 -2
  183. package/dist/tools/worktree.js +134 -129
  184. package/dist/tui/index.js +6 -6
  185. package/dist/types.d.ts +47 -2
  186. package/dist/types.js +1 -1
  187. package/dist/utils/default-part.js +1 -0
  188. package/dist/utils/sdk-probe.d.ts +23 -0
  189. package/dist/utils/sdk-probe.js +46 -7
  190. package/dist/worker.d.ts +3 -1
  191. package/dist/worker.js +6 -2
  192. package/dist/workflows/session.js +70 -2
  193. package/dist/workflows/signals.d.ts +32 -2
  194. package/dist/workflows/signals.js +25 -2
  195. package/package.json +4 -1
  196. package/workflow-bundle.js +97 -6
  197. package/dashboard/dist/assets/index-D6Xyje_n.js.map +0 -1
  198. package/dist/tools/helpers.d.ts +0 -21
  199. package/dist/tools/helpers.js +0 -25
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Headless Pi runtime (Phase 3a). Spawned by the daemon (spawnPiHeadless →
3
+ * src/adapters/pi/adapter.ts) for a recruited `agent: 'pi'` player. No human, no
4
+ * terminal: it constructs Pi's `createAgentSession` with the agent-tempo
5
+ * extension injected INLINE, and the module-scope extension singleton
6
+ * (createPiExtension, mode='headless') owns claim/heartbeat/tool-registration/
7
+ * cue-pump on `session_start`. Reuses ~everything from Phases 1–2.
8
+ *
9
+ * Lifecycle:
10
+ * 1. probe Pi SDK; resolve the model via pi-ai `getModel` — a bad/unindexed
11
+ * model fails CLEAN (exit before attach, no orphan — architect's backstop).
12
+ * 2. createAgentSession({ resourceLoader: DefaultResourceLoader({
13
+ * extensionFactories: [ext] }), model? }).
14
+ * 3. await session.bindExtensions({}) → fires session_start → the singleton
15
+ * attaches (claim + heartbeat + tools + cue pump). bindExtensions IS the
16
+ * explicit bootstrap (not "hope session_start fires").
17
+ * 4. stay alive until a shutdown signal (SIGTERM/SIGINT).
18
+ * 5. RELIABLE detach (headless owns the exit): detachAllPiRuntimesForExit()
19
+ * [await adapterExited] → session.dispose() → process exit.
20
+ *
21
+ * ESM note: the Pi SDK is an ESM-only optional dep; we import it via a
22
+ * `Function`-wrapped dynamic `import()` so tsc (module=commonjs) doesn't
23
+ * downlevel it to `require()` — Node resolves the real ESM module at runtime.
24
+ *
25
+ * Determinism boundary: client-side only.
26
+ */
27
+ import { type Config } from '../config';
28
+ import { type PiToolAccess } from './extension';
29
+ export interface RunHeadlessPiOptions {
30
+ config?: Config;
31
+ toolAccess?: PiToolAccess;
32
+ /** `provider/model` selector; absent → Pi default. */
33
+ model?: string;
34
+ /** Restart-resume: prior Pi conversation id (A4 wires SessionManager). */
35
+ continueSessionId?: string;
36
+ }
37
+ /**
38
+ * Build the `DefaultResourceLoader` options for a headless Pi player.
39
+ *
40
+ * SECURITY — S2 (MD-C deny-list soundness). The `restricted` tool gate is a
41
+ * DENY-LIST over shell/exec tool *names* (tool-capability.ts EXEC_TOOLS, via
42
+ * `classify(name) === 'exec'` — F1 replaced extension.ts's former local set). That
43
+ * guarantee — "restricted = no host execution" — holds ONLY IF no third-party
44
+ * extension can register an un-blacklisted execution tool (e.g. a custom
45
+ * `python` / `npm` / `run` tool). It therefore depends on a hard structural
46
+ * fact: which extensions Pi loads.
47
+ *
48
+ * Verified against the installed Pi SDK 0.78 source (NOT assumed):
49
+ * - `DefaultResourceLoader.reload()` (resource-loader.js:271-276) builds
50
+ * `extensionPaths = noExtensions ? cliEnabledExtensions
51
+ * : merge(cliEnabledExtensions, enabledExtensions)`
52
+ * where `enabledExtensions` (line 229) are the DISK/package extensions from
53
+ * `packageManager.resolve()` (`~/.pi/agent/extensions/`, `<cwd>/.pi/extensions/`,
54
+ * installed packages). `loadExtensions(extensionPaths)` then loads them and
55
+ * MERGES with our inline factories (lines 274-276).
56
+ * - `noExtensions` defaults to `false` (constructor, line 132) — so the naive
57
+ * loader DOES load disk extensions. That is the S2 gap.
58
+ *
59
+ * Fix (= security's "exclude the extensions dir", done structurally):
60
+ * - `noExtensions: true` → `extensionPaths` collapses to `cliEnabledExtensions`,
61
+ * which is empty because we pass NO `additionalExtensionPaths`. So
62
+ * `loadExtensions([])` registers nothing from disk/packages.
63
+ * - Inline `extensionFactories` load UNCONDITIONALLY (reload() line 275 is not
64
+ * gated by `noExtensions`), so our agent-tempo extension still attaches.
65
+ * Net: the ONLY tools present are Pi's built-ins (bash/read/edit/write/grep —
66
+ * all covered by the deny-list) + our agent-tempo MCP tools (no exec). No
67
+ * third-party tool can slip past the deny-list. Skills/prompts/themes cannot
68
+ * register tools, so they are not a vector and are left at defaults.
69
+ *
70
+ * Kept as a pure, exported helper so the `noExtensions: true` invariant has a
71
+ * unit regression test (test/pi-headless-loader.test.ts) without needing the Pi
72
+ * SDK installed.
73
+ */
74
+ export declare function buildPiResourceLoaderOptions(params: {
75
+ cwd: string;
76
+ agentDir: string;
77
+ /** The inline agent-tempo extension factory (`createPiExtension(...)`). Typed
78
+ * with a bottom param so any concrete factory arity is assignable. */
79
+ extensionFactory: (pi: never) => void;
80
+ }): Record<string, unknown>;
81
+ /**
82
+ * Boot + run a headless Pi player until shutdown. Resolves when the process has
83
+ * cleanly detached + disposed (it also calls process.exit on the terminal path).
84
+ */
85
+ export declare function runHeadlessPi(opts?: RunHeadlessPiOptions): Promise<void>;
@@ -0,0 +1,250 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildPiResourceLoaderOptions = buildPiResourceLoaderOptions;
4
+ exports.runHeadlessPi = runHeadlessPi;
5
+ /**
6
+ * Headless Pi runtime (Phase 3a). Spawned by the daemon (spawnPiHeadless →
7
+ * src/adapters/pi/adapter.ts) for a recruited `agent: 'pi'` player. No human, no
8
+ * terminal: it constructs Pi's `createAgentSession` with the agent-tempo
9
+ * extension injected INLINE, and the module-scope extension singleton
10
+ * (createPiExtension, mode='headless') owns claim/heartbeat/tool-registration/
11
+ * cue-pump on `session_start`. Reuses ~everything from Phases 1–2.
12
+ *
13
+ * Lifecycle:
14
+ * 1. probe Pi SDK; resolve the model via pi-ai `getModel` — a bad/unindexed
15
+ * model fails CLEAN (exit before attach, no orphan — architect's backstop).
16
+ * 2. createAgentSession({ resourceLoader: DefaultResourceLoader({
17
+ * extensionFactories: [ext] }), model? }).
18
+ * 3. await session.bindExtensions({}) → fires session_start → the singleton
19
+ * attaches (claim + heartbeat + tools + cue pump). bindExtensions IS the
20
+ * explicit bootstrap (not "hope session_start fires").
21
+ * 4. stay alive until a shutdown signal (SIGTERM/SIGINT).
22
+ * 5. RELIABLE detach (headless owns the exit): detachAllPiRuntimesForExit()
23
+ * [await adapterExited] → session.dispose() → process exit.
24
+ *
25
+ * ESM note: the Pi SDK is an ESM-only optional dep; we import it via a
26
+ * `Function`-wrapped dynamic `import()` so tsc (module=commonjs) doesn't
27
+ * downlevel it to `require()` — Node resolves the real ESM module at runtime.
28
+ *
29
+ * Determinism boundary: client-side only.
30
+ */
31
+ const config_1 = require("../config");
32
+ const sdk_probe_1 = require("../utils/sdk-probe");
33
+ const extension_1 = require("./extension");
34
+ const probe_1 = require("./probe");
35
+ const session_seed_1 = require("./session-seed");
36
+ const log = (...args) => {
37
+ // eslint-disable-next-line no-console
38
+ console.error('[agent-tempo:pi-headless]', ...args);
39
+ };
40
+ /**
41
+ * True dynamic ESM import that survives tsc's commonjs downleveling. `import(x)`
42
+ * with a literal would be rewritten to a `require`-based helper (breaks on an
43
+ * ESM-only package); the `Function` indirection keeps a native `import()` at
44
+ * runtime so Node loads the real ESM module.
45
+ */
46
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
47
+ const esmImport = new Function('specifier', 'return import(specifier)');
48
+ /**
49
+ * Resolve a Pi `Model` object from a `provider/model` string via pi-ai's
50
+ * `getModel`. Returns `{ model }` on success, `{ fatal }` with an actionable
51
+ * message on an invalid/unindexed model (getModel returns `undefined` — a plain
52
+ * check, no throw), or `{}` when no model was requested (Pi uses its own default
53
+ * — the 3a anthropic-default path).
54
+ */
55
+ async function resolveModel(modelStr) {
56
+ if (!modelStr)
57
+ return {};
58
+ const slash = modelStr.indexOf('/');
59
+ if (slash <= 0 || slash === modelStr.length - 1) {
60
+ return { fatal: `Invalid Pi model "${modelStr}" — expected "provider/model" (e.g. anthropic/claude-opus-4-5).` };
61
+ }
62
+ const provider = modelStr.slice(0, slash);
63
+ const modelName = modelStr.slice(slash + 1);
64
+ try {
65
+ const piAi = await esmImport(probe_1.PI_AI_PACKAGE);
66
+ const getModel = piAi.getModel;
67
+ const model = getModel(provider, modelName);
68
+ if (model === undefined || model === null) {
69
+ return {
70
+ fatal: `Pi model "${modelStr}" not found in Pi's provider index (provider="${provider}"). ` +
71
+ `Check the model id against \`pi --list-models\` / models.dev.`,
72
+ };
73
+ }
74
+ return { model };
75
+ }
76
+ catch (err) {
77
+ return { fatal: `Failed to resolve Pi model "${modelStr}": ${err instanceof Error ? err.message : String(err)}` };
78
+ }
79
+ }
80
+ /**
81
+ * Build the `DefaultResourceLoader` options for a headless Pi player.
82
+ *
83
+ * SECURITY — S2 (MD-C deny-list soundness). The `restricted` tool gate is a
84
+ * DENY-LIST over shell/exec tool *names* (tool-capability.ts EXEC_TOOLS, via
85
+ * `classify(name) === 'exec'` — F1 replaced extension.ts's former local set). That
86
+ * guarantee — "restricted = no host execution" — holds ONLY IF no third-party
87
+ * extension can register an un-blacklisted execution tool (e.g. a custom
88
+ * `python` / `npm` / `run` tool). It therefore depends on a hard structural
89
+ * fact: which extensions Pi loads.
90
+ *
91
+ * Verified against the installed Pi SDK 0.78 source (NOT assumed):
92
+ * - `DefaultResourceLoader.reload()` (resource-loader.js:271-276) builds
93
+ * `extensionPaths = noExtensions ? cliEnabledExtensions
94
+ * : merge(cliEnabledExtensions, enabledExtensions)`
95
+ * where `enabledExtensions` (line 229) are the DISK/package extensions from
96
+ * `packageManager.resolve()` (`~/.pi/agent/extensions/`, `<cwd>/.pi/extensions/`,
97
+ * installed packages). `loadExtensions(extensionPaths)` then loads them and
98
+ * MERGES with our inline factories (lines 274-276).
99
+ * - `noExtensions` defaults to `false` (constructor, line 132) — so the naive
100
+ * loader DOES load disk extensions. That is the S2 gap.
101
+ *
102
+ * Fix (= security's "exclude the extensions dir", done structurally):
103
+ * - `noExtensions: true` → `extensionPaths` collapses to `cliEnabledExtensions`,
104
+ * which is empty because we pass NO `additionalExtensionPaths`. So
105
+ * `loadExtensions([])` registers nothing from disk/packages.
106
+ * - Inline `extensionFactories` load UNCONDITIONALLY (reload() line 275 is not
107
+ * gated by `noExtensions`), so our agent-tempo extension still attaches.
108
+ * Net: the ONLY tools present are Pi's built-ins (bash/read/edit/write/grep —
109
+ * all covered by the deny-list) + our agent-tempo MCP tools (no exec). No
110
+ * third-party tool can slip past the deny-list. Skills/prompts/themes cannot
111
+ * register tools, so they are not a vector and are left at defaults.
112
+ *
113
+ * Kept as a pure, exported helper so the `noExtensions: true` invariant has a
114
+ * unit regression test (test/pi-headless-loader.test.ts) without needing the Pi
115
+ * SDK installed.
116
+ */
117
+ function buildPiResourceLoaderOptions(params) {
118
+ return {
119
+ cwd: params.cwd,
120
+ agentDir: params.agentDir,
121
+ extensionFactories: [params.extensionFactory],
122
+ // SECURITY (S2): hard-exclude all disk/package extensions. Do NOT add
123
+ // `additionalExtensionPaths` here — that would re-introduce the exec-tool
124
+ // vector this flag closes.
125
+ noExtensions: true,
126
+ };
127
+ }
128
+ /**
129
+ * Boot + run a headless Pi player until shutdown. Resolves when the process has
130
+ * cleanly detached + disposed (it also calls process.exit on the terminal path).
131
+ */
132
+ async function runHeadlessPi(opts = {}) {
133
+ const config = opts.config ?? (0, config_1.getConfig)();
134
+ const toolAccess = opts.toolAccess ?? 'restricted';
135
+ // 0) Node-floor backstop (Decision B, #645). The recruit pre-flight is the
136
+ // AUTHORITATIVE gate; this covers direct/manual launches that bypass recruit.
137
+ // Checked before the SDK probe because Pi's ESM packages can't even import on
138
+ // sub-22.19 Node — a clean floor message beats a cryptic import failure.
139
+ const nodeFloor = (0, probe_1.checkPiNodeFloor)();
140
+ if (!nodeFloor.ok) {
141
+ log(`FATAL: ${nodeFloor.reason} Exiting.`);
142
+ process.exit(3);
143
+ return;
144
+ }
145
+ // 1) Probe — the spawn entry is the only place the Pi SDK is REQUIRED.
146
+ if (!(0, sdk_probe_1.probeSdkInstall)(probe_1.PI_PACKAGE)) {
147
+ log(`FATAL: ${probe_1.PI_PACKAGE} is not installed — cannot run headless Pi. Exiting.`);
148
+ process.exit(1);
149
+ return;
150
+ }
151
+ // 2) Resolve the model BEFORE creating the session — a bad model fails clean
152
+ // (exit before attach, no half-attached orphan).
153
+ const { model, fatal } = await resolveModel(opts.model);
154
+ if (fatal) {
155
+ log(`FATAL: ${fatal} Exiting without attaching.`);
156
+ process.exit(2);
157
+ return;
158
+ }
159
+ // 3) Inline extension factory — headless mode → the MD-C tool gate is active.
160
+ const extensionFactory = (0, extension_1.createPiExtension)({ mode: 'headless', toolAccess });
161
+ // 4) Construct the Pi SDK session with the extension injected inline.
162
+ const piSdk = await esmImport(probe_1.PI_PACKAGE);
163
+ const createAgentSession = piSdk.createAgentSession;
164
+ const DefaultResourceLoader = piSdk.DefaultResourceLoader;
165
+ // H1 (#645): ALWAYS run in-memory. A disk-backed SessionManager loads
166
+ // ~/.pi/agent/sessions/<cwd>/*.jsonl on startup; a stale/partial entry with
167
+ // malformed `content` throws "content is not iterable" during Pi's
168
+ // compaction/consumption scan (agent-session.js:2486/2493). inMemory loads
169
+ // nothing from disk → that crash vector is gone. `sessionManager` and
170
+ // `resourceLoader` are INDEPENDENT createAgentSession options (no conflict with
171
+ // the noExtensions/reload() path above).
172
+ const SessionManager = piSdk.SessionManager;
173
+ const sessionManager = SessionManager.inMemory(process.cwd());
174
+ // Single appendMessage chokepoint (session-seed.ts). For a fresh recruit the
175
+ // transcript is empty → no-op; H2 supplies the durable replay read here
176
+ // (fetch_state on ENV.PI_CONTINUE_SESSION). headless.ts NEVER calls
177
+ // appendMessage directly — sanitization is the only crash lever we control.
178
+ (0, session_seed_1.seedSessionManager)(sessionManager, undefined /* H2: durable replay transcript */);
179
+ // Pi's DefaultResourceLoader REQUIRES agentDir (normalizePath does
180
+ // `.startsWith()` on it) — getAgentDir() resolves ~/.pi/agent. Pass it to BOTH
181
+ // createAgentSession and the loader. (Found in the 3a live smoke — devops.)
182
+ const getAgentDir = piSdk.getAgentDir;
183
+ const agentDir = getAgentDir();
184
+ const resourceLoader = new DefaultResourceLoader(buildPiResourceLoaderOptions({ cwd: process.cwd(), agentDir, extensionFactory }));
185
+ // CRITICAL (3a live smoke — devops): createAgentSession only calls
186
+ // resourceLoader.reload() when IT constructs the loader (sdk.js:99-101). When we
187
+ // pass our OWN loader, reload() is skipped — and DefaultResourceLoader inits
188
+ // `extensionsResult.extensions = []` (resource-loader.js:146) and only populates
189
+ // it during reload(). So without this explicit reload our extension never
190
+ // registers, and session_start fires into an empty handler list (no claim, no
191
+ // heartbeat). The SDK's own doc comment (sdk.js:74-83) prescribes this exact
192
+ // construct → reload() → pass-as-resourceLoader sequence.
193
+ await resourceLoader.reload();
194
+ const { session } = await createAgentSession({
195
+ cwd: process.cwd(),
196
+ agentDir,
197
+ ...(model ? { model } : {}),
198
+ resourceLoader,
199
+ // H1 (#645): in-memory session (seeded above via the session-seed chokepoint).
200
+ // H2 will seed it from agent-tempo durable state (ENV.PI_CONTINUE_SESSION
201
+ // saveable-state key) before this call — a true continuation, not a cue.
202
+ sessionManager,
203
+ });
204
+ // 5) Explicit bootstrap — fires session_start → the singleton claims/attaches.
205
+ await session.bindExtensions({});
206
+ // Headless session_start carries NO `session` field (interactive does), so the
207
+ // extension can't wire the cue pump's session ref itself. We hold the SDK
208
+ // session here — set it on the now-claimed runtime so the cue pump can inject.
209
+ // (3a live smoke — devops; the headless-vs-interactive session-wiring gap.)
210
+ const playerId = process.env[config_1.ENV.PLAYER_NAME] || `pi-${process.pid}`;
211
+ (0, extension_1.setRuntimeSession)((0, config_1.sessionWorkflowId)(config.ensemble, playerId), session);
212
+ log(`headless Pi session bound (toolAccess=${toolAccess}, ` +
213
+ `model=${opts.model ?? 'pi-default'}${opts.continueSessionId ? `, continue=${opts.continueSessionId}` : ''}, ` +
214
+ `sessionId=${session.sessionId ?? '?'})`);
215
+ // 6) Stay alive until a shutdown signal, then RELIABLE detach → dispose → exit.
216
+ // Keep the event loop alive with a REF'd timer: the heartbeat + cue-pump timers
217
+ // are `.unref()`'d by design (so they never block a clean exit), and the
218
+ // SIGTERM/SIGINT once-listeners aren't active handles — so WITHOUT this the loop
219
+ // drains and Node exits code 0 immediately after bindExtensions in headless mode.
220
+ // (Found in the 3a live smoke — devops.)
221
+ const keepAlive = setInterval(() => { }, 30_000);
222
+ try {
223
+ await new Promise((resolveShutdown) => {
224
+ const onSignal = (sig) => { log(`received ${sig} — shutting down`); resolveShutdown(); };
225
+ process.once('SIGTERM', () => onSignal('SIGTERM'));
226
+ process.once('SIGINT', () => onSignal('SIGINT'));
227
+ });
228
+ }
229
+ finally {
230
+ clearInterval(keepAlive);
231
+ }
232
+ // Headless owns the exit sequence: await adapterExited (unmaps the runtime)
233
+ // THEN dispose the SDK session (the dispose-fired session_shutdown finds no
234
+ // mapped runtime → no-op, so no double-detach).
235
+ try {
236
+ await (0, extension_1.detachAllPiRuntimesForExit)();
237
+ }
238
+ catch (err) {
239
+ log('detach failed (reaper backstops):', err);
240
+ }
241
+ try {
242
+ session.dispose();
243
+ }
244
+ catch (err) {
245
+ log('dispose failed:', err);
246
+ }
247
+ log('headless Pi clean-exit complete');
248
+ // eslint-disable-next-line no-process-exit
249
+ process.exit(0);
250
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * agent-tempo Pi integration — barrel.
3
+ *
4
+ * Default export is the Pi extension factory (`export default function(pi)`).
5
+ * Named exports expose the testable units and the client-side wrapper for reuse
6
+ * by the headless Pi runtime (Phase 3).
7
+ *
8
+ * Tool registration: the extension renders the shared transport-neutral tool
9
+ * descriptors (src/tools/descriptor.ts) onto Pi via `renderToPi`, deriving
10
+ * TypeBox param schemas from zod through `zod-to-typebox.ts`. There is no
11
+ * Pi-specific re-implementation of any tool.
12
+ *
13
+ * See src/pi/README.md for the Phase 0/2 findings (abrupt-death / MD-A, D12a)
14
+ * and known limitations.
15
+ */
16
+ export { default } from './extension';
17
+ export { PhaseDriver } from './phase-driver';
18
+ export type { PiPhase, WorkflowAction, PhaseDriverResult } from './phase-driver';
19
+ export { PiWorkflowClient } from './workflow-client';
20
+ export type { PiWorkflowClientOptions } from './workflow-client';
21
+ export { CuePump } from './cue-pump';
22
+ export type { CueSource, SessionResolver, CuePumpOptions } from './cue-pump';
23
+ export { renderToPi, toPiResult } from './render-tools';
24
+ export { createLazyProxy } from './lazy-proxy';
25
+ export { zodShapeToTypeBox, UnsupportedZodFeatureError } from './zod-to-typebox';
26
+ export { probePi, PI_PACKAGE, PI_AI_PACKAGE, TESTED_PI_VERSION, PI_NODE_FLOOR } from './probe';
27
+ export type { PiProbeResult } from './probe';
28
+ export type { ExtensionAPI, PiExtension, PiAgentSession, PiEventPayload, PiToolDefinition, PiToolResult, PiLifecycleEvent, } from './pi-types';
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PI_NODE_FLOOR = exports.TESTED_PI_VERSION = exports.PI_AI_PACKAGE = exports.PI_PACKAGE = exports.probePi = exports.UnsupportedZodFeatureError = exports.zodShapeToTypeBox = exports.createLazyProxy = exports.toPiResult = exports.renderToPi = exports.CuePump = exports.PiWorkflowClient = exports.PhaseDriver = exports.default = void 0;
7
+ /**
8
+ * agent-tempo Pi integration — barrel.
9
+ *
10
+ * Default export is the Pi extension factory (`export default function(pi)`).
11
+ * Named exports expose the testable units and the client-side wrapper for reuse
12
+ * by the headless Pi runtime (Phase 3).
13
+ *
14
+ * Tool registration: the extension renders the shared transport-neutral tool
15
+ * descriptors (src/tools/descriptor.ts) onto Pi via `renderToPi`, deriving
16
+ * TypeBox param schemas from zod through `zod-to-typebox.ts`. There is no
17
+ * Pi-specific re-implementation of any tool.
18
+ *
19
+ * See src/pi/README.md for the Phase 0/2 findings (abrupt-death / MD-A, D12a)
20
+ * and known limitations.
21
+ */
22
+ var extension_1 = require("./extension");
23
+ Object.defineProperty(exports, "default", { enumerable: true, get: function () { return __importDefault(extension_1).default; } });
24
+ var phase_driver_1 = require("./phase-driver");
25
+ Object.defineProperty(exports, "PhaseDriver", { enumerable: true, get: function () { return phase_driver_1.PhaseDriver; } });
26
+ var workflow_client_1 = require("./workflow-client");
27
+ Object.defineProperty(exports, "PiWorkflowClient", { enumerable: true, get: function () { return workflow_client_1.PiWorkflowClient; } });
28
+ var cue_pump_1 = require("./cue-pump");
29
+ Object.defineProperty(exports, "CuePump", { enumerable: true, get: function () { return cue_pump_1.CuePump; } });
30
+ var render_tools_1 = require("./render-tools");
31
+ Object.defineProperty(exports, "renderToPi", { enumerable: true, get: function () { return render_tools_1.renderToPi; } });
32
+ Object.defineProperty(exports, "toPiResult", { enumerable: true, get: function () { return render_tools_1.toPiResult; } });
33
+ var lazy_proxy_1 = require("./lazy-proxy");
34
+ Object.defineProperty(exports, "createLazyProxy", { enumerable: true, get: function () { return lazy_proxy_1.createLazyProxy; } });
35
+ var zod_to_typebox_1 = require("./zod-to-typebox");
36
+ Object.defineProperty(exports, "zodShapeToTypeBox", { enumerable: true, get: function () { return zod_to_typebox_1.zodShapeToTypeBox; } });
37
+ Object.defineProperty(exports, "UnsupportedZodFeatureError", { enumerable: true, get: function () { return zod_to_typebox_1.UnsupportedZodFeatureError; } });
38
+ var probe_1 = require("./probe");
39
+ Object.defineProperty(exports, "probePi", { enumerable: true, get: function () { return probe_1.probePi; } });
40
+ Object.defineProperty(exports, "PI_PACKAGE", { enumerable: true, get: function () { return probe_1.PI_PACKAGE; } });
41
+ Object.defineProperty(exports, "PI_AI_PACKAGE", { enumerable: true, get: function () { return probe_1.PI_AI_PACKAGE; } });
42
+ Object.defineProperty(exports, "TESTED_PI_VERSION", { enumerable: true, get: function () { return probe_1.TESTED_PI_VERSION; } });
43
+ Object.defineProperty(exports, "PI_NODE_FLOOR", { enumerable: true, get: function () { return probe_1.PI_NODE_FLOOR; } });
@@ -0,0 +1,67 @@
1
+ import type { InnerFrame, InnerLoopRegistry } from './inner-loop-publisher';
2
+ /** Env var carrying the per-player ingest token (threaded in at spawn). */
3
+ export declare const INGEST_TOKEN_ENV = "AGENT_TEMPO_INGEST_TOKEN";
4
+ /** DOS backstop — frames over this are dropped (summaries are already ~2KB-truncated). */
5
+ export declare const MAX_FRAME_BYTES: number;
6
+ /** Minimal `fetch` shape this client needs — injectable for tests. */
7
+ export type InnerLoopFetch = (url: string, init: {
8
+ method: string;
9
+ headers: Record<string, string>;
10
+ body?: string;
11
+ }) => Promise<{
12
+ status: number;
13
+ json(): Promise<unknown>;
14
+ }>;
15
+ export interface InnerLoopHttpClientOptions {
16
+ /** The player's ensemble (URL path segment). */
17
+ ensemble: string;
18
+ /** The player's id (URL path segment). */
19
+ playerId: string;
20
+ /** Ingest token. Defaults to `process.env[INGEST_TOKEN_ENV]`. */
21
+ ingestToken?: string;
22
+ /** Daemon port discovery. Defaults to {@link readPortFile}. */
23
+ readPort?: () => number | null;
24
+ /** HTTP transport. Defaults to global `fetch` (no-op if absent). */
25
+ fetchFn?: InnerLoopFetch;
26
+ /** Min interval between presence GETs (default 1000ms). */
27
+ presencePollMs?: number;
28
+ /** Injected clock (tests). Defaults to `Date.now`. */
29
+ now?: () => number;
30
+ }
31
+ /**
32
+ * Loopback-HTTP {@link InnerLoopRegistry}. Construct one per headless player with
33
+ * its ensemble + playerId; the publisher calls `publish` / `subscriberCount`.
34
+ * The `workflowId` argument is the player's own and is unused — the URL is built
35
+ * from the ctor ensemble + playerId.
36
+ */
37
+ export declare class InnerLoopHttpClient implements InnerLoopRegistry {
38
+ private readonly ensemble;
39
+ private readonly playerId;
40
+ private readonly ingestToken;
41
+ private readonly readPort;
42
+ private readonly fetchFn;
43
+ private readonly presencePollMs;
44
+ private readonly now;
45
+ /** Last presence count from the daemon. 0 until the first GET resolves / on failure. */
46
+ private cachedSubscribers;
47
+ /** 3d — last gateArmed flag from the daemon (folded into the presence response). */
48
+ private cachedGateArmed;
49
+ private lastPresenceRefresh;
50
+ constructor(opts: InnerLoopHttpClientOptions);
51
+ /** Whether the client can talk to the daemon at all (token + transport present). */
52
+ private get enabled();
53
+ private baseUrl;
54
+ /** Fire-and-forget POST of one frame. Drops (never throws/blocks) on any failure. */
55
+ publish(_workflowId: string, frame: InnerFrame): void;
56
+ /** Synchronous cached presence (stale-while-revalidate). Default 0 / fail-safe 0. */
57
+ subscriberCount(_workflowId: string): number;
58
+ /**
59
+ * 3d — synchronous cached `gateArmed` (same stale-while-revalidate presence GET
60
+ * that feeds subscriberCount; short-poll keeps it within ~1s). Default false /
61
+ * fail-safe false (a missed/failed presence read never spuriously engages the
62
+ * gate). The engagement check reads this together with subscriberCount.
63
+ */
64
+ gateArmed(_workflowId: string): boolean;
65
+ /** Fire a rate-limited background presence GET that updates the cache. */
66
+ private maybeRefreshPresence;
67
+ }
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InnerLoopHttpClient = exports.MAX_FRAME_BYTES = exports.INGEST_TOKEN_ENV = void 0;
4
+ /**
5
+ * Inner-loop HTTP client (3c Part 5) — the PRODUCTION {@link InnerLoopRegistry}
6
+ * impl the {@link InnerLoopPublisher} injects.
7
+ *
8
+ * Why HTTP, not in-process: a headless Pi player is a DETACHED subprocess,
9
+ * separate from the daemon that hosts the real fine-tail registry + SSE
10
+ * side-channel. So `publish` / `subscriberCount` are thin loopback-HTTP calls to
11
+ * the daemon, discovered via the port-file. This client lives on the player side
12
+ * of that boundary; lead owns the daemon endpoints.
13
+ *
14
+ * LOCKED wire contract (lead, Part 4):
15
+ * - Discovery: {@link readPortFile} → daemon HTTP port (`null` ⇒ daemon HTTP
16
+ * not up ⇒ no-op gracefully). Base `http://127.0.0.1:${port}` (loopback only).
17
+ * - Auth: `X-Ingest-Token: <token>` on BOTH calls, from `AGENT_TEMPO_INGEST_TOKEN`
18
+ * (threaded in at spawn). Token unset ⇒ no-op (publish drops, presence ⇒ 0).
19
+ * - POST `/v1/players/:ensemble/:playerId/inner/ingest` — body = the InnerFrame
20
+ * JSON object DIRECTLY (no wrapper), ≤32KB (DOS backstop). 204 ⇒ ok; ANY
21
+ * other status / network error ⇒ DROP the frame, never throw, never block
22
+ * the Pi loop (fire-and-forget).
23
+ * - GET `/v1/players/:ensemble/:playerId/inner/presence` — 200 `{subscribers:number}`;
24
+ * anything else (e.g. 403) ⇒ treat as 0.
25
+ *
26
+ * `subscriberCount` MUST be synchronous (the interface) — it returns a CACHED
27
+ * value, refreshed stale-while-revalidate: each call fires a rate-limited
28
+ * background GET (≤1/`presencePollMs`) and returns the last known count (default
29
+ * 0 until the first GET resolves, and on any failure — fail-safe: unknown ⇒ no
30
+ * forwarding). The publisher additionally rate-limits how often it calls this.
31
+ */
32
+ const port_file_1 = require("../http/port-file");
33
+ /** Env var carrying the per-player ingest token (threaded in at spawn). */
34
+ exports.INGEST_TOKEN_ENV = 'AGENT_TEMPO_INGEST_TOKEN';
35
+ /** DOS backstop — frames over this are dropped (summaries are already ~2KB-truncated). */
36
+ exports.MAX_FRAME_BYTES = 32 * 1024;
37
+ const DEFAULT_PRESENCE_POLL_MS = 1000;
38
+ const log = (...args) => {
39
+ // eslint-disable-next-line no-console
40
+ console.error('[agent-tempo:pi]', ...args);
41
+ };
42
+ /** Default transport — global `fetch` adapted to {@link InnerLoopFetch}, or a no-op. */
43
+ function resolveFetch() {
44
+ const g = globalThis.fetch;
45
+ if (typeof g !== 'function')
46
+ return null;
47
+ return g;
48
+ }
49
+ /**
50
+ * Loopback-HTTP {@link InnerLoopRegistry}. Construct one per headless player with
51
+ * its ensemble + playerId; the publisher calls `publish` / `subscriberCount`.
52
+ * The `workflowId` argument is the player's own and is unused — the URL is built
53
+ * from the ctor ensemble + playerId.
54
+ */
55
+ class InnerLoopHttpClient {
56
+ ensemble;
57
+ playerId;
58
+ ingestToken;
59
+ readPort;
60
+ fetchFn;
61
+ presencePollMs;
62
+ now;
63
+ /** Last presence count from the daemon. 0 until the first GET resolves / on failure. */
64
+ cachedSubscribers = 0;
65
+ /** 3d — last gateArmed flag from the daemon (folded into the presence response). */
66
+ cachedGateArmed = false;
67
+ lastPresenceRefresh = -Infinity;
68
+ constructor(opts) {
69
+ this.ensemble = opts.ensemble;
70
+ this.playerId = opts.playerId;
71
+ this.ingestToken = opts.ingestToken ?? process.env[exports.INGEST_TOKEN_ENV];
72
+ this.readPort = opts.readPort ?? (() => (0, port_file_1.readPortFile)());
73
+ this.fetchFn = opts.fetchFn ?? resolveFetch();
74
+ this.presencePollMs = opts.presencePollMs ?? DEFAULT_PRESENCE_POLL_MS;
75
+ this.now = opts.now ?? Date.now;
76
+ }
77
+ /** Whether the client can talk to the daemon at all (token + transport present). */
78
+ get enabled() {
79
+ return Boolean(this.ingestToken) && this.fetchFn !== null;
80
+ }
81
+ baseUrl(port) {
82
+ return `http://127.0.0.1:${port}/v1/players/` +
83
+ `${encodeURIComponent(this.ensemble)}/${encodeURIComponent(this.playerId)}/inner`;
84
+ }
85
+ /** Fire-and-forget POST of one frame. Drops (never throws/blocks) on any failure. */
86
+ publish(_workflowId, frame) {
87
+ if (!this.enabled)
88
+ return;
89
+ const port = this.readPort();
90
+ if (port == null)
91
+ return; // daemon HTTP not up → drop gracefully
92
+ let body;
93
+ try {
94
+ body = JSON.stringify(frame);
95
+ }
96
+ catch {
97
+ return; // unserializable frame → drop
98
+ }
99
+ if (Buffer.byteLength(body, 'utf8') > exports.MAX_FRAME_BYTES) {
100
+ log(`inner frame ${frame.type} exceeds ${exports.MAX_FRAME_BYTES}B — dropped`);
101
+ return;
102
+ }
103
+ void this.fetchFn(`${this.baseUrl(port)}/ingest`, {
104
+ method: 'POST',
105
+ headers: { 'X-Ingest-Token': this.ingestToken, 'Content-Type': 'application/json' },
106
+ body,
107
+ })
108
+ .then((res) => {
109
+ if (res.status !== 204) {
110
+ // 403 (loopback/token/shape) / 413 (oversize) / other — drop silently.
111
+ }
112
+ })
113
+ .catch(() => { });
114
+ }
115
+ /** Synchronous cached presence (stale-while-revalidate). Default 0 / fail-safe 0. */
116
+ subscriberCount(_workflowId) {
117
+ this.maybeRefreshPresence();
118
+ return this.cachedSubscribers;
119
+ }
120
+ /**
121
+ * 3d — synchronous cached `gateArmed` (same stale-while-revalidate presence GET
122
+ * that feeds subscriberCount; short-poll keeps it within ~1s). Default false /
123
+ * fail-safe false (a missed/failed presence read never spuriously engages the
124
+ * gate). The engagement check reads this together with subscriberCount.
125
+ */
126
+ gateArmed(_workflowId) {
127
+ this.maybeRefreshPresence();
128
+ return this.cachedGateArmed;
129
+ }
130
+ /** Fire a rate-limited background presence GET that updates the cache. */
131
+ maybeRefreshPresence() {
132
+ if (!this.enabled)
133
+ return;
134
+ const t = this.now();
135
+ if (t - this.lastPresenceRefresh < this.presencePollMs)
136
+ return;
137
+ const port = this.readPort();
138
+ if (port == null)
139
+ return;
140
+ this.lastPresenceRefresh = t;
141
+ void this.fetchFn(`${this.baseUrl(port)}/presence`, {
142
+ method: 'GET',
143
+ headers: { 'X-Ingest-Token': this.ingestToken },
144
+ })
145
+ .then(async (res) => {
146
+ if (res.status !== 200) {
147
+ this.cachedSubscribers = 0; // 403/etc → treat as no subscribers
148
+ this.cachedGateArmed = false;
149
+ return;
150
+ }
151
+ try {
152
+ const data = (await res.json());
153
+ this.cachedSubscribers = typeof data.subscribers === 'number' ? data.subscribers : 0;
154
+ this.cachedGateArmed = data.gateArmed === true;
155
+ }
156
+ catch {
157
+ this.cachedSubscribers = 0;
158
+ this.cachedGateArmed = false;
159
+ }
160
+ })
161
+ .catch(() => { this.cachedSubscribers = 0; this.cachedGateArmed = false; });
162
+ }
163
+ }
164
+ exports.InnerLoopHttpClient = InnerLoopHttpClient;