talon-agent 3.33.0 → 3.33.2

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 (31) hide show
  1. package/package.json +3 -3
  2. package/src/backend/kilo/factory.ts +39 -119
  3. package/src/backend/kilo/handler/index.ts +1 -11
  4. package/src/backend/kilo/handler/message.ts +20 -317
  5. package/src/backend/kilo/server.ts +55 -272
  6. package/src/backend/kilo/sessions.ts +5 -83
  7. package/src/backend/opencode/factory.ts +41 -118
  8. package/src/backend/opencode/handler/index.ts +1 -11
  9. package/src/backend/opencode/handler/message.ts +20 -321
  10. package/src/backend/opencode/server.ts +57 -217
  11. package/src/backend/opencode/sessions.ts +5 -83
  12. package/src/backend/remote-server/chat-turn.ts +359 -0
  13. package/src/backend/remote-server/factory.ts +150 -0
  14. package/src/backend/remote-server/index.ts +14 -19
  15. package/src/backend/remote-server/server-bindings.ts +232 -0
  16. package/src/backend/{kilo/handler → remote-server}/turn.ts +103 -67
  17. package/src/core/mesh/bridge-links.ts +319 -0
  18. package/src/core/mesh/common.ts +30 -0
  19. package/src/core/mesh/device-files.ts +710 -0
  20. package/src/core/mesh/service.ts +72 -894
  21. package/src/frontend/discord/handlers/access.ts +7 -56
  22. package/src/frontend/discord/handlers/state.ts +15 -8
  23. package/src/frontend/native/server.ts +391 -323
  24. package/src/frontend/shared/access.ts +152 -0
  25. package/src/frontend/telegram/handlers/access.ts +8 -25
  26. package/src/frontend/telegram/handlers/queue.ts +3 -35
  27. package/src/frontend/telegram/handlers/state.ts +15 -8
  28. package/src/backend/kilo/events.ts +0 -60
  29. package/src/backend/kilo/handler/state.ts +0 -9
  30. package/src/backend/opencode/handler/state.ts +0 -9
  31. package/src/backend/opencode/handler/turn.ts +0 -314
