threadwire 0.1.21 → 0.1.23
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 +108 -0
- package/README.md +5 -3
- package/docs/container-runtime.md +2 -2
- package/docs/development-container.md +45 -47
- package/docs/evidence-artifacts.md +47 -0
- package/package.json +5 -3
- package/scripts/verify-package.js +2 -0
- package/src/cli.js +124 -23
- package/src/evidence-index.js +664 -0
- package/src/evidence-store.js +180 -44
- package/src/isolated-runtime.js +1 -1
- package/src/jsonl-record-spool.js +31 -5
- package/src/kimi-model-broker.js +18 -2
- package/src/redaction.js +50 -0
- package/src/run-worker.js +10 -3
- package/src/telegram-ingress/core.js +66 -6
package/src/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import {buildProviderEnvironment, collectEvidenceRedactions, parseTelegramReques
|
|
|
15
15
|
import {WorkerControl} from "./worker-control.js"
|
|
16
16
|
import {EvidenceStore} from "./evidence-store.js"
|
|
17
17
|
import {ContextBudgetMetrics} from "./context-budget-metrics.js"
|
|
18
|
+
import {workerEventToIndexEvent} from "./evidence-index.js"
|
|
18
19
|
import {isolatedRuntimeClientFromEnvironment} from "./isolated-runtime-client.js"
|
|
19
20
|
import {validateRelayWriteProviderArguments} from "./relay-write.js"
|
|
20
21
|
import {abortable} from "./absolute-deadline.js"
|
|
@@ -40,6 +41,8 @@ const HELP = `Usage: threadwire run --provider <codex|claude|kimi|opencode> --ta
|
|
|
40
41
|
[-- <provider arguments...>]
|
|
41
42
|
threadwire evidence read --handle <opaque-handle>
|
|
42
43
|
(--bytes <offset>:<limit> | --lines <start>:<limit> | --query <literal> --context-bytes <limit>)
|
|
44
|
+
threadwire evidence inspect --handle <opaque-handle>
|
|
45
|
+
(--summary | --events [--kind <kind>] [--after <order>] [--before <order>] [--limit <positive-integer>] | --failures [--after <order>] [--limit <positive-integer>])
|
|
43
46
|
threadwire capacity [--provider <codex|kimi>]...
|
|
44
47
|
[--short-reserve-percent <0-100>] [--long-reserve-percent <0-100>]
|
|
45
48
|
[--timeout-ms <positive-integer>]
|
|
@@ -47,7 +50,7 @@ const HELP = `Usage: threadwire run --provider <codex|claude|kimi|opencode> --ta
|
|
|
47
50
|
(Emits one closed versioned JSON status document from the activity log.)`
|
|
48
51
|
|
|
49
52
|
/** @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 */
|
|
50
|
-
/** @typedef {{evidenceRead: true, request: unknown}} EvidenceParsedArguments */
|
|
53
|
+
/** @typedef {{evidenceRead: true, request: unknown} | {evidenceInspect: true, request: unknown}} EvidenceParsedArguments */
|
|
51
54
|
/** @typedef {{capacity: true, providers: import("./provider-capacity.js").CapacityProvider[], shortReservePercent: number, longReservePercent: number, timeoutMs: number}} CapacityParsedArguments */
|
|
52
55
|
/** @typedef {{probe: (provider: import("./provider-capacity.js").CapacityProvider, timeoutMs?: number) => Promise<import("./provider-capacity.js").CapacityCandidate>}} CapacityProbeDependency */
|
|
53
56
|
/**
|
|
@@ -128,31 +131,77 @@ export function parseArguments(arguments_) {
|
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
/** @param {string[]} arguments_ @returns {EvidenceParsedArguments} */
|
|
131
|
-
function parseEvidenceArguments(arguments_) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
+
export function parseEvidenceArguments(arguments_) {
|
|
135
|
+
const subcommand = arguments_[1]
|
|
136
|
+
if (subcommand !== "read" && subcommand !== "inspect") throw new Error(HELP)
|
|
137
|
+
/** @type {Record<string, string | boolean>} */
|
|
134
138
|
const options = {}
|
|
135
|
-
|
|
139
|
+
let index = 2
|
|
140
|
+
while (index < arguments_.length) {
|
|
136
141
|
const name = arguments_[index]
|
|
142
|
+
if (name === "--summary" || name === "--events" || name === "--failures") {
|
|
143
|
+
if (options[name] !== undefined) throw new Error(`Duplicate evidence option: ${name}`)
|
|
144
|
+
options[name] = true
|
|
145
|
+
index += 1
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
137
148
|
const value = arguments_[index + 1]
|
|
138
|
-
if (!name ||
|
|
149
|
+
if (!name || value === undefined) throw new Error(HELP)
|
|
150
|
+
const readOptions = ["--handle", "--bytes", "--lines", "--query", "--context-bytes"]
|
|
151
|
+
const inspectOptions = ["--handle", "--kind", "--after", "--before", "--limit"]
|
|
152
|
+
const allowed = subcommand === "read" ? readOptions : inspectOptions
|
|
153
|
+
if (!allowed.includes(name)) throw new Error(HELP)
|
|
139
154
|
if (options[name] !== undefined) throw new Error(`Duplicate evidence option: ${name}`)
|
|
140
155
|
options[name] = value
|
|
156
|
+
index += 2
|
|
141
157
|
}
|
|
142
|
-
const handle = options["--handle"]
|
|
158
|
+
const handle = /** @type {string | undefined} */ (options["--handle"])
|
|
143
159
|
if (!handle) throw new Error("--handle is required")
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
if (subcommand === "read") {
|
|
161
|
+
const selectors = ["--bytes", "--lines", "--query"].filter((name) => options[name] !== undefined)
|
|
162
|
+
if (selectors.length !== 1) throw new Error("Exactly one bounded evidence selector is required")
|
|
163
|
+
let request
|
|
164
|
+
if (options["--bytes"]) request = {handle, bytes: rangePair(/** @type {string} */ (options["--bytes"]), true)}
|
|
165
|
+
else if (options["--lines"]) request = {handle, lines: rangePair(/** @type {string} */ (options["--lines"]), false)}
|
|
166
|
+
else {
|
|
167
|
+
const context = options["--context-bytes"]
|
|
168
|
+
if (!context) throw new Error("--context-bytes is required with --query")
|
|
169
|
+
request = {handle, query: {literal: options["--query"], limit: 1, contextBytes: positiveInteger(/** @type {string} */ (context), "--context-bytes")}}
|
|
170
|
+
}
|
|
171
|
+
if (options["--query"] === undefined && options["--context-bytes"] !== undefined) throw new Error("--context-bytes requires --query")
|
|
172
|
+
return {evidenceRead: true, request}
|
|
173
|
+
}
|
|
174
|
+
const selectors = ["--summary", "--events", "--failures"].filter((name) => options[name] !== undefined)
|
|
175
|
+
if (selectors.length !== 1) throw new Error("Evidence inspect requires exactly one fixed selector")
|
|
176
|
+
const allowed = options["--summary"]
|
|
177
|
+
? ["--handle", "--summary"]
|
|
178
|
+
: options["--events"]
|
|
179
|
+
? ["--handle", "--events", "--kind", "--after", "--before", "--limit"]
|
|
180
|
+
: ["--handle", "--failures", "--after", "--limit"]
|
|
181
|
+
const allowedSet = new Set(allowed)
|
|
182
|
+
for (const key of Object.keys(options)) {
|
|
183
|
+
if (!allowedSet.has(key)) throw new Error(`Incompatible evidence inspect flag: ${key}`)
|
|
153
184
|
}
|
|
154
|
-
|
|
155
|
-
|
|
185
|
+
/** @type {import("./evidence-index.js").EvidenceInspectRequest} */
|
|
186
|
+
let request = {handle}
|
|
187
|
+
if (options["--summary"]) {
|
|
188
|
+
request = {...request, summary: true}
|
|
189
|
+
} else if (options["--events"]) {
|
|
190
|
+
/** @type {NonNullable<import("./evidence-index.js").EvidenceInspectRequest["events"]>} */
|
|
191
|
+
const events = {}
|
|
192
|
+
if (options["--kind"]) events.kind = /** @type {string} */ (options["--kind"])
|
|
193
|
+
if (options["--after"]) events.after = positiveInteger(/** @type {string} */ (options["--after"]), "--after")
|
|
194
|
+
if (options["--before"]) events.before = positiveInteger(/** @type {string} */ (options["--before"]), "--before")
|
|
195
|
+
if (options["--limit"]) events.limit = positiveInteger(/** @type {string} */ (options["--limit"]), "--limit")
|
|
196
|
+
request = {...request, events}
|
|
197
|
+
} else {
|
|
198
|
+
/** @type {NonNullable<import("./evidence-index.js").EvidenceInspectRequest["failures"]>} */
|
|
199
|
+
const failures = {}
|
|
200
|
+
if (options["--after"]) failures.after = positiveInteger(/** @type {string} */ (options["--after"]), "--after")
|
|
201
|
+
if (options["--limit"]) failures.limit = positiveInteger(/** @type {string} */ (options["--limit"]), "--limit")
|
|
202
|
+
request = {...request, failures}
|
|
203
|
+
}
|
|
204
|
+
return {evidenceInspect: true, request}
|
|
156
205
|
}
|
|
157
206
|
|
|
158
207
|
/** @param {string[]} arguments_ @returns {CapacityParsedArguments | {help: true}} */
|
|
@@ -292,13 +341,15 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
292
341
|
})}\n`)
|
|
293
342
|
return selection === null ? 2 : 0
|
|
294
343
|
}
|
|
295
|
-
if ("evidenceRead" in parsed) {
|
|
344
|
+
if ("evidenceRead" in parsed || "evidenceInspect" in parsed) {
|
|
296
345
|
const root = evidenceRoot(sourceEnvironment.THREADWIRE_EVIDENCE_ROOT)
|
|
297
346
|
if (!root) throw new Error("THREADWIRE_EVIDENCE_ROOT is required")
|
|
298
347
|
if (validateOnly) return 0
|
|
299
348
|
const store = await EvidenceStore.open({root})
|
|
300
349
|
try {
|
|
301
|
-
const result =
|
|
350
|
+
const result = "evidenceRead" in parsed
|
|
351
|
+
? await store.readBearer(parsed.request)
|
|
352
|
+
: await store.inspectBearer(parsed.request)
|
|
302
353
|
output.write(`${JSON.stringify(result)}\n`)
|
|
303
354
|
return 0
|
|
304
355
|
} finally {
|
|
@@ -360,6 +411,11 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
360
411
|
let resultEvidenceBytes = 0
|
|
361
412
|
const deliveryIdentity = randomUUID()
|
|
362
413
|
const launchDeadline = isolatedPreflight?.deadline
|
|
414
|
+
/** @param {import("./evidence-index.js").EvidenceIndexEvent} event */
|
|
415
|
+
const recordEvidenceEvent = (event) => {
|
|
416
|
+
if (evidenceStore === undefined || evidenceOwner === undefined) return Promise.resolve()
|
|
417
|
+
return evidenceStore.recordEvent(evidenceOwner, event)
|
|
418
|
+
}
|
|
363
419
|
try {
|
|
364
420
|
environment = await boundedLaunch(resolveFileBackedSettings(
|
|
365
421
|
sourceEnvironment,
|
|
@@ -381,7 +437,9 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
381
437
|
: "file",
|
|
382
438
|
runId: deliveryIdentity
|
|
383
439
|
})
|
|
440
|
+
evidenceStore.ensureRunIndex(evidenceOwner, {provider: parsed.provider})
|
|
384
441
|
const redactions = await boundedLaunch(collectEvidenceRedactions(environment), launchDeadline)
|
|
442
|
+
evidenceStore.setRunIndexRedactions(evidenceOwner, redactions)
|
|
385
443
|
evidence = await evidenceStore.createArtifact(evidenceOwner, {
|
|
386
444
|
contentType: "text/plain; charset=utf-8",
|
|
387
445
|
redactions
|
|
@@ -404,6 +462,12 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
404
462
|
if (evidence !== undefined) {
|
|
405
463
|
const promptEvidence = `prompt\n${prompt}\nprovider-stream\n`
|
|
406
464
|
await boundedLaunch(evidence.append("prompt", promptEvidence), launchDeadline)
|
|
465
|
+
await recordEvidenceEvent({
|
|
466
|
+
kind: "prompt",
|
|
467
|
+
artifactId: evidence.metadata.id,
|
|
468
|
+
provider: parsed.provider,
|
|
469
|
+
metadata: {length: Buffer.byteLength(promptEvidence, "utf8")}
|
|
470
|
+
})
|
|
407
471
|
evidencePayloadBytes += Buffer.byteLength(promptEvidence, "utf8")
|
|
408
472
|
}
|
|
409
473
|
const providerEnvironment = buildProviderEnvironment(environment, {
|
|
@@ -455,6 +519,7 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
455
519
|
else if (event.type === "tool") activity?.recordActivity(provider.name, "tool")
|
|
456
520
|
else if (event.type === "lifecycle") activity?.recordActivity(provider.name, "lifecycle")
|
|
457
521
|
acceptAdmissionEvent(admission, event)
|
|
522
|
+
await recordEvidenceEvent(workerEventToIndexEvent(event, parsed.provider))
|
|
458
523
|
if (control !== undefined) {
|
|
459
524
|
try {
|
|
460
525
|
await control.accept(event)
|
|
@@ -480,13 +545,29 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
480
545
|
metrics.recordRawChildChunk("provider_stdout", chunk.length)
|
|
481
546
|
metrics.recordRejected("provider_stream", chunk.length)
|
|
482
547
|
if (evidence === undefined) return undefined
|
|
483
|
-
return
|
|
548
|
+
return Promise.all([
|
|
549
|
+
evidence.append("provider-stdout", chunk),
|
|
550
|
+
recordEvidenceEvent({
|
|
551
|
+
kind: "provider_stdout",
|
|
552
|
+
artifactId: evidence.metadata.id,
|
|
553
|
+
provider: parsed.provider,
|
|
554
|
+
metadata: {length: chunk.length}
|
|
555
|
+
})
|
|
556
|
+
]).then(() => { evidencePayloadBytes += chunk.length })
|
|
484
557
|
},
|
|
485
558
|
onStderrChunk: (chunk) => {
|
|
486
559
|
metrics.recordRawChildChunk("provider_stderr", chunk.length)
|
|
487
560
|
metrics.recordRejected("stderr", chunk.length)
|
|
488
561
|
if (evidence === undefined) return undefined
|
|
489
|
-
return
|
|
562
|
+
return Promise.all([
|
|
563
|
+
evidence.append("provider-stderr", chunk),
|
|
564
|
+
recordEvidenceEvent({
|
|
565
|
+
kind: "provider_stderr",
|
|
566
|
+
artifactId: evidence.metadata.id,
|
|
567
|
+
provider: parsed.provider,
|
|
568
|
+
metadata: {length: chunk.length}
|
|
569
|
+
})
|
|
570
|
+
]).then(() => { evidencePayloadBytes += chunk.length })
|
|
490
571
|
}
|
|
491
572
|
}
|
|
492
573
|
let exitCode
|
|
@@ -520,6 +601,12 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
520
601
|
const providerResult = admission.createProviderResult(terminal)
|
|
521
602
|
const serializedProviderResult = `${JSON.stringify(providerResult)}\n`
|
|
522
603
|
await resultEvidence.append("provider-result", serializedProviderResult)
|
|
604
|
+
await recordEvidenceEvent({
|
|
605
|
+
kind: "provider_result",
|
|
606
|
+
artifactId: resultEvidence.metadata.id,
|
|
607
|
+
provider: parsed.provider,
|
|
608
|
+
metadata: {terminal, length: Buffer.byteLength(serializedProviderResult, "utf8")}
|
|
609
|
+
})
|
|
523
610
|
const resultArtifact = await resultEvidence.finalize()
|
|
524
611
|
resultEvidenceBytes = resultArtifact.bytes
|
|
525
612
|
resultEvidenceFinalized = true
|
|
@@ -550,7 +637,14 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
550
637
|
evidencePayloadBytes + resultEvidenceBytes,
|
|
551
638
|
2
|
|
552
639
|
)
|
|
553
|
-
|
|
640
|
+
const metricsSnapshot = `${JSON.stringify(metrics.snapshot())}\n`
|
|
641
|
+
await evidence.append("context-metrics", metricsSnapshot)
|
|
642
|
+
await recordEvidenceEvent({
|
|
643
|
+
kind: "context_metrics",
|
|
644
|
+
artifactId: evidence.metadata.id,
|
|
645
|
+
provider: parsed.provider,
|
|
646
|
+
metadata: {length: Buffer.byteLength(metricsSnapshot, "utf8")}
|
|
647
|
+
})
|
|
554
648
|
const artifact = await evidence.finalize()
|
|
555
649
|
assertArtifactProjection(artifact.bytes + resultEvidenceBytes, projectedArtifactBytes)
|
|
556
650
|
} catch (error) {
|
|
@@ -563,6 +657,13 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
563
657
|
} else if (resultEvidenceFinalized) {
|
|
564
658
|
metrics.recordArtifact(resultEvidenceBytes, 1)
|
|
565
659
|
}
|
|
660
|
+
if (evidenceStore !== undefined && evidenceOwner !== undefined && evidenceError === undefined) {
|
|
661
|
+
try {
|
|
662
|
+
await evidenceStore.finalizeRunIndex(evidenceOwner, terminal)
|
|
663
|
+
} catch (error) {
|
|
664
|
+
evidenceError ??= error
|
|
665
|
+
}
|
|
666
|
+
}
|
|
566
667
|
admission.complete({state: terminalExitCode === 0 ? "completed" : "failed", exitCode: terminalExitCode})
|
|
567
668
|
if (
|
|
568
669
|
destination.type === "telegram" && resultEvidenceFinalized
|