talon-agent 4.3.3 → 4.5.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 (118) hide show
  1. package/package.json +1 -1
  2. package/src/app.ts +1 -1
  3. package/src/backend/claude-sdk/state.ts +1 -1
  4. package/src/backend/codex/init.ts +1 -1
  5. package/src/backend/codex/state.ts +1 -1
  6. package/src/backend/openai-agents/init.ts +1 -1
  7. package/src/backend/openai-agents/state.ts +1 -1
  8. package/src/backend/remote-server/factory.ts +1 -1
  9. package/src/backend/remote-server/mcp.ts +1 -1
  10. package/src/backend/remote-server/server-bindings.ts +1 -1
  11. package/src/backend/remote-server/state.ts +1 -1
  12. package/src/backend/shared/system-prompt.ts +1 -1
  13. package/src/bootstrap.ts +2 -2
  14. package/src/cli/chat.ts +1 -1
  15. package/src/cli/config.ts +2 -2
  16. package/src/cli/index.ts +8 -0
  17. package/src/cli/memory.ts +197 -0
  18. package/src/cli/setup.ts +1 -1
  19. package/src/core/agent-runtime/backend-registry.ts +1 -1
  20. package/src/core/agent-runtime/model-ref.ts +2 -2
  21. package/src/{util/config.ts → core/config/index.ts} +8 -8
  22. package/src/core/engine/backend-controller/legacy.ts +1 -1
  23. package/src/core/engine/backend-controller/pool.ts +1 -1
  24. package/src/core/engine/backend-controller/rebind.ts +1 -1
  25. package/src/core/engine/backend-controller/state.ts +1 -1
  26. package/src/core/engine/gateway-actions/plugins.ts +1 -1
  27. package/src/core/engine/model-audit.ts +1 -1
  28. package/src/core/frontend-runtime/capabilities.ts +1 -1
  29. package/src/core/memory/import.ts +380 -0
  30. package/src/core/memory/render.ts +243 -0
  31. package/src/core/models/active-model.ts +1 -1
  32. package/src/core/plugin/builtins.ts +2 -2
  33. package/src/core/plugin/manage.ts +1 -1
  34. package/src/core/prompt/memory-view.ts +8 -8
  35. package/src/frontend/discord/admin.ts +1 -1
  36. package/src/frontend/discord/callbacks/components/index.ts +1 -1
  37. package/src/frontend/discord/callbacks/components/settings.ts +1 -1
  38. package/src/frontend/discord/callbacks/components/types.ts +1 -1
  39. package/src/frontend/discord/callbacks/modals.ts +1 -1
  40. package/src/frontend/discord/commands/admin.ts +1 -1
  41. package/src/frontend/discord/commands/definitions.ts +1 -1
  42. package/src/frontend/discord/commands/info.ts +1 -1
  43. package/src/frontend/discord/commands/router.ts +1 -1
  44. package/src/frontend/discord/commands/session.ts +1 -1
  45. package/src/frontend/discord/commands/settings.ts +1 -1
  46. package/src/frontend/discord/handlers/delivery.ts +1 -1
  47. package/src/frontend/discord/handlers/messages.ts +1 -1
  48. package/src/frontend/discord/handlers/queue.ts +1 -1
  49. package/src/frontend/discord/handlers/state.ts +1 -1
  50. package/src/frontend/discord/index.ts +1 -1
  51. package/src/frontend/discord/middleware.ts +1 -1
  52. package/src/frontend/discord/runtime.ts +1 -1
  53. package/src/frontend/native/extensions.ts +4 -1
  54. package/src/frontend/native/handlers.ts +5 -0
  55. package/src/frontend/native/index.ts +1 -1
  56. package/src/frontend/native/memory.ts +112 -0
  57. package/src/frontend/native/protocol.ts +45 -0
  58. package/src/frontend/native/routes/host.ts +9 -0
  59. package/src/frontend/native/routes/index.ts +2 -0
  60. package/src/frontend/native/routes/memory.ts +34 -0
  61. package/src/frontend/native/routes/table.ts +7 -0
  62. package/src/frontend/native/runtime.ts +1 -1
  63. package/src/frontend/native/settings.ts +1 -1
  64. package/src/frontend/shared/model-commands.ts +1 -1
  65. package/src/frontend/shared/plan-usage-report.ts +1 -1
  66. package/src/frontend/shared/reasoning-levels.ts +1 -1
  67. package/src/frontend/shared/session-status.ts +1 -1
  68. package/src/frontend/teams/index.ts +1 -1
  69. package/src/frontend/teams/runtime.ts +1 -1
  70. package/src/frontend/telegram/actions/chat-info.ts +1 -1
  71. package/src/frontend/telegram/actions/coerce.ts +15 -0
  72. package/src/frontend/telegram/actions/index.ts +1 -1
  73. package/src/frontend/telegram/actions/media.ts +1 -1
  74. package/src/frontend/telegram/actions/messaging.ts +3 -4
  75. package/src/frontend/telegram/actions/moderation/chat.ts +1 -1
  76. package/src/frontend/telegram/actions/moderation/index.ts +1 -1
  77. package/src/frontend/telegram/actions/moderation/topics.ts +1 -1
  78. package/src/frontend/telegram/actions/rich-messages.ts +51 -0
  79. package/src/frontend/telegram/actions/{shared.ts → send.ts} +9 -59
  80. package/src/frontend/telegram/admin/sessions.ts +1 -1
  81. package/src/frontend/telegram/admin.ts +1 -1
  82. package/src/frontend/telegram/callbacks/auth.ts +1 -1
  83. package/src/frontend/telegram/callbacks/effort.ts +1 -1
  84. package/src/frontend/telegram/callbacks/index.ts +3 -3
  85. package/src/frontend/telegram/callbacks/metrics.ts +1 -1
  86. package/src/frontend/telegram/callbacks/model/backend.ts +1 -1
  87. package/src/frontend/telegram/callbacks/model/control.ts +1 -1
  88. package/src/frontend/telegram/callbacks/model/select.ts +1 -1
  89. package/src/frontend/telegram/callbacks/model/types.ts +1 -1
  90. package/src/frontend/telegram/callbacks/model/views.ts +1 -1
  91. package/src/frontend/telegram/callbacks/model.ts +1 -1
  92. package/src/frontend/telegram/callbacks/pulse.ts +1 -1
  93. package/src/frontend/telegram/callbacks/{shared.ts → query.ts} +3 -2
  94. package/src/frontend/telegram/callbacks/settings.ts +1 -1
  95. package/src/frontend/telegram/callbacks/whatsapp.ts +1 -1
  96. package/src/frontend/telegram/commands/admin.ts +1 -1
  97. package/src/frontend/telegram/commands/definitions.ts +4 -0
  98. package/src/frontend/telegram/commands/index.ts +4 -1
  99. package/src/frontend/telegram/commands/info.ts +3 -0
  100. package/src/frontend/telegram/commands/memory.ts +112 -0
  101. package/src/frontend/telegram/commands/state.ts +1 -1
  102. package/src/frontend/telegram/handlers/context.ts +1 -1
  103. package/src/frontend/telegram/handlers/delivery.ts +2 -2
  104. package/src/frontend/telegram/handlers/messages.ts +1 -1
  105. package/src/frontend/telegram/handlers/queue.ts +1 -1
  106. package/src/frontend/telegram/handlers/state.ts +1 -1
  107. package/src/frontend/telegram/index.ts +1 -1
  108. package/src/frontend/telegram/middleware.ts +1 -1
  109. package/src/frontend/telegram/model-menu.ts +1 -1
  110. package/src/frontend/terminal/command-registry.ts +1 -1
  111. package/src/frontend/terminal/index.ts +1 -1
  112. package/src/frontend/whatsapp/index.ts +1 -1
  113. package/src/frontend/whatsapp/runtime.ts +1 -1
  114. package/src/storage/memory.ts +513 -0
  115. package/src/storage/repositories/memory-repo.ts +308 -0
  116. package/src/storage/sql/memory.sql +95 -0
  117. package/src/storage/sql/schema.sql +81 -0
  118. package/src/storage/sql/statements.generated.ts +146 -0
