talon-agent 3.0.5 → 3.1.1

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 (63) hide show
  1. package/package.json +8 -5
  2. package/src/backend/kilo/models/index.ts +4 -1
  3. package/src/backend/kilo/server.ts +17 -4
  4. package/src/backend/openai-agents/session.ts +1 -1
  5. package/src/backend/opencode/models/index.ts +4 -1
  6. package/src/backend/opencode/server.ts +17 -4
  7. package/src/backend/shared/system-prompt.ts +5 -0
  8. package/src/cli/events.ts +1 -1
  9. package/src/cli/index.ts +1 -1
  10. package/src/cli/tasks.ts +5 -1
  11. package/src/core/background/triggers/pid.ts +28 -0
  12. package/src/core/background/triggers/resume.ts +4 -24
  13. package/src/core/background/triggers/spawn.ts +1 -1
  14. package/src/core/engine/gateway-actions/plugins.ts +3 -3
  15. package/src/core/prompt/invalidation.ts +23 -0
  16. package/src/core/types.ts +2 -2
  17. package/src/frontend/discord/callbacks/components.ts +1 -1
  18. package/src/frontend/discord/commands/settings.ts +1 -1
  19. package/src/frontend/native/auth.ts +52 -0
  20. package/src/frontend/native/extensions.ts +2 -2
  21. package/src/frontend/native/index.ts +8 -2
  22. package/src/frontend/native/server.ts +96 -15
  23. package/src/frontend/telegram/callbacks/model.ts +1 -1
  24. package/src/frontend/telegram/commands/admin.ts +1 -1
  25. package/src/frontend/telegram/commands/settings.ts +1 -1
  26. package/src/frontend/telegram/formatting.ts +1 -1
  27. package/src/frontend/terminal/commands.ts +1 -1
  28. package/src/native/htmlents-wasm-bytes.ts +4 -4
  29. package/src/native/htmlents.ts +4 -4
  30. package/src/native/registry.ts +9 -9
  31. package/src/native/runtime.ts +2 -2
  32. package/src/native/sqlguard-wasm-bytes.ts +4 -4
  33. package/src/native/sqlguard.ts +2 -2
  34. package/src/native/strsim-wasm-bytes.ts +4 -4
  35. package/src/native/strsim.ts +3 -3
  36. package/src/plugins/github/index.ts +1 -1
  37. package/src/plugins/mem0/index.ts +1 -1
  38. package/src/plugins/mempalace/index.ts +1 -1
  39. package/src/plugins/playwright/index.ts +1 -1
  40. package/src/storage/chat-settings.ts +3 -60
  41. package/src/storage/cron-store.ts +6 -80
  42. package/src/storage/goal-store.ts +10 -19
  43. package/src/storage/history.ts +3 -14
  44. package/src/storage/journal.ts +20 -9
  45. package/src/storage/media-index.ts +2 -22
  46. package/src/storage/repositories/chat-settings-repo.ts +55 -1
  47. package/src/storage/repositories/cron-repo.ts +82 -6
  48. package/src/storage/repositories/goals-repo.ts +20 -1
  49. package/src/storage/repositories/history-repo.ts +14 -1
  50. package/src/storage/repositories/media-index-repo.ts +23 -1
  51. package/src/storage/repositories/scripts-repo.ts +16 -1
  52. package/src/storage/repositories/sessions-repo.ts +1 -1
  53. package/src/storage/repositories/triggers-repo.ts +60 -5
  54. package/src/storage/script-store.ts +2 -15
  55. package/src/storage/session-record.ts +220 -0
  56. package/src/storage/sessions.ts +22 -210
  57. package/src/storage/trigger-store.ts +6 -60
  58. package/src/types/assets.d.ts +9 -0
  59. package/src/types/effort.ts +12 -0
  60. package/src/util/config.ts +9 -2
  61. package/src/util/harden.ts +50 -0
  62. package/src/util/log.ts +2 -1
  63. package/tsconfig.json +6 -0
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Session record shapes + their constructors and normalisers — the
3
+ * vocabulary both the public store (storage/sessions.ts) and the
4
+ * repository (repositories/sessions-repo.ts) speak, extracted so the
5
+ * repo never has to import the store (which imports the repo).
6
+ *
7
+ * Mutation logic (fold/add/bucket) stays in the store; this module owns
8
+ * only "what a well-formed record looks like" and "how a raw JSON blob
9
+ * becomes one".
10
+ */
11
+
12
+ export type SessionUsage = {
13
+ totalInputTokens: number;
14
+ totalOutputTokens: number;
15
+ totalCacheRead: number;
16
+ totalCacheWrite: number;
17
+ /** Last turn's total prompt tokens (cumulative across all API calls in the turn, including tool-use loops). */
18
+ lastPromptTokens: number;
19
+ /** Actual context window fill from the last API call (last iteration's prompt tokens). */
20
+ contextTokens: number;
21
+ /** Model's context window size in tokens (from SDK modelUsage). */
22
+ contextWindow: number;
23
+ /** Number of API round-trips in the last turn (tool-use steps). */
24
+ numApiCalls: number;
25
+ /** Estimated cost in USD. */
26
+ estimatedCostUsd: number;
27
+ /** Total response time in ms (for averaging). */
28
+ totalResponseMs: number;
29
+ /** Last response time in ms. */
30
+ lastResponseMs: number;
31
+ /** Fastest response time in ms. */
32
+ fastestResponseMs: number;
33
+ };
34
+
35
+ export type MetricsCounterSet = {
36
+ queries: number;
37
+ toolCalls: number;
38
+ turnsWithTools: number;
39
+ apiCalls: number;
40
+ inputTokens: number;
41
+ outputTokens: number;
42
+ cacheReadTokens: number;
43
+ cacheWriteTokens: number;
44
+ failedTurns: number;
45
+ flowViolationRetries: number;
46
+ flowViolationCapExhausted: number;
47
+ trailingTextDropped: number;
48
+ };
49
+
50
+ export type MetricsLatencyAgg = {
51
+ count: number;
52
+ sumMs: number;
53
+ minMs: number;
54
+ maxMs: number;
55
+ };
56
+
57
+ export type MetricsGrain = {
58
+ counters: MetricsCounterSet;
59
+ latency: MetricsLatencyAgg;
60
+ toolCallsByName: Record<string, number>;
61
+ backend: Record<string, MetricsCounterSet & { latency: MetricsLatencyAgg }>;
62
+ cacheHitPercent: MetricsLatencyAgg;
63
+ toolCallsPerTurn: MetricsLatencyAgg;
64
+ apiCallsPerTurn: MetricsLatencyAgg;
65
+ };
66
+
67
+ export type SessionMetrics = {
68
+ lifetime: MetricsGrain;
69
+ buckets: Record<string, MetricsGrain>;
70
+ };
71
+
72
+ export type SessionState = {
73
+ /** Backend server-side session ID. */
74
+ sessionId: string | undefined;
75
+ /** Turn count. */
76
+ turns: number;
77
+ /** Last activity timestamp. */
78
+ lastActive: number;
79
+ /** Created timestamp. */
80
+ createdAt: number;
81
+ /** Cumulative usage stats. */
82
+ usage: SessionUsage;
83
+ /** Persistent per-session metrics, bucketed by UTC day. */
84
+ metrics: SessionMetrics;
85
+ /** ID of the last message sent by the bot in this chat. */
86
+ lastBotMessageId?: number;
87
+ /** Descriptive session name derived from first message. */
88
+ sessionName?: string;
89
+ /** Model used for this session's cost tracking. */
90
+ lastModel?: string;
91
+ };
92
+
93
+ export const emptyUsage = (): SessionUsage => ({
94
+ totalInputTokens: 0,
95
+ totalOutputTokens: 0,
96
+ totalCacheRead: 0,
97
+ totalCacheWrite: 0,
98
+ lastPromptTokens: 0,
99
+ contextTokens: 0,
100
+ contextWindow: 0,
101
+ numApiCalls: 0,
102
+ estimatedCostUsd: 0,
103
+ totalResponseMs: 0,
104
+ lastResponseMs: 0,
105
+ fastestResponseMs: Infinity,
106
+ });
107
+
108
+ export const emptyCounters = (): MetricsCounterSet => ({
109
+ queries: 0,
110
+ toolCalls: 0,
111
+ turnsWithTools: 0,
112
+ apiCalls: 0,
113
+ inputTokens: 0,
114
+ outputTokens: 0,
115
+ cacheReadTokens: 0,
116
+ cacheWriteTokens: 0,
117
+ failedTurns: 0,
118
+ flowViolationRetries: 0,
119
+ flowViolationCapExhausted: 0,
120
+ trailingTextDropped: 0,
121
+ });
122
+
123
+ export const emptyLatency = (): MetricsLatencyAgg => ({
124
+ count: 0,
125
+ sumMs: 0,
126
+ minMs: Infinity,
127
+ maxMs: 0,
128
+ });
129
+
130
+ export const emptyGrain = (): MetricsGrain => ({
131
+ counters: emptyCounters(),
132
+ latency: emptyLatency(),
133
+ toolCallsByName: {},
134
+ backend: {},
135
+ cacheHitPercent: emptyLatency(),
136
+ toolCallsPerTurn: emptyLatency(),
137
+ apiCallsPerTurn: emptyLatency(),
138
+ });
139
+
140
+ export const emptyMetrics = (): SessionMetrics => ({
141
+ lifetime: emptyGrain(),
142
+ buckets: {},
143
+ });
144
+
145
+ function normaliseLatency(raw: unknown): MetricsLatencyAgg {
146
+ const r = raw as Partial<MetricsLatencyAgg> | undefined;
147
+ const count =
148
+ typeof r?.count === "number" && Number.isFinite(r.count) ? r.count : 0;
149
+ return {
150
+ count,
151
+ sumMs:
152
+ typeof r?.sumMs === "number" && Number.isFinite(r.sumMs) ? r.sumMs : 0,
153
+ minMs:
154
+ typeof r?.minMs === "number" && Number.isFinite(r.minMs)
155
+ ? r.minMs
156
+ : count > 0
157
+ ? 0
158
+ : Infinity,
159
+ maxMs:
160
+ typeof r?.maxMs === "number" && Number.isFinite(r.maxMs) ? r.maxMs : 0,
161
+ };
162
+ }
163
+
164
+ function normaliseCounters(raw: unknown): MetricsCounterSet {
165
+ const base = emptyCounters();
166
+ const r = raw as Partial<MetricsCounterSet> | undefined;
167
+ for (const key of Object.keys(base) as Array<keyof MetricsCounterSet>) {
168
+ const value = r?.[key];
169
+ if (typeof value === "number" && Number.isFinite(value)) base[key] = value;
170
+ }
171
+ return base;
172
+ }
173
+
174
+ function normaliseGrain(raw: unknown): MetricsGrain {
175
+ const r = raw as Partial<MetricsGrain> | undefined;
176
+ const grain = emptyGrain();
177
+ grain.counters = normaliseCounters(r?.counters);
178
+ grain.latency = normaliseLatency(r?.latency);
179
+ grain.cacheHitPercent = normaliseLatency(r?.cacheHitPercent);
180
+ grain.toolCallsPerTurn = normaliseLatency(r?.toolCallsPerTurn);
181
+ grain.apiCallsPerTurn = normaliseLatency(r?.apiCallsPerTurn);
182
+ if (r?.toolCallsByName && typeof r.toolCallsByName === "object") {
183
+ for (const [key, value] of Object.entries(r.toolCallsByName)) {
184
+ if (typeof value === "number" && Number.isFinite(value)) {
185
+ grain.toolCallsByName[key] = value;
186
+ }
187
+ }
188
+ }
189
+ if (r?.backend && typeof r.backend === "object") {
190
+ for (const [key, value] of Object.entries(r.backend)) {
191
+ grain.backend[key] = {
192
+ ...normaliseCounters(value),
193
+ latency: normaliseLatency((value as { latency?: unknown }).latency),
194
+ };
195
+ }
196
+ }
197
+ return grain;
198
+ }
199
+
200
+ /**
201
+ * Coerce an untrusted/persisted metrics blob into a well-formed
202
+ * SessionMetrics: missing fields backfilled, non-finite numbers dropped,
203
+ * JSON's `minMs: null` (Infinity doesn't survive JSON.stringify) restored
204
+ * to the domain sentinel. Runs once at the load boundary (sessions-repo);
205
+ * in-memory metrics stay well-formed by construction after that.
206
+ */
207
+ export function normaliseMetrics(metrics: unknown): SessionMetrics {
208
+ const raw = metrics as Partial<SessionMetrics> | undefined;
209
+ const result: SessionMetrics = {
210
+ lifetime: normaliseGrain(raw?.lifetime),
211
+ buckets: {},
212
+ };
213
+ if (raw?.buckets && typeof raw.buckets === "object") {
214
+ for (const [day, grain] of Object.entries(raw.buckets)) {
215
+ if (/^\d{4}-\d{2}-\d{2}$/.test(day))
216
+ result.buckets[day] = normaliseGrain(grain);
217
+ }
218
+ }
219
+ return result;
220
+ }
@@ -24,87 +24,28 @@ import { files } from "../util/paths.js";
24
24
  import { importLegacyJson } from "./legacy-import.js";
