talon-agent 5.29.0 → 5.30.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 (48) hide show
  1. package/package.json +2 -1
  2. package/prompts/system/agent-brief.md +9 -6
  3. package/src/backend/claude-sdk/constants.ts +22 -0
  4. package/src/backend/claude-sdk/models/discovery.ts +3 -0
  5. package/src/backend/claude-sdk/one-shot.ts +3 -1
  6. package/src/backend/claude-sdk/options.ts +7 -1
  7. package/src/core/agents/index.ts +2 -0
  8. package/src/core/agents/prompt.ts +70 -2
  9. package/src/core/agents/registry.ts +47 -5
  10. package/src/core/agents/runner.ts +169 -31
  11. package/src/core/agents/trail.ts +141 -0
  12. package/src/core/agents/types.ts +29 -3
  13. package/src/core/agents/watchdog.ts +70 -0
  14. package/src/core/background/isolated-agent.ts +6 -2
  15. package/src/core/backup/plan.ts +4 -0
  16. package/src/core/config/index.ts +13 -4
  17. package/src/core/engine/gateway-actions/agents/control.ts +9 -2
  18. package/src/core/engine/gateway-actions/agents/preflight.ts +17 -1
  19. package/src/core/engine/gateway-actions/agents/report.ts +81 -24
  20. package/src/core/engine/gateway-actions/index.ts +3 -0
  21. package/src/core/mcp-hub/guest-scope.ts +3 -1
  22. package/src/core/mesh/devices/service.ts +7 -0
  23. package/src/core/mesh/links/bridge-links.ts +20 -0
  24. package/src/core/secrets/actions.ts +18 -0
  25. package/src/core/secrets/drop.ts +176 -0
  26. package/src/core/secrets/index.ts +11 -0
  27. package/src/core/secrets/service.ts +248 -0
  28. package/src/core/secrets/store.ts +100 -0
  29. package/src/core/tools/index.ts +2 -0
  30. package/src/core/tools/ops/agents.ts +26 -8
  31. package/src/core/tools/ops/secrets.ts +36 -0
  32. package/src/core/tools/types.ts +2 -1
  33. package/src/frontend/discord/commands/definitions.ts +21 -0
  34. package/src/frontend/discord/commands/router.ts +3 -0
  35. package/src/frontend/discord/commands/secret.ts +26 -0
  36. package/src/frontend/native/bridge/routes/host.ts +10 -0
  37. package/src/frontend/native/bridge/routes/pre-auth.ts +82 -1
  38. package/src/frontend/native/bridge/routes/table.ts +5 -0
  39. package/src/frontend/native/bridge/server.ts +32 -4
  40. package/src/frontend/native/commands/definitions.ts +6 -0
  41. package/src/frontend/native/commands/index.ts +12 -0
  42. package/src/frontend/native/surface/handlers.ts +9 -0
  43. package/src/frontend/telegram/commands/definitions.ts +4 -0
  44. package/src/frontend/telegram/commands/index.ts +3 -0
  45. package/src/frontend/telegram/commands/secret.ts +24 -0
  46. package/src/frontend/whatsapp/commands.ts +16 -1
  47. package/src/util/log.ts +1 -0
  48. package/src/util/paths.ts +6 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "talon-agent",
3
- "version": "5.29.0",
3
+ "version": "5.30.0",
4
4
  "description": "Multi-frontend AI agent with full tool access, streaming, cron jobs, and plugin system",
5
5
  "author": "The Falconry",
6
6
  "license": "Apache-2.0",
@@ -91,6 +91,7 @@
91
91
  "format": "prettier --write src/ prompts/",
92
92
  "format:check": "prettier --check src/ prompts/",
93
93
  "preflight": "bash scripts/preflight.sh",
94
+ "worktree": "node scripts/worktree.mjs",
94
95
  "ci:protect": "node .github/scripts/enforce-ci-gate.mjs",
95
96
  "build:gleam": "cd native/scheduler-core && gleam build --target javascript && node embed.mjs",
96
97
  "build:zig": "node native/textops-wasm/build.mjs",
@@ -33,16 +33,18 @@ a useful result; silence is not.
33
33
  Your parent may have spawned others alongside you. `list_peers()` shows them —
