threadwire 0.1.14 → 0.1.17
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 +11 -0
- package/README.md +9 -7
- package/docs/delegated-result-protocol.md +20 -0
- package/docs/evidence-artifacts.md +37 -5
- package/package.json +1 -1
- package/scripts/install-local-launcher.js +23 -1
- package/scripts/verify-package.js +1 -0
- package/src/activity-log-status.js +291 -0
- package/src/activity-log.js +91 -3
- package/src/cli.js +198 -37
- package/src/delegated-result-admission.js +13 -0
- package/src/evidence-store.js +93 -23
- package/src/provider-capacity-codex.js +2 -1
- package/src/providers/executable.js +17 -4
- package/src/providers/index.js +109 -5
- package/src/telegram-ingress/core.js +180 -61
- package/src/worker-control.js +11 -0
package/src/activity-log.js
CHANGED
|
@@ -4,11 +4,38 @@ import {closeSync, openSync, writeSync} from "node:fs"
|
|
|
4
4
|
|
|
5
5
|
const SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,511}$/u
|
|
6
6
|
|
|
7
|
+
const ACTIVITY_KINDS = new Set(["delta", "tool", "lifecycle"])
|
|
8
|
+
const HEALTH_DISPOSITIONS = new Set(["retrying", "blocked"])
|
|
9
|
+
const HEALTH_CATEGORIES = new Set([
|
|
10
|
+
"authentication", "permission", "rate-limit", "quota", "billing",
|
|
11
|
+
"model", "network", "protocol", "unknown"
|
|
12
|
+
])
|
|
13
|
+
const TERMINAL_STATES = new Set(["completed", "failed", "cancelled"])
|
|
14
|
+
|
|
15
|
+
const SIGNAL_CANCELLED_EXIT_CODES = new Set([130, 143])
|
|
16
|
+
|
|
17
|
+
const ACTIVITY_THROTTLE_MS = 5000
|
|
18
|
+
|
|
7
19
|
export class ActivityLog {
|
|
8
|
-
/**
|
|
9
|
-
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} path
|
|
22
|
+
* @param {{now?: () => number}} [options]
|
|
23
|
+
*/
|
|
24
|
+
constructor(path, options = {}) {
|
|
10
25
|
this.fileDescriptor = openSync(path, "a", 0o600)
|
|
11
26
|
this.closed = false
|
|
27
|
+
this.now = options.now ?? (() => Date.now())
|
|
28
|
+
this.terminalWritten = false
|
|
29
|
+
/** @type {string | null} */
|
|
30
|
+
this.lastHealthFingerprint = null
|
|
31
|
+
this.lastActivityAt = -ACTIVITY_THROTTLE_MS
|
|
32
|
+
this.activityCount = 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @param {number} pid */
|
|
36
|
+
recordController(pid) {
|
|
37
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error("Controller PID is unavailable")
|
|
38
|
+
this.write({type: "controller-started", pid})
|
|
12
39
|
}
|
|
13
40
|
|
|
14
41
|
/** @param {"codex" | "claude" | "kimi" | "opencode"} provider @param {number} pid */
|
|
@@ -23,15 +50,76 @@ export class ActivityLog {
|
|
|
23
50
|
this.write({type: "session-available", provider, sessionId})
|
|
24
51
|
}
|
|
25
52
|
|
|
53
|
+
/**
|
|
54
|
+
* @param {"codex" | "claude" | "kimi" | "opencode"} provider
|
|
55
|
+
* @param {"delta" | "tool" | "lifecycle"} kind
|
|
56
|
+
*/
|
|
57
|
+
recordActivity(provider, kind) {
|
|
58
|
+
if (!ACTIVITY_KINDS.has(kind)) throw new Error(`Unknown activity kind: ${kind}`)
|
|
59
|
+
const now = this.now()
|
|
60
|
+
if (now - this.lastActivityAt < ACTIVITY_THROTTLE_MS) return
|
|
61
|
+
this.lastActivityAt = now
|
|
62
|
+
this.activityCount += 1
|
|
63
|
+
this.write({type: "activity", provider, kind})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {"codex" | "claude" | "kimi" | "opencode"} provider
|
|
68
|
+
* @param {{disposition: "retrying" | "blocked", category: "authentication" | "permission" | "rate-limit" | "quota" | "billing" | "model" | "network" | "protocol" | "unknown", retryAfterMs?: number}} health
|
|
69
|
+
*/
|
|
70
|
+
recordHealth(provider, health) {
|
|
71
|
+
if (!HEALTH_DISPOSITIONS.has(health.disposition)) throw new Error("Unknown health disposition")
|
|
72
|
+
if (!HEALTH_CATEGORIES.has(health.category)) throw new Error("Unknown health category")
|
|
73
|
+
if (health.retryAfterMs !== undefined) {
|
|
74
|
+
if (!Number.isSafeInteger(health.retryAfterMs) || health.retryAfterMs <= 0) throw new Error("retryAfterMs must be a positive safe integer")
|
|
75
|
+
if (health.disposition !== "retrying") throw new Error("retryAfterMs is only valid for retrying disposition")
|
|
76
|
+
}
|
|
77
|
+
const fingerprint = `${health.disposition}\0${health.category}\0${health.retryAfterMs ?? ""}`
|
|
78
|
+
if (fingerprint === this.lastHealthFingerprint) return
|
|
79
|
+
this.lastHealthFingerprint = fingerprint
|
|
80
|
+
/** @type {{type: string, provider: string, disposition: string, category: string, retryAfterMs?: number}} */
|
|
81
|
+
const fact = {type: "health", provider, disposition: health.disposition, category: health.category}
|
|
82
|
+
if (health.retryAfterMs !== undefined) fact.retryAfterMs = health.retryAfterMs
|
|
83
|
+
this.write(fact)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {"codex" | "claude" | "kimi" | "opencode"} provider
|
|
88
|
+
* @param {"completed" | "failed" | "cancelled"} state
|
|
89
|
+
* @param {number} exitCode
|
|
90
|
+
*/
|
|
91
|
+
recordTerminal(provider, state, exitCode) {
|
|
92
|
+
if (this.terminalWritten) return
|
|
93
|
+
if (!TERMINAL_STATES.has(state)) throw new Error("Unknown terminal state")
|
|
94
|
+
if (!Number.isSafeInteger(exitCode) || exitCode < 0 || exitCode > 255) throw new Error("exitCode must be an integer between 0 and 255")
|
|
95
|
+
this.terminalWritten = true
|
|
96
|
+
this.write({type: "terminal", provider, state, exitCode})
|
|
97
|
+
}
|
|
98
|
+
|
|
26
99
|
close() {
|
|
27
100
|
if (this.closed) return
|
|
28
101
|
this.closed = true
|
|
29
102
|
closeSync(this.fileDescriptor)
|
|
30
103
|
}
|
|
31
104
|
|
|
32
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* @param {Record<string, unknown>} fact
|
|
107
|
+
*/
|
|
33
108
|
write(fact) {
|
|
34
109
|
if (this.closed) throw new Error("Activity log is closed")
|
|
110
|
+
fact.at = this.now()
|
|
35
111
|
writeSync(this.fileDescriptor, `${JSON.stringify(fact)}\n`)
|
|
36
112
|
}
|
|
37
113
|
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Classify a terminal exit code. Known signal-derived codes (130 = SIGINT,
|
|
117
|
+
* 143 = SIGTERM) map to cancelled; all others map to completed (0) or failed
|
|
118
|
+
* (nonzero). This is the only place that picks the terminal state constant.
|
|
119
|
+
* @param {number} exitCode
|
|
120
|
+
* @returns {"completed" | "failed" | "cancelled"}
|
|
121
|
+
*/
|
|
122
|
+
export function terminalState(exitCode) {
|
|
123
|
+
if (SIGNAL_CANCELLED_EXIT_CODES.has(exitCode)) return "cancelled"
|
|
124
|
+
return exitCode === 0 ? "completed" : "failed"
|
|
125
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import {readFile} from "node:fs/promises"
|
|
3
|
+
import {lstat, readFile, readlink, realpath} from "node:fs/promises"
|
|
4
4
|
import {randomUUID} from "node:crypto"
|
|
5
|
-
import {isAbsolute, normalize, resolve} from "node:path"
|
|
5
|
+
import {basename, dirname, isAbsolute, join, normalize, resolve} from "node:path"
|
|
6
6
|
import {stdin, stderr, stdout} from "node:process"
|
|
7
7
|
import {createFetchTransport} from "./notifiers/fetch-transport.js"
|
|
8
8
|
import {createTelegramSender, parseTelegramTarget} from "./notifiers/telegram.js"
|
|
9
9
|
import {createProvider, PROVIDERS} from "./providers/index.js"
|
|
10
10
|
import {runWorker} from "./run-worker.js"
|
|
11
|
-
import {ActivityLog} from "./activity-log.js"
|
|
11
|
+
import {ActivityLog, terminalState} from "./activity-log.js"
|
|
12
|
+
import {computeStatus, parseStatusArguments, readActivityLog} from "./activity-log-status.js"
|
|
12
13
|
import {DelegatedResultAdmission, validateContinuationHandle} from "./delegated-result-admission.js"
|
|
13
14
|
import {buildProviderEnvironment, collectEvidenceRedactions, parseTelegramRequestTimeoutMs, resolveFileBackedSettings} from "./telegram-ingress/config.js"
|
|
14
15
|
import {WorkerControl} from "./worker-control.js"
|
|
@@ -29,7 +30,7 @@ import {
|
|
|
29
30
|
import {probeCodexCapacity} from "./provider-capacity-codex.js"
|
|
30
31
|
import {probeKimiCapacity} from "./provider-capacity-kimi.js"
|
|
31
32
|
|
|
32
|
-
const HELP = `Usage: threadwire run --provider <codex|claude|kimi|opencode> --target telegram:<chat-id> | telegram:<chat-id>:<thread-id>
|
|
33
|
+
const HELP = `Usage: threadwire run --provider <codex|claude|kimi|opencode> --target telegram:<chat-id> | telegram:<chat-id>:<thread-id> | file:<absolute-path>
|
|
33
34
|
[--process-number <positive-integer>] [--cwd <directory>]
|
|
34
35
|
[--relay-write]
|
|
35
36
|
[--tool-messages] [--max-output-length <positive-integer>]
|
|
@@ -41,7 +42,9 @@ const HELP = `Usage: threadwire run --provider <codex|claude|kimi|opencode> --ta
|
|
|
41
42
|
(--bytes <offset>:<limit> | --lines <start>:<limit> | --query <literal> --context-bytes <limit>)
|
|
42
43
|
threadwire capacity [--provider <codex|kimi>]...
|
|
43
44
|
[--short-reserve-percent <0-100>] [--long-reserve-percent <0-100>]
|
|
44
|
-
[--timeout-ms <positive-integer>]
|
|
45
|
+
[--timeout-ms <positive-integer>]
|
|
46
|
+
threadwire status --activity-log <absolute-path>
|
|
47
|
+
(Emits one closed versioned JSON status document from the activity log.)`
|
|
45
48
|
|
|
46
49
|
/** @typedef {{provider: string, target: string, cwd: string, toolMessages: boolean, relayWrite: boolean, prompt?: string, promptFile?: string, processNumber?: number, maxOutputLength?: number, resumeSession?: string, transcript?: string, activityLog?: string, providerArguments: string[]}} ParsedArguments */
|
|
47
50
|
/** @typedef {{evidenceRead: true, request: unknown}} EvidenceParsedArguments */
|
|
@@ -244,6 +247,16 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
244
247
|
/** @type {DelegatedResultAdmission | undefined} */
|
|
245
248
|
let runAdmission
|
|
246
249
|
try {
|
|
250
|
+
if (arguments_[0] === "status") {
|
|
251
|
+
const statusParsed = parseStatusArguments(arguments_)
|
|
252
|
+
if (validateOnly) return 0
|
|
253
|
+
const records = await readActivityLog(statusParsed.activityLog)
|
|
254
|
+
const status = computeStatus(records, {
|
|
255
|
+
...(dependencies.now === undefined ? {} : {now: dependencies.now})
|
|
256
|
+
})
|
|
257
|
+
output.write(`${JSON.stringify(status)}\n`)
|
|
258
|
+
return 0
|
|
259
|
+
}
|
|
247
260
|
const parsed = arguments_[0] === "evidence"
|
|
248
261
|
? parseEvidenceArguments(arguments_)
|
|
249
262
|
: arguments_[0] === "capacity" ? parseCapacityArguments(arguments_) : parseArguments(arguments_)
|
|
@@ -292,12 +305,20 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
292
305
|
await store.close()
|
|
293
306
|
}
|
|
294
307
|
}
|
|
295
|
-
|
|
296
|
-
|
|
308
|
+
const destination = parseDeliveryTarget(parsed.target)
|
|
309
|
+
if (destination.type === "file" && parsed.transcript !== undefined) {
|
|
310
|
+
throw new Error("--transcript cannot be combined with a file target")
|
|
297
311
|
}
|
|
312
|
+
const transcriptPath = destination.type === "file" ? destination.path : parsed.transcript
|
|
313
|
+
if (
|
|
314
|
+
transcriptPath !== undefined && parsed.activityLog !== undefined
|
|
315
|
+
&& await canonicalFilesystemPath(transcriptPath) === await canonicalFilesystemPath(parsed.activityLog)
|
|
316
|
+
) {
|
|
317
|
+
throw new Error("Result and activity log paths must resolve to different filesystem paths")
|
|
318
|
+
}
|
|
319
|
+
if (!validateOnly) normalizedOutput = new NormalizedOutput(output, transcriptPath)
|
|
298
320
|
const metrics = new ContextBudgetMetrics()
|
|
299
321
|
runAdmission = normalizedOutput === undefined ? undefined : new DelegatedResultAdmission({output: normalizedOutput, metrics})
|
|
300
|
-
const target = parseTelegramTarget(parsed.target)
|
|
301
322
|
if (parsed.relayWrite) validateRelayWriteProviderArguments(parsed.providerArguments)
|
|
302
323
|
if (validateOnly) return 0
|
|
303
324
|
const usesIsolatedRuntime = parsed.relayWrite
|
|
@@ -322,27 +343,63 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
322
343
|
let activity
|
|
323
344
|
/** @type {Awaited<ReturnType<EvidenceStore["createArtifact"]>> | undefined} */
|
|
324
345
|
let evidence
|
|
346
|
+
/** @type {Awaited<ReturnType<EvidenceStore["createArtifact"]>> | undefined} */
|
|
347
|
+
let resultEvidence
|
|
325
348
|
/** @type {EvidenceStore | undefined} */
|
|
326
349
|
let ownedEvidenceStore
|
|
350
|
+
/** @type {EvidenceStore | undefined} */
|
|
351
|
+
let evidenceStore
|
|
352
|
+
/** @type {{destinationId: string, runId: string} | undefined} */
|
|
353
|
+
let evidenceOwner
|
|
354
|
+
/** @type {WorkerControl | undefined} */
|
|
355
|
+
let control
|
|
356
|
+
/** @type {unknown} */
|
|
357
|
+
let deliveryError
|
|
358
|
+
let resultEvidenceFinalized = false
|
|
327
359
|
let evidencePayloadBytes = 0
|
|
360
|
+
let resultEvidenceBytes = 0
|
|
361
|
+
const deliveryIdentity = randomUUID()
|
|
328
362
|
const launchDeadline = isolatedPreflight?.deadline
|
|
329
363
|
try {
|
|
330
|
-
environment = await boundedLaunch(resolveFileBackedSettings(
|
|
364
|
+
environment = await boundedLaunch(resolveFileBackedSettings(
|
|
365
|
+
sourceEnvironment,
|
|
366
|
+
destination.type === "telegram" ? ["THREADWIRE_TELEGRAM_BOT_TOKEN"] : []
|
|
367
|
+
), launchDeadline)
|
|
368
|
+
if (destination.type === "telegram" && !environment.THREADWIRE_TELEGRAM_BOT_TOKEN) {
|
|
369
|
+
throw new Error("THREADWIRE_TELEGRAM_BOT_TOKEN is required")
|
|
370
|
+
}
|
|
331
371
|
const prompt = await readPrompt(parsed, dependencies.input ?? stdin, launchDeadline?.signal)
|
|
332
372
|
const configuredEvidenceRoot = evidenceRoot(sourceEnvironment.THREADWIRE_EVIDENCE_ROOT)
|
|
333
373
|
if (dependencies.evidenceStore === undefined && configuredEvidenceRoot !== undefined) {
|
|
334
374
|
ownedEvidenceStore = await boundedLaunch(EvidenceStore.open({root: configuredEvidenceRoot}), launchDeadline)
|
|
335
375
|
}
|
|
336
|
-
|
|
376
|
+
evidenceStore = dependencies.evidenceStore ?? ownedEvidenceStore
|
|
337
377
|
if (evidenceStore !== undefined) {
|
|
338
|
-
|
|
339
|
-
destinationId:
|
|
340
|
-
|
|
378
|
+
evidenceOwner = dependencies.evidenceOwnerScope ?? evidenceStore.createOwnerScope({
|
|
379
|
+
destinationId: destination.type === "telegram"
|
|
380
|
+
? `${destination.target.chatId}:${destination.target.threadId ?? "dm"}`
|
|
381
|
+
: "file",
|
|
382
|
+
runId: deliveryIdentity
|
|
341
383
|
})
|
|
384
|
+
const redactions = await boundedLaunch(collectEvidenceRedactions(environment), launchDeadline)
|
|
342
385
|
evidence = await evidenceStore.createArtifact(evidenceOwner, {
|
|
343
386
|
contentType: "text/plain; charset=utf-8",
|
|
344
|
-
redactions
|
|
387
|
+
redactions
|
|
345
388
|
})
|
|
389
|
+
try {
|
|
390
|
+
resultEvidence = await evidenceStore.createArtifact(evidenceOwner, {
|
|
391
|
+
contentType: "application/json",
|
|
392
|
+
redactions,
|
|
393
|
+
delivery: {
|
|
394
|
+
identity: deliveryIdentity,
|
|
395
|
+
state: destination.type === "telegram" ? "delivery_pending" : "delivery_not_requested"
|
|
396
|
+
}
|
|
397
|
+
})
|
|
398
|
+
} catch (error) {
|
|
399
|
+
await evidence.abort()
|
|
400
|
+
evidence = undefined
|
|
401
|
+
throw error
|
|
402
|
+
}
|
|
346
403
|
}
|
|
347
404
|
if (evidence !== undefined) {
|
|
348
405
|
const promptEvidence = `prompt\n${prompt}\nprovider-stream\n`
|
|
@@ -361,18 +418,23 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
361
418
|
providerEnvironment
|
|
362
419
|
)
|
|
363
420
|
if (parsed.resumeSession !== undefined) admission.setContinuationHandle(parsed.resumeSession)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
421
|
+
if (destination.type === "telegram") {
|
|
422
|
+
const transport = (dependencies.transportFactory ?? createFetchTransport)(
|
|
423
|
+
/** @type {string} */ (environment.THREADWIRE_TELEGRAM_BOT_TOKEN),
|
|
424
|
+
undefined,
|
|
425
|
+
parseTelegramRequestTimeoutMs(environment)
|
|
426
|
+
)
|
|
427
|
+
control = new WorkerControl({
|
|
428
|
+
sender: createTelegramSender(destination.target, transport),
|
|
429
|
+
processNumber: parsed.processNumber ?? process.pid,
|
|
430
|
+
toolMessages: parsed.toolMessages,
|
|
431
|
+
...(dependencies.workerControlOptions ?? {}),
|
|
432
|
+
...(parsed.maxOutputLength === undefined ? {} : {maxOutputLength: parsed.maxOutputLength}),
|
|
433
|
+
metrics
|
|
434
|
+
})
|
|
435
|
+
}
|
|
375
436
|
activity = parsed.activityLog === undefined ? undefined : new ActivityLog(parsed.activityLog)
|
|
437
|
+
activity?.recordController(process.pid)
|
|
376
438
|
/** @type {import("./run-worker.js").RunWorkerOptions} */
|
|
377
439
|
const workerOptions = {
|
|
378
440
|
executable: provider.executable,
|
|
@@ -383,13 +445,22 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
383
445
|
parse: provider.parse,
|
|
384
446
|
...(provider.completion === undefined ? {} : {completion: provider.completion}),
|
|
385
447
|
...(launchDeadline === undefined ? {} : {signal: launchDeadline.signal}),
|
|
386
|
-
onEvent: (event) => {
|
|
448
|
+
onEvent: async (event) => {
|
|
387
449
|
validateNormalizedWorkerEvent(event)
|
|
388
450
|
if (event.type !== "text-delta") {
|
|
389
451
|
metrics.recordRejected(`${event.type}_progress`, Buffer.byteLength(JSON.stringify(event), "utf8"))
|
|
390
452
|
}
|
|
453
|
+
if (event.type === "text-delta") activity?.recordActivity(provider.name, "delta")
|
|
454
|
+
else if (event.type === "tool") activity?.recordActivity(provider.name, "tool")
|
|
455
|
+
else if (event.type === "lifecycle") activity?.recordActivity(provider.name, "lifecycle")
|
|
391
456
|
acceptAdmissionEvent(admission, event)
|
|
392
|
-
|
|
457
|
+
if (control !== undefined) {
|
|
458
|
+
try {
|
|
459
|
+
await control.accept(event)
|
|
460
|
+
} catch (error) {
|
|
461
|
+
deliveryError ??= error
|
|
462
|
+
}
|
|
463
|
+
}
|
|
393
464
|
},
|
|
394
465
|
onSpawn: (pid) => {
|
|
395
466
|
if (activity && pid !== undefined) activity.recordStarted(provider.name, pid)
|
|
@@ -401,6 +472,8 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
401
472
|
admission.setContinuationHandle(id)
|
|
402
473
|
activity?.recordSession(provider.name, id)
|
|
403
474
|
}
|
|
475
|
+
const health = provider.health(record)
|
|
476
|
+
if (health !== undefined) activity?.recordHealth(provider.name, health)
|
|
404
477
|
},
|
|
405
478
|
onStdoutChunk: (chunk) => {
|
|
406
479
|
metrics.recordRawChildChunk("provider_stdout", chunk.length)
|
|
@@ -437,33 +510,83 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
437
510
|
} else {
|
|
438
511
|
exitCode = await (dependencies.workerRunner ?? runWorker)(workerOptions)
|
|
439
512
|
}
|
|
440
|
-
await boundedLaunch(control.close(), launchDeadline)
|
|
441
513
|
terminalExitCode = exitCode
|
|
442
514
|
} finally {
|
|
443
|
-
|
|
515
|
+
const terminal = {state: /** @type {"completed" | "failed"} */ (terminalExitCode === 0 ? "completed" : "failed"), exitCode: terminalExitCode}
|
|
516
|
+
if (evidence !== undefined && resultEvidence !== undefined) {
|
|
517
|
+
try {
|
|
518
|
+
admission.addArtifactHandle(resultEvidence.handle)
|
|
519
|
+
const providerResult = admission.createProviderResult(terminal)
|
|
520
|
+
const serializedProviderResult = `${JSON.stringify(providerResult)}\n`
|
|
521
|
+
await resultEvidence.append("provider-result", serializedProviderResult)
|
|
522
|
+
const resultArtifact = await resultEvidence.finalize()
|
|
523
|
+
resultEvidenceBytes = resultArtifact.bytes
|
|
524
|
+
resultEvidenceFinalized = true
|
|
525
|
+
} catch (error) {
|
|
526
|
+
await evidence.abort()
|
|
527
|
+
await resultEvidence.abort()
|
|
528
|
+
admission.removeArtifactHandle(resultEvidence.handle)
|
|
529
|
+
metrics.clearArtifact()
|
|
530
|
+
terminalExitCode = 2
|
|
531
|
+
evidenceError = error
|
|
532
|
+
}
|
|
533
|
+
} else {
|
|
534
|
+
await evidence?.abort()
|
|
535
|
+
await resultEvidence?.abort()
|
|
536
|
+
}
|
|
537
|
+
if (control !== undefined) {
|
|
538
|
+
try {
|
|
539
|
+
await boundedLaunch(control.close(), launchDeadline)
|
|
540
|
+
} catch (error) {
|
|
541
|
+
deliveryError ??= error
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (evidence !== undefined && resultEvidenceFinalized) {
|
|
545
|
+
admission.addArtifactHandle(evidence.handle)
|
|
444
546
|
try {
|
|
445
|
-
admission.addArtifactHandle(evidence.handle)
|
|
446
|
-
const terminal = {state: /** @type {"completed" | "failed"} */ (terminalExitCode === 0 ? "completed" : "failed"), exitCode: terminalExitCode}
|
|
447
547
|
metrics.recordAdmission(admission.preview(terminal))
|
|
448
|
-
const projectedArtifactBytes = metrics.projectSelfInclusiveArtifact(
|
|
548
|
+
const projectedArtifactBytes = metrics.projectSelfInclusiveArtifact(
|
|
549
|
+
evidencePayloadBytes + resultEvidenceBytes,
|
|
550
|
+
2
|
|
551
|
+
)
|
|
449
552
|
await evidence.append("context-metrics", `${JSON.stringify(metrics.snapshot())}\n`)
|
|
450
553
|
const artifact = await evidence.finalize()
|
|
451
|
-
assertArtifactProjection(artifact.bytes, projectedArtifactBytes)
|
|
554
|
+
assertArtifactProjection(artifact.bytes + resultEvidenceBytes, projectedArtifactBytes)
|
|
452
555
|
} catch (error) {
|
|
453
|
-
await evidence.abort()
|
|
454
556
|
admission.removeArtifactHandle(evidence.handle)
|
|
557
|
+
await evidence.abort()
|
|
455
558
|
metrics.clearArtifact()
|
|
456
|
-
|
|
457
|
-
evidenceError
|
|
559
|
+
metrics.recordArtifact(resultEvidenceBytes, 1)
|
|
560
|
+
evidenceError ??= error
|
|
458
561
|
}
|
|
562
|
+
} else if (resultEvidenceFinalized) {
|
|
563
|
+
metrics.recordArtifact(resultEvidenceBytes, 1)
|
|
459
564
|
}
|
|
460
565
|
admission.complete({state: terminalExitCode === 0 ? "completed" : "failed", exitCode: terminalExitCode})
|
|
566
|
+
if (
|
|
567
|
+
destination.type === "telegram" && resultEvidenceFinalized
|
|
568
|
+
&& evidenceStore !== undefined && evidenceOwner !== undefined && resultEvidence !== undefined
|
|
569
|
+
) {
|
|
570
|
+
try {
|
|
571
|
+
await evidenceStore.recordDelivery(
|
|
572
|
+
evidenceOwner,
|
|
573
|
+
resultEvidence.handle,
|
|
574
|
+
control !== undefined && deliveryError === undefined ? "delivery_succeeded" : "delivery_failed"
|
|
575
|
+
)
|
|
576
|
+
} catch (error) {
|
|
577
|
+
evidenceError ??= error
|
|
578
|
+
}
|
|
579
|
+
}
|
|
461
580
|
errorOutput.write(`threadwire-context-metrics ${JSON.stringify(metrics.conciseDiagnostic())}\n`)
|
|
462
|
-
activity
|
|
581
|
+
if (activity !== undefined) {
|
|
582
|
+
activity.recordTerminal(/** @type {"codex" | "claude" | "kimi" | "opencode"} */ (parsed.provider), terminalState(terminalExitCode), terminalExitCode)
|
|
583
|
+
activity.close()
|
|
584
|
+
}
|
|
463
585
|
await boundedLaunch(ownedEvidenceStore?.close(), launchDeadline).catch(() => {})
|
|
464
586
|
launchDeadline?.close()
|
|
465
587
|
}
|
|
466
588
|
if (evidenceError !== undefined) throw evidenceError
|
|
589
|
+
if (deliveryError !== undefined) throw deliveryError
|
|
467
590
|
return terminalExitCode
|
|
468
591
|
} catch (error) {
|
|
469
592
|
let reportedError = error
|
|
@@ -485,6 +608,44 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
485
608
|
}
|
|
486
609
|
}
|
|
487
610
|
|
|
611
|
+
/** @param {string} value @returns {{type: "telegram", target: import("./types.js").TelegramTarget} | {type: "file", path: string}} */
|
|
612
|
+
function parseDeliveryTarget(value) {
|
|
613
|
+
if (!value.startsWith("file:")) return {type: "telegram", target: parseTelegramTarget(value)}
|
|
614
|
+
const path = value.slice("file:".length)
|
|
615
|
+
if (!isAbsolute(path) || normalize(path) !== path || path.includes("\0") || path === "/") {
|
|
616
|
+
throw new Error("File target must contain a normalized absolute file path")
|
|
617
|
+
}
|
|
618
|
+
return {type: "file", path}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** @param {string} path @param {Set<string>} [seen] @returns {Promise<string>} */
|
|
622
|
+
async function canonicalFilesystemPath(path, seen = new Set()) {
|
|
623
|
+
if (seen.has(path)) throw new Error("Result or activity log path contains a filesystem alias cycle")
|
|
624
|
+
seen.add(path)
|
|
625
|
+
try {
|
|
626
|
+
return await realpath(path)
|
|
627
|
+
} catch (error) {
|
|
628
|
+
if (!missingPath(error)) throw error
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
const metadata = await lstat(path)
|
|
632
|
+
if (metadata.isSymbolicLink()) {
|
|
633
|
+
return canonicalFilesystemPath(resolve(dirname(path), await readlink(path)), seen)
|
|
634
|
+
}
|
|
635
|
+
} catch (error) {
|
|
636
|
+
if (!missingPath(error)) throw error
|
|
637
|
+
}
|
|
638
|
+
const parent = dirname(path)
|
|
639
|
+
if (parent === path) return path
|
|
640
|
+
return join(await canonicalFilesystemPath(parent, seen), basename(path))
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** @param {unknown} error */
|
|
644
|
+
function missingPath(error) {
|
|
645
|
+
return typeof error === "object" && error !== null
|
|
646
|
+
&& /** @type {{code?: unknown}} */ (error).code === "ENOENT"
|
|
647
|
+
}
|
|
648
|
+
|
|
488
649
|
/** @param {string | undefined} value */
|
|
489
650
|
function evidenceRoot(value) {
|
|
490
651
|
if (value === undefined) return undefined
|
|
@@ -105,6 +105,19 @@ export class DelegatedResultAdmission {
|
|
|
105
105
|
return this.createEnvelope(terminal)
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/** @param {{state: TerminalState, exitCode: number}} terminal */
|
|
109
|
+
createProviderResult(terminal) {
|
|
110
|
+
const result = this.createEnvelope(terminal)
|
|
111
|
+
const assistantContent = truncateBoundedString(sanitizeOutput(this.conclusion), CONCLUSION_LIMIT)
|
|
112
|
+
return {
|
|
113
|
+
version: 1,
|
|
114
|
+
type: "provider_result",
|
|
115
|
+
providerState: terminal.state === "completed" ? "provider_completed" : "provider_failed",
|
|
116
|
+
...(assistantContent === undefined ? {} : {assistantContent}),
|
|
117
|
+
result
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
108
121
|
/** @param {AdmissionReference} reference */
|
|
109
122
|
addReference(reference) {
|
|
110
123
|
this.references = /** @type {AdmissionReference[]} */ (optionalReferences([...this.references, reference]) ?? [])
|