talon-agent 5.0.0 → 5.0.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 (127) hide show
  1. package/package.json +1 -1
  2. package/src/app.ts +3 -3
  3. package/src/backend/claude-sdk/factory.ts +21 -41
  4. package/src/backend/claude-sdk/host/in-process.ts +147 -0
  5. package/src/backend/codex/mcp-config.ts +1 -1
  6. package/src/backend/openai-agents/mcp-pool.ts +1 -1
  7. package/src/backend/runtime/index.ts +2 -2
  8. package/src/backend/runtime/turn/delivery.ts +1 -1
  9. package/src/backend/runtime/turn/turn-phases.ts +1 -1
  10. package/src/bootstrap.ts +2 -2
  11. package/src/cli.ts +5 -2
  12. package/src/core/agent-runtime/README.md +15 -0
  13. package/src/core/agent-runtime/agent-host.ts +387 -0
  14. package/src/core/background/cron/scheduler.ts +1 -1
  15. package/src/core/background/triggers/command.ts +1 -1
  16. package/src/{util → core/config}/harden.ts +1 -1
  17. package/src/core/config/index.ts +1 -1
  18. package/src/core/daemon/pidfile.ts +1 -1
  19. package/src/core/daemon/resource-sampler.ts +2 -2
  20. package/src/{util → core/daemon}/respawn.ts +1 -1
  21. package/src/core/doctor/index.ts +1 -1
  22. package/src/core/engine/gateway-actions/fetch-url.ts +1 -1
  23. package/src/core/engine/gateway-actions/index.ts +1 -1
  24. package/src/core/engine/gateway-actions/memory.ts +2 -2
  25. package/src/core/engine/gateway-actions/native/exec-background.ts +129 -0
  26. package/src/core/engine/gateway-actions/native/exec-remote.ts +85 -0
  27. package/src/core/engine/gateway-actions/native/exec.ts +182 -0
  28. package/src/core/engine/gateway-actions/native/index.ts +46 -0
  29. package/src/core/engine/gateway-actions/native/params.ts +46 -0
  30. package/src/core/engine/gateway-actions/native/read.ts +182 -0
  31. package/src/core/engine/gateway-actions/native/results.ts +34 -0
  32. package/src/core/engine/gateway-actions/native/search.ts +212 -0
  33. package/src/core/engine/gateway-actions/native/shell.ts +39 -0
  34. package/src/core/engine/gateway-actions/native/teleport.ts +55 -0
  35. package/src/core/engine/gateway-actions/native/write.ts +170 -0
  36. package/src/core/frontend-runtime/builtins.ts +2 -2
  37. package/src/core/mcp-hub/index.ts +1 -1
  38. package/src/core/mcp-hub/talon-server.ts +1 -1
  39. package/src/core/memory/taps.ts +1 -1
  40. package/src/core/plugin/mcp.ts +1 -1
  41. package/src/core/scripts/lua.ts +1 -1
  42. package/src/core/tools/{chat.ts → chat/chat.ts} +1 -1
  43. package/src/core/tools/{cross-send.ts → chat/cross-send.ts} +1 -1
  44. package/src/core/tools/{history.ts → chat/history.ts} +2 -2
  45. package/src/core/tools/{media.ts → chat/media.ts} +1 -1
  46. package/src/core/tools/{members.ts → chat/members.ts} +2 -2
  47. package/src/core/tools/{messaging.ts → chat/messaging.ts} +2 -2
  48. package/src/core/tools/{moderation.ts → chat/moderation.ts} +2 -2
  49. package/src/core/tools/{stickers.ts → chat/stickers.ts} +2 -2
  50. package/src/core/tools/{whatsapp.ts → chat/whatsapp.ts} +1 -1
  51. package/src/core/tools/{memory.ts → content/memory.ts} +2 -2
  52. package/src/core/tools/{web.ts → content/web.ts} +1 -1
  53. package/src/core/tools/index.ts +20 -20
  54. package/src/core/tools/{admin.ts → ops/admin.ts} +1 -1
  55. package/src/core/tools/{bridge.ts → ops/bridge.ts} +2 -2
  56. package/src/core/tools/{goals.ts → ops/goals.ts} +2 -2
  57. package/src/core/tools/{mesh.ts → ops/mesh.ts} +1 -1
  58. package/src/core/tools/{models.ts → ops/models.ts} +1 -1
  59. package/src/core/tools/{native.ts → ops/native.ts} +1 -1
  60. package/src/core/tools/{scheduling.ts → ops/scheduling.ts} +1 -1
  61. package/src/core/tools/{scripts.ts → ops/scripts.ts} +1 -1
  62. package/src/core/tools/{skills.ts → ops/skills.ts} +1 -1
  63. package/src/core/tools/{triggers.ts → ops/triggers.ts} +1 -1
  64. package/src/{util → core/vfs}/workspace.ts +2 -2
  65. package/src/frontend/discord/callbacks/components/effort.ts +1 -1
  66. package/src/frontend/discord/callbacks/components/index.ts +1 -1
  67. package/src/frontend/discord/callbacks/components/settings.ts +1 -1
  68. package/src/frontend/discord/commands/admin.ts +1 -1
  69. package/src/frontend/discord/commands/info.ts +1 -1
  70. package/src/frontend/discord/commands/interaction.ts +1 -1
  71. package/src/frontend/discord/commands/session.ts +3 -3
  72. package/src/frontend/discord/commands/settings.ts +1 -1
  73. package/src/frontend/discord/handlers/messages.ts +1 -1
  74. package/src/frontend/discord/handlers/state.ts +1 -1
  75. package/src/frontend/discord/middleware.ts +1 -1
  76. package/src/frontend/discord/ready.ts +1 -1
  77. package/src/frontend/discord/render.ts +41 -259
  78. package/src/frontend/native/chats/chats.ts +1 -1
  79. package/src/frontend/native/surface/models.ts +1 -1
  80. package/src/frontend/native/turn/context.ts +1 -1
  81. package/src/frontend/native/turn/emit.ts +1 -1
  82. package/src/frontend/{shared → presentation}/format.ts +4 -3
  83. package/src/frontend/{shared → presentation}/reasoning-levels.ts +12 -0
  84. package/src/frontend/presentation/reports.ts +493 -0
  85. package/src/frontend/{shared → presentation}/session-status.ts +1 -1
  86. package/src/frontend/teams/commands.ts +2 -2
  87. package/src/frontend/teams/turn.ts +1 -1
  88. package/src/frontend/telegram/admin/health.ts +1 -1
  89. package/src/frontend/telegram/admin/sessions.ts +1 -1
  90. package/src/frontend/telegram/callbacks/effort.ts +2 -2
  91. package/src/frontend/telegram/callbacks/metrics.ts +1 -1
  92. package/src/frontend/telegram/callbacks/model/views.ts +1 -1
  93. package/src/frontend/telegram/callbacks/query.ts +1 -1
  94. package/src/frontend/telegram/callbacks/settings.ts +2 -2
  95. package/src/frontend/telegram/commands/admin.ts +4 -4
  96. package/src/frontend/telegram/commands/info.ts +2 -2
  97. package/src/frontend/telegram/commands/session.ts +4 -4
  98. package/src/frontend/telegram/commands/settings.ts +4 -2
  99. package/src/frontend/telegram/handlers/state.ts +1 -1
  100. package/src/frontend/telegram/model-menu.ts +1 -1
  101. package/src/frontend/telegram/render/html.ts +30 -0
  102. package/src/frontend/telegram/{helpers → render}/menu.ts +46 -19
  103. package/src/frontend/telegram/render/reports.ts +182 -0
  104. package/src/frontend/terminal/builtins/context.ts +2 -2
  105. package/src/frontend/terminal/builtins/session.ts +1 -1
  106. package/src/frontend/terminal/builtins/status.ts +2 -2
  107. package/src/frontend/terminal/index.ts +2 -2
  108. package/src/frontend/terminal/renderer.ts +1 -1
  109. package/src/frontend/whatsapp/commands.ts +5 -5
  110. package/src/frontend/whatsapp/registry.ts +1 -1
  111. package/src/index.ts +5 -2
  112. package/src/util/runtime.ts +1 -1
  113. package/src/core/engine/gateway-actions/native.ts +0 -1035
  114. package/src/frontend/telegram/helpers/diagnostics.ts +0 -428
  115. package/src/frontend/telegram/helpers/format.ts +0 -42
  116. package/src/frontend/telegram/helpers/index.ts +0 -13
  117. package/src/util/cleanup-registry.ts +0 -36
  118. /package/src/{util → core/daemon}/boot-timer.ts +0 -0
  119. /package/src/{util → core/frontend-runtime}/chat-id.ts +0 -0
  120. /package/src/{util/mcp-launcher.ts → core/mcp-hub/launcher.ts} +0 -0
  121. /package/src/{util → core/tools/content}/web-content.ts +0 -0
  122. /package/src/core/tools/{mcp-env.ts → ops/mcp-env.ts} +0 -0
  123. /package/src/{util → core/weaver}/session-name.ts +0 -0
  124. /package/src/frontend/{shared → presentation}/access.ts +0 -0
  125. /package/src/frontend/{shared → presentation}/model-commands.ts +0 -0
  126. /package/src/frontend/{shared → presentation}/plan-usage-report.ts +0 -0
  127. /package/src/frontend/{shared → presentation}/status-context.ts +0 -0
