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.
- package/package.json +2 -1
- package/prompts/system/agent-brief.md +18 -7
- package/src/backend/claude-sdk/constants.ts +22 -0
- package/src/backend/claude-sdk/models/discovery.ts +3 -0
- package/src/backend/claude-sdk/one-shot.ts +6 -1
- package/src/backend/claude-sdk/options.ts +7 -1
- package/src/core/agents/index.ts +2 -0
- package/src/core/agents/prompt.ts +73 -2
- package/src/core/agents/registry.ts +47 -5
- package/src/core/agents/runner.ts +178 -31
- package/src/core/agents/scratch.ts +80 -0
- package/src/core/agents/trail.ts +141 -0
- package/src/core/agents/types.ts +29 -3
- package/src/core/agents/watchdog.ts +70 -0
- package/src/core/background/isolated-agent.ts +6 -2
- package/src/core/backup/plan.ts +4 -0
- package/src/core/config/index.ts +13 -4
- package/src/core/engine/gateway-actions/agents/control.ts +9 -2
- package/src/core/engine/gateway-actions/agents/preflight.ts +17 -1
- package/src/core/engine/gateway-actions/agents/report.ts +81 -24
- package/src/core/engine/gateway-actions/index.ts +3 -0
- package/src/core/mcp-hub/guest-scope.ts +3 -1
- package/src/core/mesh/devices/service.ts +7 -0
- package/src/core/mesh/links/bridge-links.ts +20 -0
- package/src/core/secrets/actions.ts +18 -0
- package/src/core/secrets/drop.ts +176 -0
- package/src/core/secrets/index.ts +11 -0
- package/src/core/secrets/service.ts +248 -0
- package/src/core/secrets/store.ts +100 -0
- package/src/core/tools/index.ts +2 -0
- package/src/core/tools/ops/agents.ts +26 -8
- package/src/core/tools/ops/secrets.ts +36 -0
- package/src/core/tools/types.ts +2 -1
- package/src/core/types.ts +7 -0
- package/src/frontend/discord/commands/definitions.ts +21 -0
- package/src/frontend/discord/commands/router.ts +3 -0
- package/src/frontend/discord/commands/secret.ts +26 -0
- package/src/frontend/native/bridge/routes/host.ts +10 -0
- package/src/frontend/native/bridge/routes/pre-auth.ts +82 -1
- package/src/frontend/native/bridge/routes/table.ts +5 -0
- package/src/frontend/native/bridge/server.ts +32 -4
- package/src/frontend/native/commands/definitions.ts +6 -0
- package/src/frontend/native/commands/index.ts +12 -0
- package/src/frontend/native/surface/handlers.ts +9 -0
- package/src/frontend/telegram/commands/definitions.ts +4 -0
- package/src/frontend/telegram/commands/index.ts +3 -0
- package/src/frontend/telegram/commands/secret.ts +24 -0
- package/src/frontend/whatsapp/commands.ts +16 -1
- package/src/util/log.ts +1 -0
- 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.
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
##
|
|
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.
|
|
60
|
-
|
|
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 {
|
|
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.
|
package/src/core/agents/index.ts
CHANGED
|
@@ -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
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
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];
|