@fastagent-sh/fastagent 0.18.0 → 0.20.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 (222) hide show
  1. package/README.md +25 -7
  2. package/dist/agent.d.ts +1 -1
  3. package/dist/agent.js +1 -1
  4. package/dist/atomic-write.d.ts +23 -0
  5. package/dist/atomic-write.js +53 -0
  6. package/dist/{host/node.d.ts → channel.d.ts} +24 -28
  7. package/dist/channel.js +1 -0
  8. package/dist/channels/agentcore-limits.d.ts +9 -0
  9. package/dist/channels/agentcore-limits.js +9 -0
  10. package/dist/channels/agentcore-service.d.ts +55 -0
  11. package/dist/channels/agentcore-service.js +107 -0
  12. package/dist/channels/agentcore.d.ts +22 -20
  13. package/dist/channels/agentcore.js +20 -11
  14. package/dist/channels/busy.d.ts +1 -1
  15. package/dist/channels/busy.js +1 -1
  16. package/dist/channels/control.d.ts +55 -7
  17. package/dist/channels/control.js +312 -67
  18. package/dist/channels/discover.d.ts +59 -0
  19. package/dist/{engines/pi/channel.js → channels/discover.js} +19 -34
  20. package/dist/channels/feishu/card.js +1 -1
  21. package/dist/channels/feishu/context-buffer.d.ts +1 -1
  22. package/dist/channels/feishu/context-buffer.js +4 -4
  23. package/dist/channels/feishu/feishu-api.js +6 -9
  24. package/dist/channels/feishu/feishu.d.ts +1 -1
  25. package/dist/channels/feishu/feishu.js +41 -24
  26. package/dist/channels/feishu/invoke-turn.d.ts +1 -1
  27. package/dist/channels/feishu/invoke-turn.js +3 -4
  28. package/dist/channels/feishu/preview.d.ts +1 -1
  29. package/dist/channels/feishu/preview.js +7 -3
  30. package/dist/channels/feishu/ws-ingress.d.ts +1 -1
  31. package/dist/channels/github/github.d.ts +1 -1
  32. package/dist/channels/http.d.ts +11 -7
  33. package/dist/channels/http.js +1 -109
  34. package/dist/channels/kit/attachment-path.d.ts +12 -0
  35. package/dist/channels/kit/attachment-path.js +43 -0
  36. package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +9 -0
  37. package/dist/channels/{context-buffer.js → kit/context-buffer.js} +13 -2
  38. package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +14 -3
  39. package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +17 -6
  40. package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
  41. package/dist/channels/{seen.js → kit/seen.js} +1 -1
  42. package/dist/channels/kit/signature.d.ts +12 -0
  43. package/dist/channels/kit/signature.js +17 -0
  44. package/dist/channels/{state.js → kit/state.js} +4 -7
  45. package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
  46. package/dist/channels/{stop-command.js → kit/stop-command.js} +5 -5
  47. package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +1 -1
  48. package/dist/channels/{tasks.js → kit/tasks.js} +11 -5
  49. package/dist/channels/{thread-participants.js → kit/thread-participants.js} +1 -1
  50. package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
  51. package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +19 -3
  52. package/dist/channels/{turn-store.js → kit/turn-store.js} +30 -3
  53. package/dist/channels/lark/lark.d.ts +1 -1
  54. package/dist/channels/serve.d.ts +73 -0
  55. package/dist/channels/serve.js +243 -0
  56. package/dist/channels/slack/bot-auth.js +3 -14
  57. package/dist/channels/slack/context-buffer.d.ts +2 -2
  58. package/dist/channels/slack/context-buffer.js +2 -2
  59. package/dist/channels/slack/invoke-turn.d.ts +2 -2
  60. package/dist/channels/slack/invoke-turn.js +13 -6
  61. package/dist/channels/slack/onboard.js +5 -5
  62. package/dist/channels/slack/onboarding-state.d.ts +7 -3
  63. package/dist/channels/slack/onboarding-state.js +13 -20
  64. package/dist/channels/slack/parse.js +2 -2
  65. package/dist/channels/slack/preview.d.ts +1 -1
  66. package/dist/channels/slack/preview.js +11 -3
  67. package/dist/channels/slack/register-webhook.js +1 -1
  68. package/dist/channels/slack/scaffold/slack-send.ts +2 -17
  69. package/dist/channels/slack/slack-api.js +23 -55
  70. package/dist/channels/slack/slack.d.ts +1 -1
  71. package/dist/channels/slack/slack.js +20 -20
  72. package/dist/channels/slack/welcomed.js +1 -1
  73. package/dist/channels/telegram/context-buffer.d.ts +2 -2
  74. package/dist/channels/telegram/context-buffer.js +2 -2
  75. package/dist/channels/telegram/invoke-turn.d.ts +1 -1
  76. package/dist/channels/telegram/invoke-turn.js +1 -1
  77. package/dist/channels/telegram/parse.d.ts +21 -1
  78. package/dist/channels/telegram/parse.js +67 -13
  79. package/dist/channels/telegram/preview.d.ts +1 -1
  80. package/dist/channels/telegram/preview.js +6 -2
  81. package/dist/channels/telegram/scaffold/channel.ts +7 -3
  82. package/dist/channels/telegram/telegram-api.js +5 -6
  83. package/dist/channels/telegram/telegram.d.ts +3 -3
  84. package/dist/channels/telegram/telegram.js +60 -72
  85. package/dist/channels/telegram/turn-store.d.ts +2 -2
  86. package/dist/channels/telegram/turn-store.js +2 -2
  87. package/dist/cli/add-slack.js +6 -5
  88. package/dist/cli/commands/add.js +1 -5
  89. package/dist/cli/commands/attach.d.ts +2 -4
  90. package/dist/cli/commands/attach.js +17 -15
  91. package/dist/cli/commands/deploy.d.ts +50 -5
  92. package/dist/cli/commands/deploy.js +183 -134
  93. package/dist/cli/commands/dev.js +25 -40
  94. package/dist/cli/commands/fire.js +1 -1
  95. package/dist/cli/commands/info.js +7 -5
  96. package/dist/cli/commands/init.js +1 -2
  97. package/dist/cli/commands/schedule.js +1 -1
  98. package/dist/cli/commands/start.js +80 -121
  99. package/dist/cli/commands/tool.js +5 -7
  100. package/dist/cli/program.js +1 -1
  101. package/dist/cli/serve.d.ts +22 -70
  102. package/dist/cli/serve.js +66 -257
  103. package/dist/cli/shared.d.ts +40 -10
  104. package/dist/cli/shared.js +53 -6
  105. package/dist/core.d.ts +3 -5
  106. package/dist/core.js +7 -5
  107. package/dist/deploy/agentcore/logs.d.ts +2 -2
  108. package/dist/deploy/agentcore/logs.js +2 -2
  109. package/dist/deploy/agentcore/plan.d.ts +12 -10
  110. package/dist/deploy/agentcore/plan.js +39 -31
  111. package/dist/deploy/agentcore/run.d.ts +5 -4
  112. package/dist/deploy/agentcore/run.js +13 -36
  113. package/dist/deploy/channel-ingress.d.ts +73 -0
  114. package/dist/deploy/channel-ingress.js +101 -0
  115. package/dist/deploy/docker/plan.d.ts +3 -7
  116. package/dist/deploy/docker/plan.js +4 -15
  117. package/dist/deploy/docker/run.d.ts +18 -1
  118. package/dist/deploy/docker/run.js +10 -0
  119. package/dist/deploy/fly/plan.d.ts +4 -5
  120. package/dist/deploy/fly/plan.js +7 -22
  121. package/dist/deploy/fly/run.d.ts +9 -23
  122. package/dist/deploy/fly/run.js +32 -68
  123. package/dist/deploy/preflight.d.ts +6 -7
  124. package/dist/deploy/preflight.js +27 -23
  125. package/dist/deploy/railway/plan.d.ts +4 -5
  126. package/dist/deploy/railway/plan.js +11 -26
  127. package/dist/deploy/railway/run.d.ts +9 -9
  128. package/dist/deploy/railway/run.js +40 -40
  129. package/dist/deploy/runner.js +22 -2
  130. package/dist/deploy/secrets.d.ts +18 -9
  131. package/dist/deploy/secrets.js +64 -12
  132. package/dist/dev-supervisor.js +11 -6
  133. package/dist/engines/pi/agent-session-factory.d.ts +104 -0
  134. package/dist/engines/pi/agent-session-factory.js +295 -0
  135. package/dist/engines/pi/auth.js +43 -43
  136. package/dist/engines/pi/config.d.ts +5 -5
  137. package/dist/engines/pi/config.js +1 -1
  138. package/dist/engines/pi/create.d.ts +67 -42
  139. package/dist/engines/pi/create.js +142 -72
  140. package/dist/engines/pi/definition.d.ts +42 -8
  141. package/dist/engines/pi/definition.js +114 -11
  142. package/dist/engines/pi/invoke-session.d.ts +22 -19
  143. package/dist/engines/pi/invoke-session.js +286 -100
  144. package/dist/engines/pi/login.js +39 -20
  145. package/dist/engines/pi/models.d.ts +14 -0
  146. package/dist/engines/pi/models.js +9 -1
  147. package/dist/engines/pi/open.d.ts +10 -8
  148. package/dist/engines/pi/open.js +21 -11
  149. package/dist/engines/pi/report.d.ts +0 -4
  150. package/dist/engines/pi/report.js +0 -7
  151. package/dist/engines/pi/search-tools.js +1 -1
  152. package/dist/engines/pi/service.d.ts +23 -0
  153. package/dist/engines/pi/service.js +27 -0
  154. package/dist/engines/pi/session-builder.js +87 -118
  155. package/dist/engines/pi/session-control.d.ts +28 -14
  156. package/dist/engines/pi/session-control.js +570 -438
  157. package/dist/engines/pi/session-inheritance.d.ts +30 -0
  158. package/dist/engines/pi/session-inheritance.js +261 -0
  159. package/dist/engines/pi/session-markers.d.ts +48 -0
  160. package/dist/engines/pi/session-markers.js +59 -0
  161. package/dist/engines/pi/session-settings.d.ts +15 -3
  162. package/dist/engines/pi/session-settings.js +22 -1
  163. package/dist/engines/pi/session-store.d.ts +140 -0
  164. package/dist/engines/pi/session-store.js +666 -0
  165. package/dist/engines/pi/tool-context.d.ts +36 -13
  166. package/dist/engines/pi/tool-context.js +51 -4
  167. package/dist/engines/pi/tool.d.ts +5 -5
  168. package/dist/engines/pi/tool.js +3 -3
  169. package/dist/engines/pi/turn-kit.d.ts +32 -6
  170. package/dist/engines/pi/turn-kit.js +28 -1
  171. package/dist/engines/pi/wake-tool.js +1 -1
  172. package/dist/env.js +1 -2
  173. package/dist/index.d.ts +1 -0
  174. package/dist/index.js +1 -0
  175. package/dist/loader.d.ts +49 -7
  176. package/dist/loader.js +79 -18
  177. package/dist/log.d.ts +3 -0
  178. package/dist/log.js +5 -1
  179. package/dist/node.d.ts +16 -0
  180. package/dist/node.js +20 -0
  181. package/dist/paths.d.ts +26 -0
  182. package/dist/paths.js +37 -2
  183. package/dist/pi.d.ts +14 -8
  184. package/dist/pi.js +6 -5
  185. package/dist/scaffold/add-channel.js +2 -2
  186. package/dist/scaffold/init.js +7 -2
  187. package/dist/scaffold/templates/persona.md +2 -2
  188. package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
  189. package/dist/schedule/discover.js +3 -15
  190. package/dist/schedule/state.js +4 -9
  191. package/dist/schedule/wake-alarm.d.ts +13 -7
  192. package/dist/schedule/wake-alarm.js +88 -46
  193. package/dist/schedule/wakeups.d.ts +2 -2
  194. package/dist/schedule/wakeups.js +11 -8
  195. package/dist/service.d.ts +157 -0
  196. package/dist/service.js +376 -0
  197. package/dist/session-remote.d.ts +8 -3
  198. package/dist/session-remote.js +162 -92
  199. package/dist/session.d.ts +229 -93
  200. package/dist/session.js +61 -23
  201. package/dist/telegram.d.ts +1 -1
  202. package/dist/telegram.js +1 -1
  203. package/dist/tunnel.d.ts +18 -3
  204. package/dist/tunnel.js +32 -40
  205. package/package.json +14 -5
  206. package/dist/engines/pi/channel.d.ts +0 -37
  207. package/dist/engines/pi/harness.d.ts +0 -115
  208. package/dist/engines/pi/harness.js +0 -154
  209. package/dist/engines/pi/invoke.d.ts +0 -60
  210. package/dist/engines/pi/invoke.js +0 -459
  211. package/dist/engines/pi/read-image.d.ts +0 -4
  212. package/dist/engines/pi/read-image.js +0 -62
  213. package/dist/engines/pi/sessions.d.ts +0 -60
  214. package/dist/engines/pi/sessions.js +0 -464
  215. package/dist/host/node.js +0 -52
  216. /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
  217. /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
  218. /package/dist/channels/{state.d.ts → kit/state.d.ts} +0 -0
  219. /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
  220. /package/dist/channels/{text.js → kit/text.js} +0 -0
  221. /package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +0 -0
  222. /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