34
34
  id, label, and what each was asked to do — and `message_peer(agent_id, text)`
35
35
  sends one of them a note directly, without going through your parent.
36
+ `list_peers(scope: "tree")` widens the view to every live agent working for
37
+ the same chat (your parent agent, children, cousins); any of them can be
38
+ addressed the same way, by id or by label.
36
39
 
37
40
  Use it when something you found changes _their_ work and waiting would waste
38
41
  it: a fact you both need, a dead end worth not repeating, a correction to
39
42
  something you told them earlier. Don't narrate your progress at them — a peer
40
43
  pays for every message with context it could have spent on its own job.
41
44
 
42
- You can only address peers, and they see your note at their next
43
- `check_inbox`, so it is not an interrupt. Your report still goes to your
44
- parent: peer messages are for coordination, never a substitute for
45
- `report_result`.
45
+ They see your note at their next `check_inbox`, so it is not an interrupt.
46
+ Your report still goes to your parent: peer messages are for coordination,
47
+ never a substitute for `report_result`.
46
48
 
47
49
  ## Delegating further
48
50
 
@@ -56,5 +58,6 @@ parent: peer messages are for coordination, never a substitute for
56
58
  - You have the full background tool surface (files, shell, web, plugins, and
57
59
  the messaging tools with an explicit `chat_id`). Use it, but stay inside
58
60
  the brief — you were spawned for one job.
59
- - Be efficient. You have a hard wall-clock timeout; a partial result reported
60
- in time beats a perfect one that never arrives.
61
+ - Be efficient. A run may have a wall-clock timeout, and a watchdog pings,
62
+ then ends, one that makes no tool call or output for too long; a partial
63
+ result reported in time beats a perfect one that never arrives.
@@ -47,3 +47,25 @@ export const EFFORT_MAP: Record<
47
47
 
48
48
  /** Minimum interval (ms) between streaming delta callbacks to avoid flooding frontends. */
49
49
  export const STREAM_INTERVAL = 1000;
50
+
51
+ // ── Transcript retention ───────────────────────────────────────────────────
52
+
53
+ /**
54
+ * Settings layered onto every Claude Code process Talon spawns, as the
55
+ * SDK's `settings` option (the CLI's `--settings` flag).
56
+ *
57
+ * Claude Code runs a background retention sweep (at most once a day, shared
58
+ * across every CLI process through `~/.claude/.last-cleanup`) that deletes
59
+ * session transcripts in `~/.claude/projects` older than `cleanupPeriodDays`
60
+ * — 30 days when nothing sets it. Talon resumes chats by session id, so a
61
+ * swept transcript silently breaks a long-lived chat and loses its history.
62
+ *
63
+ * The user's own `~/.claude/settings.json` only applies while user settings
64
+ * are loaded, the file parses, and HOME / CLAUDE_CONFIG_DIR match the
65
+ * user's. Setting it per run removes that dependency. The CLI rejects 0 and
66
+ * recommends a large value for long retention; 100000 days is effectively
67
+ * forever. A managed policy-tier `cleanupPeriodDays` still takes precedence.
68
+ */
69
+ export const CLAUDE_RETENTION_SETTINGS = {
70
+ cleanupPeriodDays: 100_000,
71
+ } as const;
@@ -14,6 +14,7 @@ import type { ModelInfo } from "../../../core/models/catalog.js";
14
14
  import { log, logError } from "../../../util/log.js";
15
15
  import { describeSdkModel, type SdkModelInfo } from "./parsing.js";
16
16
  import { convertSdkModels } from "./convert.js";
17
+ import { CLAUDE_RETENTION_SETTINGS } from "../constants.js";
17
18
 
