talon-agent 4.4.0 → 4.6.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 +1 -1
- package/prompts/system/memory-core-view.md +7 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/memory.ts +31 -0
- package/src/core/memory/core-view.ts +158 -0
- package/src/core/memory/flag.ts +16 -0
- package/src/core/memory/import.ts +380 -0
- package/src/core/memory/render.ts +243 -0
- package/src/core/prompt/assemble.ts +69 -16
- package/src/core/prompt/embedded-prompts.ts +20 -18
- package/src/core/prompt/memory-view.ts +8 -8
- package/src/frontend/native/handlers.ts +5 -0
- package/src/frontend/native/memory.ts +112 -0
- package/src/frontend/native/protocol.ts +45 -0
- package/src/frontend/native/routes/host.ts +9 -0
- package/src/frontend/native/routes/index.ts +2 -0
- package/src/frontend/native/routes/memory.ts +34 -0
- package/src/frontend/native/routes/table.ts +7 -0
- package/src/frontend/telegram/commands/definitions.ts +4 -0
- package/src/frontend/telegram/commands/index.ts +3 -0
- package/src/frontend/telegram/commands/info.ts +3 -0
- package/src/frontend/telegram/commands/memory.ts +112 -0
- package/src/storage/memory.ts +8 -0
- package/src/storage/metrics.ts +26 -1
|
@@ -12,12 +12,14 @@ import type {
|
|
|
12
12
|
DeviceLocation,
|
|
13
13
|
LogEntry,
|
|
14
14
|
LogLevel,
|
|
15
|
+
MemoryWhyWire,
|
|
15
16
|
ModelOption,
|
|
16
17
|
PluginItem,
|
|
17
18
|
SearchResult,
|
|
18
19
|
SkillItem,
|
|
19
20
|
ToggleResult,
|
|
20
21
|
} from "../protocol.js";
|
|
22
|
+
import type { MemoryListQuery, MemoryListResult } from "../memory.js";
|
|
21
23
|
import type { ConfigSnapshot } from "../settings.js";
|
|
22
24
|
|
|
23
25
|
/** Optional attachment references carried alongside a sent message. */
|
|
@@ -48,6 +50,13 @@ export type BridgeServerHandlers = {
|
|
|
48
50
|
): ClientMessage[];
|
|
49
51
|
/** Full-text search across chats (or one chat when `chatId` is given). */
|
|
50
52
|
search(query: string, chatId?: string): SearchResult[];
|
|
53
|
+
/**
|
|
54
|
+
* Live memory rows — a full-text search when `q` is given, else the
|
|
55
|
+
* ranked listing. Read-only: the bridge exposes no memory writes.
|
|
56
|
+
*/
|
|
57
|
+
listMemory(query: MemoryListQuery): MemoryListResult;
|
|
58
|
+
/** One memory row plus its audit trail, or null when no such id. */
|
|
59
|
+
memoryWhy(id: number): MemoryWhyWire | null;
|
|
51
60
|
/** Fire-and-forget: streams its results back through `broadcast`. */
|
|
52
61
|
send(id: string, text: string, opts?: SendOptions): void;
|
|
53
62
|
/**
|
|
@@ -7,6 +7,7 @@ import type { RouteHost } from "./host.js";
|
|
|
7
7
|
import type { BridgeRoutes } from "./table.js";
|
|
8
8
|
import { preAuthRoutes } from "./pre-auth.js";
|
|
9
9
|
import { chatRoutes } from "./chats.js";
|
|
10
|
+
import { memoryRoutes } from "./memory.js";
|
|
10
11
|
import { modelRoutes } from "./models.js";
|
|
11
12
|
import { daemonRoutes } from "./daemon.js";
|
|
12
13
|
import { meshRoutes } from "./mesh.js";
|
|
@@ -15,6 +16,7 @@ export function buildRoutes(host: RouteHost): BridgeRoutes {
|
|
|
15
16
|
return {
|
|
16
17
|
...preAuthRoutes(host),
|
|
17
18
|
...chatRoutes(host),
|
|
19
|
+
...memoryRoutes(host),
|
|
18
20
|
...modelRoutes(host),
|
|
19
21
|
...daemonRoutes(host),
|
|
20
22
|
...meshRoutes(host),
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { RouteHost } from "./host.js";
|
|
2
|
+
import type { BridgeRoutes } from "./table.js";
|
|
3
|
+
import { asPositiveInt } from "./params.js";
|
|
4
|
+
|
|
5
|
+
export function memoryRoutes(
|
|
6
|
+
host: RouteHost,
|
|
7
|
+
): Pick<BridgeRoutes, "GET /memory" | "GET /memory/why"> {
|
|
8
|
+
const { json, handlers: h } = host;
|
|
9
|
+
return {
|
|
10
|
+
// ── Memory (read-only) ─────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
// `q` turns the listing into a search; `kind` narrows either. A bad
|
|
13
|
+
// kind is a 400 naming the valid ones, not an empty list — a typo
|
|
14
|
+
// must not look like an empty memory.
|
|
15
|
+
"GET /memory": ({ res, url }) => {
|
|
16
|
+
const result = h.listMemory({
|
|
17
|
+
q: url.searchParams.get("q") ?? undefined,
|
|
18
|
+
kind: url.searchParams.get("kind") ?? undefined,
|
|
19
|
+
limit: asPositiveInt(url.searchParams.get("limit")),
|
|
20
|
+
});
|
|
21
|
+
return result.ok
|
|
22
|
+
? json(res, 200, { rows: result.rows })
|
|
23
|
+
: json(res, 400, { ok: false, error: result.error });
|
|
24
|
+
},
|
|
25
|
+
"GET /memory/why": ({ res, url }) => {
|
|
26
|
+
const raw = url.searchParams.get("id") ?? "";
|
|
27
|
+
const id = asPositiveInt(raw);
|
|
28
|
+
const why = id === undefined ? null : h.memoryWhy(id);
|
|
29
|
+
return why
|
|
30
|
+
? json(res, 200, why)
|
|
31
|
+
: json(res, 404, { ok: false, error: `No memory with id ${raw}` });
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -47,6 +47,13 @@ export const BRIDGE_ROUTE_AUTH = {
|
|
|
47
47
|
"POST /queue": "bearer",
|
|
48
48
|
"GET /history": "bearer",
|
|
49
49
|
"GET /search": "bearer",
|
|
50
|
+
|
|
51
|
+
// Memory — read-only. The typed memory store is readable over the
|
|
52
|
+
// bridge but never writable from it: asserting and dropping stay with
|
|
53
|
+
// the daemon's own write path.
|
|
54
|
+
"GET /memory": "bearer",
|
|
55
|
+
"GET /memory/why": "bearer",
|
|
56
|
+
|
|
50
57
|
"POST /send": "bearer",
|
|
51
58
|
"POST /upload": "bearer",
|
|
52
59
|
"GET /media": "bearer",
|
|
@@ -44,6 +44,10 @@ export const TELEGRAM_COMMANDS: ReadonlyArray<{
|
|
|
44
44
|
description: "Environment and native-module health",
|
|
45
45
|
},
|
|
46
46
|
{ command: "dream", description: "Force memory consolidation" },
|
|
47
|
+
{
|
|
48
|
+
command: "memory",
|
|
49
|
+
description: "What Talon remembers — list, search, why <id>",
|
|
50
|
+
},
|
|
47
51
|
{ command: "plugins", description: "List loaded plugins" },
|
|
48
52
|
{ command: "help", description: "All commands and features" },
|
|
49
53
|
];
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* - `definitions` — the TELEGRAM_COMMANDS menu (single source of truth)
|
|
6
6
|
* - `state` — shared admin-id holder + admin guard
|
|
7
7
|
* - `info` — /start /help /ping /plugins
|
|
8
|
+
* - `memory` — /memory (read-only view of the typed memory store)
|
|
8
9
|
* - `session` — /reset /status
|
|
9
10
|
* - `settings` — /model /effort /pulse /settings
|
|
10
11
|
* - `admin` — /admin /metrics /doctor /dream /soul /restart /update
|
|
@@ -19,6 +20,7 @@ import type { Bot } from "grammy";
|
|
|
19
20
|
import type { TalonConfig } from "../../../core/config/index.js";
|
|
20
21
|
import type { Backend } from "../../../core/agent-runtime/capabilities.js";
|
|
21
22
|
import { registerInfoCommands } from "./info.js";
|
|
23
|
+
import { registerMemoryCommand } from "./memory.js";
|
|
22
24
|
import { registerSessionCommands } from "./session.js";
|
|
23
25
|
import { registerSettingsCommands } from "./settings.js";
|
|
24
26
|
import { registerAdminCommands } from "./admin.js";
|
|
@@ -35,6 +37,7 @@ export function registerCommands(
|
|
|
35
37
|
): void {
|
|
36
38
|
const deps = { config, gateway };
|
|
37
39
|
registerInfoCommands(bot);
|
|
40
|
+
registerMemoryCommand(bot);
|
|
38
41
|
registerSessionCommands(bot, deps);
|
|
39
42
|
registerSettingsCommands(bot, deps);
|
|
40
43
|
registerWhatsAppPairingCommand(bot);
|
|
@@ -50,6 +50,9 @@ export function registerInfoCommands(bot: Bot): void {
|
|
|
50
50
|
" /metrics -- aggregate performance metrics (admin)",
|
|
51
51
|
" /doctor -- environment and native-module health (admin)",
|
|
52
52
|
" /dream -- force memory consolidation now",
|
|
53
|
+
// Entity-escaped: this whole message is sent with parse_mode HTML,
|
|
54
|
+
// so a literal <id> would be read as a tag and 400 the reply.
|
|
55
|
+
" /memory -- what Talon remembers; /memory why <id> for provenance",
|
|
53
56
|
" /ping -- health check with latency",
|
|
54
57
|
" /mesh -- ping and list companion mesh devices",
|
|
55
58
|
" /reset -- clear session and start fresh",
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/memory` — a read-only window on the typed memory store.
|
|
3
|
+
*
|
|
4
|
+
* Four shapes, all reads: the ranked listing, a full-text search, one
|
|
5
|
+
* row's provenance (`why <id>`) and a per-kind listing. Nothing here
|
|
6
|
+
* writes: asserting, superseding and dropping are the write path's job
|
|
7
|
+
* (rollout PR 6), so the operator can always ask what Talon remembers
|
|
8
|
+
* without the answer being able to change it.
|
|
9
|
+
*
|
|
10
|
+
* Gated exactly like `/status` — not at all. The store holds the
|
|
11
|
+
* operator's own memory, and a read of it is the least privileged thing
|
|
12
|
+
* a chat can do.
|
|
13
|
+
*
|
|
14
|
+
* Every line is model- or user-authored text reaching an HTML-parsed
|
|
15
|
+
* send, so it goes through `escapeHtml` before it is joined; the reply
|
|
16
|
+
* is chunked because a listing of 15 rows can outgrow Telegram's
|
|
17
|
+
* 4096-char cap on its own.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { Bot, Context } from "grammy";
|
|
21
|
+
import { escapeHtml } from "../formatting.js";
|
|
22
|
+
import { replyHtmlChunked } from "../admin/chunked-reply.js";
|
|
23
|
+
import {
|
|
24
|
+
formatMemory,
|
|
25
|
+
getMemory,
|
|
26
|
+
isMemoryKind,
|
|
27
|
+
listMemories,
|
|
28
|
+
memoryHistory,
|
|
29
|
+
searchMemories,
|
|
30
|
+
MEMORY_KINDS,
|
|
31
|
+
type MemoryRow,
|
|
32
|
+
} from "../../../storage/memory.js";
|
|
33
|
+
|
|
34
|
+
/** Rows per reply — a chat listing is a glance, not an export. */
|
|
35
|
+
const LIST_LIMIT = 15;
|
|
36
|
+
|
|
37
|
+
export function registerMemoryCommand(bot: Bot): void {
|
|
38
|
+
bot.command("memory", async (ctx: Context) => {
|
|
39
|
+
const arg = (ctx.match ?? "").toString().trim();
|
|
40
|
+
await replyHtmlChunked(ctx, renderMemory(arg));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Route the argument to one of the four reads. Returns ready HTML. */
|
|
45
|
+
function renderMemory(arg: string): string {
|
|
46
|
+
const why = /^why\b\s*(.*)$/is.exec(arg);
|
|
47
|
+
if (why) return renderWhy(why[1]!.trim());
|
|
48
|
+
const kind = /^kind\b\s*(.*)$/is.exec(arg);
|
|
49
|
+
if (kind) return renderKind(kind[1]!.trim());
|
|
50
|
+
if (!arg)
|
|
51
|
+
return renderRows(
|
|
52
|
+
listMemories({ limit: LIST_LIMIT }),
|
|
53
|
+
"Nothing remembered yet.",
|
|
54
|
+
);
|
|
55
|
+
return renderRows(
|
|
56
|
+
searchMemories(arg, { limit: LIST_LIMIT }),
|
|
57
|
+
`No memories matching "${arg}".`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** One escaped line per row, or the (escaped) empty-case sentence. */
|
|
62
|
+
function renderRows(rows: MemoryRow[], empty: string): string {
|
|
63
|
+
if (rows.length === 0) return escapeHtml(empty);
|
|
64
|
+
return rows.map((row) => escapeHtml(formatMemory(row))).join("\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function renderKind(kind: string): string {
|
|
68
|
+
if (!isMemoryKind(kind))
|
|
69
|
+
return escapeHtml(
|
|
70
|
+
`No such kind "${kind}". Valid kinds: ${MEMORY_KINDS.join(", ")}.`,
|
|
71
|
+
);
|
|
72
|
+
return renderRows(
|
|
73
|
+
listMemories({ kind, limit: LIST_LIMIT }),
|
|
74
|
+
`Nothing remembered under ${kind}.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Provenance for one row: the row itself, the numbers that decide where
|
|
80
|
+
* it ranks, and its audit trail. Reads by id rather than by the live
|
|
81
|
+
* listing, so a superseded or dropped row still explains itself.
|
|
82
|
+
*/
|
|
83
|
+
function renderWhy(raw: string): string {
|
|
84
|
+
const id = Number(raw);
|
|
85
|
+
if (!raw || !Number.isInteger(id))
|
|
86
|
+
return escapeHtml(`No memory with id ${raw || "(none given)"}.`);
|
|
87
|
+
const row = getMemory(id);
|
|
88
|
+
if (!row) return escapeHtml(`No memory with id ${id}.`);
|
|
89
|
+
const lines = [
|
|
90
|
+
escapeHtml(formatMemory(row)),
|
|
91
|
+
"",
|
|
92
|
+
escapeHtml(
|
|
93
|
+
`trust ${row.trust} · confidence ${row.confidence} · hits ${row.hitCount} · salience ${row.salience}`,
|
|
94
|
+
),
|
|
95
|
+
escapeHtml(
|
|
96
|
+
`created ${isoTime(row.createdAt)} · last seen ${isoTime(row.lastSeenAt)}`,
|
|
97
|
+
),
|
|
98
|
+
];
|
|
99
|
+
const history = memoryHistory(row.id);
|
|
100
|
+
if (history.length > 0) {
|
|
101
|
+
lines.push("", "<b>History</b>");
|
|
102
|
+
for (const entry of history) {
|
|
103
|
+
const reason = entry.reason ? ` — ${entry.reason}` : "";
|
|
104
|
+
lines.push(escapeHtml(`${isoTime(entry.at)} ${entry.op}${reason}`));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isoTime(ms: number): string {
|
|
111
|
+
return new Date(ms).toISOString();
|
|
112
|
+
}
|
package/src/storage/memory.ts
CHANGED
|
@@ -440,6 +440,14 @@ export function touchMemory(id: number): void {
|
|
|
440
440
|
|
|
441
441
|
// ── Reads ───────────────────────────────────────────────────────────────────
|
|
442
442
|
|
|
443
|
+
/**
|
|
444
|
+
* The idempotency key a re-import compares against: sha256 of
|
|
445
|
+
* `kind|subject|key|text`. Re-exported from the repository so
|
|
446
|
+
* core/memory/import.ts can decide skip-vs-supersede without reaching
|
|
447
|
+
* past the store (plan §3.1).
|
|
448
|
+
*/
|
|
449
|
+
export const memoryContentHash = repo.contentHash;
|
|
450
|
+
|
|
443
451
|
/** Any row by id — including superseded and dropped ones. */
|
|
444
452
|
export function getMemory(id: number): MemoryRow | undefined {
|
|
445
453
|
return repo.get(id);
|
package/src/storage/metrics.ts
CHANGED
|
@@ -22,6 +22,14 @@ const PHASE_HISTOGRAM: Record<TurnPhase, string> = {
|
|
|
22
22
|
|
|
23
23
|
const legacyCounters = new Map<string, number>();
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Process-lifetime distributions for values that are not chat-turn
|
|
27
|
+
* latencies and so have nowhere to live on a session record — currently
|
|
28
|
+
* `prompt.memory_chars`, the size of the injected memory block, which is
|
|
29
|
+
* what the `TALON_MEMORY_STORE` before/after comparison reads.
|
|
30
|
+
*/
|
|
31
|
+
const processHistograms = new Map<string, MetricsLatencyAgg>();
|
|
32
|
+
|
|
25
33
|
export type MetricsSnapshot = {
|
|
26
34
|
counters: Record<string, number>;
|
|
27
35
|
histograms: Record<
|
|
@@ -34,6 +42,18 @@ export function incrementCounter(name: string, amount = 1): void {
|
|
|
34
42
|
legacyCounters.set(name, (legacyCounters.get(name) ?? 0) + amount);
|
|
35
43
|
}
|
|
36
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Record one observation of a non-turn distribution. Lifetime-scoped and
|
|
47
|
+
* in-process, like the legacy counters: it surfaces in `getMetrics()`
|
|
48
|
+
* (count / avg / min / max), not in the daily rollup.
|
|
49
|
+
*/
|
|
50
|
+
export function recordHistogram(name: string, value: number): void {
|
|
51
|
+
if (!Number.isFinite(value)) return;
|
|
52
|
+
const agg = processHistograms.get(name) ?? emptyAgg();
|
|
53
|
+
mergeAgg(agg, { count: 1, sumMs: value, minMs: value, maxMs: value });
|
|
54
|
+
processHistograms.set(name, agg);
|
|
55
|
+
}
|
|
56
|
+
|
|
37
57
|
function addCounter(
|
|
38
58
|
counters: Record<string, number>,
|
|
39
59
|
name: string,
|
|
@@ -158,10 +178,14 @@ function buildSnapshot(
|
|
|
158
178
|
export function getMetrics(): MetricsSnapshot {
|
|
159
179
|
const counters: Record<string, number> = {};
|
|
160
180
|
for (const [key, value] of legacyCounters) addCounter(counters, key, value);
|
|
161
|
-
|
|
181
|
+
const snapshot = buildSnapshot(
|
|
162
182
|
getAllSessions().map(({ info }) => info.metrics.lifetime),
|
|
163
183
|
counters,
|
|
164
184
|
);
|
|
185
|
+
for (const [name, agg] of processHistograms) {
|
|
186
|
+
if (agg.count) snapshot.histograms[name] = snapshotAgg(agg);
|
|
187
|
+
}
|
|
188
|
+
return snapshot;
|
|
165
189
|
}
|
|
166
190
|
|
|
167
191
|
/** Today's (UTC) fleet snapshot, aggregated from the sessions' daily
|
|
@@ -177,5 +201,6 @@ export function getTodayMetrics(): MetricsSnapshot {
|
|
|
177
201
|
|
|
178
202
|
export function resetMetrics(): void {
|
|
179
203
|
legacyCounters.clear();
|
|
204
|
+
processHistograms.clear();
|
|
180
205
|
resetAllSessionMetrics();
|
|
181
206
|
}
|