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.
Files changed (70) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +0 -10
  3. package/package.json +6 -4
  4. package/src/backend/claude-sdk/handler.ts +428 -415
  5. package/src/backend/codex/auth.ts +40 -1
  6. package/src/backend/codex/handler/message.ts +290 -366
  7. package/src/backend/codex/handler/rollout-accounting.ts +137 -0
  8. package/src/backend/codex/plan-usage.ts +27 -2
  9. package/src/backend/kilo/handler/message.ts +2 -0
  10. package/src/backend/kilo/server.ts +1 -0
  11. package/src/backend/openai-agents/handler/message.ts +282 -356
  12. package/src/backend/opencode/handler/message.ts +2 -0
  13. package/src/backend/opencode/server.ts +1 -0
  14. package/src/backend/remote-server/chat-turn.ts +76 -122
  15. package/src/backend/remote-server/index.ts +4 -0
  16. package/src/backend/remote-server/lifecycle.ts +37 -0
  17. package/src/backend/remote-server/mcp.ts +73 -9
  18. package/src/backend/remote-server/model-catalog/presentation.ts +269 -228
  19. package/src/backend/remote-server/server-bindings.ts +12 -0
  20. package/src/backend/remote-server/sessions.ts +2 -2
  21. package/src/backend/remote-server/state.ts +8 -0
  22. package/src/backend/remote-server/turn.ts +93 -23
  23. package/src/backend/shared/cache-telemetry.ts +17 -1
  24. package/src/backend/shared/handler-to-events.ts +11 -16
  25. package/src/backend/shared/index.ts +14 -15
  26. package/src/backend/shared/result-events.ts +30 -0
  27. package/src/backend/shared/turn-phases.ts +277 -0
  28. package/src/bootstrap.ts +24 -10
  29. package/src/core/auth/expiry-monitor.ts +89 -0
  30. package/src/core/auth/login-flow.ts +247 -0
  31. package/src/core/auth/status.ts +193 -0
  32. package/src/core/background/triggers/exit.ts +19 -2
  33. package/src/core/background/triggers/resume.ts +14 -7
  34. package/src/core/background/triggers/state.ts +9 -0
  35. package/src/core/engine/gateway-actions/history.ts +12 -1
  36. package/src/core/errors.ts +1 -1
  37. package/src/core/mcp-hub/child-transport.ts +215 -0
  38. package/src/core/mcp-hub/children.ts +72 -13
  39. package/src/core/mcp-hub/index.ts +32 -13
  40. package/src/core/models/active-model.ts +29 -6
  41. package/src/core/tools/history.ts +10 -1
  42. package/src/core/weaver/shuttle.ts +4 -0
  43. package/src/frontend/shared/model-commands.ts +400 -0
  44. package/src/frontend/telegram/admin.ts +75 -52
  45. package/src/frontend/telegram/auth-panel.ts +205 -0
  46. package/src/frontend/telegram/callbacks/auth.ts +70 -0
  47. package/src/frontend/telegram/callbacks/index.ts +16 -0
  48. package/src/frontend/telegram/callbacks/whatsapp.ts +50 -0
  49. package/src/frontend/telegram/commands/auth.ts +58 -0
  50. package/src/frontend/telegram/commands/definitions.ts +4 -0
  51. package/src/frontend/telegram/commands/index.ts +10 -4
  52. package/src/frontend/telegram/commands/whatsapp-pairing.ts +121 -79
  53. package/src/frontend/whatsapp/actions/chat-info.ts +2 -36
  54. package/src/frontend/whatsapp/actions/history.ts +100 -0
  55. package/src/frontend/whatsapp/actions/index.ts +4 -1
  56. package/src/frontend/whatsapp/commands.ts +371 -0
  57. package/src/frontend/whatsapp/inbound.ts +24 -46
  58. package/src/frontend/whatsapp/index.ts +12 -1
  59. package/src/frontend/whatsapp/media-store.ts +20 -1
  60. package/src/frontend/whatsapp/message-store.ts +101 -24
  61. package/src/storage/history.ts +22 -1
  62. package/src/storage/repositories/history-repo.ts +14 -0
  63. package/src/storage/repositories/whatsapp-messages-repo.ts +101 -0
  64. package/src/storage/sql/history.sql +12 -0
  65. package/src/storage/sql/schema.sql +21 -0
  66. package/src/storage/sql/statements.generated.ts +50 -1
  67. package/src/storage/sql/whatsapp-messages.sql +29 -0
  68. package/src/storage/trigger-store.ts +8 -0
  69. package/src/storage/whatsapp-messages.ts +51 -0
  70. 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 parsed = JSON.parse(await readFile(authPath(), "utf8")) as {
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,
@@ -82,6 +82,7 @@ export const stopKiloServer = kilo.stop;
82
82
  export const {
83
83
  onServerStop,
84
84
  ensureServer,
85
+ trackActiveTurn,
85
86
  ensureChatMcpServer,
86
87
  ensurePluginMcpServers,
87
88
  buildToolOverrides,