@@ -60,7 +60,7 @@ const MAX_OMITTED_NAMED = 8;
60
60
  * dropped and most cheaply re-derived. `historical` is last for the mirror
61
61
  * reason: it will never change again, so it is the least urgent to carry.
62
62
  */
63
- const TIER_ORDER = [
63
+ export const TIER_ORDER = [
64
64
  "directive",
65
65
  "people",
66
66
  "active",
@@ -69,7 +69,7 @@ const TIER_ORDER = [
69
69
  "historical",
70
70
  ] as const;
71
71
 
72
- type Tier = (typeof TIER_ORDER)[number];
72
+ export type Tier = (typeof TIER_ORDER)[number];
73
73
 
74
74
  const tier = (name: Tier): number => TIER_ORDER.indexOf(name);
75
75
 
@@ -107,7 +107,7 @@ const MATCHERS: readonly { readonly tier: Tier; readonly match: RegExp }[] = [
107
107
  const STATUS_TIER = tier("status");
108
108
  const GENERAL_TIER = tier("general");
109
109
 
110
- function classify(title: string): number {
110
+ export function classify(title: string): number {
111
111
  for (const m of MATCHERS) {
112
112
  if (m.match.test(title)) return tier(m.tier);
113
113
  }
@@ -116,7 +116,7 @@ function classify(title: string): number {
116
116
 
117
117
  // ── Parsing ─────────────────────────────────────────────────────────────────
118
118
 
119
- type Section = {
119
+ export type Section = {
120
120
  /** Heading with markup and trailing qualifiers stripped — the display name. */
121
121
  readonly title: string;
122
122
  /** Whole section including its heading and any `###` children. */
@@ -131,7 +131,7 @@ type Section = {
131
131
  };
132
132
 
133
133
  /** Strip `## ` markup and bold markers from a heading line. */
134
- function headingTitle(heading: string): string {
134
+ export function headingTitle(heading: string): string {
135
135
  return heading
136
136
  .replace(/^#+\s*/, "")
137
137
  .replace(/\*\*/g, "")
@@ -144,7 +144,7 @@ function headingTitle(heading: string): string {
144
144
  * parenthetical is exactly what makes each snapshot look unique while the
145
145
  * underlying topic repeats, so removing it is what lets a family collapse.
146
146
  */
147
- function familyKey(title: string): string {
147
+ export function familyKey(title: string): string {
148
148
  return title
149
149
  .replace(/\s*\([^)]*\)\s*$/, "")
150
150
  .replace(/\s*[—–-]\s*(?:as of|run #).*$/i, "")
@@ -172,7 +172,7 @@ function recencyKey(heading: string): string {
172
172
  * the first `## `) plus one entry per `## ` section. The split is on `## `
173
173
  * only, so an `h3` travels with the section it belongs to.
174
174
  */
175
- function parseSections(content: string): {
175
+ export function parseSections(content: string): {
176
176
  preamble: string;
177
177
  sections: Section[];
178
178
  } {
@@ -201,7 +201,7 @@ function parseSections(content: string): {
201
201
  * the one that appeared first. Only the `status` tier collapses — two
202
202
  * sections about people are two different people, not two snapshots of one.
203
203
  */
204
- function collapseFamilies(sections: readonly Section[]): {
204
+ export function collapseFamilies(sections: readonly Section[]): {
205
205
  kept: Section[];
206
206
  dropped: Section[];
207
207
  } {
@@ -9,7 +9,7 @@
9
9
  import { readFileSync } from "node:fs";
10
10
  import { files, dirs } from "../../util/paths.js";
11
11
  import { tailFile } from "../../util/tail-file.js";
12
- import type { TalonConfig } from "../../util/config.js";
12
+ import type { TalonConfig } from "../../core/config/index.js";
13
13
  import type { Gateway } from "../../core/engine/gateway.js";
14
14
  import { resetSession, getAllSessions } from "../../storage/sessions.js";
15
15
  import { clearHistory } from "../../storage/history.js";
@@ -22,7 +22,7 @@
22
22
  */
23
23
 
24
24
  import { MessageFlags } from "discord.js";
25
- import type { TalonConfig } from "../../../../util/config.js";
25
+ import type { TalonConfig } from "../../../../core/config/index.js";
26
26
  import type { Gateway } from "../../../../core/engine/gateway.js";
27
27
  import { deriveNumericChatId } from "../../../../util/chat-id.js";
28
28
  import { logError } from "../../../../util/log.js";
@@ -15,7 +15,7 @@ import {
15
15
  StringSelectMenuBuilder,
16
16
  MessageFlags,
17
17
  } from "discord.js";
18
- import type { TalonConfig } from "../../../../util/config.js";
18
+ import type { TalonConfig } from "../../../../core/config/index.js";
19
19
  import type { Gateway } from "../../../../core/engine/gateway.js";
20
20
  import {
21
21
  getChatSettings,
@@ -12,7 +12,7 @@ import type {
12
12
  ButtonInteraction,
13
13
  StringSelectMenuInteraction,
14
14
  } from "discord.js";
15
- import type { TalonConfig } from "../../../../util/config.js";
15
+ import type { TalonConfig } from "../../../../core/config/index.js";
16
16
  import type { Gateway } from "../../../../core/engine/gateway.js";
17
17
 
18
18
  export type ComponentInteraction =
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { type ModalSubmitInteraction, MessageFlags } from "discord.js";
6
- import type { TalonConfig } from "../../../util/config.js";
6
+ import type { TalonConfig } from "../../../core/config/index.js";
7
7
  import type { Gateway } from "../../../core/engine/gateway.js";
8
8
  import { setChatPulseInterval } from "../../../storage/chat-settings.js";
9
9
  import { registerChat, enablePulse } from "../../../core/background/pulse.js";
@@ -10,7 +10,7 @@ import {
10
10
  ButtonStyle,
11
11
  MessageFlags,
12
12
  } from "discord.js";
13
- import type { TalonConfig } from "../../../util/config.js";
13
+ import type { TalonConfig } from "../../../core/config/index.js";
14
14
  import type { Gateway } from "../../../core/engine/gateway.js";
15
15
  import { respawnSelf } from "../../../util/respawn.js";
16
16
  import { forceDream } from "../../../core/background/dream.js";
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import { type Client, REST, Routes, SlashCommandBuilder } from "discord.js";
16
- import type { TalonConfig } from "../../../util/config.js";
16
+ import type { TalonConfig } from "../../../core/config/index.js";
17
17
  import { log, logError, logWarn } from "../../../util/log.js";
18
18
  import { getRepoRoot } from "../../../core/update/self-update.js";
19
19
 
@@ -12,7 +12,7 @@ import {
12
12
  renderMeshReport,
13
13
  renderUsageMessage,
14
14
  } from "../render.js";
15
- import type { TalonConfig } from "../../../util/config.js";
15
+ import type { TalonConfig } from "../../../core/config/index.js";
16
16
  import { collectPlanUsage } from "../../shared/plan-usage-report.js";
17
17
  import { getLoadedPlugins } from "../../../core/plugin/index.js";
18
18
  import { getMeshService } from "../../../core/mesh/index.js";
@@ -9,7 +9,7 @@ import {
9
9
  type Interaction,
10
10
  MessageFlags,
11
11
  } from "discord.js";
12
- import type { TalonConfig } from "../../../util/config.js";
12
+ import type { TalonConfig } from "../../../core/config/index.js";
13
13
  import type { Gateway } from "../../../core/engine/gateway.js";
14
14
  import { logError } from "../../../util/log.js";
15
15
  import {
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { type ChatInputCommandInteraction, MessageFlags } from "discord.js";
9
- import type { TalonConfig } from "../../../util/config.js";
9
+ import type { TalonConfig } from "../../../core/config/index.js";
10
10
  import type { Gateway } from "../../../core/engine/gateway.js";
11
11
  import {
12
12
  formatModelLabel,
@@ -10,7 +10,7 @@ import {
10
10
  StringSelectMenuBuilder,
11
11
  MessageFlags,
12
12
  } from "discord.js";
13
- import type { TalonConfig } from "../../../util/config.js";
13
+ import type { TalonConfig } from "../../../core/config/index.js";
14
14
  import type { Gateway } from "../../../core/engine/gateway.js";
15
15
  import {
16
16
  getChatSettings,
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { TextBasedChannel } from "discord.js";
8
- import type { TalonConfig } from "../../../util/config.js";
8
+ import type { TalonConfig } from "../../../core/config/index.js";
9
9
  import { execute } from "../../../core/engine/dispatcher.js";
10
10
  import { toolInputToRecord } from "../../../core/agent-runtime/events.js";
11
11
  import { appendDailyLogResponse } from "../../../storage/daily-log.js";
@@ -6,7 +6,7 @@
6
6
 
7
7
  import type { Client, Message } from "discord.js";
8
8
  import { ChannelType } from "discord.js";
9
- import type { TalonConfig } from "../../../util/config.js";
9
+ import type { TalonConfig } from "../../../core/config/index.js";
10
10
  import { setMessageFilePath } from "../../../storage/history.js";
11
11
  import { addMedia } from "../../../storage/media-index.js";
12
12
  import { deriveNumericChatId } from "../../../util/chat-id.js";
@@ -25,7 +25,7 @@ import {
25
25
  MAX_QUEUED_PER_CHAT,
26
26
  type QueuedMessage,
27
27
  } from "./state.js";
28
- import type { TalonConfig } from "../../../util/config.js";
28
+ import type { TalonConfig } from "../../../core/config/index.js";
29
29
 
30
30
  export function enqueueMessage(
31
31
  config: TalonConfig,
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { TextBasedChannel } from "discord.js";
9
- import type { TalonConfig } from "../../../util/config.js";
9
+ import type { TalonConfig } from "../../../core/config/index.js";
10
10
  import {
11
11
  createDmUserTracker,
12
12
  createNoticeCooldown,
@@ -18,7 +18,7 @@
18
18
  * outbound, connection, and the handlers/commands/callbacks trees.
19
19
  */
20
20
 
21
- import type { TalonConfig } from "../../util/config.js";
21
+ import type { TalonConfig } from "../../core/config/index.js";
22
22
  import type { ContextManager } from "../../core/types.js";
23
23
  import type { Gateway } from "../../core/engine/gateway.js";
24
24
  import { log } from "../../util/log.js";
@@ -16,7 +16,7 @@
16
16
 
17
17
  import type { Client, Message } from "discord.js";
18
18
  import { ChannelType } from "discord.js";
19
- import type { TalonConfig } from "../../util/config.js";
19
+ import type { TalonConfig } from "../../core/config/index.js";
20
20
  import { pushMessage } from "../../storage/history.js";
21
21
  import { registerChat } from "../../core/background/pulse.js";
22
22
  import { deriveNumericChatId } from "../../util/chat-id.js";
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { Client, GatewayIntentBits, Partials } from "discord.js";
12
- import type { TalonConfig } from "../../util/config.js";
12
+ import type { TalonConfig } from "../../core/config/index.js";
13
13
  import type { Gateway } from "../../core/engine/gateway.js";
14
14
 
15
15
  /** `config.discord`, narrowed once — the factory refuses to build without it. */
@@ -23,7 +23,10 @@ import { getPluginPromptAdditions } from "../../core/plugin/index.js";
23
23
  import { listPluginItems, setPluginEnabled } from "../../core/plugin/manage.js";
24
24
  import { notifyPromptInputsChanged } from "../../core/prompt/invalidation.js";
25
25
  import { listSkills, setSkillEnabled } from "../../storage/skills.js";
26
- import { rebuildSystemPrompt, type TalonConfig } from "../../util/config.js";
26
+ import {
27
+ rebuildSystemPrompt,
28
+ type TalonConfig,
29
+ } from "../../core/config/index.js";
27
30
  import { log } from "../../util/log.js";
28
31
  import type { PluginItem, SkillItem, ToggleResult } from "./protocol.js";
29
32
  import { persistConfigPatch } from "./settings.js";
@@ -18,6 +18,7 @@ import {
18
18
  } from "./extensions.js";
19
19
  import { historyPage, searchHistory } from "./history.js";
20
20
  import { readLogEntries } from "./logs.js";
21
+ import { listMemory, memoryWhy } from "./memory.js";
21
22
  import {
22
23
  describeAttachment,
23
24
  MAX_UPLOAD_BYTES,
@@ -98,6 +99,10 @@ export function buildBridgeHandlers(
98
99
  deleteChat: (id) => deleteChat(runtime, id),
99
100
  history: (id, opts) => historyPage(runtime, id, opts),
100
101
  search: (query, chatId) => searchHistory(runtime, query, chatId),
102
+ // Memory is daemon-wide state in SQLite, not per-runtime — these are
103
+ // straight delegations to the read-only half of the store.
104
+ listMemory,
105
+ memoryWhy,
101
106
  send: (id, text, opts) => {
102
107
  const entry = chats.get(id) ?? chats.ensure(id);
103
108
  // Resolve the client's references into the records this daemon minted
@@ -14,7 +14,7 @@
14
14
  * delegate to — chat-wire, context, emit, turn, models, history, ….
15
15
  */
16
16
 
17
- import type { TalonConfig } from "../../util/config.js";
17
+ import type { TalonConfig } from "../../core/config/index.js";
18
18
  import type { ContextManager } from "../../core/types.js";
19
19
  import type { Gateway } from "../../core/engine/gateway.js";
20
20
  import { log, logError } from "../../util/log.js";
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Memory reads for the client bridge — the read-only half of the typed
3
+ * memory store (`storage/memory.ts`), projected onto the wire shapes in
4
+ * protocol.ts.
5
+ *
6
+ * Read-only on purpose: a bridge client can ask what Talon remembers and
7
+ * why, and nothing more. Asserting, superseding and dropping stay with
8
+ * the write path (rollout PR 6), so a paired phone can never quietly
9
+ * rewrite the operator's memory.
10
+ *
11
+ * The one piece of policy here is the limit cap: whatever a client asks
12
+ * for, a single response carries at most `MAX_LIMIT` rows, so a stray
13
+ * `?limit=1000000` cannot turn a listing into a full-table dump.
14
+ */
15
+
16
+ import {
17
+ getMemory,
18
+ isMemoryKind,
19
+ listMemories,
20
+ memoryHistory,
21
+ searchMemories,
22
+ MEMORY_KINDS,
23
+ type MemoryHistoryRow,
24
+ type MemoryKind,
25
+ type MemoryRow,
26
+ } from "../../storage/memory.js";
27
+ import type {
28
+ MemoryHistoryWire,
29
+ MemoryRowWire,
30
+ MemoryWhyWire,
31
+ } from "./protocol.js";
32
+
33
+ /** Hard ceiling on one listing, whatever the client asks for. */
34
+ const MAX_LIMIT = 100;
35
+
36
+ /** What `GET /memory` accepts, already coerced from the query string. */
37
+ export type MemoryListQuery = {
38
+ /** Full-text query; when present the listing becomes a search. */
39
+ q?: string;
40
+ kind?: string;
41
+ limit?: number;
42
+ };
43
+
44
+ /** An ok listing, or the reason the request was rejected (a 400). */
45
+ export type MemoryListResult =
46
+ { ok: true; rows: MemoryRowWire[] } | { ok: false; error: string };
47
+
48
+ /** Project a stored row onto the wire — internals stay daemon-side. */
49
+ function toWire(row: MemoryRow): MemoryRowWire {
50
+ return {
51
+ id: row.id,
52
+ kind: row.kind,
53
+ subject: row.subject,
54
+ ...(row.key !== undefined ? { key: row.key } : {}),
55
+ text: row.text,
56
+ trust: row.trust,
57
+ confidence: row.confidence,
58
+ pinned: row.pinned,
59
+ hitCount: row.hitCount,
60
+ salience: row.salience,
61
+ createdAt: row.createdAt,
62
+ lastSeenAt: row.lastSeenAt,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Live rows: a bm25 search when `q` is given, otherwise the ranked
68
+ * listing (pinned, then salience, then recency). An unknown `kind` is
69
+ * a client error rather than an empty result — silently returning
70
+ * nothing for a typo is how a client ends up "showing" an empty memory.
71
+ */
72
+ export function listMemory(query: MemoryListQuery = {}): MemoryListResult {
73
+ const raw = query.kind?.trim();
74
+ let kind: MemoryKind | undefined;
75
+ if (raw) {
76
+ if (!isMemoryKind(raw))
77
+ return {
78
+ ok: false,
79
+ error: `Unknown kind "${raw}" (expected one of ${MEMORY_KINDS.join(", ")})`,
80
+ };
81
+ kind = raw;
82
+ }
83
+ const limit = Math.min(query.limit ?? MAX_LIMIT, MAX_LIMIT);
84
+ const q = query.q?.trim();
85
+ const rows = q
86
+ ? searchMemories(q, { ...(kind ? { kind } : {}), limit })
87
+ : listMemories({ ...(kind ? { kind } : {}), limit });
88
+ return { ok: true, rows: rows.map(toWire) };
89
+ }
90
+
91
+ /**
92
+ * One row plus its audit trail, or null when no such id. Reads by id
93
+ * rather than from the live listing, so a superseded or dropped row
94
+ * still explains itself — that is the whole point of "why".
95
+ */
96
+ export function memoryWhy(id: number): MemoryWhyWire | null {
97
+ const row = getMemory(id);
98
+ if (!row) return null;
99
+ return {
100
+ row: toWire(row),
101
+ history: memoryHistory(row.id).map(toHistoryWire),
102
+ };
103
+ }
104
+
105
+ /** One audit entry on the wire — before/after text stays daemon-side. */
106
+ function toHistoryWire(entry: MemoryHistoryRow): MemoryHistoryWire {
107
+ return {
108
+ op: entry.op,
109
+ at: entry.at,
110
+ ...(entry.reason !== undefined ? { reason: entry.reason } : {}),
111
+ };
112
+ }
@@ -263,6 +263,51 @@ export type SkillItem = {
263
263
  */
264
264
  export type ToggleResult = { ok: boolean; error?: string };
265
265
 
266
+ /**
267
+ * One row of the typed memory store, as `GET /memory` lists it.
268
+ *
269
+ * A projection, not the stored row: the internal `source`, `contentHash`
270
+ * and supersede/drop pointers stay daemon-side, and the numbers that
271
+ * decide where a row ranks (trust, confidence, hits, salience) come
272
+ * along so a client can show WHY something is remembered, not just
273
+ * what. `kind` and `trust` are the store's vocabularies as plain
274
+ * strings — a client renders what it gets rather than refusing an
275
+ * unknown one.
276
+ */
277
+ export type MemoryRowWire = {
278
+ id: number;
279
+ kind: string;
280
+ subject: string;
281
+ /** Present only on keyed `state` rows. */
282
+ key?: string;
283
+ text: string;
284
+ trust: string;
285
+ /** 0..1. */
286
+ confidence: number;
287
+ pinned: boolean;
288
+ hitCount: number;
289
+ salience: number;
290
+ /** Epoch milliseconds. */
291
+ createdAt: number;
292
+ /** Epoch milliseconds. */
293
+ lastSeenAt: number;
294
+ };
295
+
296
+ /** One audit entry from a row's trail, oldest first in `MemoryWhyWire`. */
297
+ export type MemoryHistoryWire = {
298
+ /** assert | supersede | drop | merge | pin | unpin | replace_state. */
299
+ op: string;
300
+ /** Epoch milliseconds. */
301
+ at: number;
302
+ reason?: string;
303
+ };
304
+
305
+ /** `GET /memory/why?id=` — one row plus everything that happened to it. */
306
+ export type MemoryWhyWire = {
307
+ row: MemoryRowWire;
308
+ history: MemoryHistoryWire[];
309
+ };
310
+
266
311
  // Mesh device shapes are canonical in core (the mesh is daemon-wide state,
267
312
  // readable from every frontend); re-exported here so bridge clients keep
268
313
  // depending on the protocol module alone.
@@ -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",
@@ -8,7 +8,7 @@
8
8
  * message-id minter and the broadcast sink; the modules own the behaviour.
9
9
  */
10
10
 
11
- import type { TalonConfig } from "../../util/config.js";
11
+ import type { TalonConfig } from "../../core/config/index.js";
12
12
  import type { Gateway } from "../../core/engine/gateway.js";
13
13
  import { getMeshService, type MeshService } from "../../core/mesh/index.js";
14
14
  import { NativeChats } from "./chats.js";
@@ -14,7 +14,7 @@
14
14
  import { existsSync, readFileSync, mkdirSync } from "node:fs";
15
15
  import writeFileAtomic from "write-file-atomic";
16
16
  import { dirname } from "node:path";
17
- import type { TalonConfig } from "../../util/config.js";
17
+ import type { TalonConfig } from "../../core/config/index.js";
18
18
  import { files as pathFiles } from "../../util/paths.js";
19
19
  import { setTimezone } from "../../util/time.js";
20
20
  import { resolveModel } from "../../core/models/catalog.js";
@@ -10,7 +10,7 @@
10
10
  * about a socket, a bot API, or a keyboard.
11
11
  */
12
12
 
13
- import type { TalonConfig } from "../../util/config.js";
13
+ import type { TalonConfig } from "../../core/config/index.js";
14
14
  import type { Backend } from "../../core/agent-runtime/capabilities.js";
15
15
  import type { ReasoningEffortLevel } from "../../core/types.js";
16
16
  import {
@@ -9,7 +9,7 @@
9
9
  * never silence.
10
10
  */
11
11
 
12
- import type { TalonConfig } from "../../util/config.js";
12
+ import type { TalonConfig } from "../../core/config/index.js";
13
13
  import type { PlanUsage } from "../../core/agent-runtime/capabilities.js";
14
14
  import {
15
15
  listAvailableBackends,
@@ -4,7 +4,7 @@ import type {
4
4
  } from "../../core/types.js";
5
5
  import type { Backend } from "../../core/agent-runtime/capabilities.js";
6
6
  import { resolveActiveModelForChat } from "../../core/models/active-model.js";
7
- import type { TalonConfig } from "../../util/config.js";
7
+ import type { TalonConfig } from "../../core/config/index.js";
8
8
  import {
9
9
  normalizeReasoningLevels,
10
10
  supportsReasoningLevel,
@@ -9,7 +9,7 @@
9
9
  * markup.
10
10
  */
11
11
 
12
- import type { TalonConfig } from "../../util/config.js";
12
+ import type { TalonConfig } from "../../core/config/index.js";
13
13
  import type { Backend } from "../../core/agent-runtime/capabilities.js";
14
14
  import {
15
15
  resetSession,
@@ -12,7 +12,7 @@
12
12
  * chat-discovery, poll, commands, turn, outbound.
13
13
  */
14
14
 
15
- import type { TalonConfig } from "../../util/config.js";
15
+ import type { TalonConfig } from "../../core/config/index.js";
16
16
  import type { ContextManager } from "../../core/types.js";
17
17
  import type { Gateway } from "../../core/engine/gateway.js";
18
18
  import { log } from "../../util/log.js";
@@ -7,7 +7,7 @@
7
7
  * poll, commands, turn, outbound) takes as its first parameter.
8
8
  */
9
9
 
10
- import type { TalonConfig } from "../../util/config.js";
10
+ import type { TalonConfig } from "../../core/config/index.js";
11
11
  import type { Gateway } from "../../core/engine/gateway.js";
12
12
  import type { GraphClient } from "./graph.js";
13
13