threadwire 0.1.15 → 0.1.18

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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  import {createHash, randomUUID, timingSafeEqual} from "node:crypto"
4
4
  import {abortable} from "../absolute-deadline.js"
5
+ import {DelegatedResultAdmission} from "../delegated-result-admission.js"
5
6
  import {evidenceTelegramDestination} from "../evidence-store.js"
6
7
  import {NoticeQueue} from "../notice-queue.js"
7
8
  import {createFetchTransport} from "../notifiers/fetch-transport.js"
@@ -136,8 +137,19 @@ export async function dispatchWorker(job, config, dependencies = {}) {
136
137
  let control
137
138
  /** @type {Awaited<ReturnType<import("../evidence-store.js").EvidenceStore["createArtifact"]>> | undefined} */
138
139
  let evidence
140
+ /** @type {Awaited<ReturnType<import("../evidence-store.js").EvidenceStore["createArtifact"]>> | undefined} */
141
+ let resultEvidence
142
+ /** @type {{destinationId: string, runId: string} | undefined} */
143
+ let evidenceOwner
144
+ let resultEvidenceFinalized = false
145
+ /** @type {unknown} */
146
+ let evidenceError
147
+ /** @type {unknown} */
148
+ let deliveryError
149
+ const deliveryIdentity = randomUUID()
150
+ const admission = new DelegatedResultAdmission({output: {write: () => {}}})
139
151
  try {
140
- provider = createProviderImpl(job.provider, [], job.prompt, undefined, providerEnvironment)
152
+ provider = createProviderImpl(job.provider, [], job.prompt, undefined, providerEnvironment, {cwd})
141
153
  const transport = createFetchTransportImpl(config.botToken, undefined, config.telegramRequestTimeoutMs)
142
154
  sender = createTelegramSenderImpl(job.target, transport)
143
155
  control = new WorkerControlImpl({
@@ -147,18 +159,99 @@ export async function dispatchWorker(job, config, dependencies = {}) {
147
159
  NoticeQueueClass: NoticeQueueImpl,
148
160
  RelayClass: RelayImpl
149
161
  })
150
- const evidenceOwner = dependencies.evidenceStore?.createOwnerScope({
162
+ evidenceOwner = dependencies.evidenceStore?.createOwnerScope({
151
163
  destinationId: evidenceTelegramDestination(job.target, job.senderId),
152
- runId: randomUUID()
153
- })
154
- evidence = evidenceOwner === undefined ? undefined : await dependencies.evidenceStore?.createArtifact(evidenceOwner, {
155
- contentType: "text/plain; charset=utf-8",
156
- redactions: await collectEvidenceRedactions(dependencies.providerEnvironment ?? {})
164
+ runId: deliveryIdentity
157
165
  })
166
+ if (evidenceOwner !== undefined) {
167
+ const redactions = await collectEvidenceRedactions(dependencies.providerEnvironment ?? {})
168
+ evidence = await dependencies.evidenceStore?.createArtifact(evidenceOwner, {
169
+ contentType: "text/plain; charset=utf-8",
170
+ redactions
171
+ })
172
+ try {
173
+ resultEvidence = await dependencies.evidenceStore?.createArtifact(evidenceOwner, {
174
+ contentType: "application/json",
175
+ redactions,
176
+ delivery: {identity: deliveryIdentity, state: "delivery_pending"}
177
+ })
178
+ } catch (error) {
179
+ await evidence?.abort()
180
+ evidence = undefined
181
+ throw error
182
+ }
183
+ }
158
184
  } catch (error) {
159
185
  earlyKimiPreflight?.deadline?.close()
160
186
  throw error
161
187
  }
188
+ /** @param {import("../types.js").WorkerEvent} event */
189
+ const acceptEvent = async (event) => {
190
+ if (event.type === "text-delta") admission.acceptConclusionEvent(event)
191
+ try {
192
+ await control.accept(event)
193
+ } catch (error) {
194
+ deliveryError ??= error
195
+ }
196
+ }
197
+ /** @param {unknown} record */
198
+ const acceptRecord = (record) => {
199
+ const id = job.provider === "kimi" && isolatedRuntimeClient !== undefined
200
+ ? kimiSessionEnvelopeId(record)
201
+ : provider.sessionId(record)
202
+ if (id !== undefined) {
203
+ admission.setContinuationHandle(id)
204
+ dependencies.activity?.recordSession(provider.name, id)
205
+ }
206
+ }
207
+ /** @param {number} exitCode */
208
+ const finalizeEvidence = async (exitCode) => {
209
+ if (resultEvidence === undefined || resultEvidenceFinalized) return
210
+ let rawEvidenceReady = false
211
+ if (evidence !== undefined) {
212
+ try {
213
+ await evidence.finalize()
214
+ rawEvidenceReady = true
215
+ } catch (error) {
216
+ await evidence.abort()
217
+ evidenceError ??= error
218
+ }
219
+ }
220
+ admission.addArtifactHandle(resultEvidence.handle)
221
+ if (rawEvidenceReady) admission.addArtifactHandle(/** @type {NonNullable<typeof evidence>} */ (evidence).handle)
222
+ const terminal = {
223
+ state: /** @type {"completed" | "failed"} */ (exitCode === 0 ? "completed" : "failed"),
224
+ exitCode
225
+ }
226
+ await resultEvidence.append("provider-result", `${JSON.stringify(admission.createProviderResult(terminal))}\n`)
227
+ await resultEvidence.finalize()
228
+ resultEvidenceFinalized = true
229
+ }
230
+ const finishDelivery = async () => {
231
+ if (resultEvidence !== undefined && deliveryError === undefined) {
232
+ try {
233
+ await control.acceptEvidenceHandle(resultEvidence.handle)
234
+ } catch (error) {
235
+ deliveryError ??= error
236
+ }
237
+ }
238
+ try {
239
+ await control.close()
240
+ } catch (error) {
241
+ deliveryError ??= error
242
+ }
243
+ if (
244
+ resultEvidenceFinalized && resultEvidence !== undefined && evidenceOwner !== undefined
245
+ && dependencies.evidenceStore !== undefined
246
+ ) {
247
+ await dependencies.evidenceStore.recordDelivery(
248
+ evidenceOwner,
249
+ resultEvidence.handle,
250
+ deliveryError === undefined ? "delivery_succeeded" : "delivery_failed"
251
+ )
252
+ }
253
+ if (deliveryError !== undefined) throw deliveryError
254
+ }
162
255
  let evidenceTransferred = false
163
256
  try {
164
257
  await evidence?.append("prompt", `prompt\n${job.prompt}\nprovider-stream\n`)
@@ -181,33 +274,56 @@ export async function dispatchWorker(job, config, dependencies = {}) {
181
274
  prompt: job.prompt,
182
275
  providerArguments: [],
183
276
  ...(preflight.deadline === undefined ? {} : {deadline: preflight.deadline}),
184
- onEvent: async (event) => control.accept(event),
185
- onRecord: async (record) => {
186
- const id = job.provider === "kimi" ? kimiSessionEnvelopeId(record) : provider.sessionId(record)
187
- if (id !== undefined) dependencies.activity?.recordSession(provider.name, id)
188
- },
277
+ onEvent: acceptEvent,
278
+ onRecord: async (record) => acceptRecord(record),
189
279
  ...(job.provider === "kimi" ? {} : {
190
280
  onStdoutChunk: (chunk) => evidence?.append("provider-stdout", chunk),
191
281
  onStderrChunk: (chunk) => evidence?.append("provider-stderr", chunk)
192
282
  })
193
283
  })
194
- let exitCode
284
+ let exitCode = 2
285
+ /** @type {Error | undefined} */
286
+ let executionError
195
287
  try {
196
288
  exitCode = await abortable(isolatedRun.completion, preflight.deadline?.signal)
197
289
  } catch (error) {
198
- await isolatedRun.cancel(error instanceof Error ? error : new Error("Isolated runtime run failed"))
199
- throw error
290
+ executionError = error instanceof Error ? error : new Error("Isolated runtime run failed")
291
+ try {
292
+ await isolatedRun.cancel(executionError)
293
+ } catch (cancelError) {
294
+ executionError = new AggregateError(
295
+ [executionError, cancelError],
296
+ "Isolated runtime failed and cleanup could not be confirmed"
297
+ )
298
+ }
200
299
  }
201
- if (exitCode !== 0) throw new Error(`Isolated ${job.provider === "kimi" ? "Kimi" : "Codex"} worker exited with status ${exitCode}`)
202
- await control.close()
203
- if (evidence !== undefined) {
204
- await evidence.finalize()
205
- await sender.send(`Evidence: ${evidence.handle}`)
300
+ await finalizeEvidence(exitCode)
301
+ let propagatedExecutionError = executionError
302
+ try {
303
+ await finishDelivery()
304
+ } catch (error) {
305
+ if (propagatedExecutionError === undefined) throw error
306
+ propagatedExecutionError = new AggregateError(
307
+ [propagatedExecutionError, error],
308
+ propagatedExecutionError.message
309
+ )
206
310
  }
311
+ if (evidenceError !== undefined) {
312
+ if (propagatedExecutionError === undefined) throw evidenceError
313
+ propagatedExecutionError = new AggregateError(
314
+ [propagatedExecutionError, evidenceError],
315
+ propagatedExecutionError.message
316
+ )
317
+ }
318
+ if (propagatedExecutionError !== undefined) throw propagatedExecutionError
319
+ if (exitCode !== 0) throw new Error(`Isolated ${job.provider === "kimi" ? "Kimi" : "Codex"} worker exited with status ${exitCode}`)
207
320
  return
208
321
  } catch (error) {
209
322
  await control.close().catch(() => {})
210
- await evidence?.abort()
323
+ if (!resultEvidenceFinalized) {
324
+ await evidence?.abort()
325
+ await resultEvidence?.abort()
326
+ }
211
327
  throw error
212
328
  } finally {
213
329
  isolatedDeadline?.close()
@@ -260,15 +376,12 @@ export async function dispatchWorker(job, config, dependencies = {}) {
260
376
  provider: provider.name,
261
377
  parse: provider.parse,
262
378
  ...(provider.completion === undefined ? {} : {completion: provider.completion}),
263
- onEvent: async (event) => control.accept(event),
379
+ onEvent: acceptEvent,
264
380
  onSpawn: (pid) => {
265
381
  if (pid !== undefined) dependencies.activity?.recordStarted(provider.name, pid)
266
382
  spawnGate.resolve()
267
383
  },
268
- onRecord: async (record) => {
269
- const id = provider.sessionId(record)
270
- if (id !== undefined) dependencies.activity?.recordSession(provider.name, id)
271
- },
384
+ onRecord: async (record) => acceptRecord(record),
272
385
  onStdoutChunk: (chunk) => evidence?.append("provider-stdout", chunk),
273
386
  onStderrChunk: (chunk) => evidence?.append("provider-stderr", chunk)
274
387
  })
@@ -278,49 +391,61 @@ export async function dispatchWorker(job, config, dependencies = {}) {
278
391
  // reported. Always notify settlement so concurrency slots are released.
279
392
  evidenceTransferred = true
280
393
  void (async () => {
394
+ let exitCode = 2
281
395
  /** @type {Error | undefined} */
282
- let nativeKimiExitFailure
396
+ let failure
397
+ let failureReported = false
283
398
  try {
284
399
  try {
285
- const exitCode = await running
400
+ exitCode = await running
286
401
  if (job.provider === "kimi" && spawnGate.settled) {
287
- nativeKimiExitFailure = exitCode === 0 ? undefined : new Error(`Kimi worker exited with status ${exitCode}`)
288
- await control.accept({
402
+ await acceptEvent({
289
403
  type: "lifecycle",
290
404
  phase: exitCode === 0 ? "completed" : "failed",
291
405
  summary: exitCode === 0 ? "Kimi worker completed" : "Kimi worker failed"
292
406
  })
293
- if (nativeKimiExitFailure !== undefined) throw nativeKimiExitFailure
407
+ if (exitCode !== 0) failure = new Error(`Kimi worker exited with status ${exitCode}`)
294
408
  }
295
409
  } catch (error) {
296
- const failure = nativeKimiExitFailure ?? (error instanceof Error ? error : new Error(safeFailureMessage(error)))
297
- if (spawnGate.settled) {
298
- reportBackgroundFailure(failure)
299
- } else {
300
- spawnGate.reject(failure)
301
- }
302
- try {
303
- await control.close()
304
- } catch {
305
- /* Best-effort close after worker failure. */
306
- }
307
- return
410
+ failure = error instanceof Error ? error : new Error(safeFailureMessage(error))
411
+ }
412
+ if (!spawnGate.settled && failure === undefined) {
413
+ exitCode = 2
414
+ failure = new Error("Worker exited before spawn was observed")
308
415
  }
309
416
 
310
417
  try {
311
- await control.close()
418
+ await finalizeEvidence(exitCode)
312
419
  } catch (error) {
313
- if (spawnGate.settled) {
314
- reportBackgroundFailure(error)
315
- } else {
316
- spawnGate.reject(error instanceof Error ? error : new Error(safeFailureMessage(error)))
317
- }
318
- return
420
+ await evidence?.abort()
421
+ await resultEvidence?.abort()
422
+ failure ??= error instanceof Error ? error : new Error(safeFailureMessage(error))
423
+ }
424
+ if (evidenceError !== undefined) {
425
+ failure ??= evidenceError instanceof Error ? evidenceError : new Error(safeFailureMessage(evidenceError))
319
426
  }
320
427
 
321
- if (!spawnGate.settled) {
322
- spawnGate.reject(new Error("Worker exited before spawn was observed"))
428
+ if (spawnGate.settled && failure !== undefined) {
429
+ reportBackgroundFailure(failure)
430
+ failureReported = true
323
431
  }
432
+
433
+ if (evidence === undefined || resultEvidenceFinalized) {
434
+ try {
435
+ await finishDelivery()
436
+ } catch (error) {
437
+ failure ??= error instanceof Error ? error : new Error(safeFailureMessage(error))
438
+ }
439
+ } else {
440
+ try {
441
+ await control.close()
442
+ } catch (error) {
443
+ failure ??= error instanceof Error ? error : new Error(safeFailureMessage(error))
444
+ }
445
+ }
446
+
447
+ if (!spawnGate.settled) spawnGate.reject(/** @type {Error} */ (failure))
448
+ else if (failure !== undefined && !failureReported) reportBackgroundFailure(failure)
324
449
  } catch (error) {
325
450
  if (spawnGate.settled) {
326
451
  reportBackgroundFailure(error)
@@ -328,15 +453,6 @@ export async function dispatchWorker(job, config, dependencies = {}) {
328
453
  spawnGate.reject(error instanceof Error ? error : new Error(safeFailureMessage(error)))
329
454
  }
330
455
  } finally {
331
- if (evidence !== undefined) {
332
- try {
333
- await evidence.finalize()
334
- await sender.send(`Evidence: ${evidence.handle}`)
335
- } catch (error) {
336
- await evidence.abort()
337
- reportBackgroundFailure(error)
338
- }
339
- }
340
456
  notifySettled()
341
457
  }
342
458
  })()
@@ -344,7 +460,10 @@ export async function dispatchWorker(job, config, dependencies = {}) {
344
460
  await spawned
345
461
  } finally {
346
462
  try {
347
- if (!evidenceTransferred) await evidence?.abort()
463
+ if (!evidenceTransferred) {
464
+ await evidence?.abort()
465
+ await resultEvidence?.abort()
466
+ }
348
467
  } finally {
349
468
  earlyKimiPreflight?.deadline?.close()
350
469
  }
@@ -98,6 +98,17 @@ export class WorkerControl {
98
98
  })
99
99
  }
100
100
 
101
+ /** @param {string} handle */
102
+ acceptEvidenceHandle(handle) {
103
+ if (!/^evidence_[A-Za-z0-9_-]{43}$/u.test(handle)) throw new Error("Invalid evidence handle")
104
+ return this.enqueueOperation(async () => {
105
+ this.ensureHealthy()
106
+ this.renderedNotices.push({text: `Evidence: ${handle}`})
107
+ await this.flushLoop(false)
108
+ this.ensureHealthy()
109
+ })
110
+ }
111
+
101
112
  close() {
102
113
  if (!this.closePromise) {
103
114
  this.closePromise = this.enqueueOperation(async () => {