threadwire 0.1.21 → 0.1.22

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/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
- if (arguments_[1] !== "read") throw new Error(HELP)
133
- /** @type {Record<string, string>} */
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
- for (let index = 2; index < arguments_.length; index += 2) {
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 || !value || !["--handle", "--bytes", "--lines", "--query", "--context-bytes"].includes(name)) throw new Error(HELP)
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
- const selectors = ["--bytes", "--lines", "--query"].filter((name) => options[name] !== undefined)
145
- if (selectors.length !== 1) throw new Error("Exactly one bounded evidence selector is required")
146
- let request
147
- if (options["--bytes"]) request = {handle, bytes: rangePair(options["--bytes"], true)}
148
- else if (options["--lines"]) request = {handle, lines: rangePair(options["--lines"], false)}
149
- else {
150
- const context = options["--context-bytes"]
151
- if (!context) throw new Error("--context-bytes is required with --query")
152
- request = {handle, query: {literal: options["--query"], limit: 1, contextBytes: positiveInteger(context, "--context-bytes")}}
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
- if (options["--query"] === undefined && options["--context-bytes"] !== undefined) throw new Error("--context-bytes requires --query")
155
- return {evidenceRead: true, request}
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 = await store.readBearer(parsed.request)
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 evidence.append("provider-stdout", chunk).then(() => { evidencePayloadBytes += chunk.length })
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 evidence.append("provider-stderr", chunk).then(() => { evidencePayloadBytes += chunk.length })
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
- await evidence.append("context-metrics", `${JSON.stringify(metrics.snapshot())}\n`)
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