talon-agent 5.28.0 → 5.30.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 (56) hide show
  1. package/package.json +3 -2
  2. package/prompts/system/agent-brief.md +9 -6
  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 +3 -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 +70 -2
  9. package/src/core/agents/registry.ts +47 -5
  10. package/src/core/agents/runner.ts +227 -39
  11. package/src/core/agents/trail.ts +141 -0
  12. package/src/core/agents/types.ts +43 -5
  13. package/src/core/agents/watchdog.ts +70 -0
  14. package/src/core/background/isolated-agent.ts +6 -2
  15. package/src/core/backup/plan.ts +4 -0
  16. package/src/core/config/index.ts +17 -4
  17. package/src/core/engine/gateway-actions/agents/control.ts +9 -2
  18. package/src/core/engine/gateway-actions/agents/preflight.ts +17 -1
  19. package/src/core/engine/gateway-actions/agents/report.ts +81 -24
  20. package/src/core/engine/gateway-actions/index.ts +3 -0
  21. package/src/core/mcp-hub/guest-scope.ts +3 -1
  22. package/src/core/mesh/devices/service.ts +7 -0
  23. package/src/core/mesh/links/bridge-links.ts +20 -0
  24. package/src/core/secrets/actions.ts +18 -0
  25. package/src/core/secrets/drop.ts +176 -0
  26. package/src/core/secrets/index.ts +11 -0
  27. package/src/core/secrets/service.ts +248 -0
  28. package/src/core/secrets/store.ts +100 -0
  29. package/src/core/tools/index.ts +2 -0
  30. package/src/core/tools/ops/agents.ts +30 -12
  31. package/src/core/tools/ops/secrets.ts +36 -0
  32. package/src/core/tools/types.ts +2 -1
  33. package/src/frontend/discord/commands/definitions.ts +21 -0
  34. package/src/frontend/discord/commands/router.ts +3 -0
  35. package/src/frontend/discord/commands/secret.ts +26 -0
  36. package/src/frontend/discord/handlers/messages.ts +6 -4
  37. package/src/frontend/native/bridge/routes/host.ts +10 -0
  38. package/src/frontend/native/bridge/routes/pre-auth.ts +82 -1
  39. package/src/frontend/native/bridge/routes/table.ts +5 -0
  40. package/src/frontend/native/bridge/server.ts +32 -4
  41. package/src/frontend/native/commands/definitions.ts +6 -0
  42. package/src/frontend/native/commands/index.ts +12 -0
  43. package/src/frontend/native/surface/handlers.ts +9 -0
  44. package/src/frontend/telegram/actions/chat-info.ts +22 -5
  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/telegram/handlers/messages.ts +7 -5
  49. package/src/frontend/whatsapp/commands.ts +16 -1
  50. package/src/frontend/whatsapp/messages/media-store.ts +4 -2
  51. package/src/storage/media-index.ts +43 -11
  52. package/src/storage/repositories/media-index-repo.ts +12 -1
  53. package/src/storage/sql/media-index.sql +4 -0
  54. package/src/storage/sql/statements.generated.ts +2 -0
  55. package/src/util/log.ts +1 -0
  56. package/src/util/paths.ts +6 -0