@@ -0,0 +1,387 @@
1
+ /**
2
+ * The agent-host seam — the contract between the daemon and the process
3
+ * that hosts the Claude Agent SDK.
4
+ *
5
+ * `docs/agent-host-sidecar.md` Phase 1. Today the SDK runs inside the
6
+ * daemon: an OOM or an SDK bug takes down every frontend with it. The
7
+ * sidecar moves it into its own process, and this file is the boundary
8
+ * that move happens across — written first, on purpose, so Phase 2 is a
9
+ * transport swap rather than a redesign.
10
+ *
11
+ * Two halves:
12
+ *
13
+ * - **The wire vocabulary.** `HostRequest` / `HostReply` / `HostEvent` /
14
+ * `HostNotice` are the NDJSON messages of the design's protocol
15
+ * table, one TypeScript type per row, plus `parseHostMessage` /
16
+ * `serializeHostMessage` — the codec both sides run, replayed against
17
+ * `protocol/fixtures/agent-host_v1.json` in
18
+ * `src/__tests__/agent-host-protocol.test.ts`.
19
+ * - **The client.** `AgentHostClient` is what the daemon holds. Phase 1
20
+ * ships one implementation (`backend/claude-sdk/host/in-process.ts`,
21
+ * a direct call-through); Phase 2 adds a second that speaks the
22
+ * messages above over a child process's stdio. The `Backend` object
23
+ * the rest of the daemon sees is identical either way.
24
+ *
25
+ * Layering: the interface and the codec live in `core/` because `core/`
26
+ * may not import `backend/` (depcruise `core-not-to-backend`). Every
27
+ * implementation lives under `backend/`.
28
+ *
29
+ * Wire types vs client types
30
+ * ──────────────────────────
31
+ * They are deliberately not the same types. Two client arguments do not
32
+ * survive a process boundary and the wire shapes say so:
33
+ *
34
+ * - `OneShotAgentParams` carries an `AbortController` and an
35
+ * `appendLog` callback. `HostOneShotParams` is the serialisable
36
+ * subset; Phase 2 maps `appendLog` onto `log` notices and the
37
+ * abort onto an `interrupt`-shaped request.
38
+ * - `hello.config` is the `claude-sdk` slice of `TalonConfig` as
39
+ * JSON. The in-process client takes the real `TalonConfig` object.
40
+ *
41
+ * Forward compatibility: unknown fields on a known message type are
42
+ * additive evolution and must be preserved, never rejected. An unknown
43
+ * `type` parses to `HostUnknown`, which callers log and drop — the codec
44
+ * never throws, so one bad line can't kill a turn or the loop reading it.
45
+ */
46
+
47
+ import type { AgentEvent, AgentError } from "./events.js";
48
+ import type { ChatRunParams, PlanUsage } from "./capabilities.js";
49
+ import type {
50
+ OneShotAgentParams,
51
+ OneShotUsage,
52
+ ReasoningEffortLevel,
53
+ UnifiedModelInfo,
54
+ } from "../types.js";
55
+
56
+ /** Wire-format version. Bump only on a breaking change — prefer additive. */
57
+ export const AGENT_HOST_PROTOCOL_VERSION = 1;
58
+
59
+ // ── Shared payload shapes ───────────────────────────────────────────────────
60
+
61
+ /**
62
+ * The serialisable half of `OneShotAgentParams`. `abortController` and
63
+ * `appendLog` are host-local concerns (see the file header); everything
64
+ * else is exactly what a background run needs.
65
+ *
66
+ * Unexported on purpose — it is reachable as
67
+ * `Extract<HostRequest, { type: "one_shot" }>["params"]`, and a second
68
+ * name for the same shape is a thing to keep in sync for nothing.
69
+ */
70
+ interface HostOneShotParams {
71
+ prompt: string;
72
+ systemPrompt: string;
73
+ workspace: string;
74
+ model: string;
75
+ reasoningEffort?: ReasoningEffortLevel;
76
+ contextLabel: string;
77
+ }
78
+
79
+ /**
80
+ * The MCP diff `set_mcp_servers` / `refresh_tools` answer with — the same
81
+ * shape `ToolRuntime.refreshTools` returns to the dispatcher today.
82
+ */
83
+ export interface HostToolRefresh {
84
+ added: string[];
85
+ removed: string[];
86
+ errors: Record<string, string>;
87
+ }
88
+
89
+ /**
90
+ * What the host knows about one chat's session. `sessionId` is the SDK's
91
+ * resume handle; the context figures are the ones `warm_session` populates
92
+ * — which is why this query exists at all. In-process those numbers land
93
+ * in the daemon's own session store; across a process boundary they have
94
+ * to be asked for.
95
+ */
96
+ export interface HostSessionInfo {
97
+ chatId: string;
98
+ sessionId?: string;
99
+ turns: number;
100
+ contextTokens: number;
101
+ contextWindow: number;
102
+ }
103
+
104
+ /** `ready`'s payload — the handshake answer, minus the envelope. */
105
+ export interface HostReadyInfo {
106
+ protocol: number;
107
+ /** The host build's version. */
108
+ host: string;
109
+ /**
110
+ * The Claude Agent SDK version the host is running. Absent in-process,
111
+ * where there is no separately-pinned SDK to report (Phase 4 gives the
112
+ * host its own `package.json`).
113
+ */
114
+ sdk?: string;
115
+ }
116
+
117
+ // ── Daemon → host ───────────────────────────────────────────────────────────
118
+
119
+ /**
120
+ * Every request the daemon can send. Each carries an `id`; the reply
121
+ * carries the same `id`. Turn-shaped requests (`run_turn`, `one_shot`)
122
+ * additionally carry a `runId`, which every streamed `event` repeats.
123
+ */
124
+ export type HostRequest =
125
+ | {
126
+ type: "hello";
127
+ id: string;
128
+ protocol: number;
129
+ /** The daemon's version, for the host's compatibility log line. */
130
+ daemon: string;
131
+ /** The `claude-sdk` slice of `TalonConfig`, as JSON. */
132
+ config: Record<string, unknown>;
133
+ }
134
+ | { type: "run_turn"; id: string; runId: string; params: ChatRunParams }
135
+ | { type: "interrupt"; id: string; chatId: string }
136
+ | { type: "one_shot"; id: string; runId: string; params: HostOneShotParams }
137
+ | { type: "warm_session"; id: string; chatId: string }
138
+ | {
139
+ type: "set_mcp_servers";
140
+ id: string;
141
+ chatId: string;
142
+ /** SDK `McpServerConfig` map, opaque here — the host hands it to the SDK. */
143
+ servers: Record<string, unknown>;
144
+ }
145
+ | { type: "refresh_tools"; id: string; chatId: string }
146
+ | { type: "list_models"; id: string; filter?: "free" | "all" }
147
+ | { type: "plan_usage"; id: string }
148
+ | { type: "session_info"; id: string; chatId: string }
149
+ | { type: "reset_session"; id: string; chatId: string }
150
+ | { type: "shutdown"; id: string };
151
+
152
+ // ── Host → daemon ───────────────────────────────────────────────────────────
153
+
154
+ /**
155
+ * Every reply. `ok` is the generic ack, with one optional field per
156
+ * request that has something to say back; the queries get their own
157
+ * types so a reply is never ambiguous with the request that asked for it
158
+ * (`list_models` → `models`, `plan_usage` → `usage`, `session_info` →
159
+ * `session`).
160
+ */
161
+ export type HostReply =
162
+ | ({ type: "ready"; id: string } & HostReadyInfo)
163
+ | {
164
+ type: "ok";
165
+ id: string;
166
+ /** `interrupt` — a running turn was found and signalled. */
167
+ interrupted?: boolean;
168
+ /** `set_mcp_servers` / `refresh_tools` — `null` when the chat has no live query. */
169
+ tools?: HostToolRefresh | null;
170
+ /** `reset_session` — host-side per-chat state was dropped. */
171
+ cleared?: boolean;
172
+ }
173
+ | { type: "run_done"; id: string; runId: string; usage?: OneShotUsage }
174
+ | { type: "error"; id: string; error: AgentError }
175
+ | { type: "models"; id: string; models: UnifiedModelInfo[]; total: number }
176
+ | { type: "usage"; id: string; usage?: PlanUsage }
177
+ | { type: "session"; id: string; session?: HostSessionInfo }
178
+ | { type: "bye"; id: string };
179
+
180
+ /**
181
+ * One turn event. The `AgentEvent` union is unchanged and unwrapped —
182
+ * that is the whole point of the seam: the daemon's consumers switch on
183
+ * `event.type` exactly as they do against an in-process backend.
184
+ */
185
+ export interface HostEvent {
186
+ type: "event";
187
+ runId: string;
188
+ event: AgentEvent;
189
+ }
190
+
191
+ /**
192
+ * Unsolicited host → daemon traffic, carrying no `id` because nothing
193
+ * asked for it. `log` is the host's stdout logging (stderr is tailed by
194
+ * the supervisor like an MCP child's); `metric` forwards the `cache.*`
195
+ * and turn counters the host records so the daemon's rollups are
196
+ * unchanged by the move.
197
+ */
198
+ export type HostNotice =
199
+ | {
200
+ type: "log";
201
+ level: "debug" | "info" | "warn" | "error";
202
+ component: string;
203
+ msg: string;
204
+ }
205
+ | { type: "metric"; name: string; value: number };
206
+
207
+ /** Anything that can appear on the wire, either direction. */
208
+ export type HostMessage = HostRequest | HostReply | HostEvent | HostNotice;
209
+
210
+ /**
211
+ * A line the codec could not place. Never thrown — returned, so the
212
+ * reader logs it and drops it. `raw` is whatever came off the wire so
213
+ * the log can say what was skipped.
214
+ */
215
+ export interface HostUnknown {
216
+ type: "unknown";
217
+ reason: "malformed_json" | "not_an_object" | "unknown_type";
218
+ raw: unknown;
219
+ }
220
+
221
+ // ── The type registry the codec discriminates on ────────────────────────────
222
+
223
+ /**
224
+ * `satisfies` rejects typos here; the `AssertNever` checks in
225
+ * `src/__tests__/agent-host-protocol.test.ts` fail to compile when a new
226
+ * member joins a union without being listed — which forces a fixture
227
+ * sample too, because the fixture test asserts these lists exactly.
228
+ */
229
+ export const HOST_REQUEST_TYPES = [
230
+ "hello",
231
+ "run_turn",
232
+ "interrupt",
233
+ "one_shot",
234
+ "warm_session",
235
+ "set_mcp_servers",
236
+ "refresh_tools",
237
+ "list_models",
238
+ "plan_usage",
239
+ "session_info",
240
+ "reset_session",
241
+ "shutdown",
242
+ ] as const satisfies readonly HostRequest["type"][];
243
+
244
+ export const HOST_REPLY_TYPES = [
245
+ "ready",
246
+ "ok",
247
+ "run_done",
248
+ "error",
249
+ "models",
250
+ "usage",
251
+ "session",
252
+ "bye",
253
+ ] as const satisfies readonly HostReply["type"][];
254
+
255
+ export const HOST_NOTICE_TYPES = [
256
+ "log",
257
+ "metric",
258
+ ] as const satisfies readonly HostNotice["type"][];
259
+
260
+ const KNOWN_TYPES: ReadonlySet<string> = new Set<string>([
261
+ ...HOST_REQUEST_TYPES,
262
+ ...HOST_REPLY_TYPES,
263
+ ...HOST_NOTICE_TYPES,
264
+ "event",
265
+ ]);
266
+
267
+ // ── Codec ───────────────────────────────────────────────────────────────────
268
+
269
+ /**
270
+ * Render one message as its NDJSON line — no trailing newline, so the
271
+ * transport owns the framing. Symmetric with `parseHostMessage`.
272
+ */
273
+ export function serializeHostMessage(message: HostMessage): string {
274
+ return JSON.stringify(message);
275
+ }
276
+
277
+ /**
278
+ * Parse one NDJSON line. Total: malformed JSON, non-objects and unknown
279
+ * `type`s all come back as `HostUnknown` rather than throwing, because a
280
+ * single bad line from a newer peer must not take down the read loop.
281
+ *
282
+ * Unknown FIELDS on a known type are preserved as-is — additive evolution
283
+ * is the protocol's normal path, and dropping them here would silently
284
+ * downgrade a message the other end meant to send.
285
+ */
286
+ export function parseHostMessage(line: string): HostMessage | HostUnknown {
287
+ let parsed: unknown;
288
+ try {
289
+ parsed = JSON.parse(line);
290
+ } catch {
291
+ return { type: "unknown", reason: "malformed_json", raw: line };
292
+ }
293
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
294
+ return { type: "unknown", reason: "not_an_object", raw: parsed };
295
+ }
296
+ const type = (parsed as { type?: unknown }).type;
297
+ if (typeof type !== "string" || !KNOWN_TYPES.has(type)) {
298
+ return { type: "unknown", reason: "unknown_type", raw: parsed };
299
+ }
300
+ return parsed as HostMessage;
301
+ }
302
+
303
+ // ── The client the daemon holds ─────────────────────────────────────────────
304
+
305
+ /**
306
+ * What the daemon calls instead of reaching into `backend/claude-sdk/`
307
+ * directly. One method per protocol-table row, with the signatures the
308
+ * claude-sdk `BackendFactory` actually uses today — no capability the
309
+ * backend does not have is invented here.
310
+ *
311
+ * Bound into the `Backend` object in Phase 1: `hello`, `runTurn`,
312
+ * `interrupt`, `runOneShot`, `warmSession`, `refreshTools`, `listModels`,
313
+ * `planUsage`. Defined-but-unbound: `setMcpServers` (the primitive
314
+ * `refreshTools` is built from), `sessionInfo` and `resetSession` (the
315
+ * Claude SDK backend exposes neither a `getSessionSnapshot` nor a
316
+ * `resetChat` slot, and Phase 1 may not change the `Backend` object), and
317
+ * `shutdown` (nothing to stop until there is a process).
318
+ */
319
+ export interface AgentHostClient {
320
+ /**
321
+ * Handshake + initialisation. In-process this is `initAgent(config)`;
322
+ * across a process it is `hello` → `ready`. Must resolve before any
323
+ * other call — model discovery happens here.
324
+ */
325
+ hello(): Promise<HostReadyInfo>;
326
+
327
+ /**
328
+ * One chat turn. The returned stream is the canonical `AgentEvent`
329
+ * sequence, `run_started` first and `completed`/`error` last, exactly
330
+ * as `ChatBackend.runChatTurn` promises.
331
+ */
332
+ runTurn(params: ChatRunParams): AsyncIterable<AgentEvent>;
333
+
334
+ /** Best-effort stop of a chat's in-flight turn. `true` when one was signalled. */
335
+ interrupt(chatId: string): Promise<boolean>;
336
+
337
+ /**
338
+ * One background run (heartbeat / dream / cron). Resolves with the
339
+ * run's usage when the SDK reports it.
340
+ *
341
+ * Callback-shaped, not a stream: `OneShotAgentParams.appendLog` is how
342
+ * the background producers write their markdown logs today, and turning
343
+ * that into an event stream would be a behaviour change, not a seam.
344
+ * The design's "stream as above" row is Phase 2's problem, and the wire
345
+ * type (`HostOneShotParams`) already records what has to give.
346
+ */
347
+ runOneShot(params: OneShotAgentParams): Promise<OneShotUsage | void>;
348
+
349
+ /** Cold-start hint: spawn a throwaway query to prime the context figures. */
350
+ warmSession(chatId: string): Promise<void>;
351
+
352
+ /**
353
+ * Install an MCP server set on the chat's live query. `null` when the
354
+ * chat has no query in flight. The primitive `refreshTools` is built
355
+ * from; the two-phase teardown lives in the host, not the caller,
356
+ * because `Query` handles do not cross a process boundary.
357
+ */
358
+ setMcpServers(
359
+ chatId: string,
360
+ servers: Record<string, unknown>,
361
+ ): Promise<HostToolRefresh | null>;
362
+
363
+ /** Re-derive the chat's MCP config from the live plugin registry. */
364
+ refreshTools(chatId: string): Promise<HostToolRefresh | null>;
365
+
366
+ /**
367
+ * The model catalog, as the host discovered it from the SDK. The other
368
+ * seven `ModelCatalog` members are pure daemon-side formatting over
369
+ * `core/models/catalog.ts`, which `hello` populates — see
370
+ * `docs/agent-host-sidecar.md` Phase 1.
371
+ */
372
+ listModels(
373
+ filter?: "free" | "all",
374
+ ): Promise<{ models: UnifiedModelInfo[]; total: number }>;
375
+
376
+ /** Subscription rate-limit windows for `/status`. */
377
+ planUsage(): Promise<PlanUsage | undefined>;
378
+
379
+ /** What the host knows about a chat's session, including the context figures. */
380
+ sessionInfo(chatId: string): Promise<HostSessionInfo | undefined>;
381
+
382
+ /** Drop the host's per-chat state. `true` when there was something to drop. */
383
+ resetSession(chatId: string): Promise<boolean>;
384
+
385
+ /** Drain in-flight turns, then stop. A no-op while the host is in-process. */
386
+ shutdown(): Promise<void>;
387
+ }
@@ -34,7 +34,7 @@ import {
34
34
  } from "../../../storage/cron.js";
