threadwire 0.1.5 → 0.1.8
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/CHANGELOG.md +40 -0
- package/README.md +17 -4
- package/TELEGRAM-INGRESS.md +8 -2
- package/bin/isolated-runtime.js +5 -0
- package/bin/model-broker.js +5 -0
- package/docs/card-10520-plan.md +45 -0
- package/docs/container-runtime.md +3 -2
- package/docs/delegated-result-protocol.md +161 -0
- package/docs/evidence-artifacts.md +106 -0
- package/docs/isolated-provider-runtime.md +137 -0
- package/package.json +5 -1
- package/scripts/provider-shims/front-door.sh.template +18 -0
- package/scripts/verify-package.js +22 -1
- package/src/absolute-deadline.js +94 -0
- package/src/cli.js +362 -42
- package/src/context-budget-metrics.js +180 -0
- package/src/delegated-result-admission.js +377 -0
- package/src/docker-api.js +131 -0
- package/src/evidence-store.js +1472 -0
- package/src/isolated-runtime-client.js +149 -0
- package/src/isolated-runtime.js +982 -0
- package/src/isolated-state.js +409 -0
- package/src/isolated-worker.js +123 -0
- package/src/model-broker-policy.js +139 -0
- package/src/model-broker.js +313 -0
- package/src/mount-policy.js +28 -0
- package/src/normalized-output.js +68 -0
- package/src/notice-queue.js +22 -7
- package/src/providers/opencode.js +42 -18
- package/src/relay-write.js +44 -0
- package/src/relay.js +6 -6
- package/src/run-worker.js +158 -40
- package/src/telegram-ingress/command.js +16 -0
- package/src/telegram-ingress/config.js +96 -11
- package/src/telegram-ingress/core.js +201 -89
- package/src/telegram-ingress/http.js +17 -1
- package/src/telegram-webhook.js +22 -0
- package/src/types.js +2 -2
- package/src/worker-control.js +294 -0
- package/src/hermes-protocol.js +0 -126
|
@@ -1,30 +1,35 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import {createHash, timingSafeEqual} from "node:crypto"
|
|
3
|
+
import {createHash, randomUUID, timingSafeEqual} from "node:crypto"
|
|
4
|
+
import {evidenceTelegramDestination} from "../evidence-store.js"
|
|
4
5
|
import {NoticeQueue} from "../notice-queue.js"
|
|
5
6
|
import {createFetchTransport} from "../notifiers/fetch-transport.js"
|
|
6
7
|
import {createTelegramSender} from "../notifiers/telegram.js"
|
|
7
8
|
import {createProvider} from "../providers/index.js"
|
|
8
9
|
import {Relay} from "../relay.js"
|
|
9
10
|
import {runWorker} from "../run-worker.js"
|
|
11
|
+
import {WorkerControl} from "../worker-control.js"
|
|
10
12
|
import {resolveWorkspaceProfile} from "../workspace-profile.js"
|
|
11
|
-
import {buildProviderEnvironment} from "./config.js"
|
|
12
|
-
import {parseCodeCommand} from "./command.js"
|
|
13
|
+
import {buildProviderEnvironment, collectEvidenceRedactions} from "./config.js"
|
|
14
|
+
import {parseCodeCommand, parseEvidenceCommand} from "./command.js"
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* @typedef {{platform: "telegram", chatId: string, threadId: number | null}} TelegramTarget
|
|
16
|
-
* @typedef {{provider: "codex" | "claude" | "opencode", prompt: string, target: TelegramTarget}} AcceptedJob
|
|
18
|
+
* @typedef {{provider: "codex" | "claude" | "opencode", prompt: string, target: TelegramTarget, senderId: string} | {type: "evidence", request: unknown, target: TelegramTarget, senderId: string}} AcceptedJob
|
|
17
19
|
* @typedef {{
|
|
18
20
|
* createProvider?: typeof createProvider,
|
|
19
21
|
* createFetchTransport?: typeof createFetchTransport,
|
|
20
22
|
* createTelegramSender?: typeof createTelegramSender,
|
|
21
23
|
* runWorker?: typeof runWorker,
|
|
24
|
+
* WorkerControl?: typeof WorkerControl,
|
|
22
25
|
* NoticeQueue?: typeof NoticeQueue,
|
|
23
26
|
* Relay?: typeof Relay,
|
|
24
27
|
* processNumber?: number,
|
|
25
28
|
* providerEnvironment?: NodeJS.ProcessEnv,
|
|
26
29
|
* workspaceProfileOperations?: import("../workspace-profile.js").WorkspaceProfileOperations,
|
|
27
30
|
* activity?: Pick<import("../activity-log.js").ActivityLog, "recordWorkspace" | "recordStarted" | "recordSession" | "close">,
|
|
31
|
+
* evidenceStore?: import("../evidence-store.js").EvidenceStore,
|
|
32
|
+
* isolatedRuntimeClient?: Pick<import("../isolated-runtime-client.js").IsolatedRuntimeClient, "preflight" | "run">,
|
|
28
33
|
* onWorkerFailure?: (message: string) => void,
|
|
29
34
|
* onWorkerSettled?: () => void
|
|
30
35
|
* }} IngressDependencies
|
|
@@ -55,11 +60,12 @@ export function interpretUpdate(update, config) {
|
|
|
55
60
|
if (!isRecord(update)) return null
|
|
56
61
|
const message = update.message
|
|
57
62
|
if (!isRecord(message)) return null
|
|
63
|
+
if ([
|
|
64
|
+
"forward_origin", "forward_from", "forward_from_chat", "forward_signature",
|
|
65
|
+
"forward_sender_name", "is_automatic_forward"
|
|
66
|
+
].some((field) => Object.prototype.hasOwnProperty.call(message, field))) return null
|
|
58
67
|
if (typeof message.text !== "string") return null
|
|
59
68
|
|
|
60
|
-
const command = parseCodeCommand(message.text)
|
|
61
|
-
if (!command) return null
|
|
62
|
-
|
|
63
69
|
const chatId = telegramIdString(isRecord(message.chat) ? message.chat.id : undefined)
|
|
64
70
|
if (chatId === null || !config.allowedChatIds.has(chatId)) return null
|
|
65
71
|
|
|
@@ -69,7 +75,11 @@ export function interpretUpdate(update, config) {
|
|
|
69
75
|
const target = deriveTarget(chatId, message.message_thread_id)
|
|
70
76
|
if (target === null) return null
|
|
71
77
|
|
|
72
|
-
|
|
78
|
+
const command = parseCodeCommand(message.text)
|
|
79
|
+
if (command) return {provider: command.provider, prompt: command.prompt, target, senderId}
|
|
80
|
+
const evidence = parseEvidenceCommand(message.text)
|
|
81
|
+
if (evidence) return {type: "evidence", request: evidence, target, senderId}
|
|
82
|
+
return null
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
/**
|
|
@@ -83,10 +93,12 @@ export function interpretUpdate(update, config) {
|
|
|
83
93
|
* @returns {Promise<void>}
|
|
84
94
|
*/
|
|
85
95
|
export async function dispatchWorker(job, config, dependencies = {}) {
|
|
96
|
+
if ("type" in job) throw new Error("Worker dispatch requires a worker job")
|
|
86
97
|
const createProviderImpl = dependencies.createProvider ?? createProvider
|
|
87
98
|
const createFetchTransportImpl = dependencies.createFetchTransport ?? createFetchTransport
|
|
88
99
|
const createTelegramSenderImpl = dependencies.createTelegramSender ?? createTelegramSender
|
|
89
100
|
const runWorkerImpl = dependencies.runWorker ?? runWorker
|
|
101
|
+
const WorkerControlImpl = dependencies.WorkerControl ?? WorkerControl
|
|
90
102
|
const NoticeQueueImpl = dependencies.NoticeQueue ?? NoticeQueue
|
|
91
103
|
const RelayImpl = dependencies.Relay ?? Relay
|
|
92
104
|
const processNumber = dependencies.processNumber ?? process.pid
|
|
@@ -106,111 +118,211 @@ export async function dispatchWorker(job, config, dependencies = {}) {
|
|
|
106
118
|
)
|
|
107
119
|
const transport = createFetchTransportImpl(config.botToken, undefined, config.telegramRequestTimeoutMs)
|
|
108
120
|
const sender = createTelegramSenderImpl(job.target, transport)
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
const control = new WorkerControlImpl({
|
|
122
|
+
sender,
|
|
123
|
+
processNumber,
|
|
124
|
+
toolMessages: config.toolMessages ?? false,
|
|
125
|
+
NoticeQueueClass: NoticeQueueImpl,
|
|
126
|
+
RelayClass: RelayImpl
|
|
127
|
+
})
|
|
128
|
+
const evidenceOwner = dependencies.evidenceStore?.createOwnerScope({
|
|
129
|
+
destinationId: evidenceTelegramDestination(job.target, job.senderId),
|
|
130
|
+
runId: randomUUID()
|
|
131
|
+
})
|
|
132
|
+
const evidence = evidenceOwner === undefined ? undefined : await dependencies.evidenceStore?.createArtifact(evidenceOwner, {
|
|
133
|
+
contentType: "text/plain; charset=utf-8",
|
|
134
|
+
redactions: await collectEvidenceRedactions(dependencies.providerEnvironment ?? {})
|
|
135
|
+
})
|
|
136
|
+
let evidenceTransferred = false
|
|
137
|
+
try {
|
|
138
|
+
await evidence?.append("prompt", `prompt\n${job.prompt}\nprovider-stream\n`)
|
|
139
|
+
|
|
140
|
+
if (job.provider === "codex" && dependencies.isolatedRuntimeClient !== undefined) {
|
|
141
|
+
evidenceTransferred = true
|
|
142
|
+
try {
|
|
143
|
+
const preflight = await dependencies.isolatedRuntimeClient.preflight({
|
|
144
|
+
provider: "codex",
|
|
145
|
+
profile: workspace.profile,
|
|
146
|
+
repositoryRoot: workspace.repositoryRoot,
|
|
147
|
+
cwd: workspace.cwd,
|
|
148
|
+
providerArguments: []
|
|
149
|
+
})
|
|
150
|
+
const exitCode = await dependencies.isolatedRuntimeClient.run({
|
|
151
|
+
preflightId: preflight.preflightId,
|
|
152
|
+
prompt: job.prompt,
|
|
153
|
+
providerArguments: [],
|
|
154
|
+
...(preflight.deadline === undefined ? {} : {deadline: preflight.deadline}),
|
|
155
|
+
onEvent: async (event) => control.accept(event),
|
|
156
|
+
onRecord: async (record) => {
|
|
157
|
+
const id = provider.sessionId(record)
|
|
158
|
+
if (id !== undefined) dependencies.activity?.recordSession(provider.name, id)
|
|
159
|
+
},
|
|
160
|
+
onStdoutChunk: (chunk) => evidence?.append("provider-stdout", chunk),
|
|
161
|
+
onStderrChunk: (chunk) => evidence?.append("provider-stderr", chunk)
|
|
162
|
+
})
|
|
163
|
+
if (exitCode !== 0) throw new Error(`Isolated Codex worker exited with status ${exitCode}`)
|
|
164
|
+
await control.close()
|
|
165
|
+
if (evidence !== undefined) {
|
|
166
|
+
await evidence.finalize()
|
|
167
|
+
await sender.send(`Evidence: ${evidence.handle}`)
|
|
168
|
+
}
|
|
169
|
+
return
|
|
170
|
+
} catch (error) {
|
|
171
|
+
await control.close().catch(() => {})
|
|
172
|
+
await evidence?.abort()
|
|
173
|
+
throw error
|
|
174
|
+
} finally {
|
|
175
|
+
dependencies.onWorkerSettled?.()
|
|
176
|
+
}
|
|
123
177
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
178
|
+
|
|
179
|
+
/** @type {{resolve: () => void, reject: (error: Error) => void, settled: boolean}} */
|
|
180
|
+
const spawnGate = {
|
|
181
|
+
settled: false,
|
|
182
|
+
resolve() {},
|
|
183
|
+
reject() {}
|
|
128
184
|
}
|
|
129
|
-
|
|
185
|
+
const spawned = new Promise((resolve, reject) => {
|
|
186
|
+
spawnGate.resolve = () => {
|
|
187
|
+
if (spawnGate.settled) return
|
|
188
|
+
spawnGate.settled = true
|
|
189
|
+
resolve(undefined)
|
|
190
|
+
}
|
|
191
|
+
spawnGate.reject = (error) => {
|
|
192
|
+
if (spawnGate.settled) return
|
|
193
|
+
spawnGate.settled = true
|
|
194
|
+
reject(error)
|
|
195
|
+
}
|
|
196
|
+
})
|
|
130
197
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
198
|
+
/** @param {unknown} error */
|
|
199
|
+
const reportBackgroundFailure = (error) => {
|
|
200
|
+
const failure = error instanceof Error ? error : new Error(safeFailureMessage(error))
|
|
201
|
+
try {
|
|
202
|
+
dependencies.onWorkerFailure?.(safeFailureMessage(failure))
|
|
203
|
+
} catch {
|
|
137
204
|
/* Observer errors must not become unhandled rejections. */
|
|
205
|
+
}
|
|
138
206
|
}
|
|
139
|
-
}
|
|
140
207
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
208
|
+
const notifySettled = () => {
|
|
209
|
+
try {
|
|
210
|
+
dependencies.onWorkerSettled?.()
|
|
211
|
+
} catch {
|
|
145
212
|
/* Observer errors must not become unhandled rejections. */
|
|
213
|
+
}
|
|
146
214
|
}
|
|
147
|
-
}
|
|
148
215
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
216
|
+
const running = runWorkerImpl({
|
|
217
|
+
executable: provider.executable,
|
|
218
|
+
arguments: provider.arguments,
|
|
219
|
+
cwd: workspace.cwd,
|
|
220
|
+
environment: providerEnvironment,
|
|
221
|
+
parse: provider.parse,
|
|
222
|
+
onEvent: async (event) => control.accept(event),
|
|
223
|
+
onSpawn: (pid) => {
|
|
224
|
+
if (pid !== undefined) dependencies.activity?.recordStarted(provider.name, pid)
|
|
225
|
+
spawnGate.resolve()
|
|
226
|
+
},
|
|
227
|
+
onRecord: async (record) => {
|
|
228
|
+
const id = provider.sessionId(record)
|
|
229
|
+
if (id !== undefined) dependencies.activity?.recordSession(provider.name, id)
|
|
230
|
+
},
|
|
231
|
+
onStdoutChunk: (chunk) => evidence?.append("provider-stdout", chunk),
|
|
232
|
+
onStderrChunk: (chunk) => evidence?.append("provider-stderr", chunk)
|
|
233
|
+
})
|
|
165
234
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
235
|
+
// Detached observer: never resolves the spawn gate from process exit — only
|
|
236
|
+
// from onSpawn. Pre-spawn failure rejects the gate; post-spawn failure is
|
|
237
|
+
// reported. Always notify settlement so concurrency slots are released.
|
|
238
|
+
evidenceTransferred = true
|
|
239
|
+
void (async () => {
|
|
171
240
|
try {
|
|
172
|
-
await running
|
|
173
|
-
} catch (error) {
|
|
174
|
-
const failure = error instanceof Error ? error : new Error(safeFailureMessage(error))
|
|
175
|
-
if (spawnGate.settled) {
|
|
176
|
-
reportBackgroundFailure(failure)
|
|
177
|
-
} else {
|
|
178
|
-
spawnGate.reject(failure)
|
|
179
|
-
}
|
|
180
241
|
try {
|
|
181
|
-
await
|
|
182
|
-
} catch {
|
|
242
|
+
await running
|
|
243
|
+
} catch (error) {
|
|
244
|
+
const failure = error instanceof Error ? error : new Error(safeFailureMessage(error))
|
|
245
|
+
if (spawnGate.settled) {
|
|
246
|
+
reportBackgroundFailure(failure)
|
|
247
|
+
} else {
|
|
248
|
+
spawnGate.reject(failure)
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
await control.close()
|
|
252
|
+
} catch {
|
|
183
253
|
/* Best-effort close after worker failure. */
|
|
254
|
+
}
|
|
255
|
+
return
|
|
184
256
|
}
|
|
185
|
-
return
|
|
186
|
-
}
|
|
187
257
|
|
|
188
|
-
|
|
189
|
-
|
|
258
|
+
try {
|
|
259
|
+
await control.close()
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (spawnGate.settled) {
|
|
262
|
+
reportBackgroundFailure(error)
|
|
263
|
+
} else {
|
|
264
|
+
spawnGate.reject(error instanceof Error ? error : new Error(safeFailureMessage(error)))
|
|
265
|
+
}
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!spawnGate.settled) {
|
|
270
|
+
spawnGate.reject(new Error("Worker exited before spawn was observed"))
|
|
271
|
+
}
|
|
190
272
|
} catch (error) {
|
|
191
273
|
if (spawnGate.settled) {
|
|
192
274
|
reportBackgroundFailure(error)
|
|
193
275
|
} else {
|
|
194
276
|
spawnGate.reject(error instanceof Error ? error : new Error(safeFailureMessage(error)))
|
|
195
277
|
}
|
|
196
|
-
|
|
278
|
+
} finally {
|
|
279
|
+
if (evidence !== undefined) {
|
|
280
|
+
try {
|
|
281
|
+
await evidence.finalize()
|
|
282
|
+
await sender.send(`Evidence: ${evidence.handle}`)
|
|
283
|
+
} catch (error) {
|
|
284
|
+
await evidence.abort()
|
|
285
|
+
reportBackgroundFailure(error)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
notifySettled()
|
|
197
289
|
}
|
|
290
|
+
})()
|
|
198
291
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
292
|
+
await spawned
|
|
293
|
+
} finally {
|
|
294
|
+
if (!evidenceTransferred) await evidence?.abort()
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** @param {Extract<AcceptedJob, {type: "evidence"}>} job @param {{botToken: string, telegramRequestTimeoutMs?: number}} config @param {IngressDependencies} dependencies */
|
|
299
|
+
export async function dispatchEvidenceRead(job, config, dependencies) {
|
|
300
|
+
if (!dependencies.evidenceStore) throw new Error("Evidence retrieval is unavailable")
|
|
301
|
+
const transport = (dependencies.createFetchTransport ?? createFetchTransport)(config.botToken, undefined, config.telegramRequestTimeoutMs)
|
|
302
|
+
const sender = (dependencies.createTelegramSender ?? createTelegramSender)(job.target, transport)
|
|
303
|
+
const result = await dependencies.evidenceStore.readTelegram({...job.target, senderId: job.senderId}, job.request)
|
|
304
|
+
const label = result.encoding === "base64"
|
|
305
|
+
? `Evidence (${result.contentType}, base64, truncated):\n`
|
|
306
|
+
: `Evidence (${result.contentType}, redacted, truncated):\n`
|
|
307
|
+
const boundedData = telegramEvidenceText(result.data, Math.max(0, 3_000 - Buffer.byteLength(label)))
|
|
308
|
+
await sender.send({
|
|
309
|
+
text: result.encoding === "base64"
|
|
310
|
+
? `Evidence (${result.contentType}, base64${boundedData.truncated ? ", truncated" : ""}):\n${boundedData.text}`
|
|
311
|
+
: `Evidence (${result.contentType}${result.redacted ? ", redacted" : ""}${result.truncated || boundedData.truncated ? ", truncated" : ""}):\n${boundedData.text}`
|
|
312
|
+
})
|
|
313
|
+
}
|
|
212
314
|
|
|
213
|
-
|
|
315
|
+
/** @param {string} value @param {number} maxBytes */
|
|
316
|
+
function telegramEvidenceText(value, maxBytes) {
|
|
317
|
+
let text = ""
|
|
318
|
+
let bytes = 0
|
|
319
|
+
for (const character of value) {
|
|
320
|
+
const width = Buffer.byteLength(character, "utf8")
|
|
321
|
+
if (bytes + width > maxBytes) return {text, truncated: true}
|
|
322
|
+
text += character
|
|
323
|
+
bytes += width
|
|
324
|
+
}
|
|
325
|
+
return {text, truncated: false}
|
|
214
326
|
}
|
|
215
327
|
|
|
216
328
|
/** @param {string} chatId @param {unknown} threadId @returns {TelegramTarget | null} */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import {createConcurrencyLimiter} from "./concurrency.js"
|
|
4
|
-
import {dispatchWorker, interpretUpdate, secretsEqual} from "./core.js"
|
|
4
|
+
import {dispatchEvidenceRead, dispatchWorker, interpretUpdate, secretsEqual} from "./core.js"
|
|
5
5
|
import {createUpdateGuard, parseUpdateId} from "./update-guard.js"
|
|
6
6
|
import {WorkspaceProviderMismatchError} from "../workspace-profile.js"
|
|
7
7
|
|
|
@@ -112,6 +112,22 @@ async function handleRequest(request, response, config, dependencies) {
|
|
|
112
112
|
return
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
if ("type" in job) {
|
|
116
|
+
try {
|
|
117
|
+
await dispatchEvidenceRead(job, {
|
|
118
|
+
botToken: config.botToken,
|
|
119
|
+
telegramRequestTimeoutMs: config.telegramRequestTimeoutMs
|
|
120
|
+
}, dependencies)
|
|
121
|
+
dependencies.updateGuard.complete(updateId)
|
|
122
|
+
send(response, 200)
|
|
123
|
+
} catch (error) {
|
|
124
|
+
dependencies.updateGuard.release(updateId)
|
|
125
|
+
dependencies.onOperationalError?.(error instanceof Error ? error.message : "Evidence retrieval failed")
|
|
126
|
+
send(response, 500)
|
|
127
|
+
}
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
115
131
|
if (!dependencies.concurrency.tryAcquire()) {
|
|
116
132
|
dependencies.updateGuard.release(updateId)
|
|
117
133
|
send(response, 503)
|
package/src/telegram-webhook.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import {createServer} from "node:http"
|
|
4
4
|
import {ActivityLog} from "./activity-log.js"
|
|
5
|
+
import {EvidenceStore} from "./evidence-store.js"
|
|
5
6
|
import {buildProviderEnvironment, parseIngressConfig, resolveIngressEnvironment} from "./telegram-ingress/config.js"
|
|
6
7
|
import {createWebhookHandler} from "./telegram-ingress/http.js"
|
|
8
|
+
import {isolatedRuntimeClientFromEnvironment} from "./isolated-runtime-client.js"
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Start the standalone Threadwire Telegram webhook service.
|
|
@@ -15,6 +17,7 @@ import {createWebhookHandler} from "./telegram-ingress/http.js"
|
|
|
15
17
|
* onWorkerFailure?: (message: string) => void,
|
|
16
18
|
* providerEnvironment?: NodeJS.ProcessEnv,
|
|
17
19
|
* activity?: Pick<ActivityLog, "recordWorkspace" | "recordStarted" | "recordSession" | "close">,
|
|
20
|
+
* evidenceStore?: EvidenceStore,
|
|
18
21
|
* handlerDependencies?: import("./telegram-ingress/http.js").WebhookDependencies
|
|
19
22
|
* }} [options]
|
|
20
23
|
* @returns {Promise<{server: import("node:http").Server, config: import("./telegram-ingress/config.js").IngressConfig, handler: (request: import("node:http").IncomingMessage, response: import("node:http").ServerResponse) => void}>}
|
|
@@ -32,16 +35,35 @@ export async function startTelegramWebhook(options = {}) {
|
|
|
32
35
|
const providerEnvironment = buildProviderEnvironment(
|
|
33
36
|
options.providerEnvironment ?? environment
|
|
34
37
|
)
|
|
38
|
+
const isolatedConfigured = environment.THREADWIRE_ISOLATED_RUNTIME_URL !== undefined
|
|
39
|
+
|| environment.THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN !== undefined
|
|
40
|
+
const isolatedRuntimeClient = options.handlerDependencies?.isolatedRuntimeClient
|
|
41
|
+
?? (isolatedConfigured ? isolatedRuntimeClientFromEnvironment(environment) : undefined)
|
|
35
42
|
const activity = options.activity ?? options.handlerDependencies?.activity ?? new ActivityLog("/var/lib/threadwire/activity/threadwire.jsonl")
|
|
43
|
+
const evidenceStore = options.evidenceStore ?? options.handlerDependencies?.evidenceStore ?? await EvidenceStore.open({
|
|
44
|
+
root: config.evidenceRoot ?? "/var/lib/threadwire/evidence"
|
|
45
|
+
})
|
|
36
46
|
|
|
37
47
|
const handler = createWebhookHandler(config, {
|
|
38
48
|
...options.handlerDependencies,
|
|
39
49
|
activity,
|
|
50
|
+
evidenceStore,
|
|
40
51
|
providerEnvironment,
|
|
52
|
+
...(isolatedRuntimeClient === undefined ? {} : {isolatedRuntimeClient}),
|
|
41
53
|
onOperationalError,
|
|
42
54
|
onWorkerFailure
|
|
43
55
|
})
|
|
44
56
|
const server = createServerImpl(handler)
|
|
57
|
+
if (options.evidenceStore === undefined && options.handlerDependencies?.evidenceStore === undefined) {
|
|
58
|
+
const cleanupTimer = setInterval(() => {
|
|
59
|
+
void evidenceStore.cleanup().catch((error) => onOperationalError(error instanceof Error ? error.message : "Evidence cleanup failed"))
|
|
60
|
+
}, 60 * 60 * 1000)
|
|
61
|
+
cleanupTimer.unref()
|
|
62
|
+
server.once("close", () => {
|
|
63
|
+
clearInterval(cleanupTimer)
|
|
64
|
+
void evidenceStore.close()
|
|
65
|
+
})
|
|
66
|
+
}
|
|
45
67
|
|
|
46
68
|
if (options.listen) {
|
|
47
69
|
await options.listen(server, config.port, config.host)
|
package/src/types.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* @typedef {{type: "diagnostic", level: "warning" | "error", summary: string}} DiagnosticEvent
|
|
8
8
|
* @typedef {TextDeltaEvent | ToolEvent | LifecycleEvent | DiagnosticEvent} WorkerEvent
|
|
9
9
|
* @typedef {{platform: "telegram", chatId: string, threadId: number | null}} TelegramTarget
|
|
10
|
-
* @typedef {{text: string, format?: "javascript-code", label?: string, batchSeparator?: string}} TextNotice
|
|
11
|
-
* @typedef {{activity: {key: string, phase: "started" | "finished"}, text: string, parseMode?: "HTML"}} ActivityNotice
|
|
10
|
+
* @typedef {{text: string, format?: "javascript-code", label?: string, batchSeparator?: string, metricClass?: string}} TextNotice
|
|
11
|
+
* @typedef {{activity: {key: string, phase: "started" | "finished"}, text: string, parseMode?: "HTML", metricClass?: string}} ActivityNotice
|
|
12
12
|
* @typedef {TextNotice | ActivityNotice} RenderedNotice
|
|
13
13
|
* @typedef {{text: string, parseMode?: "HTML"}} OutgoingMessage
|
|
14
14
|
* @typedef {{messageId: number}} SentMessage
|