@@ -73,7 +73,19 @@ function importLegacyMediaIndex(): void {
73
73
 
74
74
  // ── CRUD ────────────────────────────────────────────────────────────────────
75
75
 
76
- export function addMedia(entry: Omit<MediaEntry, "id">): void {
76
+ /**
77
+ * Index a downloaded file and dedupe it against identical content.
78
+ *
79
+ * Resolves to the path the message's media now lives at — the
80
+ * canonical copy when the download duplicated one already on disk
81
+ * (the fresh file is then deleted), otherwise `entry.filePath`.
82
+ * Callers MUST use the resolved path for anything they show the
83
+ * model: the original path may no longer exist once this settles.
84
+ *
85
+ * The row is written synchronously, so it is queryable before the
86
+ * hash finishes.
87
+ */
88
+ export async function addMedia(entry: Omit<MediaEntry, "id">): Promise<string> {
77
89
  try {
78
90
  repo.upsert(entry);
79
91
  } catch (err) {
@@ -85,13 +97,32 @@ export function addMedia(entry: Omit<MediaEntry, "id">): void {
85
97
  recordError(
86
98
  `Media index write failed: ${err instanceof Error ? err.message : err}`,
87
99
  );
88
- return;
100
+ return entry.filePath;
89
101
  }
90
- // Hash + dedupe off the hot path — the caller is mid-message-handling
91
- // and the row is already queryable without the hash.
92
- void hashAndDedupe(entry).catch((err) =>
93
- logError("media", `Media content hash failed for ${entry.filePath}`, err),
102
+ // Serialize dedupe: two identical files hashed concurrently (e.g. an
103
+ // album) could otherwise each pick the other as canonical and both
104
+ // get unlinked.
105
+ const run = dedupeChain.then(() => hashAndDedupe(entry));
106
+ dedupeChain = run.then(
107
+ () => undefined,
108
+ () => undefined,
94
109
  );
110
+ try {
111
+ return await run;
112
+ } catch (err) {
113
+ logError("media", `Media content hash failed for ${entry.filePath}`, err);
114
+ return entry.filePath;
115
+ }
116
+ }
117
+
118
+ let dedupeChain: Promise<void> = Promise.resolve();
119
+
120
+ /** The indexed entry for a message, if any. */
121
+ export function getMediaForMessage(
122
+ chatId: string,
123
+ msgId: number,
124
+ ): MediaEntry | undefined {
125
+ return repo.byMessage(chatId, msgId);
95
126
  }
96
127
 
97
128
  /**
@@ -101,15 +132,15 @@ export function addMedia(entry: Omit<MediaEntry, "id">): void {
101
132
  * canonical copy and drop the duplicate file, so re-posted media costs
102
133
  * one copy on disk no matter how many messages carry it.
103
134
  */
104
- async function hashAndDedupe(entry: Omit<MediaEntry, "id">): Promise<void> {
105
- if (!existsSync(entry.filePath)) return; // gone already (expiry, tests)
135
+ async function hashAndDedupe(entry: Omit<MediaEntry, "id">): Promise<string> {
136
+ if (!existsSync(entry.filePath)) return entry.filePath; // gone already (expiry, tests)
106
137
  const hash = await blake3HexFile(entry.filePath);
107
138
  repo.setContentHash(entry.chatId, entry.msgId, hash);
108
139
 
109
140
  const canonical = repo.firstByContentHash(hash, entry.chatId, entry.msgId);
110
- if (!canonical) return;
111
- if (canonical.filePath === entry.filePath) return; // re-download of the same path
112
- if (!existsSync(canonical.filePath)) return; // canonical copy lost — keep ours
141
+ if (!canonical) return entry.filePath;
142
+ if (canonical.filePath === entry.filePath) return entry.filePath; // re-download of the same path
143
+ if (!existsSync(canonical.filePath)) return entry.filePath; // canonical copy lost — keep ours
113
144
 
114
145
  repo.setFilePath(entry.chatId, entry.msgId, canonical.filePath);
115
146
  setMessageFilePath(entry.chatId, entry.msgId, canonical.filePath);
@@ -131,6 +162,7 @@ async function hashAndDedupe(entry: Omit<MediaEntry, "id">): Promise<void> {
131
162
  );
132
163
  }
133
164
  }
165
+ return canonical.filePath;
134
166
  }
135
167
 
136
168
  /** Get recent media for a chat, newest first. */
@@ -33,7 +33,7 @@ export type MediaEntry = {
33
33
  timestamp: number;
34
34
  /**
35
35
  * BLAKE3 hex digest of the file contents (native/blake3-wasm).
36
- * Filled in asynchronously after addMedia; undefined until hashed.
36
+ * Filled in by addMedia before it resolves; undefined until hashed.
37
37
  */
38
38
  contentHash?: string;
39
39
  };
@@ -112,6 +112,17 @@ export function firstByContentHash(
112
112
  return row ? rowToEntry(row) : undefined;
113
113
  }
114
114
 
115
+ /** The entry for one message, if indexed. */
116
+ export function byMessage(
117
+ chatId: string,
118
+ msgId: number,
119
+ ): MediaEntry | undefined {
120
+ const row = getDatabase()
121
+ .prepare(mediaIndexSql.byMessage)
122
+ .get(chatId, msgId) as Row | undefined;
123
+ return row ? rowToEntry(row) : undefined;
124
+ }
125
+
115
126
  /** Entries currently pointing at a file — dedupe makes this > 1. */
116
127
  export function countByFilePath(filePath: string): number {
117
128
  const row = getDatabase()
@@ -43,3 +43,7 @@ ORDER BY timestamp ASC, rowid ASC LIMIT 1
43
43
 
44
44
  -- name: countByFilePath
45
45
  SELECT COUNT(*) AS n FROM media_index WHERE file_path = ?
46
+
47
+ -- name: byMessage
48
+ SELECT chat_id, msg_id, sender_name, type, file_path, caption, timestamp, content_hash
49
+ FROM media_index WHERE chat_id = ? AND msg_id = ?
@@ -767,6 +767,8 @@ FROM media_index
767
767
  WHERE content_hash = ? AND NOT (chat_id = ? AND msg_id = ?)
768
768
  ORDER BY timestamp ASC, rowid ASC LIMIT 1`,
769
769
  countByFilePath: `SELECT COUNT(*) AS n FROM media_index WHERE file_path = ?`,
770
+ byMessage: `SELECT chat_id, msg_id, sender_name, type, file_path, caption, timestamp, content_hash
771
+ FROM media_index WHERE chat_id = ? AND msg_id = ?`,
770
772
  } as const;
771
773
 
772
774
  export const memorySql = {
package/src/util/log.ts CHANGED
@@ -72,6 +72,7 @@ export type LogComponent =
72
72
  | "memory"
73
73
  | "stickers"
74
74
  | "backup"
75
+ | "secrets"
75
76
  | "router"
76
77
  | "backend-controller"
77
78
  | "fetch";
package/src/util/paths.ts CHANGED
@@ -88,6 +88,12 @@ export const dirs = {
88
88
  skills: resolve(TALON_ROOT, "workspace", "skills"),
89
89
  /** Key material (bridge TLS identity, release keys): ~/.talon/keys/ */
90
90
  keys: resolve(TALON_ROOT, "keys"),
91
+ /**
92
+ * Operator secrets, one file per value (mode 600): ~/.talon/secrets/.
93
+ * Written by the secret drop (`/secret <name>`, `request_secret`) so a
94
+ * password never has to travel through a chat message.
95
+ */
96
+ secrets: resolve(TALON_ROOT, "secrets"),
91
97
  /**
92
98
  * The talon:// namespace on disk: ~/.talon/ns/. Holds one symlink per
93
99
  * file-backed mount (home → workspace/, skills/, …); while the daemon