package/README.md CHANGED
@@ -157,21 +157,39 @@ const agent = createPiAgent({
157
157
 
158
158
  The root export intentionally contains the supported surface only.
159
159
 
160
+ Engine-neutral and runtime-neutral are different properties, and the entries are layered by them —
161
+ each layer drops one, and every layer's dependency list is asserted in CI:
162
+
163
+ | | engine-neutral | runtime-neutral | costs |
164
+ |---|---|---|---|
165
+ | `/core`, `/session` | yes | yes | nothing |
166
+ | `/node` | yes | no (filesystem, clock, environment) | `@hono/node-server`, `croner` |
167
+ | `/pi` | no | no | the pi runtime |
168
+
169
+ `Provider` and `ProviderAuth` are exported as types because our options name them; the factory that
170
+ builds one, `createProvider`, comes from `@earendil-works/pi-ai` — add it as a direct dependency when
171
+ you register a custom provider.
172
+
160
173
  | Area | Examples | Stability |
161
174
  |---|---|---|
162
175
  | Contract | `Agent`, `AgentEvent`, `collect` | Stable within SPEC v0.1 |
163
- | Channels/host | `createInvokeHandler`, `nodeListener`, `serveNode`, `router`, `Routes` | Reference implementation, pre-1.0 |
176
+ | Directory → service | `createAgentService` (pi opener), `mountAgentService` + `MountableAgent` (neutral assembly), `AgentService` | The supported way to mount an agent directory in an app |
177
+ | Mounting | `createInvokeHandler`, `Routes`, `ChannelHandler` | Reference implementation, pre-1.0 |
178
+ | Node binding | `nodeListener`, `serveNode` (from `/node`) | The one runtime-specific piece; see below |
164
179
  | pi assembly | `createPiAgentFromDir`, `createPiAgentFromDefinition`, `createPiAgent` | Usable now, may tighten before 1.0 |
165
- | Tool/channel authoring | `defineTool`, `z`, `loadTools`, `loadChannels`, `ChannelModule` | Usable now, may tighten before 1.0 |
166
- | Injection ports | `PiSessionStore`, `inMemorySessionStore`, `jsonlSessionStore`, `Lease`, `Provider`, `createProvider` | Public because options reference them |
167
- | Not exported | L0 harness adapter, pi harness factory, prompt/config internals | Internal modules; no compatibility promise |
180
+ | Tool/channel authoring | `defineTool`, `z`, `defineSchedule`, `ChannelModule` | Usable now, may tighten before 1.0 |
181
+ | Injection ports | `PiSessionRecordStore`, `piSessionRecordStore`, `piInMemorySessionRecordStore`, `Lease`, `Provider` | Public because options reference them |
182
+ | Not exported | The assembly's parts `router`, `createControlPlane`, `loadTools`/`loadChannels`/`loadSchedules`, `createScheduler` — and prompt/config internals | `createAgentService` does this; no compatibility promise |
168
183
 
169
- Subpath exports:
184
+ Subpath entry points (`./package.json` is also exported, for tools that read the version):
170
185
 
171
- - `@fastagent-sh/fastagent/core` — engine-neutral contract, consumption helpers, channel/host kit, schedules;
186
+ - `@fastagent-sh/fastagent/core` — engine-neutral contract, consumption helpers, channel kit, schedules, and the session-control clients (`connectSessionControl`, `connectAgent`). **Zero third-party dependencies**, enforced by test;
187
+ - `@fastagent-sh/fastagent/node` — the engine-neutral pieces that need a Node runtime: `mountAgentService` (the assembly), `serveNode` / `nodeListener` (the `node:http` ↔ Fetch binding);
188
+ - `@fastagent-sh/fastagent/session` — the engine-neutral session-control contract (types and error codes);
172
189
  - `@fastagent-sh/fastagent/pi` — the pi reference implementation;
173
190
  - `@fastagent-sh/fastagent/github` — GitHub webhook channel;
174
191
  - `@fastagent-sh/fastagent/telegram` — Telegram bot channel;
192
+ - `@fastagent-sh/fastagent/slack` — Slack Events API bot channel;
175
193
  - `@fastagent-sh/fastagent/feishu` — canonical Feishu bot channel (飞书, open.feishu.cn);
176
194
  - `@fastagent-sh/fastagent/lark` — Lark-international compatibility profile over the Feishu engine.
177
195
 
@@ -196,7 +214,7 @@ FastAgent is pre-1.0. The stable design center is the Agent Handler contract in
196
214
  The neutral contract leaves room for capabilities that are not complete product features yet:
197
215
 
198
216
  - **Durable execution**: Telegram, Slack, and Feishu/Lark accepted turns replay at least once today; general durability and exactly-once execution remain future backend work.
199
- - **Sandboxed execution** — `ExecutionEnv` governs the default coding tools, but ② project context and author-written `tools/` still reach the local process; a complete sandbox adapter is future work.
217
+ - **Sandboxed execution** — all seven coding tools, ② project context, channels, and author-written `tools/` reach the local process directly; a sandbox around the complete agent process is future work.
200
218
  - **Observability export** — leveled logs and per-turn traces exist today; an OpenTelemetry exporter does not.
201
219
  - **More harness bindings and channels** — pi is the built-in harness; another harness can implement the Agent contract, and community channels can use the channel kit.
202
220
  - **More deploy targets** — local Docker, Fly, Railway, and AWS Bedrock AgentCore ship today; the generated container is the portable path for other hosts.
package/dist/agent.d.ts CHANGED
@@ -86,7 +86,7 @@ export type AgentEvent = {
86
86
  export declare const SESSION_BUSY_CODE = "session_busy";
87
87
  /**
88
88
  * The `failed.code` set when a run was DELIBERATELY stopped — a control-plane abort, or any
89
- * harness-level abort the engine attributes (`stopReason: "aborted"`) — rather than failing on its
89
+ * engine-level abort it attributes (`stopReason: "aborted"`) — rather than failing on its
90
90
  * own. Channels can render cancellation distinctly from an error, and MUST treat it as a settled
91
91
  * outcome — durable turn-intent cleanup included — so a deliberate stop is never replayed as a
92
92
  * fresh turn on restart. Exported as a constant for the same reason as {@link SESSION_BUSY_CODE}:
package/dist/agent.js CHANGED
@@ -14,7 +14,7 @@
14
14
  export const SESSION_BUSY_CODE = "session_busy";
15
15
  /**
16
16
  * The `failed.code` set when a run was DELIBERATELY stopped — a control-plane abort, or any
17
- * harness-level abort the engine attributes (`stopReason: "aborted"`) — rather than failing on its
17
+ * engine-level abort it attributes (`stopReason: "aborted"`) — rather than failing on its
18
18
  * own. Channels can render cancellation distinctly from an error, and MUST treat it as a settled
19
19
  * outcome — durable turn-intent cleanup included — so a deliberate stop is never replayed as a
20
20
  * fresh turn on restart. Exported as a constant for the same reason as {@link SESSION_BUSY_CODE}:
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Write a file so a reader sees the whole thing or nothing: same-directory temp, then rename.
3
+ *
4
+ * The temp name is fixed (`<path>.tmp`). It holds wherever one process writes one state root: a
5
+ * deployment runs one container, `dev`'s supervisor respawns its worker only after the old one has
6
+ * EXITED (dev-supervisor.ts), and these writes are synchronous, so an exited process has none in
7
+ * flight. Slack's onboarding state is the one file with two writers — `add slack`, and the
8
+ * config-token rotation inside `--tunnel` webhook registration — so a second terminal running
9
+ * `add slack --replace-config` can overlap a live `dev --tunnel`. Kept fixed anyway: that window is
10
+ * a single write at tunnel startup, its repair is the `add slack --replace-config` the registration
11
+ * failure already prints, and the fixed name is the seam several channel tests use to inject a write
12
+ * failure by occupying that path with a directory. Revisit for a writer that is neither rare nor
13
+ * self-repairing.
14
+ *
15
+ * `mode` is applied to the temp first, so the content is never briefly world-readable.
16
+ *
17
+ * The `chmod` is NOT redundant with the `mode` option: `writeFileSync` applies `mode` only when it
18
+ * CREATES the file, so a temp left behind by a crashed writer keeps its old, possibly loose
19
+ * permissions and the rename publishes them (verified: 0644 survives a `{ mode: 0o600 }` write).
20
+ * It runs on the temp, before the rename — the final path is then never observable with the wrong
21
+ * permissions, which a chmod after the rename cannot promise.
22
+ */
23
+ export declare function writeFileAtomic(path: string, data: string | Buffer, mode?: number): void;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * One spelling of "a reader sees the whole file or none of it", after five copies of it drifted
3
+ * apart: two identical, three with different temp names and different permission handling.
4
+ *
5
+ * Synchronous, and there is no async sibling: every caller either sits on a path where a KB-sized
6
+ * write must complete BEFORE a transport ACK (channel state — an ACKed delivery is not redelivered)
7
+ * or on a CLI/startup path where the cost is not observable. An async spelling would buy one of them
8
+ * nothing and cost this module a second set of rules to keep true.
9
+ */
10
+ import { chmodSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
11
+ import { dirname } from "node:path";
12
+ /**
13
+ * Write a file so a reader sees the whole thing or nothing: same-directory temp, then rename.
14
+ *
15
+ * The temp name is fixed (`<path>.tmp`). It holds wherever one process writes one state root: a
16
+ * deployment runs one container, `dev`'s supervisor respawns its worker only after the old one has
17
+ * EXITED (dev-supervisor.ts), and these writes are synchronous, so an exited process has none in
18
+ * flight. Slack's onboarding state is the one file with two writers — `add slack`, and the
19
+ * config-token rotation inside `--tunnel` webhook registration — so a second terminal running
20
+ * `add slack --replace-config` can overlap a live `dev --tunnel`. Kept fixed anyway: that window is
21
+ * a single write at tunnel startup, its repair is the `add slack --replace-config` the registration
22
+ * failure already prints, and the fixed name is the seam several channel tests use to inject a write
23
+ * failure by occupying that path with a directory. Revisit for a writer that is neither rare nor
24
+ * self-repairing.
25
+ *
26
+ * `mode` is applied to the temp first, so the content is never briefly world-readable.
27
+ *
28
+ * The `chmod` is NOT redundant with the `mode` option: `writeFileSync` applies `mode` only when it
29
+ * CREATES the file, so a temp left behind by a crashed writer keeps its old, possibly loose
30
+ * permissions and the rename publishes them (verified: 0644 survives a `{ mode: 0o600 }` write).
31
+ * It runs on the temp, before the rename — the final path is then never observable with the wrong
32
+ * permissions, which a chmod after the rename cannot promise.
33
+ */
34
+ export function writeFileAtomic(path, data, mode) {
35
+ mkdirSync(dirname(path), { recursive: true });
36
+ const tmp = `${path}.tmp`;
37
+ try {
38
+ writeFileSync(tmp, data, mode === undefined ? undefined : { mode });
39
+ if (mode !== undefined)
40
+ chmodSync(tmp, mode);
41
+ renameSync(tmp, path);
42
+ }
43
+ catch (error) {
44
+ try {
45
+ rmSync(tmp, { force: true });
46
+ }
47
+ catch {
48
+ // `force` only forgives ENOENT: if the temp is a directory, rmSync throws its own error and
49
+ // would replace the write failure that actually explains what went wrong.
50
+ }
51
+ throw error;
52
+ }
53
+ }
@@ -1,8 +1,28 @@
1
- import type { Agent } from "../agent.ts";
2
- import type { SessionControl } from "../session.ts";
3
- /** A mounted request handler (a channel's fetch, or a plain route like health). */
1
+ /**
2
+ * The Channel contract the trigger side of the product boundary (core.md §1), beside `agent.ts`
3
+ * (what an engine implements) and `session.ts` (the serving control plane). Pure types, no runtime
4
+ * dependency; importing a host, a framework, or an engine here is forbidden, as in those two.
5
+ *
6
+ * §7 fixes two module forms: a function is a route channel, an object with `connect` is a long
7
+ * connection. An agent directory ships hand-written `channels/*.ts` against them, which is why they
8
+ * live away from the code that serves them — a WebSocket ingress needs `LongConnection` and has no
9
+ * HTTP in it, and a `ChannelModule` import must not drag `node:http` in behind it.
10
+ *
11
+ * How a route table becomes a running server: `channels/serve.ts`.
12
+ */
13
+ import type { Agent } from "./agent.ts";
14
+ import type { SessionControl } from "./session.ts";
15
+ /** A mounted request handler (a channel's fetch, or a plain route like health). Fetch-shaped by
16
+ * contract (SPEC §11): the one signature every runtime and embedding app already speaks. */
4
17
  export type ChannelHandler = (req: Request) => Response | Promise<Response>;
5
- /** This deployment's HTTP surface: route key → handler. Key is `"/path"` or `"METHOD /path"`. */
18
+ /**
19
+ * This deployment's HTTP surface: route key → handler.
20
+ *
21
+ * A key is `"/path"` (any method) or `"METHOD /path"`, with a LITERAL path — small on purpose, so
22
+ * that "would these two fight over a request?" is string equality rather than a prediction about a
23
+ * matcher, and no channel silently shadows another. `assertRouteKey` in `channels/serve.ts`
24
+ * enforces it; a handler owning a prefix is a `PrefixMount`, never a key.
25
+ */
6
26
  export type Routes = Record<string, ChannelHandler>;
7
27
  /**
8
28
  * What the framework hands a channel at mount time: the assembled agent plus the resolved state ROOT
@@ -34,27 +54,3 @@ export interface LongConnectionChannelModule {
34
54
  name: string;
35
55
  connect(ctx: ChannelContext, signal: AbortSignal): LongConnection;
36
56
  }
37
- /** Parse a route key: `"METHOD /path"` → `{ method, path }`, or `"/path"` → `{ path }` (any method). */
38
- export declare function parseRouteKey(key: string): {
39
- method?: string;
40
- path: string;
41
- };
42
- /**
43
- * Compose a {@link Routes} table into one handler: exact pathname match (optionally method-qualified),
44
- * 405 when the path exists under another method, 404 otherwise. No params/wildcards.
45
- */
46
- export declare function router(routes: Routes): ChannelHandler;
47
- /**
48
- * Serve `handler` on a Node HTTP server. Thin mechanism: bind, report the port, let the caller stop
49
- * accepting or force-close active connections — no logging/signals/exit (the CLI owns those).
50
- * `listening` resolves with the bound port (useful for port 0) or rejects on a bind error.
51
- * `host` is the bind address; unset means all interfaces (what containers need).
52
- */
53
- export declare function serveNode(handler: ChannelHandler, options: {
54
- port: number;
55
- host?: string;
56
- }): {
57
- listening: Promise<number>;
58
- close: () => Promise<void>;
59
- closeAllConnections: () => void;
60
- };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,12 @@
1
+ /**
2
+ * The HOST's body ceilings, and the one place they are computed.
3
+ *
4
+ * Lambda Function URLs cap a request at 6 MB, so the forwarder cannot deliver more than that no
5
+ * matter what the adapter accepts; the body arrives base64-encoded (×4/3) inside a JSON envelope, so
6
+ * the ORIGINAL body ceiling is smaller still. This is a real capability difference from a resident
7
+ * host — the GitHub channel's own contract is 25 MiB — which is why `deploy agentcore` states it at
8
+ * plan time rather than letting an oversized payload surface as an opaque 502.
9
+ */
1
10
  /**
2
11
  * Maximum original webhook body after reserving JSON-envelope overhead and base64 expansion.
3
12
  * Enforced by both the public forwarder and the runtime adapter.
@@ -1,3 +1,12 @@
1
+ /**
2
+ * The HOST's body ceilings, and the one place they are computed.
3
+ *
4
+ * Lambda Function URLs cap a request at 6 MB, so the forwarder cannot deliver more than that no
5
+ * matter what the adapter accepts; the body arrives base64-encoded (×4/3) inside a JSON envelope, so
6
+ * the ORIGINAL body ceiling is smaller still. This is a real capability difference from a resident
7
+ * host — the GitHub channel's own contract is 25 MiB — which is why `deploy agentcore` states it at
8
+ * plan time rather than letting an oversized payload surface as an opaque 502.
9
+ */
1
10
  /** AWS Lambda Function URLs accept request payloads up to 6 MB. */
2
11
  const FUNCTION_URL_REQUEST_LIMIT = 6 * 1000 * 1000;
3
12
  /**
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The AgentCore serving assembly — the same product as `mountAgentService`, built differently
3
+ * because the host is.
4
+ *
5
+ * Two facts drive every difference. There is no public URL (the adapter's `POST /invocations` is the
6
+ * only ingress, and cron slots arrive through it from an external clock, so no resident timers), and
7
+ * **the state mount at boot is PRE-RESTORE** — empty after every version update. Discovering channels
8
+ * eagerly would therefore cache that emptiness (thread participation, delivery dedup, pending turns)
9
+ * and then clobber the restored files with it, so channels are constructed lazily on the first
10
+ * envelope. Everywhere else the state root is durable at boot and a broken channel fails startup.
11
+ *
12
+ * That is a different assembly, not a flag on the shared one: handler shape, discovery timing, clock
13
+ * source, long-connection support and shutdown all differ. What it is NOT is a different product —
14
+ * it returns the same {@link AgentService}, so `start` picks an assembly once and everything after
15
+ * that point is common.
16
+ */
17
+ import type { Agent } from "../agent.ts";
18
+ import type { Routes } from "../channel.ts";
19
+ import type { LoadedSchedule } from "../schedule/schedule.ts";
20
+ import { type AgentService, type MountableAgent } from "../service.ts";
21
+ import { type AgentcoreAdapterOptions } from "./agentcore.ts";
22
+ export interface MountAgentcoreServiceOptions {
23
+ /** Wrap the opened agent before anything binds to it (the CLI's turn trace). */
24
+ wrapAgent?: (agent: Agent) => Agent;
25
+ /** Runs once the state snapshot is restored. The wake-alarm reconcile passes through here because
26
+ * its sink is a PROCESS-global: the process entry owns that, not a service that can be closed. */
27
+ onStateReady?: () => void;
28
+ control?: {
29
+ tunnel?: boolean;
30
+ host?: string;
31
+ };
32
+ }
33
+ /** Is this process running inside the AgentCore Runtime? Set by the generated deploy artifacts. */
34
+ export declare function isAgentcoreRuntime(): boolean;
35
+ export declare function mountAgentcoreService(opened: MountableAgent, options?: MountAgentcoreServiceOptions): Promise<AgentService>;
36
+ /**
37
+ * Mount the AgentCore Runtime adapter (`POST /invocations` + `GET /ping`) — the deployed container's
38
+ * ONLY reachable surface (channels/agentcore.ts). Wired by `start` when `FASTAGENT_AGENTCORE=1` (set
39
+ * by the generated deploy artifacts, never by hand).
40
+ *
41
+ * The adapter IS the surface: the agent's channels live in a table INSIDE the envelope dispatch, a
42
+ * separate namespace from these two paths, so a channel route named `/invocations` is reached
43
+ * through the Function URL as itself and cannot shadow anything.
44
+ */
45
+ export declare function mountAgentcore(options: {
46
+ agent: Agent;
47
+ stateRoot: string;
48
+ schedules: readonly LoadedSchedule[];
49
+ onStateReady?: () => void;
50
+ /** Cancels the adapter's process-global registrations on close. */
51
+ signal?: AbortSignal;
52
+ /** The channel surface, constructed on the first envelope AFTER the state-snapshot restore — never
53
+ * at boot, where the mount is pre-restore (channels/agentcore.ts). */
54
+ channels: AgentcoreAdapterOptions["channels"];
55
+ }): Routes;
@@ -0,0 +1,107 @@
1
+ import { log } from "../log.js";
2
+ import { fireScheduleOnce } from "../schedule/scheduler.js";
3
+ import { assertNoControlPlaneCollision, mountSessionControl, routesFor, startSchedules, } from "../service.js";
4
+ import { UnknownScheduleError, agentcoreRoutes } from "./agentcore.js";
5
+ import { createStateSync } from "./agentcore-state.js";
6
+ import { activeWork } from "./busy.js";
7
+ import { router } from "./serve.js";
8
+ /** Is this process running inside the AgentCore Runtime? Set by the generated deploy artifacts. */
9
+ export function isAgentcoreRuntime() {
10
+ return process.env.FASTAGENT_AGENTCORE === "1";
11
+ }
12
+ export async function mountAgentcoreService(opened, options = {}) {
13
+ const { agentDir, workspace, stateRoot, sessionControl } = opened;
14
+ const agent = options.wrapAgent?.(opened.agent) ?? opened.agent;
15
+ // The control plane mounts over an EMPTY route surface: the lazy channels join it later, and the
16
+ // collision rule runs again then (below) against what they actually brought.
17
+ const withControl = mountSessionControl({}, sessionControl, stateRoot, { ...options.control, agent });
18
+ const scheduled = await startSchedules(agentDir, agent, stateRoot, opened.selfSchedule, {
19
+ externalClock: true,
20
+ });
21
+ const lazyChannels = async () => {
22
+ const lazy = await routesFor(agentDir, agent, stateRoot, sessionControl, { builtinInvoke: false });
23
+ if (lazy.longConnections.length > 0) {
24
+ throw new Error(`long-connection channel(s) ${lazy.longConnections.map((c) => c.name).join(", ")} cannot serve on ` +
25
+ `AgentCore (scale-to-zero severs resident connections) — use the channel's webhook form`);
26
+ }
27
+ // The SAME rule mountSessionControl applies, through the same function: its check ran against an
28
+ // empty base at boot, so it has to run again once the channels are real.
29
+ for (const plane of withControl.mounts)
30
+ assertNoControlPlaneCollision(lazy.routes, plane);
31
+ return { routes: lazy.routes, mounts: withControl.mounts };
32
+ };
33
+ // The adapter registers process-global listeners; this is what takes them down on close.
34
+ const closed = new AbortController();
35
+ const adapterRoutes = mountAgentcore({
36
+ signal: closed.signal,
37
+ agent,
38
+ stateRoot,
39
+ schedules: scheduled.schedules,
40
+ onStateReady: options.onStateReady,
41
+ channels: lazyChannels,
42
+ });
43
+ const handler = router(adapterRoutes, withControl.mounts);
44
+ log.info(`[fastagent] agentcore: serving POST /invocations + GET /ping (FASTAGENT_AGENTCORE=1)`);
45
+ let unannounce;
46
+ return {
47
+ handler,
48
+ agent,
49
+ // The adapter IS the surface here; the channel routes arrive lazily BEHIND it. Reporting `{}`
50
+ // would make the startup line claim nothing is served.
51
+ routes: adapterRoutes,
52
+ agentDir,
53
+ workspace,
54
+ // Unknown at boot by design — a channel list here would be the pre-restore emptiness.
55
+ channels: { routes: [], longConnections: [], builtinInvoke: false },
56
+ schedules: scheduled.schedules,
57
+ ready: Promise.resolve(), // nothing to open: no port of our own, no resident connections
58
+ ...(withControl.control ? { control: withControl.control } : {}),
59
+ announce(boundPort) {
60
+ unannounce = withControl.announce(boundPort);
61
+ },
62
+ async close() {
63
+ // UNTESTED, deliberately noted: no test observes these timers being cleared. Installing fake
64
+ // timers early enough to count them deadlocks the assembly's own IO. What IS tested is that
65
+ // close() runs and is idempotent; the stop itself rides on scheduler.stop()'s own tests.
66
+ scheduled.stop();
67
+ closed.abort();
68
+ unannounce?.(); // a stale discovery file would point `attach` at a stopped service
69
+ },
70
+ };
71
+ }
72
+ /**
73
+ * Mount the AgentCore Runtime adapter (`POST /invocations` + `GET /ping`) — the deployed container's
74
+ * ONLY reachable surface (channels/agentcore.ts). Wired by `start` when `FASTAGENT_AGENTCORE=1` (set
75
+ * by the generated deploy artifacts, never by hand).
76
+ *
77
+ * The adapter IS the surface: the agent's channels live in a table INSIDE the envelope dispatch, a
78
+ * separate namespace from these two paths, so a channel route named `/invocations` is reached
79
+ * through the Function URL as itself and cannot shadow anything.
80
+ */
81
+ export function mountAgentcore(options) {
82
+ const { agent, stateRoot, schedules, onStateReady, channels, signal } = options;
83
+ return agentcoreRoutes({
84
+ channels,
85
+ agent,
86
+ stateRoot,
87
+ isBusy: () => activeWork() > 0,
88
+ // Cross-deploy durability: AgentCore wipes the state mount on every runtime version update, so
89
+ // the state root is restored from (and pushed to) an S3 snapshot through presigned URLs the
90
+ // forwarder mints per envelope. Always wired on this path — the platform gives no other way to
91
+ // keep an agent's memory across a deploy.
92
+ stateSync: createStateSync({ stateRoot }),
93
+ // What separates a forwarder envelope from any IAM principal's InvokeAgentRuntime call. Absent =
94
+ // no forwarder in this topology, so only the public `invoke` kind is servable.
95
+ ingressSecret: process.env.FASTAGENT_INGRESS_SECRET,
96
+ onStateReady,
97
+ ...(signal ? { signal } : {}),
98
+ fire: schedules.length === 0
99
+ ? undefined
100
+ : (name, slot) => {
101
+ const schedule = schedules.find((s) => s.name === name);
102
+ if (!schedule)
103
+ throw new UnknownScheduleError(name);
104
+ return fireScheduleOnce({ agent, stateRoot, schedule, slot });
105
+ },
106
+ });
107
+ }
@@ -1,15 +1,8 @@
1
1
  import type { Agent } from "../agent.ts";
2
2
  import type { StateSync, StateUrls } from "./agentcore-state.ts";
3
- import type { Routes } from "../host/node.ts";
3
+ import type { Routes } from "../channel.ts";
4
+ import { type PrefixMount } from "../channels/serve.ts";
4
5
  import type { ScheduleFireOutcome } from "../schedule/scheduler.ts";
5
- /**
6
- * The HOST's webhook body limit, and the one place it is computed. Lambda Function URLs cap a request
7
- * at 6 MB, so the forwarder cannot deliver more than that no matter what the adapter accepts; the
8
- * body arrives base64-encoded (×4/3) inside a JSON envelope, so the ORIGINAL body ceiling is smaller
9
- * still. This is a real capability difference from a resident host — the GitHub channel's own
10
- * contract is 25 MiB — so `deploy agentcore` says so at plan time rather than letting an oversized
11
- * payload surface as an opaque 502.
12
- */
13
6
  /** What the forwarder Lambda / EventBridge deliver in the `/invocations` payload. Every kind may
14
7
  * carry `wake` — the forwarder's self-resolved public URL, which the adapter persists so the wake
15
8
  * ALARM sink (schedule/wake-alarm.ts) can call back without the URL being baked anywhere. */
@@ -71,14 +64,21 @@ export interface WebhookReply {
71
64
  headers: Record<string, string>;
72
65
  bodyB64: string;
73
66
  }
67
+ /** What the lazy factory hands back: literal routes plus any prefix-owning mounts (the control
68
+ * plane), so the adapter's INNER dispatch is assembled exactly like a direct host's. */
69
+ export interface RouteSurface {
70
+ routes: Routes;
71
+ mounts?: readonly PrefixMount[];
72
+ }
74
73
  export interface AgentcoreAdapterOptions {
75
- /** The serving routes a direct deployment would mount (channels or the builtin invoke + health).
76
- * The serving path passes a LAZY factory: channel construction loads channel state and replays
77
- * durable turn intent, so on AgentCore it must not run until the state root is authoritative —
78
- * which happens at the first envelope's `stateSync.ready()` (the restore URLs only an envelope
79
- * carries), never at boot, where the mount is pre-restore (empty after every version update).
80
- * An eager `Routes` value remains supported for wirings whose state root is already durable. */
81
- routes: Routes | (() => Promise<Routes> | Routes);
74
+ /** The serving routes a direct deployment would mount (channels or the builtin invoke + health),
75
+ * built LAZILY: channel construction loads channel state and replays durable turn intent, so on
76
+ * AgentCore it must not run until the state root is authoritative — which happens at the first
77
+ * envelope's `stateSync.ready()` (the restore URLs only an envelope carries), never at boot, where
78
+ * the mount is pre-restore (empty after every version update). A factory, not a value: there is no
79
+ * moment during construction at which the right answer is knowable. May answer synchronously the
80
+ * resolution chain normalizes it either way. */
81
+ channels: () => Promise<RouteSurface> | RouteSurface;
82
82
  agent: Agent;
83
83
  /** Where the forwarder URL from envelopes is persisted for the wake-alarm sink (the state root). */
84
84
  stateRoot: string;
@@ -94,15 +94,17 @@ export interface AgentcoreAdapterOptions {
94
94
  /** FASTAGENT_INGRESS_SECRET: what makes an envelope the FORWARDER's rather than any IAM principal's.
95
95
  * Undefined = nothing can be trusted, so only the public `invoke` kind is served. */
96
96
  ingressSecret?: string;
97
+ /** Cancels the adapter's process-global registrations. Without it a closed adapter keeps saving
98
+ * state on every later idle edge — including work belonging to whatever mounted after it. */
99
+ signal?: AbortSignal;
97
100
  /** Runs ONCE, after the state root is authoritative (post-restore) — the wake-alarm reconcile, which
98
101
  * at boot would see the mount the platform just wiped and conclude there is nothing pending. */
99
102
  onStateReady?: () => void;
100
103
  }
101
104
  /**
102
- * Build the AgentCore serving surface: `{ "POST /invocations", "GET /ping" }`. The caller merges it
103
- * over its routes (collision-checked at the mount site, serve.ts) the inner routes stay mounted
104
- * too, which is harmless (AgentCore routes only /invocations and /ping into the container) and keeps
105
- * a local `curl` debug surface.
105
+ * Build the AgentCore serving surface: `{ "POST /invocations", "GET /ping" }` the whole of what the
106
+ * platform routes into the container. The agent's own channels are not beside these: they are a table
107
+ * inside the envelope dispatch below, reached only by unwrapping a forwarder envelope.
106
108
  */
107
109
  export declare function agentcoreRoutes(options: AgentcoreAdapterOptions): Routes;
108
110
  /** Thrown by the mount-site `fire` binding when the envelope names a schedule this workspace does
@@ -28,7 +28,7 @@
28
28
  import { Buffer } from "node:buffer";
29
29
  import { timingSafeEqual } from "node:crypto";
30
30
  import { beginWork, onIdle } from "./busy.js";
31
- import { router } from "../host/node.js";
31
+ import { router } from "../channels/serve.js";
32
32
  import { log } from "../log.js";
33
33
  import { rememberWakeAlarmUrl } from "../schedule/wake-alarm.js";
34
34
  import { readBodyCapped } from "./body.js";
@@ -48,14 +48,13 @@ function secretMatches(actual, expected) {
48
48
  return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes);
49
49
  }
50
50
  /**
51
- * Build the AgentCore serving surface: `{ "POST /invocations", "GET /ping" }`. The caller merges it
52
- * over its routes (collision-checked at the mount site, serve.ts) the inner routes stay mounted
53
- * too, which is harmless (AgentCore routes only /invocations and /ping into the container) and keeps
54
- * a local `curl` debug surface.
51
+ * Build the AgentCore serving surface: `{ "POST /invocations", "GET /ping" }` the whole of what the
52
+ * platform routes into the container. The agent's own channels are not beside these: they are a table
53
+ * inside the envelope dispatch below, reached only by unwrapping a forwarder envelope.
55
54
  */
56
55
  export function agentcoreRoutes(options) {
57
- const { routes, agent, stateRoot, isBusy, fire, stateSync, ingressSecret, onStateReady } = options;
58
- // Lazy channel construction (see AgentcoreAdapterOptions.routes) — resolved ONCE per process, on
56
+ const { channels, agent, stateRoot, isBusy, fire, stateSync, ingressSecret, onStateReady } = options;
57
+ // Lazy channel construction (see AgentcoreAdapterOptions.channels) — resolved ONCE per process, on
59
58
  // the first trusted envelope after the state root is authoritative, and the outcome is cached
60
59
  // EITHER WAY. Success: the same resident channels a direct host keeps. Failure too: construction
61
60
  // is an ACTIVATION with side effects — loadChannels builds every healthy channel (starting its
@@ -71,8 +70,8 @@ export function agentcoreRoutes(options) {
71
70
  // The factory runs INSIDE the chain: a synchronous throw must land in the cached rejection,
72
71
  // not escape before `dispatchP` is assigned (which would silently re-run the activation).
73
72
  dispatchP = Promise.resolve()
74
- .then(() => (typeof routes === "function" ? routes() : routes))
75
- .then(router);
73
+ .then(channels)
74
+ .then((surface) => router(surface.routes, surface.mounts));
76
75
  dispatchP.catch(() => { }); // observed here so the CACHED rejection is never "unhandled"
77
76
  }
78
77
  return dispatchP;
@@ -80,8 +79,10 @@ export function agentcoreRoutes(options) {
80
79
  const invokeHandler = createInvokeHandler(agent);
81
80
  // Snapshot on the 0-in-flight edge: webhook channels ACK fast and finish the turn in the
82
81
  // background, so "the request returned" is NOT when the state root settles.
83
- if (stateSync)
84
- onIdle(() => stateSync.save());
82
+ if (stateSync) {
83
+ const off = onIdle(() => stateSync.save());
84
+ options.signal?.addEventListener("abort", off, { once: true });
85
+ }
85
86
  let warnedUnsnapshotted = false;
86
87
  let stateReadyFired = false;
87
88
  const handleInvocation = async (req) => {
@@ -299,6 +300,14 @@ export function agentcoreRoutes(options) {
299
300
  };
300
301
  // Settle-then-snapshot: when the envelope leaves nothing in flight, its writes are final now (a
301
302
  // background turn instead reports through the idle edge above).
303
+ //
304
+ // A public `invoke` is the one kind this MISSES, measurably: it answers with an unconsumed SSE
305
+ // stream, so the turn runs after this line and its records wait for the next envelope's snapshot.
306
+ // Counting it as in-flight work would close that and cost more than it buys — the stream drains at
307
+ // the CLIENT's pace, so one parked reader pins /ping at HealthyBusy and defeats the idle reclaim,
308
+ // which is the quota-exhaustion failure the ping contract below warns about. The kinds that carry
309
+ // conversations (webhook, schedule-fire, wake-poke) all run their turn inside the request or
310
+ // through `beginWork`, so they land on one of the two edges; `invoke` is the direct/debug door.
302
311
  const invocations = async (req) => {
303
312
  const response = await handleInvocation(req);
304
313
  if (stateSync && !isBusy())
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * SHARED process-wide in-flight work signal. Channels ACK a webhook fast and run the turn
3
- * fire-and-forget on this process's event loop (host/node.ts) — so "is this process busy?" is not
3
+ * fire-and-forget on this process's event loop (serve.ts) — so "is this process busy?" is not
4
4
  * derivable from open HTTP requests. The two shared execution primitives (turn-queue chains,
5
5
  * task-tracker side tasks) report here; a serving surface that must stay alive while background
6
6
  * work runs (the AgentCore adapter's /ping → HealthyBusy) reads it.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * SHARED process-wide in-flight work signal. Channels ACK a webhook fast and run the turn
3
- * fire-and-forget on this process's event loop (host/node.ts) — so "is this process busy?" is not
3
+ * fire-and-forget on this process's event loop (serve.ts) — so "is this process busy?" is not
4
4
  * derivable from open HTTP requests. The two shared execution primitives (turn-queue chains,
5
5
  * task-tracker side tasks) report here; a serving surface that must stay alive while background
6
6
  * work runs (the AgentCore adapter's /ping → HealthyBusy) reads it.