18
19
  type ProbeOptions = {
19
20
  cwd?: string;
@@ -62,6 +63,8 @@ async function probeSupportedModels(
62
63
  prompt: neverYield(),
63
64
  options: {
64
65
  ...probeOptions,
66
+ // Any spawned CLI may run the daily retention sweep.
67
+ settings: { ...CLAUDE_RETENTION_SETTINGS },
65
68
  model: seedModel,
66
69
  abortController: abort,
67
70
  } as Parameters<typeof query>[0]["options"],
@@ -17,7 +17,7 @@ import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
17
17
  import type { OneShotAgentParams, OneShotUsage } from "../../core/types.js";
18
18
  import { log, logWarn } from "../../util/log.js";
19
19
  import { ALLOWED_TOOLS_BACKGROUND } from "../../core/constants.js";
20
- import { EFFORT_MAP } from "./constants.js";
20
+ import { CLAUDE_RETENTION_SETTINGS, EFFORT_MAP } from "./constants.js";
21
21
  import { buildMcpServers, buildPluginMcpServers } from "./options.js";
22
22
  import { isBackgroundToolContext } from "../../core/agents/context.js";
23
23
  import { warnIfBelowCacheMinimum } from "../runtime/cache/cache-telemetry.js";
@@ -97,6 +97,8 @@ export async function runOneShotAgent(
97
97
  ...(oneShotConfig.claudeBinary
98
98
  ? { pathToClaudeCodeExecutable: oneShotConfig.claudeBinary }
99
99
  : {}),
100
+ // Keep session transcripts: interrupted sub-agents resume from them.
101
+ settings: { ...CLAUDE_RETENTION_SETTINGS },
100
102
  mcpServers: assembleMcpServers(contextLabel),
101
103
  // Whitelist of SDK built-in tools for background contexts (heartbeat,
102
104
  // dream). Same as chat minus `Agent` — nested sub-agent dispatch from
@@ -37,7 +37,11 @@ import {
37
37
  gatewayToken,
38
38
  } from "../../core/engine/gateway-auth.js";
39
39
  import { getConfig, getBridgePort } from "./state.js";
40
- import { ALLOWED_TOOLS_CHAT, EFFORT_MAP } from "./constants.js";
40
+ import {
41
+ ALLOWED_TOOLS_CHAT,
42
+ CLAUDE_RETENTION_SETTINGS,
43
+ EFFORT_MAP,
44
+ } from "./constants.js";
41
45
  import {
42
46
  isGuestTurn,
43
47
  isGuestPluginAllowed,
@@ -482,6 +486,8 @@ export function buildSdkOptions(
482
486
  ...(config.claudeBinary
483
487
  ? { pathToClaudeCodeExecutable: config.claudeBinary }
484
488
  : {}),
489
+ // Keep session transcripts: Talon resumes chats from them.
490
+ settings: { ...CLAUDE_RETENTION_SETTINGS },
485
491
  // Whitelist of SDK built-in tools. Anything not listed (e.g. WebSearch,
486
492
  // WebFetch, Monitor, PushNotification, RemoteTrigger, Plan/Worktree/Todo
487
493
  // helpers, AskUserQuestion, ScheduleWakeup) is unavailable to the model.
@@ -21,6 +21,7 @@ export { AgentRegistry, agentRegistry } from "./registry.js";
21
21
  export {
22
22
  DEFAULT_AGENT_CAPS,
23
23
  clampTimeout,
24
+ describeTimeout,
24
25
  getAgentCaps,
25
26
  initAgents,
26
27
  interruptAgentsForRestart,
@@ -36,4 +37,5 @@ export {
36
37
  initAgentDelivery,
37
38
  } from "./delivery.js";
38
39
  export { describeParent, wantsPreflight } from "./prompt.js";
40
+ export { recordInterimMessage } from "./trail.js";
39
41
  export type { AgentCaps, AgentParent, AgentRecord } from "./types.js";
@@ -11,6 +11,7 @@ import { resolve } from "node:path";
11
11
  import { dirs } from "../../util/paths.js";
12
12
  import { loadSystemTemplate } from "../prompt/templates.js";
13
13
  import type { AgentParent, AgentRecord } from "./types.js";
14
+ import { trailIsEmpty } from "./trail.js";
14
15
 
15
16
  /** Where sub-agent run logs live: `~/.talon/workspace/logs/agents/`. */
16
17
  const AGENT_LOGS_DIR = resolve(dirs.logs, "agents");
@@ -60,7 +61,11 @@ const PREFLIGHT_INSTRUCTION =
60
61
  "the repo (or call the run_preflight tool with cwd set to your checkout). " +
61
62
  "Push only when it is green. If it is red, fix it — or, when a failure " +
62
63
  "is genuinely out of scope, say in the PR body which step failed and why " +
63
- "you pushed anyway.";
64
+ "you pushed anyway. Make your checkout with `node scripts/worktree.mjs " +
65
+ "add /tmp/fix-<name> <branch>` (run it in an existing talon checkout): " +
66
+ "its node_modules is hardlinked from a shared store, so do NOT run " +
67
+ "`npm ci` there (~1.2 GB each). Clean up with `node scripts/worktree.mjs " +
68
+ "remove <path>`.";
64
69
 
65
70
  /** A brief that opens, updates or talks about a pull request. */
66
71
  const PR_BRIEF = /\bPRs?\b|pull[ -]requests?/i;
@@ -181,6 +186,69 @@ function usageLine(record: AgentRecord): string {
181
186
  );
182
187
  }
183
188
 
189
+ /** Human duration for prompts: "45 min" / "30s". */
190
+ function minutes(ms: number): string {
191
+ return ms >= 60_000
192
+ ? `${Math.round(ms / 60_000)} min`
193
+ : `${Math.round(ms / 1000)}s`;
194
+ }
195
+
196
+ /**
197
+ * The "what it was doing" block appended to a report when the run did not
198
+ * end `done` — so a kill or a timeout never throws the work away.
199
+ */
200
+ function renderTrail(record: AgentRecord): string {
201
+ const trail = record.trail;
202
+ if (record.state === "done" || trailIsEmpty(trail) || !trail) return "";
203
+ const parts: string[] = [];
204
+ if (trail.messages.length > 0) {
205
+ parts.push(
206
+ `Its last interim messages:\n` +
207
+ trail.messages.map((m) => `- ${m}`).join("\n"),
208
+ );
209
+ }
210
+ if (trail.notes.length > 0) {
211
+ parts.push(
212
+ `Its last progress notes:\n` +
213
+ trail.notes.map((n) => `- ${n}`).join("\n"),
214
+ );
215
+ }
216
+ if (trail.files.length > 0) {
217
+ parts.push(
218
+ `Files it wrote or edited:\n` +
219
+ trail.files.map((f) => `- ${f}`).join("\n"),
220
+ );
221
+ }
222
+ return (
223
+ `\n\n--- What it had done before it ended (run log: ` +
224
+ `${agentLogPath(record.id)}) ---\n\n${parts.join("\n\n")}`
225
+ );
226
+ }
227
+
228
+ /** Mailbox note the watchdog leaves an agent that has gone quiet. */
229
+ export function buildStallPing(idleMs: number, stallMs: number): string {
230
+ return (
231
+ `[Watchdog] No tool call or output from you for ${minutes(idleMs)}. ` +
232
+ `If you are stuck, report_result now with what you have. Your parent ` +
233
+ `is told at ${minutes(2 * stallMs)} of silence and the run is killed ` +
234
+ `at ${minutes(3 * stallMs)}.`
235
+ );
236
+ }
237
+
238
+ /** The interim note a parent gets when its agent has stalled. */
239
+ export function buildStallWarning(
240
+ record: AgentRecord,
241
+ idleMs: number,
242
+ killInMs: number,
243
+ ): string {
244
+ return (
245
+ `[Watchdog] Agent ${record.id} "${record.label}" has made no progress ` +
246
+ `(no tool call or output) for ${minutes(idleMs)}. It will be killed in ` +
247
+ `${minutes(killInMs)} unless it resumes. Use send_to_agent to nudge it, ` +
248
+ `or kill_agent to end it now.`
249
+ );
250
+ }
251
+
184
252
  /**
185
253
  * The wake prompt a parent chat receives when one of its agents settles.
186
254
  * Shaped like the trigger wake-up: a `[System: …]` header telling the model
@@ -203,7 +271,7 @@ export function buildSettlementPrompt(record: AgentRecord): string {
203
271
  `an agent you spawned earlier. Decide whether to act on it, tell the ` +
204
272
  `user, or do nothing.]\n\n` +
205
273
  `[Agent "${record.label}" (${record.id}) — ${record.state}]\n\n` +
206
- `${body}${usageLine(record)}`
274
+ `${body}${renderTrail(record)}${usageLine(record)}`
207
275
  );
208
276
  }
209
277
 
@@ -22,6 +22,7 @@ import type {
22
22
  AgentRecord,
23
23
  AgentResult,
24
24
  AgentState,
25
+ AgentTrail,
25
26
  } from "./types.js";
26
27
  import type { TaskUsage } from "../tasks/types.js";
27
28
  import type { AgentSettledEvent, AgentSpawnedEvent } from "../bus/events.js";
@@ -73,6 +74,8 @@ export interface AgentSettlement {
73
74
  readonly result?: AgentResult;
74
75
  readonly error?: string;
75
76
  readonly usage?: TaskUsage;
77
+ /** What the run had been doing — see `trail.ts`. */
78
+ readonly trail?: AgentTrail;
76
79
  }
77
80
 
78
81
  export type RegisterOutcome =
@@ -559,6 +562,7 @@ export class AgentRegistry {
559
562
  if (patch.result !== undefined) record.result = patch.result;
560
563
  if (patch.error !== undefined) record.error = patch.error;
561
564
  if (patch.usage !== undefined) record.usage = patch.usage;
565
+ if (patch.trail !== undefined) record.trail = patch.trail;
562
566
 
563
567
  const settled = snapshot(entry);
564
568
  this.persist(entry);
@@ -677,11 +681,9 @@ export class AgentRegistry {
677
681
  /**
678
682
  * An agent's live **peers** — the other agents sharing its parent.
679
683
  *
680
- * This is the addressing scope for agent-to-agent messaging, and it is
681
- * deliberately narrower than "everything under the same chat". A swarm is
682
- * a set of siblings spawned for one job, so siblings are the useful unit;
683
- * widening to the whole chat tree would let an agent reach a cousin from an
684
- * unrelated piece of work it knows nothing about.
684
+ * The default view of `list_peers`: a swarm is a set of siblings spawned
685
+ * for one job, so siblings are the useful unit to show first. Messaging
686
+ * reaches the whole tree (`treeOf`).
685
687
  *
686
688
  * Live only: a settled agent has no mailbox to deliver into, and offering
687
689
  * it as a peer would only produce a delivery failure one call later.
@@ -698,6 +700,46 @@ export class AgentRegistry {
698
700
  return peers.sort((a, b) => a.createdAt - b.createdAt);
699
701
  }
700
702
 
703
+ /**
704
+ * Every other live agent in this agent's tree — the agents whose ancestry
705
+ * roots in the same chat (parent, children, siblings, cousins), oldest
706
+ * first. This is the addressing scope of `message_peer`: an agent working
707
+ * for one chat can reach any live agent working for that chat, and none
708
+ * working for another.
709
+ */
710
+ treeOf(id: string): AgentRecord[] {
711
+ const self = this.get(id);
712
+ if (!self) return [];
713
+ const root = this.rootChat(self);
714
+ if (root === null) return [];
715
+ const tree: AgentRecord[] = [];
716
+ for (const entry of this.live.values()) {
717
+ const record = snapshot(entry);
718
+ if (record.id === id) continue;
719
+ if (this.rootChat(record) === root) tree.push(record);
720
+ }
721
+ return tree.sort((a, b) => a.createdAt - b.createdAt);
722
+ }
723
+
724
+ /**
725
+ * Resolve `target` — an agent id, else an exact label — to a live agent
726
+ * in `id`'s tree. A label two live agents share is ambiguous and resolves
727
+ * to nothing, with both candidates returned so the caller can say which.
728
+ */
729
+ findInTree(
730
+ id: string,
731
+ target: string,
732
+ ):
733
+ | { readonly ok: true; readonly record: AgentRecord }
734
+ | { readonly ok: false; readonly candidates: AgentRecord[] } {
735
+ const tree = this.treeOf(id);
736
+ const byId = tree.find((record) => record.id === target);
737
+ if (byId) return { ok: true, record: byId };
738
+ const byLabel = tree.filter((record) => record.label === target);
739
+ if (byLabel.length === 1) return { ok: true, record: byLabel[0]! };
740
+ return { ok: false, candidates: byLabel };
741
+ }
742
+
701
743
  /** Live agents plus the bounded settled ring, oldest first. */
702
744
  list(): AgentRecord[] {
703
745
  const records = [...this.history];