talon-agent 5.29.0 → 5.31.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 (50) hide show
  1. package/package.json +2 -1
  2. package/prompts/system/agent-brief.md +18 -7
  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 +6 -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 +73 -2
  9. package/src/core/agents/registry.ts +47 -5
  10. package/src/core/agents/runner.ts +178 -31
  11. package/src/core/agents/scratch.ts +80 -0
  12. package/src/core/agents/trail.ts +141 -0
  13. package/src/core/agents/types.ts +29 -3
  14. package/src/core/agents/watchdog.ts +70 -0
  15. package/src/core/background/isolated-agent.ts +6 -2
  16. package/src/core/backup/plan.ts +4 -0
  17. package/src/core/config/index.ts +13 -4
  18. package/src/core/engine/gateway-actions/agents/control.ts +9 -2
  19. package/src/core/engine/gateway-actions/agents/preflight.ts +17 -1
  20. package/src/core/engine/gateway-actions/agents/report.ts +81 -24
  21. package/src/core/engine/gateway-actions/index.ts +3 -0
  22. package/src/core/mcp-hub/guest-scope.ts +3 -1
  23. package/src/core/mesh/devices/service.ts +7 -0
  24. package/src/core/mesh/links/bridge-links.ts +20 -0
  25. package/src/core/secrets/actions.ts +18 -0
  26. package/src/core/secrets/drop.ts +176 -0
  27. package/src/core/secrets/index.ts +11 -0
  28. package/src/core/secrets/service.ts +248 -0
  29. package/src/core/secrets/store.ts +100 -0
  30. package/src/core/tools/index.ts +2 -0
  31. package/src/core/tools/ops/agents.ts +26 -8
  32. package/src/core/tools/ops/secrets.ts +36 -0
  33. package/src/core/tools/types.ts +2 -1
  34. package/src/core/types.ts +7 -0
  35. package/src/frontend/discord/commands/definitions.ts +21 -0
  36. package/src/frontend/discord/commands/router.ts +3 -0
  37. package/src/frontend/discord/commands/secret.ts +26 -0
  38. package/src/frontend/native/bridge/routes/host.ts +10 -0
  39. package/src/frontend/native/bridge/routes/pre-auth.ts +82 -1
  40. package/src/frontend/native/bridge/routes/table.ts +5 -0
  41. package/src/frontend/native/bridge/server.ts +32 -4
  42. package/src/frontend/native/commands/definitions.ts +6 -0
  43. package/src/frontend/native/commands/index.ts +12 -0
  44. package/src/frontend/native/surface/handlers.ts +9 -0
  45. package/src/frontend/telegram/commands/definitions.ts +4 -0
  46. package/src/frontend/telegram/commands/index.ts +3 -0
  47. package/src/frontend/telegram/commands/secret.ts +24 -0
  48. package/src/frontend/whatsapp/commands.ts +16 -1
  49. package/src/util/log.ts +1 -0
  50. 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.31.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,22 +33,32 @@ 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
 
49
51
  {% if canSpawn %}You may spawn your own sub-agents with `spawn_agent` (current depth {{depth}}, cap {{maxDepth}}) when the work genuinely splits into independent pieces. You are then responsible for them: `wait_for_agent`, `send_to_agent`, `kill_agent`, and folding their reports into yours.{% else %}You are at the maximum sub-agent depth ({{maxDepth}}) — `spawn_agent` will be refused. Do this work yourself.{% endif %}
50
52
 
51
- ## Boundaries
53
+ {% if scratchDir %}## Scratch space
54
+
55
+ Your private temp directory is `{{scratchDir}}` — `TMPDIR` points there for
56
+ your shells and tools. Put scratch files, clones and build output there
57
+ rather than in shared `/tmp`, where other agents are working. It is deleted
58
+ when you finish successfully and kept if your run fails, so leave anything
59
+ worth keeping somewhere permanent.
60
+
61
+ {% endif %}## Boundaries
52
62
 
53
63
  - Do **not** message the user's chat directly unless the brief explicitly
54
64
  tells you to. Your report goes to the agent or chat that spawned you, and
@@ -56,5 +66,6 @@ parent: peer messages are for coordination, never a substitute for
56
66
  - You have the full background tool surface (files, shell, web, plugins, and
57
67
  the messaging tools with an explicit `chat_id`). Use it, but stay inside
58
68
  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.