25
25
  import * as repo from "./repositories/sessions-repo.js";
26
26
 
27
- export type SessionUsage = {
28
- totalInputTokens: number;
29
- totalOutputTokens: number;
30
- totalCacheRead: number;
31
- totalCacheWrite: number;
32
- /** Last turn's total prompt tokens (cumulative across all API calls in the turn, including tool-use loops). */
33
- lastPromptTokens: number;
34
- /** Actual context window fill from the last API call (last iteration's prompt tokens). */
35
- contextTokens: number;
36
- /** Model's context window size in tokens (from SDK modelUsage). */
37
- contextWindow: number;
38
- /** Number of API round-trips in the last turn (tool-use steps). */
39
- numApiCalls: number;
40
- /** Estimated cost in USD. */
41
- estimatedCostUsd: number;
42
- /** Total response time in ms (for averaging). */
43
- totalResponseMs: number;
44
- /** Last response time in ms. */
45
- lastResponseMs: number;
46
- /** Fastest response time in ms. */
47
- fastestResponseMs: number;
48
- };
49
-
50
- export type MetricsCounterSet = {
51
- queries: number;
52
- toolCalls: number;
53
- turnsWithTools: number;
54
- apiCalls: number;
55
- inputTokens: number;
56
- outputTokens: number;
57
- cacheReadTokens: number;
58
- cacheWriteTokens: number;
59
- failedTurns: number;
60
- flowViolationRetries: number;
61
- flowViolationCapExhausted: number;
62
- trailingTextDropped: number;
63
- };
64
-
65
- export type MetricsLatencyAgg = {
66
- count: number;
67
- sumMs: number;
68
- minMs: number;
69
- maxMs: number;
70
- };
71
-
72
- export type MetricsGrain = {
73
- counters: MetricsCounterSet;
74
- latency: MetricsLatencyAgg;
75
- toolCallsByName: Record<string, number>;
76
- backend: Record<string, MetricsCounterSet & { latency: MetricsLatencyAgg }>;
77
- cacheHitPercent: MetricsLatencyAgg;
78
- toolCallsPerTurn: MetricsLatencyAgg;
79
- apiCallsPerTurn: MetricsLatencyAgg;
80
- };
81
-
82
- export type SessionMetrics = {
83
- lifetime: MetricsGrain;
84
- buckets: Record<string, MetricsGrain>;
85
- };
86
-
87
- export type SessionState = {
88
- /** Backend server-side session ID. */
89
- sessionId: string | undefined;
90
- /** Turn count. */
91
- turns: number;
92
- /** Last activity timestamp. */
93
- lastActive: number;
94
- /** Created timestamp. */
95
- createdAt: number;
96
- /** Cumulative usage stats. */
97
- usage: SessionUsage;
98
- /** Persistent per-session metrics, bucketed by UTC day. */
99
- metrics: SessionMetrics;
100
- /** ID of the last message sent by the bot in this chat. */
101
- lastBotMessageId?: number;
102
- /** Descriptive session name derived from first message. */
103
- sessionName?: string;
104
- /** Model used for this session's cost tracking. */
105
- lastModel?: string;
106
- };
107
-
27
+ export type {
28
+ MetricsCounterSet,
29
+ MetricsGrain,
30
+ MetricsLatencyAgg,
31
+ SessionMetrics,
32
+ SessionState,
33
+ SessionUsage,
34
+ } from "./session-record.js";
35
+ export { emptyMetrics, normaliseMetrics } from "./session-record.js";
36
+ import {
37
+ emptyCounters,
38
+ emptyGrain,
39
+ emptyLatency,
40
+ emptyMetrics,
41
+ emptyUsage,
42
+ type MetricsCounterSet,
43
+ type MetricsGrain,
44
+ type MetricsLatencyAgg,
45
+ type SessionMetrics,
46
+ type SessionState,
47
+ type SessionUsage,
48
+ } from "./session-record.js";
108
49
  // In-memory cache over the sessions table. Reads serve live references
