talon-agent 3.0.3 → 3.0.5
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 +1 -1
- package/src/backend/claude-sdk/models/discovery.ts +4 -0
- package/src/backend/claude-sdk/warm.ts +4 -0
- package/src/backend/remote-server/mcp.ts +2 -1
- package/src/core/engine/gateway-actions/native.ts +15 -4
- package/src/core/mesh/service.ts +8 -6
- package/src/core/plugin/manage.ts +1 -1
- package/src/core/soul/centrality.ts +3 -3
- package/src/core/soul/critic.ts +4 -1
- package/src/core/soul/embedder.ts +2 -2
- package/src/core/soul/talon-embedder.ts +1 -1
- package/src/frontend/telegram/formatting.ts +15 -10
- package/src/plugins/github/index.ts +3 -3
package/package.json
CHANGED
|
@@ -52,6 +52,10 @@ async function probeSupportedModels(
|
|
|
52
52
|
reject(new Error("aborted")),
|
|
53
53
|
);
|
|
54
54
|
});
|
|
55
|
+
// Unreachable — the promise above only ever rejects. The yield* both
|
|
56
|
+
// satisfies require-yield and documents that this stream produces
|
|
57
|
+
// nothing by design (streaming-input mode with no user message).
|
|
58
|
+
yield* [] as never[];
|
|
55
59
|
};
|
|
56
60
|
|
|
57
61
|
const q = query({
|
|
@@ -43,6 +43,10 @@ export async function warmSession(chatId: string): Promise<void> {
|
|
|
43
43
|
reject(new Error("aborted")),
|
|
44
44
|
);
|
|
45
45
|
});
|
|
46
|
+
// Unreachable — the promise above only ever rejects. The yield* both
|
|
47
|
+
// satisfies require-yield and documents that this stream produces
|
|
48
|
+
// nothing by design (streaming-input mode with no user message).
|
|
49
|
+
yield* [] as never[];
|
|
46
50
|
};
|
|
47
51
|
|
|
48
52
|
const q = query({
|
|
@@ -133,7 +133,8 @@ export async function ensureChatMcpServer<TClient extends RemoteAgentClient>(
|
|
|
133
133
|
// Rotate out other chat servers BEFORE registering this one. The
|
|
134
134
|
// heartbeat sentinel server is exempt (it has no real chat to leak
|
|
135
135
|
// into and stays connected for cron / trigger paths).
|
|
136
|
-
|
|
136
|
+
// Snapshot: rotation removes entries from the live set mid-iteration.
|
|
137
|
+
for (const other of Array.from(state.registeredMcpServers)) {
|
|
137
138
|
if (
|
|
138
139
|
!other.startsWith(`${TALON_MCP_SERVER_NAME}-`) ||
|
|
139
140
|
other === serverName ||
|
|
@@ -672,6 +672,16 @@ async function edit(
|
|
|
672
672
|
}
|
|
673
673
|
}
|
|
674
674
|
|
|
675
|
+
// Binary guard: decoding binary as UTF-8 is lossy (invalid sequences
|
|
676
|
+
// become U+FFFD), so a read→replace→write round-trip would corrupt every
|
|
677
|
+
// non-text byte — even when old_string matches a clean region.
|
|
678
|
+
if (content.includes("\0")) {
|
|
679
|
+
return {
|
|
680
|
+
ok: false,
|
|
681
|
+
text: `${shown} looks binary — refusing a text edit that would corrupt it. Use bash for byte-level changes.`,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
675
685
|
const count = from ? content.split(from).length - 1 : 0;
|
|
676
686
|
if (count === 0) {
|
|
677
687
|
return {
|
|
@@ -693,13 +703,14 @@ async function edit(
|
|
|
693
703
|
replaceAll === true
|
|
694
704
|
? content.split(from).join(to)
|
|
695
705
|
: spliceOnce(content, from, to);
|
|
706
|
+
const firstLine = content.slice(0, content.indexOf(from)).split("\n").length;
|
|
696
707
|
|
|
697
708
|
if (active) {
|
|
698
709
|
const res = await svc.writeFileToDevice(active.deviceId, p, updated);
|
|
699
710
|
return res.ok
|
|
700
711
|
? {
|
|
701
712
|
ok: true,
|
|
702
|
-
text: `Edited ${shown} [${active.deviceName}] (${count} replacement${count === 1 ? "" : "s"}).`,
|
|
713
|
+
text: `Edited ${shown} [${active.deviceName}] (${count} replacement${count === 1 ? "" : "s"}, first at line ${firstLine}).`,
|
|
703
714
|
}
|
|
704
715
|
: res;
|
|
705
716
|
}
|
|
@@ -713,7 +724,7 @@ async function edit(
|
|
|
713
724
|
}
|
|
714
725
|
return {
|
|
715
726
|
ok: true,
|
|
716
|
-
text: `Edited ${shown} [local] (${count} replacement${count === 1 ? "" : "s"}).`,
|
|
727
|
+
text: `Edited ${shown} [local] (${count} replacement${count === 1 ? "" : "s"}, first at line ${firstLine}).`,
|
|
717
728
|
};
|
|
718
729
|
}
|
|
719
730
|
|
|
@@ -733,8 +744,8 @@ async function glob(
|
|
|
733
744
|
// -name, path patterns via -path). `command -v` gates the choice so a
|
|
734
745
|
// no-match rg exit (1) isn't misread as "rg missing, run find too".
|
|
735
746
|
const findExpr = pat.includes("/")
|
|
736
|
-
? `-path ${shellQuote(`*${pat}`)}`
|
|
737
|
-
: `-name ${shellQuote(pat)}`;
|
|
747
|
+
? `-type f -path ${shellQuote(`*${pat}`)}`
|
|
748
|
+
: `-type f -name ${shellQuote(pat)}`;
|
|
738
749
|
const cmd =
|
|
739
750
|
`if command -v rg >/dev/null 2>&1; ` +
|
|
740
751
|
`then rg --files -g ${shellQuote(pat)} ${shellQuote(root)}; ` +
|
package/src/core/mesh/service.ts
CHANGED
|
@@ -193,7 +193,9 @@ export class MeshService {
|
|
|
193
193
|
// Stamp arrival against our own clock so waitForFreshLocation is immune
|
|
194
194
|
// to device clock skew.
|
|
195
195
|
this.receivedAt.set(loc.deviceId, Date.now());
|
|
196
|
-
|
|
196
|
+
// Snapshot: a notified waiter removes itself (and new waiters may be
|
|
197
|
+
// added) mid-iteration — iterate a copy, not the live set.
|
|
198
|
+
for (const notify of Array.from(this.waiters)) notify();
|
|
197
199
|
return loc;
|
|
198
200
|
}
|
|
199
201
|
|
|
@@ -372,11 +374,11 @@ export class MeshService {
|
|
|
372
374
|
|
|
373
375
|
/** `ring_device`: make the device sound/vibrate so it can be found. */
|
|
374
376
|
ringDevice(query?: unknown, message?: unknown): Promise<MeshToolResult> {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
?
|
|
378
|
-
:
|
|
379
|
-
});
|
|
377
|
+
const note =
|
|
378
|
+
typeof message === "string" && message.trim()
|
|
379
|
+
? message.trim().slice(0, 200)
|
|
380
|
+
: undefined;
|
|
381
|
+
return this.commandTool(query, "ring", note ? { message: note } : {});
|
|
380
382
|
}
|
|
381
383
|
|
|
382
384
|
/**
|
|
@@ -77,7 +77,7 @@ export function setPluginEnabled(
|
|
|
77
77
|
if (!name) return { ok: false, error: "A plugin name is required." };
|
|
78
78
|
|
|
79
79
|
if (isBuiltinPlugin(name)) {
|
|
80
|
-
const section = { ...
|
|
80
|
+
const section = { ...builtinSection(config, name), enabled };
|
|
81
81
|
(config as unknown as Record<string, unknown>)[name] = section;
|
|
82
82
|
return { ok: true, name, persist: { [name]: section } };
|
|
83
83
|
}
|
|
@@ -48,7 +48,7 @@ export function pagerank(
|
|
|
48
48
|
const idx = new Map(nodes.map((h, i) => [h, i]));
|
|
49
49
|
|
|
50
50
|
// Weighted out-adjacency over coactivation edges (only among value nodes).
|
|
51
|
-
const outWeight =
|
|
51
|
+
const outWeight = Array.from({ length: n }, () => 0);
|
|
52
52
|
const edges: { from: number; to: number; w: number }[] = [];
|
|
53
53
|
for (const from of nodes) {
|
|
54
54
|
for (const e of dag.edgesFrom(from, "coactivation")) {
|
|
@@ -61,7 +61,7 @@ export function pagerank(
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
// Teleport / personalization vector.
|
|
64
|
-
const teleport =
|
|
64
|
+
const teleport = Array.from({ length: n }, () => 1 / n);
|
|
65
65
|
if (opts.personalization && opts.personalization.size > 0) {
|
|
66
66
|
let total = 0;
|
|
67
67
|
for (const [, w] of opts.personalization) total += Math.max(0, w);
|
|
@@ -74,7 +74,7 @@ export function pagerank(
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
let rank =
|
|
77
|
+
let rank = Array.from({ length: n }, () => 1 / n);
|
|
78
78
|
for (let it = 0; it < iterations; it++) {
|
|
79
79
|
const next = teleport.map((t) => (1 - damping) * t);
|
|
80
80
|
|
package/src/core/soul/critic.ts
CHANGED
|
@@ -66,7 +66,10 @@ const SYCOPHANCY = [
|
|
|
66
66
|
|
|
67
67
|
// Emoji detection without a heavy dependency: pictographic + symbol ranges.
|
|
68
68
|
const EMOJI_RE =
|
|
69
|
-
|
|
69
|
+
// Variation selectors are combining marks — alternated separately so the
|
|
70
|
+
// character class holds only standalone pictographic ranges
|
|
71
|
+
// (no-misleading-character-class).
|
|
72
|
+
/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2190}-\u{21FF}\u{2B00}-\u{2BFF}\u{1F000}-\u{1F0FF}]|[\u{FE00}-\u{FE0F}]/gu;
|
|
70
73
|
|
|
71
74
|
function countOccurrences(
|
|
72
75
|
haystack: string,
|
|
@@ -63,7 +63,7 @@ export function cosineDistance(
|
|
|
63
63
|
export function centroid(vectors: readonly (readonly number[])[]): number[] {
|
|
64
64
|
if (vectors.length === 0) return [];
|
|
65
65
|
const dim = vectors[0]!.length;
|
|
66
|
-
const acc =
|
|
66
|
+
const acc = Array.from({ length: dim }, () => 0);
|
|
67
67
|
for (const v of vectors) {
|
|
68
68
|
for (let i = 0; i < dim; i++) acc[i]! += v[i]!;
|
|
69
69
|
}
|
|
@@ -110,7 +110,7 @@ export class HashingEmbedder implements Embedder {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
private embedOne(text: string): number[] {
|
|
113
|
-
const vec =
|
|
113
|
+
const vec = Array.from({ length: this.dim }, () => 0);
|
|
114
114
|
const lower = text.toLowerCase();
|
|
115
115
|
const add = (feature: string): void => {
|
|
116
116
|
const h = createHash("md5").update(feature).digest();
|
|
@@ -67,7 +67,7 @@ export class TalonEmbedder implements Embedder {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
const vec =
|
|
70
|
+
const vec = Array.from({ length: this.dim }, () => 0);
|
|
71
71
|
for (const [feature, count] of counts) {
|
|
72
72
|
const h = createHash("md5").update(feature).digest();
|
|
73
73
|
const idx = ((h[0]! << 16) | (h[1]! << 8) | h[2]!) % this.dim;
|
|
@@ -69,24 +69,29 @@ export function markdownToTelegramHtml(text: string): string {
|
|
|
69
69
|
);
|
|
70
70
|
// Italic: _text_ (surrounded by non-word or start/end)
|
|
71
71
|
processed = processed.replace(/(?<!\w)_(.+?)_(?!\w)/g, "<i>$1</i>");
|
|
72
|
-
// Links: [text](url) — only
|
|
73
|
-
//
|
|
72
|
+
// Links: [text](url) — only safe URL schemes become anchors. Both text
|
|
73
|
+
// and url were already HTML-escaped by step 3 (quotes included, so the
|
|
74
|
+
// href attribute can't be broken out of); escaping again here corrupted
|
|
75
|
+
// every & in a query string into &amp;.
|
|
74
76
|
processed = processed.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) =>
|
|
75
|
-
/^https?:\/\//i.test(url)
|
|
76
|
-
? `<a href="${escapeHtml(url)}">${escapeHtml(text)}</a>`
|
|
77
|
-
: text,
|
|
77
|
+
/^https?:\/\//i.test(url) ? `<a href="${url}">${text}</a>` : text,
|
|
78
78
|
);
|
|
79
79
|
// Strikethrough: ~~text~~
|
|
80
80
|
processed = processed.replace(/~~(.+?)~~/g, "<s>$1</s>");
|
|
81
81
|
|
|
82
|
-
//
|
|
82
|
+
// Steps 5+6: Restore code spans and fenced blocks. The replacement MUST
|
|
83
|
+
// go through a function: with a string, String.replace interprets $-
|
|
84
|
+
// substitution patterns in the *code content* ($& re-inserts the
|
|
85
|
+
// placeholder, $\` splices the whole preceding message), which is how
|
|
86
|
+
// \`$&\` in a code span used to leak a stranded INLINECODEn into chat.
|
|
83
87
|
for (let i = 0; i < inlineCode.length; i++) {
|
|
84
|
-
processed = processed.replace(
|
|
88
|
+
processed = processed.replace(
|
|
89
|
+
`\x00INLINECODE${i}\x00`,
|
|
90
|
+
() => inlineCode[i],
|
|
91
|
+
);
|
|
85
92
|
}
|
|
86
|
-
|
|
87
|
-
// Step 6: Restore fenced code blocks.
|
|
88
93
|
for (let i = 0; i < codeBlocks.length; i++) {
|
|
89
|
-
processed = processed.replace(`\x00CODEBLOCK${i}\x00`, codeBlocks[i]);
|
|
94
|
+
processed = processed.replace(`\x00CODEBLOCK${i}\x00`, () => codeBlocks[i]);
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
return processed;
|
|
@@ -98,9 +98,9 @@ export function createGitHubPlugin(config: { token?: string }): TalonPlugin {
|
|
|
98
98
|
},
|
|
99
99
|
|
|
100
100
|
getEnvVars() {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
const vars: Record<string, string> = {};
|
|
102
|
+
if (token) vars.GITHUB_PERSONAL_ACCESS_TOKEN = token;
|
|
103
|
+
return vars;
|
|
104
104
|
},
|
|
105
105
|
};
|
|
106
106
|
}
|