69
+ - Be efficient. A run may have a wall-clock timeout, and a watchdog pings,
70
+ then ends, one that makes no tool call or output for too long; a partial
71
+ 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";
@@ -91,12 +91,17 @@ export async function runOneShotAgent(
91
91
  systemPrompt,
92
92
  ...thinkingConfig,
93
93
  cwd: workspace,
94
+ // A sub-agent's private TMPDIR (and any other per-run vars), layered
95
+ // over the daemon's environment — the SDK replaces, not merges, `env`.
96
+ ...(params.env ? { env: { ...process.env, ...params.env } } : {}),
94
97
  permissionMode: "bypassPermissions" as const,
95
98
  allowDangerouslySkipPermissions: true,
96
99
  abortController,
97
100
  ...(oneShotConfig.claudeBinary
98
101
  ? { pathToClaudeCodeExecutable: oneShotConfig.claudeBinary }
99
102
  : {}),
103
+ // Keep session transcripts: interrupted sub-agents resume from them.
104
+ settings: { ...CLAUDE_RETENTION_SETTINGS },
100
105
  mcpServers: assembleMcpServers(contextLabel),
101
106
  // Whitelist of SDK built-in tools for background contexts (heartbeat,
102
107
  // 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");
@@ -39,8 +40,11 @@ export function buildAgentSystemPrompt(args: {
39
40
  parent: AgentParent;
40
41
  depth: number;
41
42
  maxDepth: number;
43
+ /** The agent's private TMPDIR, when one was created. */
44
+ scratchDir?: string;
42
45
  }): string {
43
46
  return loadSystemTemplate("agent-brief", {
47
+ scratchDir: args.scratchDir ?? "",
44
48
  agentId: args.agentId,
45
49
  label: args.label,
46
50
  parent: describeParent(args.parent),
@@ -60,7 +64,11 @@ const PREFLIGHT_INSTRUCTION =
60
64
  "the repo (or call the run_preflight tool with cwd set to your checkout). " +
61
65
  "Push only when it is green. If it is red, fix it — or, when a failure " +
62
66
  "is genuinely out of scope, say in the PR body which step failed and why " +
63
- "you pushed anyway.";
67
+ "you pushed anyway. Make your checkout with `node scripts/worktree.mjs " +
68
+ "add /tmp/fix-<name> <branch>` (run it in an existing talon checkout): " +
69
+ "its node_modules is hardlinked from a shared store, so do NOT run " +
70
+ "`npm ci` there (~1.2 GB each). Clean up with `node scripts/worktree.mjs " +
71
+ "remove <path>`.";
64
72
 
65
73
  /** A brief that opens, updates or talks about a pull request. */
66
74
  const PR_BRIEF = /\bPRs?\b|pull[ -]requests?/i;
@@ -181,6 +189,69 @@ function usageLine(record: AgentRecord): string {
181
189
  );
182
190
  }
183
191
 
192
+ /** Human duration for prompts: "45 min" / "30s". */
193
+ function minutes(ms: number): string {
194
+ return ms >= 60_000
195
+ ? `${Math.round(ms / 60_000)} min`
196
+ : `${Math.round(ms / 1000)}s`;
197
+ }
198
+
199
+ /**
200
+ * The "what it was doing" block appended to a report when the run did not
201
+ * end `done` — so a kill or a timeout never throws the work away.
202
+ */
203
+ function renderTrail(record: AgentRecord): string {
204
+ const trail = record.trail;
205
+ if (record.state === "done" || trailIsEmpty(trail) || !trail) return "";
206
+ const parts: string[] = [];
207
+ if (trail.messages.length > 0) {
208
+ parts.push(
209
+ `Its last interim messages:\n` +
210
+ trail.messages.map((m) => `- ${m}`).join("\n"),
211
+ );
212
+ }
213
+ if (trail.notes.length > 0) {
214
+ parts.push(
215
+ `Its last progress notes:\n` +
216
+ trail.notes.map((n) => `- ${n}`).join("\n"),
217
+ );
218
+ }
219
+ if (trail.files.length > 0) {
220
+ parts.push(
221
+ `Files it wrote or edited:\n` +
222
+ trail.files.map((f) => `- ${f}`).join("\n"),
223
+ );
224
+ }
225
+ return (
226
+ `\n\n--- What it had done before it ended (run log: ` +
227
+ `${agentLogPath(record.id)}) ---\n\n${parts.join("\n\n")}`
228
+ );
229
+ }
230
+
231
+ /** Mailbox note the watchdog leaves an agent that has gone quiet. */
232
+ export function buildStallPing(idleMs: number, stallMs: number): string {
233
+ return (
234
+ `[Watchdog] No tool call or output from you for ${minutes(idleMs)}. ` +
235
+ `If you are stuck, report_result now with what you have. Your parent ` +
236
+ `is told at ${minutes(2 * stallMs)} of silence and the run is killed ` +
237
+ `at ${minutes(3 * stallMs)}.`
238
+ );
239
+ }
240
+
241
+ /** The interim note a parent gets when its agent has stalled. */
242
+ export function buildStallWarning(
243
+ record: AgentRecord,
244
+ idleMs: number,
245
+ killInMs: number,
246
+ ): string {
247
+ return (
248
+ `[Watchdog] Agent ${record.id} "${record.label}" has made no progress ` +
249
+ `(no tool call or output) for ${minutes(idleMs)}. It will be killed in ` +
250
+ `${minutes(killInMs)} unless it resumes. Use send_to_agent to nudge it, ` +
251
+ `or kill_agent to end it now.`
252
+ );
253
+ }
254
+
184
255
  /**
185
256
  * The wake prompt a parent chat receives when one of its agents settles.
186
257
  * Shaped like the trigger wake-up: a `[System: …]` header telling the model
@@ -203,7 +274,7 @@ export function buildSettlementPrompt(record: AgentRecord): string {
203
274
  `an agent you spawned earlier. Decide whether to act on it, tell the ` +
204
275
  `user, or do nothing.]\n\n` +
205
276
  `[Agent "${record.label}" (${record.id}) — ${record.state}]\n\n` +
206
- `${body}${usageLine(record)}`
277
+ `${body}${renderTrail(record)}${usageLine(record)}`
207
278
  );
208
279
  }
209
280
 
@@ -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];