109
50
  // from here; writes go through persist() so each mutation commits.
110
51
  const cache = new Map<string, SessionState>();
@@ -273,135 +214,6 @@ function persist(chatId: string, session: SessionState): void {
273
214
 
274
215
  // ── Core operations ─────────────────────────────────────────────────────────
275
216
 
276
- const emptyUsage = (): SessionUsage => ({
277
- totalInputTokens: 0,
278
- totalOutputTokens: 0,
279
- totalCacheRead: 0,
280
- totalCacheWrite: 0,
281
- lastPromptTokens: 0,
282
- contextTokens: 0,
283
- contextWindow: 0,
284
- numApiCalls: 0,
285
- estimatedCostUsd: 0,
286
- totalResponseMs: 0,
287
- lastResponseMs: 0,
288
- fastestResponseMs: Infinity,
289
- });
290
-
291
- const emptyCounters = (): MetricsCounterSet => ({
292
- queries: 0,
293
- toolCalls: 0,
294
- turnsWithTools: 0,
295
- apiCalls: 0,
296
- inputTokens: 0,
297
- outputTokens: 0,
298
- cacheReadTokens: 0,
299
- cacheWriteTokens: 0,
300
- failedTurns: 0,
301
- flowViolationRetries: 0,
302
- flowViolationCapExhausted: 0,
303
- trailingTextDropped: 0,
304
- });
305
-
306
- const emptyLatency = (): MetricsLatencyAgg => ({
307
- count: 0,
308
- sumMs: 0,
309
- minMs: Infinity,
310
- maxMs: 0,
311
- });
312
-
313
- const emptyGrain = (): MetricsGrain => ({
314
- counters: emptyCounters(),
315
- latency: emptyLatency(),
316
- toolCallsByName: {},
317
- backend: {},
318
- cacheHitPercent: emptyLatency(),
319
- toolCallsPerTurn: emptyLatency(),
320
- apiCallsPerTurn: emptyLatency(),
321
- });
322
-
323
- export const emptyMetrics = (): SessionMetrics => ({
324
- lifetime: emptyGrain(),
325
- buckets: {},
326
- });
327
-
328
- function normaliseLatency(raw: unknown): MetricsLatencyAgg {
329
- const r = raw as Partial<MetricsLatencyAgg> | undefined;
330
- const count =
331
- typeof r?.count === "number" && Number.isFinite(r.count) ? r.count : 0;
332
- return {
333
- count,
334
- sumMs:
335
- typeof r?.sumMs === "number" && Number.isFinite(r.sumMs) ? r.sumMs : 0,
336
- minMs:
337
- typeof r?.minMs === "number" && Number.isFinite(r.minMs)
338
- ? r.minMs
339
- : count > 0
340
- ? 0
341
- : Infinity,
342
- maxMs:
343
- typeof r?.maxMs === "number" && Number.isFinite(r.maxMs) ? r.maxMs : 0,
344
- };
345
- }
346
-
347
- function normaliseCounters(raw: unknown): MetricsCounterSet {
348
- const base = emptyCounters();
349
- const r = raw as Partial<MetricsCounterSet> | undefined;
350
- for (const key of Object.keys(base) as Array<keyof MetricsCounterSet>) {
351
- const value = r?.[key];
352
- if (typeof value === "number" && Number.isFinite(value)) base[key] = value;
353
- }
354
- return base;
355
- }
356
-
357
- function normaliseGrain(raw: unknown): MetricsGrain {
358
- const r = raw as Partial<MetricsGrain> | undefined;
359
- const grain = emptyGrain();
360
- grain.counters = normaliseCounters(r?.counters);
361
- grain.latency = normaliseLatency(r?.latency);
362
- grain.cacheHitPercent = normaliseLatency(r?.cacheHitPercent);
363
- grain.toolCallsPerTurn = normaliseLatency(r?.toolCallsPerTurn);
364
- grain.apiCallsPerTurn = normaliseLatency(r?.apiCallsPerTurn);
365
- if (r?.toolCallsByName && typeof r.toolCallsByName === "object") {
366
- for (const [key, value] of Object.entries(r.toolCallsByName)) {
367
- if (typeof value === "number" && Number.isFinite(value)) {
368
- grain.toolCallsByName[key] = value;
369
- }
370
- }
371
- }
372
- if (r?.backend && typeof r.backend === "object") {
373
- for (const [key, value] of Object.entries(r.backend)) {
374
- grain.backend[key] = {
375
- ...normaliseCounters(value),
376
- latency: normaliseLatency((value as { latency?: unknown }).latency),
377
- };
378
- }
379
- }
380
- return grain;
381
- }
382
-
383
- /**
384
- * Coerce an untrusted/persisted metrics blob into a well-formed
385
- * SessionMetrics: missing fields backfilled, non-finite numbers dropped,
386
- * JSON's `minMs: null` (Infinity doesn't survive JSON.stringify) restored
387
- * to the domain sentinel. Runs once at the load boundary (sessions-repo);
388
- * in-memory metrics stay well-formed by construction after that.
389
- */
390
- export function normaliseMetrics(metrics: unknown): SessionMetrics {
391
- const raw = metrics as Partial<SessionMetrics> | undefined;
392
- const result: SessionMetrics = {
393
- lifetime: normaliseGrain(raw?.lifetime),
394
- buckets: {},
395
- };
396
- if (raw?.buckets && typeof raw.buckets === "object") {
397
- for (const [day, grain] of Object.entries(raw.buckets)) {
398
- if (/^\d{4}-\d{2}-\d{2}$/.test(day))
399
- result.buckets[day] = normaliseGrain(grain);
400
- }
401
- }
402
- return result;
403
- }
404
-
405
217
  function addLatency(agg: MetricsLatencyAgg, value: number): void {
406
218
  if (!Number.isFinite(value)) return;
407
219
  agg.count += 1;
@@ -27,66 +27,12 @@ import * as repo from "./repositories/triggers-repo.js";
27
27
 
28
28
  // ── Types ────────────────────────────────────────────────────────────────────
29
29
 
30
- export type TriggerLanguage = "bash" | "python" | "node" | "lua";
31
-
32
- export type TriggerStatus =
33
- | "pending" // created, not yet spawned (transient)
34
- | "running" // child process alive
35
- | "fired" // exited 0 fired final wake message
36
- | "errored" // exited non-zero — fired error wake message
37
- | "cancelled" // killed by user (trigger_cancel)
38
- | "timed_out" // killed by hard timeout
39
- | "terminated"; // killed by Talon shutdown / restart
40
-
41
- export type Trigger = {
42
- id: string;
43
- chatId: string;
44
- numericChatId: number;
45
- name: string;
46
- language: TriggerLanguage;
47
- /** Absolute path to the script body on disk. */
48
- scriptPath: string;
49
- /** Absolute path to the run log (interleaved stdout+stderr). */
50
- logPath: string;
51
- description?: string;
52
- status: TriggerStatus;
53
- createdAt: number;
54
- startedAt?: number;
55
- endedAt?: number;
56
- /** PID of the child process while running (cleared on exit). */
57
- pid?: number;
58
- /** Linux /proc/<pid>/stat field 22 (start time in jiffies) captured at
59
- * spawn. Used by killOrphan to defend against PID reuse — start time is
60
- * monotonic per boot and unchanged by exec(), so a match guarantees the
61
- * current owner of the PID is the same process we spawned. Undefined on
62
- * non-Linux platforms. */
63
- pidStarttime?: number;
64
- /** Hard timeout in seconds. Default 24h, max 7d. */
65
- timeoutSeconds: number;
66
- /** Exit code on terminal status. */
67
- exitCode?: number;
68
- /** Total wake-ups fired for this trigger — sum of mid-run TALON_FIRE: lines
69
- * plus the terminal exit fire. Incremented every time fireWake() runs. */
70
- fireCount: number;
71
- lastFireAt?: number;
72
- /** Truncated tail of the most recent fire payload (for diagnostics). */
73
- lastFirePayload?: string;
74
- lastError?: string;
75
- /** If true, the trigger is respawned on Talon startup if it was still
76
- * active when Talon went down. Triggers in any terminal state
77
- * (fired/errored/cancelled) are NOT respawned — only ones interrupted
78
- * by Talon shutdown or crash. (Persistent triggers have no hard timeout,
79
- * so timed_out is unreachable for them — see spawnTrigger.) */
80
- persistent?: boolean;
81
- /**
82
- * Optional model override for the wake-up turn — a model id valid on the
83
- * chat's own backend. Unset = inherit the chat's model (preferred). When set,
84
- * the fired wake-up runs on this (typically cheaper) model instead, while
85
- * still resuming the chat session (restricted to the same backend so
86
- * continuity is preserved).
87
- */
88
- model?: string;
89
- };
30
+ export type {
31
+ Trigger,
32
+ TriggerLanguage,
33
+ TriggerStatus,
34
+ } from "./repositories/triggers-repo.js";
35
+ import type { Trigger, TriggerLanguage } from "./repositories/triggers-repo.js";
90
36
 
91
37
  // ── Constants ────────────────────────────────────────────────────────────────
92
38
 
@@ -0,0 +1,9 @@
1
+ // Ambient declaration for prompt file-asset imports. The Bun-only
2
+ // `import x from "./foo.md" with { type: "file" }` form (used only by the
3
+ // generated embedded-prompts.ts, reached through the `bun` condition of
4
+ // the `#prompt-assets` package import) resolves to the embedded file path
5
+ // as a string. tsx/node never load that module — they use disk-prompts.ts.
6
+ declare module "*.md" {
7
+ const path: string;
8
+ export default path;
9
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Reasoning-effort vocabulary — a dependency-free primitive shared by
3
+ * every layer (storage persists it, core resolves it, backends map it
4
+ * to provider knobs, frontends render pickers over it).
5
+ *
6
+ * Lives in src/types/ (a leaf: imports nothing from src/) so the layers
7
+ * below core/ can name the type without importing upward. core/types.ts
8
+ * re-exports it for the engine-side importers.
9
+ */
10
+
11
+ export type ReasoningEffortLevel =
12
+ "off" | "minimal" | "low" | "medium" | "high" | "max" | "xhigh";
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, mkdirSync } from "node:fs";
2
2
  import writeFileAtomic from "write-file-atomic";
3
3
  import { z } from "zod";
4
4
  import { dirs, files as pathFiles } from "./paths.js";
5
+ import { hardenTalonPermissions } from "./harden.js";
5
6
  import { setTimezone } from "./time.js";
6
7
  import { BACKEND_IDS } from "../core/agent-runtime/model-ref.js";
7
8
  import {
@@ -137,6 +138,8 @@ const frontendEnum = z.enum([
137
138
  * Defaults are loopback-only and unauthenticated (single-machine use). To
138
139
  * reach Talon remotely, set `host: "0.0.0.0"` and a `token` — the bridge
139
140
  * then requires `Authorization: Bearer <token>` (or `?token=` for SSE).
141
+ * A non-loopback bind with no token auto-mints a persistent one
142
+ * (~/.talon/keys/bridge-token) rather than serving the LAN open.
140
143
  */
141
144
  const nativeConfigSchema = z
142
145
  .object({
@@ -150,8 +153,9 @@ const nativeConfigSchema = z
150
153
  host: z.string().default("127.0.0.1"),
151
154
  /**
152
155
  * Optional shared secret. When set, every request must present it as a
153
- * bearer token (header) or `?token=` query param (SSE). Required in
154
- * practice whenever `host` is not loopback.
156
+ * bearer token (header) or `?token=` query param (SSE). When unset on a
157
+ * non-loopback `host`, the bridge mints and persists one automatically
158
+ * (~/.talon/keys/bridge-token) — the network never gets an open bridge.
155
159
  */
156
160
  token: z.string().optional(),
157
161
  /**
@@ -596,9 +600,11 @@ function ensureConfigFile(): boolean {
596
600
  if (!existsSync(dirs.root)) mkdirSync(dirs.root, { recursive: true });
597
601
  if (!existsSync(dirs.data)) mkdirSync(dirs.data, { recursive: true });
598
602
  if (!existsSync(CONFIG_FILE)) {
603
+ // Owner-only from birth: the config accumulates bot tokens and API keys.
599
604
  writeFileAtomic.sync(
600
605
  CONFIG_FILE,
601
606
  JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n",
607
+ { mode: 0o600 },
602
608
  );
603
609
  return true;
604
610
  }
@@ -613,6 +619,7 @@ function ensureConfigFile(): boolean {
613
619
 
614
620
  export function loadConfig(): TalonConfig {
615
621
  ensureConfigFile();
622
+ hardenTalonPermissions();
616
623
  const fileConfig = normalizeDeprecatedFrontendConfig(loadConfigFile());
617
624
 
618
625
  // Runtime frontend override (TALON_FRONTEND_OVERRIDE). Lets the native
@@ -0,0 +1,50 @@
1
+ /**
2
+ * At-rest permission hardening for ~/.talon/.
3
+ *
4
+ * The tree holds credentials (config.json carries bot tokens, API keys and
5
+ * the bridge token; .user-session is a full Telegram login) plus chat
6
+ * history and logs. Node creates most of it with umask defaults, so on a
7
+ * multi-user machine everything is group/world readable. This pass clamps
8
+ * the sensitive surface to owner-only on every boot — idempotent, cheap,
9
+ * and it repairs installs created before the tightened modes existed.
10
+ *
11
+ * Best-effort by design: a missing path just hasn't been created yet, and
12
+ * a chmod failure (read-only mount, foreign owner) must never block boot.
13
+ * Windows has no POSIX modes worth mapping, so the pass is a no-op there.
14
+ */
15
+
16
+ import { chmodSync } from "node:fs";
17
+ import { dirs, files } from "./paths.js";
18
+
19
+ const OWNER_DIR = 0o700;
20
+ const OWNER_FILE = 0o600;
21
+
22
+ /** Owner-only directories: everything under them inherits the protection. */
23
+ const HARDENED_DIRS: readonly string[] = [dirs.root, dirs.data, dirs.keys];
24
+
25
+ /** Belt-and-braces owner-only files, should they ever move out of the tree. */
26
+ const HARDENED_FILES: readonly string[] = [
27
+ files.config,
28
+ files.log,
29
+ files.database,
30
+ files.userSession,
31
+ ];
32
+
33
+ /** Clamp permissions on the sensitive parts of ~/.talon/. */
34
+ export function hardenTalonPermissions(): void {
35
+ if (process.platform === "win32") return;
36
+ for (const dir of HARDENED_DIRS) {
37
+ try {
38
+ chmodSync(dir, OWNER_DIR);
39
+ } catch {
40
+ // absent or not ours — nothing to protect yet
41
+ }
42
+ }
43
+ for (const file of HARDENED_FILES) {
44
+ try {
45
+ chmodSync(file, OWNER_FILE);
46
+ } catch {
47
+ // absent or not ours — nothing to protect yet
48
+ }
49
+ }
50
+ }
package/src/util/log.ts CHANGED
@@ -137,7 +137,8 @@ if (!quiet) {
137
137
  if (!IS_VITEST) {
138
138
  streams.push({
139
139
  level: "trace",
140
- stream: createWriteStream(LOG_FILE, { flags: "a" }),
140
+ // 0600: turns and tool output land here — same sensitivity as history.
141
+ stream: createWriteStream(LOG_FILE, { flags: "a", mode: 0o600 }),
141
142
  });
142
143
  }
143
144
 
package/tsconfig.json CHANGED
@@ -5,6 +5,12 @@
5
5
  "moduleResolution": "bundler",
6
6
  "resolveJsonModule": true,
7
7
  "strict": true,
8
+ // Beyond-strict gates. Two more are measured but not yet on — the fix
9
+ // waves are their own PRs: noUncheckedIndexedAccess (666 errors),
10
+ // exactOptionalPropertyTypes (303 errors).
11
+ "noImplicitOverride": true,
12
+ "noFallthroughCasesInSwitch": true,
13
+ "verbatimModuleSyntax": true,
8
14
  "esModuleInterop": true,
9
15
  "skipLibCheck": true,
10
16
  "outDir": "dist",