talon-agent 3.23.0 → 3.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/backend/codex/discovery.ts +8 -1
- package/src/core/engine/gateway.ts +14 -2
- package/src/core/errors.ts +5 -0
- package/src/core/tools/bridge.ts +7 -1
- package/src/core/weaver/weaver.ts +11 -0
- package/src/frontend/discord/commands/session.ts +1 -0
- package/src/frontend/discord/handlers/queue.ts +7 -0
- package/src/frontend/shared/session-status.ts +8 -0
- package/src/frontend/telegram/commands/session.ts +1 -0
- package/src/frontend/telegram/handlers/queue.ts +9 -0
package/package.json
CHANGED
|
@@ -90,7 +90,14 @@ interface CodexCacheFile {
|
|
|
90
90
|
|
|
91
91
|
/** Where the Codex CLI writes its model cache. Exported for tests. */
|
|
92
92
|
export function getCodexCachePath(): string {
|
|
93
|
-
|
|
93
|
+
// Honor CODEX_HOME like token-usage.ts and plan-usage.ts do — the CLI
|
|
94
|
+
// itself relocates with it, so a pinned deployment would otherwise
|
|
95
|
+
// read a cache the CLI never writes. (Also what lets tests fake the
|
|
96
|
+
// path under bun, whose os.homedir() ignores $HOME.)
|
|
97
|
+
const home = process.env.CODEX_HOME?.trim();
|
|
98
|
+
return home && home.length > 0
|
|
99
|
+
? join(home, "models_cache.json")
|
|
100
|
+
: join(homedir(), ".codex", "models_cache.json");
|
|
94
101
|
}
|
|
95
102
|
|
|
96
103
|
/** Default soft timeout when callers await an in-flight discovery. */
|
|
@@ -596,16 +596,28 @@ export class Gateway {
|
|
|
596
596
|
resolve();
|
|
597
597
|
return;
|
|
598
598
|
}
|
|
599
|
-
this.server
|
|
599
|
+
const server = this.server;
|
|
600
|
+
const settle = (): void => {
|
|
600
601
|
this.server = null;
|
|
601
602
|
this.port = 0;
|
|
602
603
|
resolve();
|
|
604
|
+
};
|
|
605
|
+
// Bun's closeAllConnections() (≤1.3.x) doesn't sever live SSE
|
|
606
|
+
// streams, so close()'s callback can never fire there and stop()
|
|
607
|
+
// would hang shutdown (and every test teardown). Resolve on a
|
|
608
|
+
// deadline either way: by then no new connections are accepted,
|
|
609
|
+
// and the process this runs in is exiting anyway.
|
|
610
|
+
const deadline = setTimeout(settle, 2_000);
|
|
611
|
+
deadline.unref?.();
|
|
612
|
+
server.close(() => {
|
|
613
|
+
clearTimeout(deadline);
|
|
614
|
+
settle();
|
|
603
615
|
});
|
|
604
616
|
// `close()` only stops NEW connections; MCP hub sessions hold
|
|
605
617
|
// long-lived SSE streams that would keep the close callback from
|
|
606
618
|
// ever firing. Terminate them — everything on this server is
|
|
607
619
|
// localhost request/response or SSE, safe to drop at stop time.
|
|
608
|
-
|
|
620
|
+
server.closeAllConnections();
|
|
609
621
|
});
|
|
610
622
|
}
|
|
611
623
|
}
|
package/src/core/errors.ts
CHANGED
|
@@ -18,6 +18,10 @@ type ErrorReason =
|
|
|
18
18
|
| "bad_request"
|
|
19
19
|
| "forbidden"
|
|
20
20
|
| "telegram_api"
|
|
21
|
+
// The user stopped the turn (/stop, talon kill). Not a fault: the
|
|
22
|
+
// frontends deliver nothing for it — the stop acknowledgement already
|
|
23
|
+
// told the user what happened.
|
|
24
|
+
| "stopped"
|
|
21
25
|
| "unknown";
|
|
22
26
|
|
|
23
27
|
/**
|
|
@@ -342,6 +346,7 @@ const FRIENDLY_MESSAGES: Record<ErrorReason, string> = {
|
|
|
342
346
|
bad_request: "Something went wrong. Try /reset if this keeps happening.",
|
|
343
347
|
forbidden: "Permission denied for this action.",
|
|
344
348
|
telegram_api: "Telegram API error. Try again.",
|
|
349
|
+
stopped: "⏹ Stopped.",
|
|
345
350
|
unknown: "Something went wrong. Try again or /reset.",
|
|
346
351
|
};
|
|
347
352
|
|
package/src/core/tools/bridge.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Agent, fetch as undiciFetch } from "undici";
|
|
9
|
+
import { isBunRuntime } from "../../util/runtime.js";
|
|
9
10
|
import type { BridgeFunction } from "./types.js";
|
|
10
11
|
|
|
11
12
|
/** Default wall-clock budget for a bridge action. */
|
|
@@ -95,8 +96,13 @@ export function createBridge(
|
|
|
95
96
|
json(): Promise<unknown>;
|
|
96
97
|
};
|
|
97
98
|
try {
|
|
99
|
+
// Bun has no 300s header watchdog and aliases the npm undici
|
|
100
|
+
// module to its own internals (so the Agent knobs do nothing
|
|
101
|
+
// there anyway) — its global fetch serves long-haul calls fine,
|
|
102
|
+
// governed by the AbortSignal alone. The undici detour is a
|
|
103
|
+
// Node-only workaround.
|
|
98
104
|
resp =
|
|
99
|
-
timeoutMs > BUILTIN_FETCH_HEADERS_CEILING_MS
|
|
105
|
+
timeoutMs > BUILTIN_FETCH_HEADERS_CEILING_MS && !isBunRuntime()
|
|
100
106
|
? await undiciFetch(`${bridgeUrl}/action`, {
|
|
101
107
|
...init,
|
|
102
108
|
dispatcher: dispatcher(),
|
|
@@ -24,6 +24,7 @@ import type { ContextManager, ExecuteParams, ExecuteResult } from "../types.js";
|
|
|
24
24
|
import { bus } from "../bus/index.js";
|
|
25
25
|
import { taskTable, type TaskHandle } from "../tasks/index.js";
|
|
26
26
|
import { log, logDebug, logWarn } from "../../util/log.js";
|
|
27
|
+
import { TalonError } from "../errors.js";
|
|
27
28
|
import { Loom } from "./loom.js";
|
|
28
29
|
import { carryTurnEvents } from "./shuttle.js";
|
|
29
30
|
import type { Thread, ThreadSnapshot } from "./thread.js";
|
|
@@ -141,6 +142,16 @@ export class Weaver {
|
|
|
141
142
|
return result;
|
|
142
143
|
} catch (err) {
|
|
143
144
|
task.fail(err);
|
|
145
|
+
if (lifecycle.killed) {
|
|
146
|
+
// The backend didn't manage a clean interrupt-completion (some
|
|
147
|
+
// SDK versions surface an interrupted turn as an error result).
|
|
148
|
+
// The user asked for this outcome — don't let it unwind as a
|
|
149
|
+
// fault the frontend then reports to the chat.
|
|
150
|
+
throw new TalonError("Turn stopped by user", {
|
|
151
|
+
reason: "stopped",
|
|
152
|
+
cause: err,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
144
155
|
throw err;
|
|
145
156
|
} finally {
|
|
146
157
|
this.activeCount--;
|
|
@@ -108,6 +108,7 @@ export async function handleStatus(
|
|
|
108
108
|
`**Workspace** ${formatBytes(s.diskBytes)}`,
|
|
109
109
|
`**Session** ${s.sessionName ? `"${s.sessionName}" ` : ""}${s.sessionId ? "`" + s.sessionId.slice(0, 8) + "...`" : "_(new)_"} · ${s.sessionAge} old`,
|
|
110
110
|
`**Uptime** ${s.uptime} · ${s.activeSessionCount} active session${s.activeSessionCount === 1 ? "" : "s"}`,
|
|
111
|
+
`**Runtime** ${s.runtime} · ${formatBytes(s.rssBytes)} RSS`,
|
|
111
112
|
];
|
|
112
113
|
await i.editReply(lines.join("\n"));
|
|
113
114
|
}
|
|
@@ -93,6 +93,13 @@ async function flushQueue(chatId: string): Promise<void> {
|
|
|
93
93
|
recordMessageProcessed();
|
|
94
94
|
} catch (err) {
|
|
95
95
|
const classified = classify(err);
|
|
96
|
+
// A user-initiated /stop is an outcome, not a fault — the stop command
|
|
97
|
+
// already acknowledged it; don't re-report it as an error.
|
|
98
|
+
if (classified.reason === "stopped") {
|
|
99
|
+
log("bot", `[${chatId}] turn stopped by user`);
|
|
100
|
+
recordMessageSettled();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
96
103
|
const promptPreview = combinedPrompt.slice(0, 100).replace(/\n/g, " ");
|
|
97
104
|
logError(
|
|
98
105
|
"bot",
|
|
@@ -88,6 +88,10 @@ export interface SessionStatusData {
|
|
|
88
88
|
sessionId: string | undefined;
|
|
89
89
|
uptime: string;
|
|
90
90
|
activeSessionCount: number;
|
|
91
|
+
/** Runtime name + version, e.g. "Bun 1.3.9" or "Node 24.4.0". */
|
|
92
|
+
runtime: string;
|
|
93
|
+
/** Daemon resident set size, in bytes. */
|
|
94
|
+
rssBytes: number;
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
/**
|
|
@@ -202,5 +206,9 @@ export async function collectSessionStatus(
|
|
|
202
206
|
sessionId: info.sessionId,
|
|
203
207
|
uptime: formatDuration(process.uptime() * 1000),
|
|
204
208
|
activeSessionCount: getActiveSessionCount(),
|
|
209
|
+
runtime: process.versions.bun
|
|
210
|
+
? `Bun ${process.versions.bun}`
|
|
211
|
+
: `Node ${process.versions.node}`,
|
|
212
|
+
rssBytes: process.memoryUsage().rss,
|
|
205
213
|
};
|
|
206
214
|
}
|
|
@@ -90,6 +90,7 @@ export function registerSessionCommands(
|
|
|
90
90
|
`<b>Workspace</b> ${formatBytes(s.diskBytes)}`,
|
|
91
91
|
`<b>Session</b> ${s.sessionName ? `"${escapeHtml(s.sessionName)}" ` : ""}${s.sessionId ? "<code>" + escapeHtml(s.sessionId.slice(0, 8)) + "...</code>" : "<i>(new)</i>"} · ${s.sessionAge} old`,
|
|
92
92
|
`<b>Uptime</b> ${s.uptime} · ${s.activeSessionCount} active session${s.activeSessionCount === 1 ? "" : "s"}`,
|
|
93
|
+
`<b>Runtime</b> ${escapeHtml(s.runtime)} · ${formatBytes(s.rssBytes)} RSS`,
|
|
93
94
|
];
|
|
94
95
|
await ctx.reply(lines.join("\n"), { parse_mode: "HTML" });
|
|
95
96
|
});
|
|
@@ -188,6 +188,15 @@ async function flushQueue(chatId: string): Promise<void> {
|
|
|
188
188
|
recordMessageProcessed();
|
|
189
189
|
} catch (err) {
|
|
190
190
|
const classified = classify(err);
|
|
191
|
+
// A user-initiated /stop is an outcome, not a fault: the stop command
|
|
192
|
+
// already acknowledged it, so delivering the unwound turn's error here
|
|
193
|
+
// would just contradict that with noise.
|
|
194
|
+
if (classified.reason === "stopped") {
|
|
195
|
+
log("bot", `[${chatId}] turn stopped by user`);
|
|
196
|
+
lastHandledMessageIdByChat.set(chatId, last.messageId);
|
|
197
|
+
recordMessageSettled();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
191
200
|
const chatType = last.isGroup ? "group" : "DM";
|
|
192
201
|
const promptPreview = promptWithContext.slice(0, 100).replace(/\n/g, " ");
|
|
193
202
|
logError(
|