talon-agent 3.34.0 → 3.35.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/LICENSE +1 -1
- package/README.md +0 -10
- package/package.json +6 -4
- package/src/backend/claude-sdk/handler.ts +428 -415
- package/src/backend/codex/auth.ts +40 -1
- package/src/backend/codex/handler/message.ts +290 -366
- package/src/backend/codex/handler/rollout-accounting.ts +137 -0
- package/src/backend/codex/plan-usage.ts +27 -2
- package/src/backend/kilo/handler/message.ts +2 -0
- package/src/backend/kilo/server.ts +1 -0
- package/src/backend/openai-agents/handler/message.ts +282 -356
- package/src/backend/opencode/handler/message.ts +2 -0
- package/src/backend/opencode/server.ts +1 -0
- package/src/backend/remote-server/chat-turn.ts +76 -122
- package/src/backend/remote-server/index.ts +4 -0
- package/src/backend/remote-server/lifecycle.ts +37 -0
- package/src/backend/remote-server/mcp.ts +73 -9
- package/src/backend/remote-server/model-catalog/presentation.ts +269 -228
- package/src/backend/remote-server/server-bindings.ts +12 -0
- package/src/backend/remote-server/sessions.ts +2 -2
- package/src/backend/remote-server/state.ts +8 -0
- package/src/backend/remote-server/turn.ts +93 -23
- package/src/backend/shared/cache-telemetry.ts +17 -1
- package/src/backend/shared/handler-to-events.ts +11 -16
- package/src/backend/shared/index.ts +14 -15
- package/src/backend/shared/result-events.ts +30 -0
- package/src/backend/shared/turn-phases.ts +277 -0
- package/src/bootstrap.ts +24 -10
- package/src/core/auth/expiry-monitor.ts +89 -0
- package/src/core/auth/login-flow.ts +247 -0
- package/src/core/auth/status.ts +193 -0
- package/src/core/background/triggers/exit.ts +19 -2
- package/src/core/background/triggers/resume.ts +14 -7
- package/src/core/background/triggers/state.ts +9 -0
- package/src/core/engine/gateway-actions/history.ts +12 -1
- package/src/core/errors.ts +1 -1
- package/src/core/mcp-hub/child-transport.ts +215 -0
- package/src/core/mcp-hub/children.ts +72 -13
- package/src/core/mcp-hub/index.ts +32 -13
- package/src/core/models/active-model.ts +29 -6
- package/src/core/tools/history.ts +10 -1
- package/src/core/weaver/shuttle.ts +4 -0
- package/src/frontend/shared/model-commands.ts +400 -0
- package/src/frontend/telegram/admin.ts +75 -52
- package/src/frontend/telegram/auth-panel.ts +205 -0
- package/src/frontend/telegram/callbacks/auth.ts +70 -0
- package/src/frontend/telegram/callbacks/index.ts +16 -0
- package/src/frontend/telegram/callbacks/whatsapp.ts +50 -0
- package/src/frontend/telegram/commands/auth.ts +58 -0
- package/src/frontend/telegram/commands/definitions.ts +4 -0
- package/src/frontend/telegram/commands/index.ts +10 -4
- package/src/frontend/telegram/commands/whatsapp-pairing.ts +121 -79
- package/src/frontend/whatsapp/actions/chat-info.ts +2 -36
- package/src/frontend/whatsapp/actions/history.ts +100 -0
- package/src/frontend/whatsapp/actions/index.ts +4 -1
- package/src/frontend/whatsapp/commands.ts +371 -0
- package/src/frontend/whatsapp/inbound.ts +24 -46
- package/src/frontend/whatsapp/index.ts +12 -1
- package/src/frontend/whatsapp/media-store.ts +20 -1
- package/src/frontend/whatsapp/message-store.ts +101 -24
- package/src/storage/history.ts +22 -1
- package/src/storage/repositories/history-repo.ts +14 -0
- package/src/storage/repositories/whatsapp-messages-repo.ts +101 -0
- package/src/storage/sql/history.sql +12 -0
- package/src/storage/sql/schema.sql +21 -0
- package/src/storage/sql/statements.generated.ts +50 -1
- package/src/storage/sql/whatsapp-messages.sql +29 -0
- package/src/storage/trigger-store.ts +8 -0
- package/src/storage/whatsapp-messages.ts +51 -0
- package/src/util/watchdog.ts +32 -7
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rollout-JSONL usage accounting for one Codex turn.
|
|
3
|
+
*
|
|
4
|
+
* Codex's `turn.completed.usage` is CUMULATIVE across every API call in
|
|
5
|
+
* the turn — never in the per-turn units the shared stream state (and
|
|
6
|
+
* everything downstream: /status, the companion's per-message counts)
|
|
7
|
+
* speaks. The rollout JSONL's totals diffed against the pre-turn baseline
|
|
8
|
+
* are this turn's real usage; that is the ONLY authoritative source. The
|
|
9
|
+
* SDK figure is a last-resort fallback when the rollout can't be read,
|
|
10
|
+
* and it overstates multi-call turns.
|
|
11
|
+
*
|
|
12
|
+
* The same baseline serves the throttled mid-turn poll (live /status) and
|
|
13
|
+
* the final settlement, which the success path and the terminal-failure
|
|
14
|
+
* path both run so failed turns account for the tokens they burned.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Usage } from "@openai/codex-sdk";
|
|
18
|
+
import {
|
|
19
|
+
pushLiveUsage,
|
|
20
|
+
recordTokens,
|
|
21
|
+
type StreamState,
|
|
22
|
+
} from "../../shared/index.js";
|
|
23
|
+
import { CODEX_LIVE_POLL_INTERVAL_MS } from "../constants.js";
|
|
24
|
+
import {
|
|
25
|
+
readLastRolloutSnapshot,
|
|
26
|
+
type CodexRolloutSnapshot,
|
|
27
|
+
} from "../token-usage.js";
|
|
28
|
+
|
|
29
|
+
type RolloutTotals = NonNullable<CodexRolloutSnapshot["totals"]>;
|
|
30
|
+
|
|
31
|
+
export type RolloutAccounting = {
|
|
32
|
+
/** Thread id once `thread.started` lands; polls are no-ops before it. */
|
|
33
|
+
threadId: string | undefined;
|
|
34
|
+
/** Throttled mid-turn rollout tail — fire-and-forget, never throws. */
|
|
35
|
+
pollLive(): void;
|
|
36
|
+
/** Final authoritative settlement; `usage` is the SDK's fallback figure. */
|
|
37
|
+
settle(usage: Usage | null): Promise<void>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Capture the cumulative token totals BEFORE the turn runs.
|
|
42
|
+
* `total_token_usage` accumulates across the whole session file, so this
|
|
43
|
+
* turn's usage = post-turn totals minus this baseline. Fresh threads have
|
|
44
|
+
* no rollout yet → zero baseline. `null` = resumed thread whose baseline
|
|
45
|
+
* couldn't be read.
|
|
46
|
+
*/
|
|
47
|
+
export async function createRolloutAccounting(inputs: {
|
|
48
|
+
state: StreamState;
|
|
49
|
+
sessionId: string | undefined;
|
|
50
|
+
}): Promise<RolloutAccounting> {
|
|
51
|
+
const { state } = inputs;
|
|
52
|
+
const baseline: RolloutTotals | null = inputs.sessionId
|
|
53
|
+
? ((await readLastRolloutSnapshot(inputs.sessionId).catch(() => null))
|
|
54
|
+
?.totals ?? null)
|
|
55
|
+
: { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 };
|
|
56
|
+
|
|
57
|
+
let pollInFlight = false;
|
|
58
|
+
let lastPollAt = 0;
|
|
59
|
+
|
|
60
|
+
const accounting: RolloutAccounting = {
|
|
61
|
+
threadId: undefined,
|
|
62
|
+
|
|
63
|
+
// The Codex CLI appends a `token_count` event to the rollout JSONL
|
|
64
|
+
// after every API call, so tailing it during the turn gives live
|
|
65
|
+
// context-fill / token / API-call stats long before `turn.completed`.
|
|
66
|
+
// In-flight guard — never blocks the event loop.
|
|
67
|
+
pollLive() {
|
|
68
|
+
if (!accounting.threadId || pollInFlight) return;
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
if (now - lastPollAt < CODEX_LIVE_POLL_INTERVAL_MS) return;
|
|
71
|
+
pollInFlight = true;
|
|
72
|
+
lastPollAt = now;
|
|
73
|
+
readLastRolloutSnapshot(accounting.threadId)
|
|
74
|
+
.then((snap) => {
|
|
75
|
+
if (!snap) return;
|
|
76
|
+
applyContext(state, snap);
|
|
77
|
+
// Same delta-vs-baseline math as the final settlement, which
|
|
78
|
+
// recomputes and overwrites — a torn mid-turn read can't
|
|
79
|
+
// corrupt the committed numbers.
|
|
80
|
+
if (snap.totals && baseline) {
|
|
81
|
+
state.sdkInputTokens = Math.max(
|
|
82
|
+
0,
|
|
83
|
+
snap.totals.inputTokens - baseline.inputTokens,
|
|
84
|
+
);
|
|
85
|
+
state.sdkOutputTokens = Math.max(
|
|
86
|
+
0,
|
|
87
|
+
snap.totals.outputTokens - baseline.outputTokens,
|
|
88
|
+
);
|
|
89
|
+
state.sdkCacheRead = Math.max(
|
|
90
|
+
0,
|
|
91
|
+
snap.totals.cachedInputTokens - baseline.cachedInputTokens,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
pushLiveUsage(state);
|
|
95
|
+
})
|
|
96
|
+
.catch(() => {})
|
|
97
|
+
.finally(() => {
|
|
98
|
+
pollInFlight = false;
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
async settle(usage) {
|
|
103
|
+
const last = accounting.threadId
|
|
104
|
+
? await readLastRolloutSnapshot(accounting.threadId).catch(() => null)
|
|
105
|
+
: null;
|
|
106
|
+
if (last) applyContext(state, last);
|
|
107
|
+
if (last?.totals && baseline) {
|
|
108
|
+
recordTokens(state, {
|
|
109
|
+
inputTokens: last.totals.inputTokens - baseline.inputTokens,
|
|
110
|
+
outputTokens: last.totals.outputTokens - baseline.outputTokens,
|
|
111
|
+
cacheRead: last.totals.cachedInputTokens - baseline.cachedInputTokens,
|
|
112
|
+
cacheWrite: 0, // Codex doesn't report cache writes
|
|
113
|
+
});
|
|
114
|
+
} else if (usage) {
|
|
115
|
+
recordTokens(state, {
|
|
116
|
+
inputTokens: usage.input_tokens,
|
|
117
|
+
outputTokens: usage.output_tokens,
|
|
118
|
+
cacheRead: usage.cached_input_tokens,
|
|
119
|
+
cacheWrite: 0, // Codex doesn't report cache writes
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
return accounting;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function applyContext(state: StreamState, snap: CodexRolloutSnapshot): void {
|
|
128
|
+
if (snap.usage) {
|
|
129
|
+
state.contextTokens = snap.usage.contextTokens;
|
|
130
|
+
if (snap.usage.contextWindow) {
|
|
131
|
+
state.contextWindow = snap.usage.contextWindow;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (typeof snap.numApiCalls === "number") {
|
|
135
|
+
state.numApiCalls = snap.numApiCalls;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* missing or expired token, or any transport failure.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { readFile } from "node:fs/promises";
|
|
14
|
+
import { readFile, stat } from "node:fs/promises";
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { logWarn } from "../../util/log.js";
|
|
@@ -26,6 +26,12 @@ const CACHE_TTL_MS = 60_000;
|
|
|
26
26
|
|
|
27
27
|
let cache: { value: PlanUsage; fetchedAt: number } | undefined;
|
|
28
28
|
let inFlight: Promise<PlanUsage | undefined> | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* `auth.json` mtime of a token the endpoint rejected with 401. An
|
|
31
|
+
* invalidated login stays invalid until `codex login` rewrites the file,
|
|
32
|
+
* so the poller warns once and stops calling until the mtime moves.
|
|
33
|
+
*/
|
|
34
|
+
let rejectedAuthMtimeMs: number | undefined;
|
|
29
35
|
|
|
30
36
|
function authPath(): string {
|
|
31
37
|
const home = process.env.CODEX_HOME?.trim();
|
|
@@ -37,11 +43,18 @@ function authPath(): string {
|
|
|
37
43
|
interface CodexAuth {
|
|
38
44
|
accessToken: string;
|
|
39
45
|
accountId?: string;
|
|
46
|
+
/** `auth.json` mtime — identifies the login the token came from. */
|
|
47
|
+
mtimeMs: number;
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
async function readAuth(): Promise<CodexAuth | undefined> {
|
|
43
51
|
try {
|
|
44
|
-
const
|
|
52
|
+
const path = authPath();
|
|
53
|
+
const [raw, stats] = await Promise.all([
|
|
54
|
+
readFile(path, "utf8"),
|
|
55
|
+
stat(path),
|
|
56
|
+
]);
|
|
57
|
+
const parsed = JSON.parse(raw) as {
|
|
45
58
|
auth_mode?: string;
|
|
46
59
|
tokens?: { access_token?: string; account_id?: string };
|
|
47
60
|
};
|
|
@@ -52,6 +65,7 @@ async function readAuth(): Promise<CodexAuth | undefined> {
|
|
|
52
65
|
...(parsed.tokens?.account_id
|
|
53
66
|
? { accountId: parsed.tokens.account_id }
|
|
54
67
|
: {}),
|
|
68
|
+
mtimeMs: stats.mtimeMs,
|
|
55
69
|
};
|
|
56
70
|
} catch {
|
|
57
71
|
return undefined;
|
|
@@ -123,6 +137,8 @@ export function parseCodexUsage(body: unknown): PlanUsage | undefined {
|
|
|
123
137
|
async function load(): Promise<PlanUsage | undefined> {
|
|
124
138
|
const auth = await readAuth();
|
|
125
139
|
if (!auth) return undefined;
|
|
140
|
+
if (auth.mtimeMs === rejectedAuthMtimeMs) return undefined;
|
|
141
|
+
rejectedAuthMtimeMs = undefined;
|
|
126
142
|
|
|
127
143
|
try {
|
|
128
144
|
const res = await fetch(USAGE_ENDPOINT, {
|
|
@@ -133,6 +149,15 @@ async function load(): Promise<PlanUsage | undefined> {
|
|
|
133
149
|
},
|
|
134
150
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
135
151
|
});
|
|
152
|
+
if (res.status === 401) {
|
|
153
|
+
rejectedAuthMtimeMs = auth.mtimeMs;
|
|
154
|
+
logWarn(
|
|
155
|
+
"agent",
|
|
156
|
+
"codex usage: endpoint returned 401 — Codex login expired; " +
|
|
157
|
+
"run `codex login` (not retried until auth.json changes)",
|
|
158
|
+
);
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
136
161
|
if (!res.ok) {
|
|
137
162
|
logWarn("agent", `codex usage: endpoint returned ${res.status}`);
|
|
138
163
|
return undefined;
|
|
@@ -7,6 +7,7 @@ import type { QueryParams, QueryResult } from "../../shared/handler-types.js";
|
|
|
7
7
|
import { runRemoteChatTurn } from "../../remote-server/chat-turn.js";
|
|
8
8
|
import {
|
|
9
9
|
ensureServer,
|
|
10
|
+
trackActiveTurn,
|
|
10
11
|
ensureSession,
|
|
11
12
|
ensureChatMcpServer,
|
|
12
13
|
ensurePluginMcpServers,
|
|
@@ -26,6 +27,7 @@ export function handleMessage(params: QueryParams): Promise<QueryResult> {
|
|
|
26
27
|
label: "Kilo",
|
|
27
28
|
getConfig,
|
|
28
29
|
ensureServer,
|
|
30
|
+
trackActiveTurn,
|
|
29
31
|
parseModelSelection: parseStoredKiloModelSelection,
|
|
30
32
|
resolveProviderID,
|
|
31
33
|
ensureSession,
|