35
35
  import { appendDailyLog } from "../../../storage/daily-log.js";
36
36
  import { log, logError, logWarn } from "../../../util/log.js";
37
- import { numericChatIdFor } from "../../../util/chat-id.js";
37
+ import { numericChatIdFor } from "../../frontend-runtime/chat-id.js";
38
38
  import { runJobOneShot } from "./job-oneshot.js";
39
39
  import {
40
40
  jobAllowsRun,
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { spawnSync } from "node:child_process";
8
8
  import type { Trigger } from "../../../storage/triggers.js";
9
- import { selfInvocation } from "../../../util/mcp-launcher.js";
9
+ import { selfInvocation } from "../../mcp-hub/launcher.js";
10
10
  import { LUA_RUN_SUBCOMMAND } from "../../scripts/lua.js";
11
11
 
12
12
  export function commandForLanguage(
@@ -14,7 +14,7 @@
14
14
  */
15
15
 
16
16
  import { chmodSync } from "node:fs";
17
- import { dirs, files } from "./paths.js";
17
+ import { dirs, files } from "../../util/paths.js";
18
18
 
19
19
  const OWNER_DIR = 0o700;
20
20
  const OWNER_FILE = 0o600;
@@ -2,7 +2,7 @@ import { existsSync, readFileSync, mkdirSync } from "node:fs";
2
2
  import writeFileAtomic from "write-file-atomic";
3
3
  import { z } from "zod";
4
4
  import { dirs, files as pathFiles } from "../../util/paths.js";
5
- import { hardenTalonPermissions } from "../../util/harden.js";
5
+ import { hardenTalonPermissions } from "./harden.js";
6
6
  import { setTimezone } from "../../util/time.js";
7
7
  import { BACKEND_IDS } from "../agent-runtime/model-ref.js";
8
8
  import { REASONING_LEVEL_ORDER } from "../models/reasoning-levels.js";
@@ -11,7 +11,7 @@
11
11
  * gateway may fall back to a different port on EADDRINUSE, so the
12
12
  * port is only known at runtime).
13
13
  * - Removal is guarded by pid. During a `/restart` handoff
14
- * (util/respawn.ts) the successor overwrites the file with its own
14
+ * (./respawn.ts) the successor overwrites the file with its own
15
15
  * pid *before* the dying parent finishes its graceful shutdown — an
16
16
  * unconditional unlink there would orphan the new daemon, making
17
17
  * `talon stop`/`talon restart` report "not running" and spawn
@@ -13,7 +13,7 @@
13
13
  * - `boot.total_ms` — process start → frontends listening. The figure
14
14
  * `Ready in …` already logs, kept as a distribution so successive
15
15
  * restarts can be compared instead of grepped.
16
- * - `boot.<phase>_ms` — each awaited startup phase (`util/boot-timer.ts`
16
+ * - `boot.<phase>_ms` — each awaited startup phase (`./boot-timer.ts`
17
17
  * records them), so a slow boot names its own culprit.
18
18
  * - `boot.rss_mb` / `boot.heap_mb` — what the process costs the moment
19
19
  * it is serving, before any turn has run. The floor an alternative
@@ -27,7 +27,7 @@
27
27
  * and a metric that can take the process down is worse than no metric.
28
28
  */
29
29
 
30
- import { bootPhases } from "../../util/boot-timer.js";
30
+ import { bootPhases } from "./boot-timer.js";
31
31
  import { recordHistogram } from "../../storage/metrics.js";
32
32
 
33
33
  /** One minute. Idle RSS moves slowly; a tighter loop would only add noise. */
@@ -29,7 +29,7 @@
29
29
  */
30
30
 
31
31
  import { spawn } from "node:child_process";
32
- import { log, logError } from "./log.js";
32
+ import { log, logError } from "../../util/log.js";
33
33
 
34
34
  let pendingReason: string | null = null;
35
35
 
@@ -423,7 +423,7 @@ export async function collectDoctorReport(opts: {
423
423
  // is which is the difference between "why didn't my prompt update?"
424
424
  // and a one-line answer.
425
425
  {
426
- const { promptSeedReport } = await import("../../util/workspace.js");
426
+ const { promptSeedReport } = await import("../vfs/workspace.js");
427
427
  try {
428
428
  const { tracking, edited } = promptSeedReport();
429
429
  if (tracking.length + edited.length > 0) {
@@ -18,7 +18,7 @@ import {
18
18
  isHtmlContent,
19
19
  isTextContent,
20
20
  matchesBinaryKind,
21
- } from "../../../util/web-content.js";
21
+ } from "../../tools/content/web-content.js";
22
22
  import { dirs } from "../../../util/paths.js";
23
23
  import type { SharedActionHandlers } from "./types.js";
24
24
 
@@ -43,7 +43,7 @@ import {
43
43
  whatsappAccountHandlers,
44
44
  whatsappAccountChatFreeActions,
45
45
  } from "./whatsapp-account.js";
46
- import { nativeHandlers } from "./native.js";
46
+ import { nativeHandlers } from "./native/index.js";
47
47
 
48
48
  // Null-prototype so a request `action` of "toString" / "constructor" / etc.
49
49
  // can't resolve an inherited Object.prototype method — `handlers[action]` only
@@ -42,7 +42,7 @@ import {
42
42
  type MemorySource,
43
43
  type MemoryTrust,
44
44
  } from "../../../storage/memory.js";
45
- import { chatScope } from "../../../util/chat-id.js";
45
+ import { chatScope } from "../../frontend-runtime/chat-id.js";
46
46
  import { log } from "../../../util/log.js";
47
47
  import type { ActionResult } from "../../types.js";
48
48
  import type { SharedActionHandlers } from "./types.js";
@@ -68,7 +68,7 @@ const KIND_LIST = MEMORY_KINDS.join(", ");
68
68
  * for its own principal, so that is `agent`.
69
69
  *
70
70
  * "Group" is read off the canonical chat-id grammar (`chatScope` in
71
- * util/chat-id.ts), which is the only identity a gateway action holds.
71
+ * core/frontend-runtime/chat-id.ts), which is the only identity a gateway action holds.
72
72
  * When the grammar cannot tell — Teams' `teams_chat_…` is 1:1 and group
73
73
  * alike — this fails closed to `group_chat`: over-restricting a claim
74
74
  * costs a pin, under-restricting one is a permanent prompt injection.
@@ -0,0 +1,129 @@
1
+ /**
2
+ * `native_bash` with background:true — the detached local launch.
3
+ *
4
+ * POSIX-only by contract (own process group, `kill -- -pid` to stop, survives
5
+ * a daemon restart), with a short settle window so a command that dies
6
+ * immediately still reports like a normal foreground run.
7
+ */
8
+
9
+ import { spawn } from "node:child_process";
10
+ import { closeSync, openSync } from "node:fs";
11
+ import { mkdir, readFile } from "node:fs/promises";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { renderExec, type Result } from "./results.js";
15
+
16
+ /** How long a background launch waits to catch fast failures. */
17
+ const BACKGROUND_SETTLE_MS = 1_200;
18
+ /** Where background job output lands (one log file per job). */
19
+ const BACKGROUND_LOG_DIR = join(tmpdir(), "talon-bash");
20
+
21
+ /**
22
+ * Launch a command detached from the request cycle: its own process group,
23
+ * stdout+stderr appended to a per-job log file, tool returns immediately.
24
+ * This is the sanctioned path for streaming/long-running commands (adb
25
+ * logcat, dev servers, watchers) that would otherwise burn the whole
26
+ * foreground timeout and come back "killed".
27
+ *
28
+ * A short settle window catches fast failures (typo'd binary, instant
29
+ * non-zero exit) so those still surface as a normal error instead of a
30
+ * "started" message pointing at a log with one line in it.
31
+ */
32
+ export async function bashBackground(
33
+ cmd: string,
34
+ cwd: string | undefined,
35
+ ): Promise<Result> {
36
+ // The background contract is POSIX-shaped end to end: detached process
37
+ // group, `kill -- -pid` to stop, survives daemon restarts. Windows has
38
+ // none of those (and the CI legs showed the detached writer's output not
39
+ // reaching the log) — refuse loudly with the native alternative instead
40
+ // of pretending.
41
+ if (process.platform === "win32") {
42
+ return {
43
+ ok: false,
44
+ text:
45
+ "background:true needs POSIX process groups and isn't supported on a Windows " +
46
+ "daemon host. Run it foreground with a bound command (`timeout 30 …`, `head -n 200`) " +
47
+ "or start it yourself: `powershell Start-Process -WindowStyle Hidden` with output redirected to a file.",
48
+ };
49
+ }
50
+ try {
51
+ await mkdir(BACKGROUND_LOG_DIR, { recursive: true });
52
+ } catch (err) {
53
+ return {
54
+ ok: false,
55
+ text: `Cannot create log dir ${BACKGROUND_LOG_DIR}: ${(err as Error).message}`,
56
+ };
57
+ }
58
+ const slug =
59
+ cmd
60
+ .replace(/[^a-zA-Z0-9]+/g, "-")
61
+ .replace(/^-+|-+$/g, "")
62
+ .slice(0, 40) || "job";
63
+ const logPath = join(BACKGROUND_LOG_DIR, `${Date.now()}-${slug}.log`);
64
+ let fd: number;
65
+ try {
66
+ fd = openSync(logPath, "a");
67
+ } catch (err) {
68
+ return {
69
+ ok: false,
70
+ text: `Cannot open log file ${logPath}: ${(err as Error).message}`,
71
+ };
72
+ }
73
+ // Always detached: the win32 guard above returned already, so this only
74
+ // runs on POSIX where the job gets its own process group.
75
+ const child = spawn("bash", ["-c", cmd], {
76
+ ...(cwd ? { cwd } : {}),
77
+ env: process.env,
78
+ detached: true,
79
+ stdio: ["ignore", fd, fd],
80
+ });
81
+ return new Promise((resolvePromise) => {
82
+ let settled = false;
83
+ const done = (r: Result) => {
84
+ if (settled) return;
85
+ settled = true;
86
+ try {
87
+ closeSync(fd);
88
+ } catch {
89
+ // parent's dup only; the child keeps its own copy either way
90
+ }
91
+ resolvePromise(r);
92
+ };
93
+ child.on("error", (err) =>
94
+ done({ ok: false, text: `Failed to start: ${err.message}` }),
95
+ );
96
+ // Fast failure inside the settle window → report it like a normal run.
97
+ child.on("close", (code) => {
98
+ void (async () => {
99
+ let logged = "";
100
+ try {
101
+ logged = await readFile(logPath, "utf8");
102
+ } catch {
103
+ // log unreadable — report the exit alone
104
+ }
105
+ done({
106
+ ok: (code ?? 0) === 0,
107
+ text:
108
+ `Background command exited almost immediately (exit ${code ?? 0}).\n` +
109
+ renderExec("local", `exit ${code ?? 0}`, logged, "") +
110
+ `\nFull log: ${logPath}`,
111
+ });
112
+ })();
113
+ });
114
+ setTimeout(() => {
115
+ if (settled) return;
116
+ child.unref();
117
+ done({
118
+ ok: true,
119
+ text: [
120
+ `🚀 Started in background [local] — pid ${child.pid}.`,
121
+ `Output (stdout+stderr) → ${logPath}`,
122
+ `Follow it with read/bash (e.g. \`tail -n 50 ${logPath}\`).`,
123
+ `Stop it with \`kill -- -${child.pid}\` (whole process group).`,
124
+ `Unsupervised: it keeps running until it exits or is killed — it even survives a Talon restart.`,
125
+ ].join("\n"),
126
+ });
127
+ }, BACKGROUND_SETTLE_MS);
128
+ });
129
+ }