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.
- package/package.json +8 -5
- package/src/backend/kilo/models/index.ts +4 -1
- package/src/backend/kilo/server.ts +17 -4
- package/src/backend/openai-agents/session.ts +1 -1
- package/src/backend/opencode/models/index.ts +4 -1
- package/src/backend/opencode/server.ts +17 -4
- package/src/backend/shared/system-prompt.ts +5 -0
- package/src/cli/events.ts +1 -1
- package/src/cli/index.ts +1 -1
- package/src/cli/tasks.ts +5 -1
- package/src/core/background/triggers/pid.ts +28 -0
- package/src/core/background/triggers/resume.ts +4 -24
- package/src/core/background/triggers/spawn.ts +1 -1
- package/src/core/engine/gateway-actions/plugins.ts +3 -3
- package/src/core/prompt/invalidation.ts +23 -0
- package/src/core/types.ts +2 -2
- package/src/frontend/discord/callbacks/components.ts +1 -1
- package/src/frontend/discord/commands/settings.ts +1 -1
- package/src/frontend/native/auth.ts +52 -0
- package/src/frontend/native/extensions.ts +2 -2
- package/src/frontend/native/index.ts +8 -2
- package/src/frontend/native/server.ts +96 -15
- package/src/frontend/telegram/callbacks/model.ts +1 -1
- package/src/frontend/telegram/commands/admin.ts +1 -1
- package/src/frontend/telegram/commands/settings.ts +1 -1
- package/src/frontend/telegram/formatting.ts +1 -1
- package/src/frontend/terminal/commands.ts +1 -1
- package/src/native/htmlents-wasm-bytes.ts +4 -4
- package/src/native/htmlents.ts +4 -4
- package/src/native/registry.ts +9 -9
- package/src/native/runtime.ts +2 -2
- package/src/native/sqlguard-wasm-bytes.ts +4 -4
- package/src/native/sqlguard.ts +2 -2
- package/src/native/strsim-wasm-bytes.ts +4 -4
- package/src/native/strsim.ts +3 -3
- package/src/plugins/github/index.ts +1 -1
- package/src/plugins/mem0/index.ts +1 -1
- package/src/plugins/mempalace/index.ts +1 -1
- package/src/plugins/playwright/index.ts +1 -1
- package/src/storage/chat-settings.ts +3 -60
- package/src/storage/cron-store.ts +6 -80
- package/src/storage/goal-store.ts +10 -19
- package/src/storage/history.ts +3 -14
- package/src/storage/journal.ts +20 -9
- package/src/storage/media-index.ts +2 -22
- package/src/storage/repositories/chat-settings-repo.ts +55 -1
- package/src/storage/repositories/cron-repo.ts +82 -6
- package/src/storage/repositories/goals-repo.ts +20 -1
- package/src/storage/repositories/history-repo.ts +14 -1
- package/src/storage/repositories/media-index-repo.ts +23 -1
- package/src/storage/repositories/scripts-repo.ts +16 -1
- package/src/storage/repositories/sessions-repo.ts +1 -1
- package/src/storage/repositories/triggers-repo.ts +60 -5
- package/src/storage/script-store.ts +2 -15
- package/src/storage/session-record.ts +220 -0
- package/src/storage/sessions.ts +22 -210
- package/src/storage/trigger-store.ts +6 -60
- package/src/types/assets.d.ts +9 -0
- package/src/types/effort.ts +12 -0
- package/src/util/config.ts +9 -2
- package/src/util/harden.ts +50 -0
- package/src/util/log.ts +2 -1
- package/tsconfig.json +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talon-agent",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "Multi-frontend AI agent with full tool access, streaming, cron jobs, and plugin system",
|
|
5
5
|
"author": "Dylan Neve",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"src/native/",
|
|
43
43
|
"src/plugins/",
|
|
44
44
|
"src/storage/",
|
|
45
|
+
"src/types/",
|
|
45
46
|
"src/util/",
|
|
46
47
|
"src/app.ts",
|
|
47
48
|
"src/bootstrap.ts",
|
|
@@ -70,22 +71,22 @@
|
|
|
70
71
|
"test:openai-agents:backend": "vitest run --reporter=verbose --reporter=json --outputFile=openai-agents-backend-results.json src/__tests__/integration/openai-agents-live-discovery.test.ts",
|
|
71
72
|
"build:sql": "tsx scripts/embed-sql.ts",
|
|
72
73
|
"build:prompts": "tsx scripts/embed-prompts.ts",
|
|
73
|
-
"build:wasm": "node native/blake3-wasm/build.mjs",
|
|
74
|
+
"build:wasm": "node native/blake3-wasm/build.mjs && node native/strsim-wasm/build.mjs && node native/sqlguard-wasm/build.mjs && node native/htmlents-wasm/build.mjs",
|
|
74
75
|
"tarball:check": "node .github/scripts/tarball-check.mjs",
|
|
75
76
|
"build:stub-sea": "node src/__tests__/integration/stub-claude/build-sea.mjs",
|
|
76
77
|
"test:watch": "vitest",
|
|
77
78
|
"test:coverage": "vitest run --coverage",
|
|
78
79
|
"typecheck": "tsc --noEmit",
|
|
79
80
|
"lint": "oxlint src/",
|
|
81
|
+
"depcruise": "depcruise src",
|
|
82
|
+
"ratchets": "node scripts/check-ratchets.mjs",
|
|
80
83
|
"knip": "knip",
|
|
81
84
|
"format": "prettier --write src/ prompts/",
|
|
82
85
|
"format:check": "prettier --check src/ prompts/",
|
|
83
86
|
"ci:protect": "node .github/scripts/enforce-ci-gate.mjs",
|
|
84
87
|
"build:gleam": "cd native/scheduler-core && gleam build --target javascript && node embed.mjs",
|
|
85
88
|
"build:zig": "node native/textops-wasm/build.mjs",
|
|
86
|
-
"build:
|
|
87
|
-
"build:cpp": "node native/htmlents-cpp/build.mjs",
|
|
88
|
-
"build:native": "npm run build:wasm && npm run build:zig && npm run build:c && npm run build:cpp && npm run build:gleam",
|
|
89
|
+
"build:native": "npm run build:wasm && npm run build:zig && npm run build:gleam",
|
|
89
90
|
"build:driver": "node native/talon-driver/build.mjs",
|
|
90
91
|
"build:driver:all": "node native/talon-driver/build.mjs --all",
|
|
91
92
|
"build:warden": "node native/talon-warden/build.mjs",
|
|
@@ -130,9 +131,11 @@
|
|
|
130
131
|
"zod": "^4.3.6"
|
|
131
132
|
},
|
|
132
133
|
"devDependencies": {
|
|
134
|
+
"@swc/core": "^1.15.46",
|
|
133
135
|
"@types/node": "^26.0.0",
|
|
134
136
|
"@types/write-file-atomic": "^4.0.3",
|
|
135
137
|
"@vitest/coverage-v8": "^4.1.3",
|
|
138
|
+
"dependency-cruiser": "^18.1.0",
|
|
136
139
|
"fast-check": "^4.6.0",
|
|
137
140
|
"knip": "^6.3.1",
|
|
138
141
|
"oxlint": "^1.59.0",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Kilo ids routinely contain "/" and ":" (e.g. "inclusionai/ling-2.6-1t:free").
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { ensureServer } from "../server.js";
|
|
16
|
+
import { ensureServer, onServerStop } from "../server.js";
|
|
17
17
|
import { createRemoteModelCatalogModule } from "../../remote-server/model-catalog/index.js";
|
|
18
18
|
|
|
19
19
|
export type {
|
|
@@ -41,6 +41,9 @@ const kiloModels = createRemoteModelCatalogModule({
|
|
|
41
41
|
quickPickLimit: 24,
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
// A stopped server invalidates the catalog it served.
|
|
45
|
+
onServerStop(kiloModels.clearCache);
|
|
46
|
+
|
|
44
47
|
export const getOpenCodeModelCatalog = kiloModels.getCatalog;
|
|
45
48
|
export const clearModelCatalogCache = kiloModels.clearCache;
|
|
46
49
|
export const getOpenCodeModelInfo = kiloModels.getModelInfo;
|
|
@@ -25,13 +25,12 @@ import type { TalonConfig } from "../../util/config.js";
|
|
|
25
25
|
import type { FrontendName } from "../../core/agent-runtime/backend-registry.js";
|
|
26
26
|
import { logWarn } from "../../util/log.js";
|
|
27
27
|
import { buildDeliveryContract } from "../shared/delivery-contract.js";
|
|
28
|
-
import { clearModelCatalogCache } from "./models/index.js";
|
|
29
28
|
import {
|
|
30
29
|
guessProviderID,
|
|
31
30
|
getBucketPriority,
|
|
32
31
|
normalizeModelLookup,
|
|
33
|
-
parseOpenCodeModelQuery,
|
|
34
|
-
} from "
|
|
32
|
+
parseRemoteModelQuery as parseOpenCodeModelQuery,
|
|
33
|
+
} from "../remote-server/model-catalog/index.js";
|
|
35
34
|
import {
|
|
36
35
|
createRemoteServerState,
|
|
37
36
|
ensureRemoteServer as ensureRemoteServerShared,
|
|
@@ -153,6 +152,18 @@ async function prewarmPluginMcpServers(): Promise<void> {
|
|
|
153
152
|
await ensurePluginMcpServers(client, "prewarm");
|
|
154
153
|
}
|
|
155
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Callbacks run when the server stops — cache invalidation lives with the
|
|
157
|
+
* caches. The models module registers its clear here at load time, so the
|
|
158
|
+
* server never has to import it (which would be a cycle).
|
|
159
|
+
*/
|
|
160
|
+
const stopHooks = new Set<() => void>();
|
|
161
|
+
|
|
162
|
+
/** Register a callback to run whenever the Kilo server is stopped. */
|
|
163
|
+
export function onServerStop(hook: () => void): void {
|
|
164
|
+
stopHooks.add(hook);
|
|
165
|
+
}
|
|
166
|
+
|
|
156
167
|
/**
|
|
157
168
|
* Stop the local Kilo server (if we own it) and clear caches.
|
|
158
169
|
*
|
|
@@ -160,7 +171,9 @@ async function prewarmPluginMcpServers(): Promise<void> {
|
|
|
160
171
|
* server, this leaves it running — we don't own it.
|
|
161
172
|
*/
|
|
162
173
|
export function stopKiloServer(): void {
|
|
163
|
-
stopRemoteServer(state,
|
|
174
|
+
stopRemoteServer(state, () => {
|
|
175
|
+
for (const hook of stopHooks) hook();
|
|
176
|
+
});
|
|
164
177
|
}
|
|
165
178
|
|
|
166
179
|
// ── Server lifecycle ────────────────────────────────────────────────────────
|
|
@@ -251,7 +251,7 @@ export class TalonSession extends MemorySession {
|
|
|
251
251
|
return current;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
async addItems(items: AgentInputItem[]): Promise<void> {
|
|
254
|
+
override async addItems(items: AgentInputItem[]): Promise<void> {
|
|
255
255
|
await super.addItems(items);
|
|
256
256
|
await this.enforceCap();
|
|
257
257
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* stay at 4 and only short separator-free ids are embedded raw.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { ensureServer } from "../server.js";
|
|
15
|
+
import { ensureServer, onServerStop } from "../server.js";
|
|
16
16
|
import { createRemoteModelCatalogModule } from "../../remote-server/model-catalog/index.js";
|
|
17
17
|
|
|
18
18
|
export type {
|
|
@@ -40,6 +40,9 @@ const opencodeModels = createRemoteModelCatalogModule({
|
|
|
40
40
|
quickPickLimit: 4,
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
// A stopped server invalidates the catalog it served.
|
|
44
|
+
onServerStop(opencodeModels.clearCache);
|
|
45
|
+
|
|
43
46
|
export const getOpenCodeModelCatalog = opencodeModels.getCatalog;
|
|
44
47
|
export const clearModelCatalogCache = opencodeModels.clearCache;
|
|
45
48
|
export const getOpenCodeModelInfo = opencodeModels.getModelInfo;
|
|
@@ -25,13 +25,12 @@ import type { TalonConfig } from "../../util/config.js";
|
|
|
25
25
|
import type { FrontendName } from "../../core/agent-runtime/backend-registry.js";
|
|
26
26
|
import { logWarn } from "../../util/log.js";
|
|
27
27
|
import { buildDeliveryContract } from "../shared/delivery-contract.js";
|
|
28
|
-
import { clearModelCatalogCache } from "./models/index.js";
|
|
29
28
|
import {
|
|
30
29
|
guessProviderID,
|
|
31
30
|
getBucketPriority,
|
|
32
31
|
normalizeModelLookup,
|
|
33
|
-
parseOpenCodeModelQuery,
|
|
34
|
-
} from "
|
|
32
|
+
parseRemoteModelQuery as parseOpenCodeModelQuery,
|
|
33
|
+
} from "../remote-server/model-catalog/index.js";
|
|
35
34
|
import {
|
|
36
35
|
createRemoteServerState,
|
|
37
36
|
ensureRemoteServer as ensureRemoteServerShared,
|
|
@@ -108,8 +107,22 @@ async function prewarmPluginMcpServers(): Promise<void> {
|
|
|
108
107
|
await ensurePluginMcpServers(client, "prewarm");
|
|
109
108
|
}
|
|
110
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Callbacks run when the server stops — cache invalidation lives with the
|
|
112
|
+
* caches. The models module registers its clear here at load time, so the
|
|
113
|
+
* server never has to import it (which would be a cycle).
|
|
114
|
+
*/
|
|
115
|
+
const stopHooks = new Set<() => void>();
|
|
116
|
+
|
|
117
|
+
/** Register a callback to run whenever the OpenCode server is stopped. */
|
|
118
|
+
export function onServerStop(hook: () => void): void {
|
|
119
|
+
stopHooks.add(hook);
|
|
120
|
+
}
|
|
121
|
+
|
|
111
122
|
export function stopOpenCodeServer(): void {
|
|
112
|
-
stopRemoteServer(state,
|
|
123
|
+
stopRemoteServer(state, () => {
|
|
124
|
+
for (const hook of stopHooks) hook();
|
|
125
|
+
});
|
|
113
126
|
}
|
|
114
127
|
|
|
115
128
|
// ── Server lifecycle ────────────────────────────────────────────────────────
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
type TalonConfig,
|
|
42
42
|
} from "../../util/config.js";
|
|
43
43
|
import { getPluginPromptAdditions } from "../../core/plugin/index.js";
|
|
44
|
+
import { onPromptInputsChanged } from "../../core/prompt/invalidation.js";
|
|
44
45
|
import { frontendForChatId, nonTerminalFrontends } from "./frontends.js";
|
|
45
46
|
|
|
46
47
|
// ── Types ───────────────────────────────────────────────────────────────────
|
|
@@ -112,6 +113,10 @@ export function clearSystemPromptSnapshots(): void {
|
|
|
112
113
|
snapshots.clear();
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
// Core and frontends invalidate through the core-side seam — they never
|
|
117
|
+
// import this module (layer rule: nothing above backend/ reaches into it).
|
|
118
|
+
onPromptInputsChanged(clearSystemPromptSnapshots);
|
|
119
|
+
|
|
115
120
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
116
121
|
|
|
117
122
|
/**
|
package/src/cli/events.ts
CHANGED
|
@@ -52,7 +52,7 @@ async function showHistory(limit: number): Promise<void> {
|
|
|
52
52
|
const { readJournal } = await import("../storage/journal.js");
|
|
53
53
|
let entries;
|
|
54
54
|
try {
|
|
55
|
-
entries = readJournal({ limit });
|
|
55
|
+
entries = readJournal<TalonEvent & { at: number }>({ limit });
|
|
56
56
|
} catch (err) {
|
|
57
57
|
console.log(
|
|
58
58
|
` ${pc.red("✖")} Could not read the journal: ${err instanceof Error ? err.message : err}\n`,
|
package/src/cli/index.ts
CHANGED
|
@@ -172,7 +172,7 @@ export async function runCli(): Promise<void> {
|
|
|
172
172
|
mainMenu();
|
|
173
173
|
break;
|
|
174
174
|
default: {
|
|
175
|
-
// "did you mean ...?" via the
|
|
175
|
+
// "did you mean ...?" via the native similarity core (native/strsim-wasm).
|
|
176
176
|
const { closestMatch } = await import("../native/strsim.js");
|
|
177
177
|
const suggestion = closestMatch(command, CLI_COMMANDS);
|
|
178
178
|
const hint = suggestion
|
package/src/cli/tasks.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
TaskRecord,
|
|
17
17
|
TaskState,
|
|
18
18
|
} from "../core/tasks/index.js";
|
|
19
|
+
import type { PublishedEvent } from "../core/bus/index.js";
|
|
19
20
|
|
|
20
21
|
function formatDuration(ms: number): string {
|
|
21
22
|
const seconds = Math.floor(ms / 1000);
|
|
@@ -120,7 +121,10 @@ async function journalTasks(
|
|
|
120
121
|
const { readJournal } = await import("../storage/journal.js");
|
|
121
122
|
const seen = new Set(liveTasks.map((t) => `${t.id}:${t.queuedAt}`));
|
|
122
123
|
const historical: TaskRecord[] = [];
|
|
123
|
-
for (const entry of readJournal({
|
|
124
|
+
for (const entry of readJournal<PublishedEvent>({
|
|
125
|
+
type: "task.settled",
|
|
126
|
+
limit,
|
|
127
|
+
})) {
|
|
124
128
|
if (entry.event.type !== "task.settled") continue;
|
|
125
129
|
const task = entry.event.task;
|
|
126
130
|
// (id, queuedAt) identifies a run across surfaces — per-process ids
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PID-starttime probe — the /proc read shared by spawn (capture at fork)
|
|
3
|
+
* and resume (compare after restart). Lives alone so neither module has
|
|
4
|
+
* to import the other for it.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read field 22 (start time in jiffies since boot) from /proc/<pid>/stat.
|
|
11
|
+
* Returns undefined if /proc isn't available (non-Linux) or the read fails.
|
|
12
|
+
*
|
|
13
|
+
* Parsing note: the `comm` field (2nd) is wrapped in parens and may itself
|
|
14
|
+
* contain ')' — the safe parse finds the LAST ')' and splits the rest on
|
|
15
|
+
* space. After that split, index 19 corresponds to field 22.
|
|
16
|
+
*/
|
|
17
|
+
export function readPidStarttimeSync(pid: number): number | undefined {
|
|
18
|
+
try {
|
|
19
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf-8");
|
|
20
|
+
const lastParen = stat.lastIndexOf(")");
|
|
21
|
+
if (lastParen < 0) return undefined;
|
|
22
|
+
const tail = stat.slice(lastParen + 2).split(" ");
|
|
23
|
+
const starttime = Number(tail[19]);
|
|
24
|
+
return Number.isFinite(starttime) ? starttime : undefined;
|
|
25
|
+
} catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Post-restart resume — respawn persistent triggers, fire late wakes for
|
|
3
|
-
* recently-terminated ones, plus the
|
|
4
|
-
*
|
|
3
|
+
* recently-terminated ones, plus the orphan-kill used to avoid duplicate
|
|
4
|
+
* spawns after an unclean crash. The /proc PID-starttime probe both this
|
|
5
|
+
* and spawn use lives in ./pid.ts.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
import { readFileSync } from "node:fs";
|
|
8
8
|
import {
|
|
9
9
|
getAllTriggers,
|
|
10
10
|
updateTrigger,
|
|
@@ -15,6 +15,7 @@ import { log, logError } from "../../../util/log.js";
|
|
|
15
15
|
import { depsHolder } from "./state.js";
|
|
16
16
|
import { fireWake } from "./output.js";
|
|
17
17
|
import { spawnTrigger } from "./spawn.js";
|
|
18
|
+
import { readPidStarttimeSync } from "./pid.js";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* After the dispatcher is wired, walk the store and clean up leftover state
|
|
@@ -68,27 +69,6 @@ export async function resumeAfterRestart(): Promise<void> {
|
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
/**
|
|
72
|
-
* Read field 22 (start time in jiffies since boot) from /proc/<pid>/stat.
|
|
73
|
-
* Returns undefined if /proc isn't available (non-Linux) or the read fails.
|
|
74
|
-
*
|
|
75
|
-
* Parsing note: the `comm` field (2nd) is wrapped in parens and may itself
|
|
76
|
-
* contain ')' — the safe parse finds the LAST ')' and splits the rest on
|
|
77
|
-
* space. After that split, index 19 corresponds to field 22.
|
|
78
|
-
*/
|
|
79
|
-
export function readPidStarttimeSync(pid: number): number | undefined {
|
|
80
|
-
try {
|
|
81
|
-
const stat = readFileSync(`/proc/${pid}/stat`, "utf-8");
|
|
82
|
-
const lastParen = stat.lastIndexOf(")");
|
|
83
|
-
if (lastParen < 0) return undefined;
|
|
84
|
-
const tail = stat.slice(lastParen + 2).split(" ");
|
|
85
|
-
const starttime = Number(tail[19]);
|
|
86
|
-
return Number.isFinite(starttime) ? starttime : undefined;
|
|
87
|
-
} catch {
|
|
88
|
-
return undefined;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
72
|
/**
|
|
93
73
|
* Probe a stored PID from a previous Talon run and SIGKILL it if it's still
|
|
94
74
|
* alive AND really is our former child (not a recycled PID). Used by
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
import { commandForLanguage } from "./command.js";
|
|
26
26
|
import { handleStdoutLine, handleStderrLine } from "./output.js";
|
|
27
27
|
import { handleTimeout, finalizeExit, failTrigger } from "./exit.js";
|
|
28
|
-
import { readPidStarttimeSync } from "./
|
|
28
|
+
import { readPidStarttimeSync } from "./pid.js";
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Spawn a trigger's script as a supervised child process.
|
|
@@ -22,8 +22,8 @@ export async function performPluginReload(
|
|
|
22
22
|
const { reloadPlugins, getPluginPromptAdditions } =
|
|
23
23
|
await import("../../plugin/index.js");
|
|
24
24
|
const { rebuildSystemPrompt } = await import("../../../util/config.js");
|
|
25
|
-
const {
|
|
26
|
-
await import("
|
|
25
|
+
const { notifyPromptInputsChanged } =
|
|
26
|
+
await import("../../prompt/invalidation.js");
|
|
27
27
|
|
|
28
28
|
// reloadPlugins reads + validates config internally — no double read.
|
|
29
29
|
// Frontends are derived from config if not explicitly provided.
|
|
@@ -37,7 +37,7 @@ export async function performPluginReload(
|
|
|
37
37
|
// Plugin prompt additions changed — drop per-session prompt
|
|
38
38
|
// snapshots so every chat's next turn picks up the new prompt
|
|
39
39
|
// (deliberate one-time cache re-write per live session).
|
|
40
|
-
|
|
40
|
+
notifyPromptInputsChanged();
|
|
41
41
|
|
|
42
42
|
return { names };
|
|
43
43
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt-input invalidation — the core-side seam callers use when prompt
|
|
3
|
+
* inputs change out from under live sessions on purpose (plugin reload,
|
|
4
|
+
* skill toggle).
|
|
5
|
+
*
|
|
6
|
+
* Whoever caches assembled prompts registers a hook here (today: the
|
|
7
|
+
* per-session snapshot store in backend/shared/system-prompt.ts, at its
|
|
8
|
+
* module load). Core and frontends call `notifyPromptInputsChanged()`
|
|
9
|
+
* and never import the cache — the dependency points backend → core,
|
|
10
|
+
* as the layer rule requires.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const hooks = new Set<() => void>();
|
|
14
|
+
|
|
15
|
+
/** Register a callback run whenever prompt inputs are invalidated. */
|
|
16
|
+
export function onPromptInputsChanged(hook: () => void): void {
|
|
17
|
+
hooks.add(hook);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Invalidate: every registered prompt cache drops its state. */
|
|
21
|
+
export function notifyPromptInputsChanged(): void {
|
|
22
|
+
for (const hook of hooks) hook();
|
|
23
|
+
}
|
package/src/core/types.ts
CHANGED
|
@@ -29,8 +29,8 @@ export type UnifiedModelInfo = {
|
|
|
29
29
|
unavailableReason?: string;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
export type ReasoningEffortLevel
|
|
33
|
-
|
|
32
|
+
export type { ReasoningEffortLevel } from "../types/effort.js";
|
|
33
|
+
import type { ReasoningEffortLevel } from "../types/effort.js";
|
|
34
34
|
|
|
35
35
|
/** Result of resolving a user's model query. */
|
|
36
36
|
export type UnifiedModelResolution =
|
|
@@ -34,9 +34,9 @@ import {
|
|
|
34
34
|
setChatModelForBackend,
|
|
35
35
|
setChatBackend,
|
|
36
36
|
setChatEffort,
|
|
37
|
-
resolveModelName,
|
|
38
37
|
type EffortLevel,
|
|
39
38
|
} from "../../../storage/chat-settings.js";
|
|
39
|
+
import { resolveModelId as resolveModelName } from "../../../core/models/catalog.js";
|
|
40
40
|
import {
|
|
41
41
|
registerChat,
|
|
42
42
|
disablePulse,
|
|
@@ -18,9 +18,9 @@ import {
|
|
|
18
18
|
setChatBackend,
|
|
19
19
|
setChatEffort,
|
|
20
20
|
setChatPulseInterval,
|
|
21
|
-
resolveModelName,
|
|
22
21
|
type EffortLevel,
|
|
23
22
|
} from "../../../storage/chat-settings.js";
|
|
23
|
+
import { resolveModelId as resolveModelName } from "../../../core/models/catalog.js";
|
|
24
24
|
import {
|
|
25
25
|
registerChat,
|
|
26
26
|
disablePulse,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge auth token — the shared secret behind every non-/health route.
|
|
3
|
+
*
|
|
4
|
+
* The operator can set one explicitly (`native.token`); this module covers
|
|
5
|
+
* the case where they didn't but the bridge is about to bind a non-loopback
|
|
6
|
+
* host. Serving the full agent API unauthenticated to the LAN is never an
|
|
7
|
+
* acceptable default, so a token is minted once and persisted under
|
|
8
|
+
* ~/.talon/keys/ — stable across restarts so paired clients keep working.
|
|
9
|
+
*
|
|
10
|
+
* The token value itself never goes to the log (SECURITY.md treats
|
|
11
|
+
* credentials in logs as a vulnerability); same-machine clients pick it up
|
|
12
|
+
* from the 0600 discovery file, remote clients read it from the key file.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { randomBytes } from "node:crypto";
|
|
16
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { resolve } from "node:path";
|
|
18
|
+
import { dirs } from "../../util/paths.js";
|
|
19
|
+
import { log } from "../../util/log.js";
|
|
20
|
+
|
|
21
|
+
const TOKEN_FILE = "bridge-token";
|
|
22
|
+
/** 32 random bytes → 43 base64url chars; comfortably beyond brute force. */
|
|
23
|
+
const TOKEN_BYTES = 32;
|
|
24
|
+
|
|
25
|
+
/** Path of the persisted token, for operator-facing messages. */
|
|
26
|
+
export function bridgeTokenPath(dir: string = dirs.keys): string {
|
|
27
|
+
return resolve(dir, TOKEN_FILE);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load the persisted auto-generated bridge token, minting it on first use.
|
|
32
|
+
* Lives under ~/.talon/keys/ with owner-only permissions, like the TLS
|
|
33
|
+
* identity next to it.
|
|
34
|
+
*/
|
|
35
|
+
export function loadOrCreateBridgeToken(dir: string = dirs.keys): string {
|
|
36
|
+
const path = bridgeTokenPath(dir);
|
|
37
|
+
try {
|
|
38
|
+
const existing = readFileSync(path, "utf-8").trim();
|
|
39
|
+
if (existing) return existing;
|
|
40
|
+
} catch {
|
|
41
|
+
// first boot — nothing persisted yet
|
|
42
|
+
}
|
|
43
|
+
const token = randomBytes(TOKEN_BYTES).toString("base64url");
|
|
44
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
45
|
+
writeFileSync(path, `${token}\n`, { mode: 0o600 });
|
|
46
|
+
chmodSync(path, 0o600); // mode above is ignored when the file exists
|
|
47
|
+
log(
|
|
48
|
+
"native",
|
|
49
|
+
`Minted bridge auth token (non-loopback bind with no native.token) — pair remote clients with the value in ${path}`,
|
|
50
|
+
);
|
|
51
|
+
return token;
|
|
52
|
+
}
|
|
@@ -21,7 +21,7 @@ import type { Backend } from "../../core/agent-runtime/capabilities.js";
|
|
|
21
21
|
import { performPluginReload } from "../../core/engine/gateway-actions/plugins.js";
|
|
22
22
|
import { getPluginPromptAdditions } from "../../core/plugin/index.js";
|
|
23
23
|
import { listPluginItems, setPluginEnabled } from "../../core/plugin/manage.js";
|
|
24
|
-
import {
|
|
24
|
+
import { notifyPromptInputsChanged } from "../../core/prompt/invalidation.js";
|
|
25
25
|
import { listSkills, setSkillEnabled } from "../../storage/skill-store.js";
|
|
26
26
|
import { rebuildSystemPrompt, type TalonConfig } from "../../util/config.js";
|
|
27
27
|
import { log } from "../../util/log.js";
|
|
@@ -88,6 +88,6 @@ export function toggleSkill(
|
|
|
88
88
|
log("native", `Skill ${name} ${enabled ? "enabled" : "disabled"} via bridge`);
|
|
89
89
|
rebuildSystemPrompt(config, getPluginPromptAdditions());
|
|
90
90
|
backend?.control?.updateSystemPrompt?.(config.systemPrompt);
|
|
91
|
-
|
|
91
|
+
notifyPromptInputsChanged();
|
|
92
92
|
return { ok: true };
|
|
93
93
|
}
|
|
@@ -75,6 +75,7 @@ import { createNativeActionHandler } from "./actions.js";
|
|
|
75
75
|
import { getMeshService } from "../../core/mesh/index.js";
|
|
76
76
|
import { removeBridgeDiscovery, writeBridgeDiscovery } from "./discovery.js";
|
|
77
77
|
import { isLoopbackHost, loadOrCreateBridgeTlsIdentity } from "./tls.js";
|
|
78
|
+
import { loadOrCreateBridgeToken } from "./auth.js";
|
|
78
79
|
import { readLogEntries } from "./logs.js";
|
|
79
80
|
import {
|
|
80
81
|
BRIDGE_PROTOCOL_VERSION,
|
|
@@ -1200,11 +1201,16 @@ export function createNativeFrontend(
|
|
|
1200
1201
|
// Encrypted by default the moment the bridge leaves the machine; loopback
|
|
1201
1202
|
// stays plain HTTP unless explicitly opted in (`native.tls`).
|
|
1202
1203
|
const bridgeTls = nativeCfg.tls ?? !isLoopbackHost(bridgeHost);
|
|
1204
|
+
// Never serve the agent API to the network unauthenticated: a non-loopback
|
|
1205
|
+
// bind with no configured token gets a persistent auto-minted one instead.
|
|
1206
|
+
const bridgeToken =
|
|
1207
|
+
nativeCfg.token ??
|
|
1208
|
+
(isLoopbackHost(bridgeHost) ? undefined : loadOrCreateBridgeToken());
|
|
1203
1209
|
const server = new BridgeServer(
|
|
1204
1210
|
{
|
|
1205
1211
|
host: bridgeHost,
|
|
1206
1212
|
port: nativeCfg.port ?? 19880,
|
|
1207
|
-
token:
|
|
1213
|
+
token: bridgeToken,
|
|
1208
1214
|
startedAt,
|
|
1209
1215
|
...(bridgeTls ? { tls: () => loadOrCreateBridgeTlsIdentity() } : {}),
|
|
1210
1216
|
},
|
|
@@ -1274,7 +1280,7 @@ export function createNativeFrontend(
|
|
|
1274
1280
|
const fingerprint = server.getFingerprint();
|
|
1275
1281
|
await writeBridgeDiscovery({
|
|
1276
1282
|
port: server.getPort(),
|
|
1277
|
-
token:
|
|
1283
|
+
token: bridgeToken,
|
|
1278
1284
|
scheme: server.getScheme(),
|
|
1279
1285
|
...(fingerprint ? { fingerprint } : {}),
|
|
1280
1286
|
startedAt: Date.parse(startedAt),
|