@@ -1,314 +0,0 @@
1
- /**
2
- * Run one OpenCode turn over the HTTP API + SSE event stream.
3
- *
4
- * `runOpenCodeTurn` subscribes to SSE before issuing the prompt (so no early
5
- * events are lost), fires `promptAsync`, awaits the SSE close, then reads the
6
- * authoritative parts list. `subscribeToTurnEvents` translates relevant SSE
7
- * events into shared stream-state mutations and observes natural turn idle.
8
- *
9
- * The streaming + event-processing logic is shared with the Kilo backend via
10
- * `backend/remote-server/events.ts`.
11
- */
12
-
13
- import type { OpencodeClient } from "@opencode-ai/sdk/v2";
14
- import { logWarn } from "../../../util/log.js";
15
- import { errMsg } from "../server.js";
16
- import {
17
- approvePendingPermissions,
18
- extractPartsSummary,
19
- extractAssistantUsage,
20
- rejectPendingQuestions,
21
- type OpenCodeAssistantInfo,
22
- } from "../sessions.js";
23
- import { createStreamState, recordTokens, sleep } from "../../shared/index.js";
24
- import {
25
- processStreamEvent,
26
- finalizePartsIntoState,
27
- } from "../../remote-server/events.js";
28
- import { subscribeSseStream } from "../../remote-server/sse-stream.js";
29
- import { awaitRemoteTurn } from "../../remote-server/turn-timeout.js";
30
- import { findLastAssistantMessage as findLastAssistantMessageShared } from "../../remote-server/messages.js";
31
-
32
- export interface RunOpenCodeTurnInputs {
33
- oc: OpencodeClient;
34
- sessionId: string;
35
- prompt: string;
36
- systemPrompt: string;
37
- providerID: string;
38
- modelID: string;
39
- state: ReturnType<typeof createStreamState>;
40
- chatId: string;
41
- seenQuestionIds: Set<string>;
42
- seenPermissionIds: Set<string>;
43
- seenToolCallIds: Set<string>;
44
- toolOverrides?: Record<string, boolean>;
45
- onStreamDelta?: (accumulated: string, phase?: "thinking" | "text") => void;
46
- onTextBlock?: (text: string) => Promise<void>;
47
- onToolUse?: (toolName: string, input: Record<string, unknown>) => void;
48
- }
49
-
50
- /**
51
- * Run one OpenCode turn end-to-end. SSE-first strategy identical to Kilo:
52
- * subscribe before prompting, fire `promptAsync`, await the SSE close, then
53
- * read the authoritative parts list. Avoids the sync `session.prompt` which
54
- * would hang our await when the upstream model stalls.
55
- */
56
- export async function runOpenCodeTurn(
57
- inputs: RunOpenCodeTurnInputs,
58
- ): Promise<void> {
59
- const {
60
- oc,
61
- sessionId,
62
- prompt,
63
- systemPrompt,
64
- providerID,
65
- modelID,
66
- state,
67
- chatId,
68
- seenQuestionIds,
69
- seenPermissionIds,
70
- seenToolCallIds,
71
- toolOverrides,
72
- onStreamDelta,
73
- onTextBlock,
74
- onToolUse,
75
- } = inputs;
76
-
77
- // SSE subscription FIRST — early `session.turn.open` and
78
- // `message.part.updated` events can fire immediately after promptAsync
79
- // returns, so the iterator must already be alive.
80
- const sseAbort = new AbortController();
81
- const sseDone = subscribeToTurnEvents({
82
- oc,
83
- sessionId,
84
- state,
85
- chatId,
86
- seenToolCallIds,
87
- onStreamDelta,
88
- onTextBlock,
89
- onToolUse,
90
- abortSignal: sseAbort.signal,
91
- });
92
-
93
- // Headless-interaction watchdog: resolve upstream questions and any
94
- // permission requests that escaped the session ruleset.
95
- const questionWatchdog = (async () => {
96
- while (!sseAbort.signal.aborted) {
97
- try {
98
- await Promise.all([
99
- rejectPendingQuestions(oc, sessionId, chatId, seenQuestionIds),
100
- approvePendingPermissions(oc, sessionId, chatId, seenPermissionIds),
101
- ]);
102
- } catch (err) {
103
- logWarn(
104
- "agent",
105
- `[${chatId}] question watchdog failed: ${errMsg(err)}`,
106
- );
107
- }
108
- await sleep(350, sseAbort.signal);
109
- }
110
- })();
111
-
112
- try {
113
- // Fire and forget — promptAsync returns immediately. The await below
114
- // is on the SSE close event.
115
- await awaitRemoteTurn(
116
- (async () => {
117
- await oc.session.promptAsync({
118
- sessionID: sessionId,
119
- parts: [{ type: "text", text: prompt }],
120
- model: { providerID, modelID },
121
- system: systemPrompt,
122
- ...(toolOverrides ? { tools: toolOverrides } : {}),
123
- });
124
-
125
- // Await turn completion via SSE.
126
- await sseDone;
127
- })(),
128
- { client: oc, sessionId, chatId, label: "OpenCode" },
129
- );
130
-
131
- // Read authoritative final state from the messages endpoint.
132
- const messagesResp = await oc.session.messages({ sessionID: sessionId });
133
- const messages = Array.isArray(messagesResp.data)
134
- ? (messagesResp.data as Array<Record<string, unknown>>)
135
- : [];
136
- const lastAssistant = findLastAssistantMessage(messages);
137
- const parts = lastAssistant?.parts ?? [];
138
- const assistantInfo = lastAssistant?.info;
139
-
140
- finalizePartsIntoState({
141
- parts,
142
- state,
143
- seenToolCallIds,
144
- extractPartsSummary,
145
- onToolUse,
146
- });
147
-
148
- if (assistantInfo) {
149
- const usage = extractAssistantUsage(assistantInfo);
150
- if (state.sdkInputTokens === 0 && state.sdkOutputTokens === 0) {
151
- recordTokens(state, {
152
- inputTokens: usage.inputTokens,
153
- outputTokens: usage.outputTokens,
154
- cacheRead: usage.cacheRead,
155
- cacheWrite: usage.cacheWrite,
156
- });
157
- }
158
- }
159
- } catch (err) {
160
- // Explicit user interrupts abort intentionally — swallow that close path.
161
- if (state.turnTerminated && /abort/i.test(errMsg(err))) {
162
- return;
163
- }
164
- throw err;
165
- } finally {
166
- sseAbort.abort();
167
- // A dead SSE socket may ignore the local abort flag until another event
168
- // arrives. Bound cleanup so a timed-out turn cannot wedge its caller in
169
- // the finally block it was meant to escape.
170
- await Promise.race([sseDone.catch(() => {}), sleep(1_000)]);
171
- await questionWatchdog.catch(() => {});
172
- try {
173
- await Promise.all([
174
- rejectPendingQuestions(oc, sessionId, chatId, seenQuestionIds),
175
- approvePendingPermissions(oc, sessionId, chatId, seenPermissionIds),
176
- ]);
177
- } catch {
178
- /* noop */
179
- }
180
- }
181
- }
182
-
183
- /**
184
- * Find the most recent assistant message in a session-messages list and
185
- * surface its parts + assistant info. Shared walker — see
186
- * `remote-server/messages.ts`.
187
- */
188
- const findLastAssistantMessage = (
189
- messages: Array<Record<string, unknown>>,
190
- ): {
191
- parts: Array<Record<string, unknown>>;
192
- info?: OpenCodeAssistantInfo;
193
- } | null => findLastAssistantMessageShared<OpenCodeAssistantInfo>(messages);
194
-
195
- interface SubscribeInputs {
196
- oc: OpencodeClient;
197
- sessionId: string;
198
- state: ReturnType<typeof createStreamState>;
199
- chatId: string;
200
- seenToolCallIds: Set<string>;
201
- onStreamDelta?: (accumulated: string, phase?: "thinking" | "text") => void;
202
- onTextBlock?: (text: string) => Promise<void>;
203
- onToolUse?: (toolName: string, input: Record<string, unknown>) => void;
204
- abortSignal: AbortSignal;
205
- }
206
-
207
- /**
208
- * Subscribe to OpenCode's global SSE event stream and translate relevant
209
- * events into stream-state mutations / callback firings. Only events scoped
210
- * to our `sessionId` are processed; others are dropped silently. Errors are
211
- * logged but do not propagate.
212
- */
213
- async function subscribeToTurnEvents(inputs: SubscribeInputs): Promise<void> {
214
- const {
215
- oc,
216
- sessionId,
217
- state,
218
- chatId,
219
- seenToolCallIds,
220
- onStreamDelta,
221
- onTextBlock,
222
- onToolUse,
223
- abortSignal,
224
- } = inputs;
225
-
226
- // `subscribeSseStream` handles the narrowing with a runtime guard + the
227
- // subscribe-failed warning.
228
- const stream = await subscribeSseStream(oc, chatId);
229
- if (!stream) throw new Error("OpenCode SSE connection unavailable");
230
-
231
- try {
232
- for await (const evt of stream) {
233
- if (abortSignal.aborted) break;
234
- if (!evt || typeof evt !== "object") continue;
235
-
236
- // OpenCode's SSE wire format wraps every event in
237
- // `{payload: {type, properties}}` — same as Kilo.
238
- const payload =
239
- evt && typeof evt === "object" && "payload" in evt
240
- ? (evt as { payload?: unknown }).payload
241
- : evt;
242
- if (!payload || typeof payload !== "object") continue;
243
- const event = payload as {
244
- type?: string;
245
- properties?: Record<string, unknown>;
246
- };
247
-
248
- // session.error scope-filter to our own sessionId before attributing
249
- // the error to this chat — the SSE stream is global and a heartbeat
250
- // session.error would otherwise pollute the chat's log.
251
- if (event.type === "session.error") {
252
- const props = event.properties ?? {};
253
- const evtSessionID =
254
- typeof props.sessionID === "string" ? props.sessionID : undefined;
255
- if (evtSessionID && evtSessionID !== sessionId) {
256
- continue;
257
- }
258
- const errProp = props.error as
259
- | {
260
- name?: string;
261
- message?: string;
262
- data?: Record<string, unknown>;
263
- }
264
- | undefined;
265
- // MessageAbortedError is expected for an explicit user interrupt.
266
- const isOurAbort =
267
- state.turnTerminated &&
268
- (errProp?.name === "MessageAbortedError" ||
269
- /abort/i.test(errProp?.name ?? "") ||
270
- /abort/i.test(errProp?.message ?? ""));
271
- if (errProp && !isOurAbort) {
272
- const detail = [
273
- errProp.name && `name=${errProp.name}`,
274
- errProp.message && `message=${errProp.message}`,
275
- errProp.data && `data=${JSON.stringify(errProp.data)}`,
276
- ]
277
- .filter(Boolean)
278
- .join(" ");
279
- logWarn("agent", `[${chatId}] OpenCode session.error: ${detail}`);
280
- const msg = errProp.message ?? errProp.name;
281
- if (msg) state.syntheticError = msg;
282
- }
283
- return;
284
- }
285
-
286
- const outcome = await processStreamEvent(event, {
287
- chatId,
288
- sessionId,
289
- state,
290
- seenToolCallIds,
291
- backendLabel: "OpenCode",
292
- onStreamDelta,
293
- onTextBlock,
294
- onToolUse,
295
- });
296
-
297
- if (outcome.kind === "terminator_fired") {
298
- // Aborting here can race with a prompt accepted immediately after
299
- // this turn settles and cancel that next turn on the reused session.
300
- // Delivery is already complete, so wait for natural idle instead.
301
- continue;
302
- }
303
-
304
- if (outcome.kind === "stop") {
305
- if (outcome.reason === "out_of_scope") continue;
306
- return; // turn.close or idle — stop iterating
307
- }
308
- }
309
- } catch (err) {
310
- if (!abortSignal.aborted) {
311
- logWarn("agent", `[${chatId}] SSE iteration failed: ${errMsg(err)}`);
312
- }
313
- }
314
- }