threadwire 0.1.13 → 0.1.14

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.
@@ -5,14 +5,16 @@ import {AbsoluteDeadline, deadlineAfter, abortable, readResponseCapped} from "./
5
5
 
6
6
  const MAX_RESPONSE_BYTES = 2_097_152
7
7
  const MAX_RAW_OUTPUT_BYTES = 1_048_576
8
+ const DEFAULT_CLEANUP_TIMEOUT_MS = 10_000
8
9
 
9
10
  export class IsolatedRuntimeClient {
10
- /** @param {{url: string, controlToken: string, timeoutMs?: number | string, fetchImplementation?: typeof fetch}} options */
11
+ /** @param {{url: string, controlToken: string, timeoutMs?: number | string, cleanupTimeoutMs?: number | string, fetchImplementation?: typeof fetch}} options */
11
12
  constructor(options) {
12
13
  this.url = normalizedUrl(options.url)
13
14
  this.controlToken = nonempty(options.controlToken, "isolated runtime control token")
14
15
  this.fetch = options.fetchImplementation ?? fetch
15
16
  this.timeoutMs = optionalDuration(options.timeoutMs)
17
+ this.cleanupTimeoutMs = optionalDuration(options.cleanupTimeoutMs) ?? DEFAULT_CLEANUP_TIMEOUT_MS
16
18
  }
17
19
 
18
20
  /**
@@ -79,6 +81,113 @@ export class IsolatedRuntimeClient {
79
81
  }
80
82
  }
81
83
 
84
+ /**
85
+ * Start a container-backed run whose request side remains open as the
86
+ * cancellation channel. Closing that side asks the runtime to stop its exact
87
+ * worker container while leaving the response available for terminal cleanup
88
+ * confirmation.
89
+ * @param {{
90
+ * preflightId: string, prompt: string, providerArguments: string[],
91
+ * resumeSession?: string,
92
+ * signal?: AbortSignal,
93
+ * deadline?: import("./absolute-deadline.js").AbsoluteDeadline,
94
+ * onEvent: (event: import("./types.js").WorkerEvent) => unknown | Promise<unknown>,
95
+ * onRecord?: (record: unknown) => unknown | Promise<unknown>,
96
+ * onStdoutChunk?: (chunk: Buffer) => unknown | Promise<unknown>,
97
+ * onStderrChunk?: (chunk: Buffer) => unknown | Promise<unknown>
98
+ * }} options
99
+ */
100
+ startRun(options) {
101
+ const deadline = options.deadline ?? (this.timeoutMs === undefined ? undefined : deadlineAfter(this.timeoutMs, {signal: options.signal, timeoutMessage: "Isolated runtime launch timeout"}))
102
+ const operation = deadline ?? new AbsoluteDeadline(undefined, {signal: options.signal, timeoutMessage: "Isolated runtime launch timeout"})
103
+ const requestController = new AbortController()
104
+ /** @type {ReadableStreamDefaultController<Uint8Array>} */
105
+ let bodyController
106
+ let bodyClosed = false
107
+ const body = new ReadableStream({
108
+ start(controller) {
109
+ bodyController = controller
110
+ controller.enqueue(new TextEncoder().encode(`${JSON.stringify({
111
+ preflightId: options.preflightId,
112
+ prompt: options.prompt,
113
+ providerArguments: options.providerArguments,
114
+ ...(options.resumeSession === undefined ? {} : {resumeSession: options.resumeSession})
115
+ })}\n`))
116
+ }
117
+ })
118
+ const closeRequest = () => {
119
+ if (bodyClosed) return
120
+ bodyClosed = true
121
+ bodyController.close()
122
+ }
123
+ /** @type {Error | undefined} */
124
+ let consumerError
125
+ /** @type {(error: Error) => void} */
126
+ let rejectConsumer
127
+ const consumerFailure = new Promise((_resolve, reject) => { rejectConsumer = reject })
128
+ const transportCompletion = (async () => {
129
+ const response = await this.fetch(`${this.url}/run`, {
130
+ method: "POST",
131
+ redirect: "error",
132
+ headers: {
133
+ authorization: `Bearer ${this.controlToken}`,
134
+ "content-type": "application/x-ndjson",
135
+ connection: "close",
136
+ "x-threadwire-run-lifecycle": "1",
137
+ ...(operation.expiresAt === undefined ? {} : {"x-threadwire-launch-deadline": String(operation.expiresAt)})
138
+ },
139
+ body,
140
+ duplex: "half",
141
+ signal: requestController.signal
142
+ })
143
+ if (!response.ok) {
144
+ const bytes = await readResponseCapped(response, MAX_RESPONSE_BYTES, requestController.signal)
145
+ throw new Error(safeRemoteError(bytes))
146
+ }
147
+ return consumeRunStream(response, options, {
148
+ signal: requestController.signal,
149
+ onConsumerError(error) {
150
+ if (consumerError !== undefined) return
151
+ consumerError = error
152
+ rejectConsumer(error)
153
+ }
154
+ })
155
+ })()
156
+ const runCompletion = Promise.race([transportCompletion, consumerFailure])
157
+ /** @type {Promise<void> | undefined} */
158
+ let cancellation
159
+ const cancel = (reason = new Error("Isolated runtime run cancelled")) => {
160
+ if (cancellation !== undefined) return cancellation
161
+ closeRequest()
162
+ const cleanup = deadlineAfter(this.cleanupTimeoutMs, {timeoutMessage: "Isolated runtime cleanup confirmation timeout"})
163
+ cancellation = (async () => {
164
+ try {
165
+ await abortable(transportCompletion, cleanup.signal)
166
+ } catch (error) {
167
+ requestController.abort(reason)
168
+ throw new Error("Isolated runtime cleanup could not be confirmed", {cause: error})
169
+ } finally {
170
+ cleanup.close()
171
+ }
172
+ })()
173
+ return cancellation
174
+ }
175
+ const completion = (async () => {
176
+ try {
177
+ return await abortable(runCompletion, operation.signal)
178
+ } catch (error) {
179
+ if (operation.signal.aborted || error === consumerError) {
180
+ await cancel(error instanceof Error ? error : new Error("Isolated runtime run failed"))
181
+ }
182
+ throw error
183
+ } finally {
184
+ closeRequest()
185
+ if (options.deadline === undefined) operation.close()
186
+ }
187
+ })()
188
+ return {completion, cancel}
189
+ }
190
+
82
191
  /** @param {string} path @param {unknown} body */
83
192
  async request(path, body, deadline) {
84
193
  const signal = deadline.signal
@@ -121,6 +230,17 @@ async function consumeRunStream(response, options, operation) {
121
230
  let rawBytes = 0
122
231
  /** @type {number | undefined} */
123
232
  let terminal
233
+ let consumerFailed = false
234
+ const invokeConsumer = async (callback) => {
235
+ if (consumerFailed) return
236
+ try {
237
+ await abortable(callback(), operation.signal)
238
+ } catch (error) {
239
+ if (operation.onConsumerError === undefined) throw error
240
+ consumerFailed = true
241
+ operation.onConsumerError(error instanceof Error ? error : new Error("Isolated runtime consumer failed"))
242
+ }
243
+ }
124
244
  const consume = async (line) => {
125
245
  if (line.length === 0 || terminal !== undefined) throw new Error("Isolated runtime emitted an invalid response")
126
246
  let frame
@@ -136,13 +256,13 @@ async function consumeRunStream(response, options, operation) {
136
256
  rawBytes += data.length
137
257
  if (rawBytes > MAX_RAW_OUTPUT_BYTES) throw new Error("Isolated runtime emitted an invalid response")
138
258
  const callback = frame.channel === "stdout" ? options.onStdoutChunk : options.onStderrChunk
139
- await abortable(callback?.(data), operation.signal)
259
+ if (callback !== undefined) await invokeConsumer(() => callback(data))
140
260
  return
141
261
  }
142
262
  if (frame.type === "record") {
143
263
  if (Object.keys(frame).sort().join(",") !== "record,type") throw new Error("Isolated runtime emitted an invalid response")
144
- await abortable(options.onRecord?.(frame.record), operation.signal)
145
- if (isWorkerEventEnvelope(frame.record)) await abortable(options.onEvent(frame.record.event), operation.signal)
264
+ if (options.onRecord !== undefined) await invokeConsumer(() => options.onRecord(frame.record))
265
+ if (isWorkerEventEnvelope(frame.record)) await invokeConsumer(() => options.onEvent(frame.record.event))
146
266
  return
147
267
  }
148
268
  if (frame.type === "terminal" && Object.keys(frame).sort().join(",") === "exitCode,type" && Number.isSafeInteger(frame.exitCode)) {
@@ -29,6 +29,7 @@ const GRANT_LEASE_MS = 60_000
29
29
  const GRANT_LEASE_MAX_MS = 3_600_000
30
30
  const PREFLIGHT_TIMEOUT_MS = 30_000
31
31
  const MAX_RAW_OUTPUT_BYTES = 1_048_576
32
+ const WORKER_STOP_GRACE_SECONDS = 3
32
33
 
33
34
  /** @param {{environment?: NodeJS.ProcessEnv, docker?: DockerApi, fetchImplementation?: typeof fetch, now?: () => number}} [options] */
34
35
  export async function startIsolatedRuntime(options = {}) {
@@ -160,21 +161,32 @@ export async function startIsolatedRuntime(options = {}) {
160
161
  const releaseIncoming = incomingRequests.acquire("run")
161
162
  const bodyOperation = new AbsoluteDeadline(Date.now() + preflightTimeoutMs, {signal: shutdown.signal, request, response, timeoutMessage: "Isolated runtime operation timed out"})
162
163
  let body
164
+ let runLifecycle
163
165
  try {
164
- body = parseRun(await readJson(request, MAX_RUN_BYTES, bodyOperation.signal))
166
+ if (request.headers["x-threadwire-run-lifecycle"] === "1") {
167
+ runLifecycle = await readRunLifecycle(request, MAX_RUN_BYTES, bodyOperation.signal)
168
+ body = parseRun(runLifecycle.value)
169
+ } else {
170
+ body = parseRun(await readJson(request, MAX_RUN_BYTES, bodyOperation.signal))
171
+ }
165
172
  } finally {
166
173
  bodyOperation.close()
167
174
  releaseIncoming()
168
175
  }
169
176
  const preflight = preflights.consume(body.preflightId)
170
177
  if (!preflight) throw new ClientError("Isolated runtime preflight expired")
178
+ const operationSignal = runLifecycle === undefined
179
+ ? shutdown.signal
180
+ : AbortSignal.any([shutdown.signal, runLifecycle.signal])
171
181
  const operation = new AbsoluteDeadline(minimumDeadline(preflight.launchDeadlineAt,
172
182
  requestedDeadline(request.headers["x-threadwire-launch-deadline"], workerTimeoutMs)), {
173
- signal: shutdown.signal, request, response, timeoutMessage: "Isolated runtime launch timed out", disconnectMessage: "Isolated runtime caller disconnected"
183
+ signal: operationSignal, request, response, timeoutMessage: "Isolated runtime launch timed out", disconnectMessage: "Isolated runtime caller disconnected",
184
+ destroyRequestOnAbort: runLifecycle === undefined
174
185
  })
175
186
  const task = taskIdentity(preflight)
176
187
  let releaseRun
177
188
  let releaseLineage
189
+ let retainLineage = false
178
190
  try {
179
191
  releaseRun = activeRuns.acquire(task)
180
192
  releaseLineage = activeLineages.acquire(preflight.state.lineage)
@@ -188,6 +200,7 @@ export async function startIsolatedRuntime(options = {}) {
188
200
  if (!response.headersSent) response.writeHead(200, {"content-type": "application/x-ndjson"})
189
201
  if (!response.write(`${JSON.stringify(frame)}\n`)) await abortable(once(response, "drain"), operation.signal)
190
202
  }
203
+ if (runLifecycle !== undefined && !response.headersSent) response.writeHead(200, {"content-type": "application/x-ndjson"})
191
204
  const result = await runIsolatedWorker({
192
205
  docker, workerImage, brokerUrl, brokerAdminToken, brokerContainer, relayImage, relayWorkerUrl,
193
206
  fetchImplementation, preflight, ...body, task, now, stateRegistry, sessionTtlMs, operation, grantLeaseMs,
@@ -197,9 +210,13 @@ export async function startIsolatedRuntime(options = {}) {
197
210
  for (const chunk of result.rawChunks) await writeFrame({type: "chunk", ...chunk})
198
211
  await writeFrame({type: "terminal", exitCode: result.exitCode})
199
212
  response.end()
213
+ } catch (error) {
214
+ if (error instanceof ContainerCleanupConfirmationError) retainLineage = true
215
+ throw error
200
216
  } finally {
201
217
  operation.close()
202
- releaseLineage?.()
218
+ runLifecycle?.close()
219
+ if (!retainLineage) releaseLineage?.()
203
220
  releaseRun?.()
204
221
  void collectState()
205
222
  }
@@ -209,7 +226,9 @@ export async function startIsolatedRuntime(options = {}) {
209
226
  response.end()
210
227
  } catch (error) {
211
228
  if (response.headersSent) {
212
- if (!response.writableEnded && !response.destroyed) response.end(`${JSON.stringify({type: "terminal", exitCode: 1})}\n`)
229
+ if (!response.writableEnded && !response.destroyed) {
230
+ response.end(error instanceof ContainerCleanupConfirmationError ? undefined : `${JSON.stringify({type: "terminal", exitCode: 1})}\n`)
231
+ }
213
232
  return
214
233
  }
215
234
  sendJson(response, error instanceof ClientError ? 400 : 503, {
@@ -367,6 +386,9 @@ export async function runIsolatedWorker(options) {
367
386
  let grantRenewalTimer
368
387
  let runTracked = false
369
388
  let runStage = "state"
389
+ let workerResult
390
+ let runError
391
+ let cleanupFailure
370
392
  const operation = options.operation
371
393
  const worktreeSubpath = options.worktreeVolume === undefined
372
394
  ? ""
@@ -632,10 +654,10 @@ export async function runIsolatedWorker(options) {
632
654
  throw error
633
655
  }
634
656
  }
635
- return result
657
+ workerResult = result
636
658
  } catch (error) {
637
659
  process.stderr.write(`threadwire-runtime: isolated run failed at ${runStage}\n`)
638
- throw error
660
+ runError = error
639
661
  } finally {
640
662
  let resourcesRemoved = true
641
663
  if (grantRenewalTimer !== undefined) clearInterval(grantRenewalTimer)
@@ -658,10 +680,17 @@ export async function runIsolatedWorker(options) {
658
680
  resourcesRemoved = false
659
681
  process.stderr.write("threadwire-isolated-runtime: validator cleanup failed\n")
660
682
  })
661
- if (containerId !== undefined) await cleanupDocker(() => options.docker.removeContainer(containerId, cleanupOptions())).catch(() => {
662
- resourcesRemoved = false
663
- process.stderr.write("threadwire-isolated-runtime: worker cleanup failed\n")
664
- })
683
+ if (containerId !== undefined) {
684
+ try {
685
+ await stopAndRemoveWorkerContainer(options.docker, containerId, cleanupOptions())
686
+ } catch (error) {
687
+ resourcesRemoved = false
688
+ cleanupFailure = error instanceof ContainerCleanupConfirmationError
689
+ ? error
690
+ : new ContainerCleanupConfirmationError({cause: error})
691
+ process.stderr.write("threadwire-isolated-runtime: worker cleanup failed\n")
692
+ }
693
+ }
665
694
  if (relayId !== undefined) await cleanupDocker(() => options.docker.removeContainer(relayId, cleanupOptions())).catch(() => {
666
695
  resourcesRemoved = false
667
696
  process.stderr.write("threadwire-isolated-runtime: relay cleanup failed\n")
@@ -677,16 +706,43 @@ export async function runIsolatedWorker(options) {
677
706
  resourcesRemoved = false
678
707
  process.stderr.write("threadwire-isolated-runtime: egress network cleanup failed\n")
679
708
  })
680
- if (freshState && !keepFreshState) await cleanupDocker(() => options.docker.removeVolume(state.volume, cleanupOptions())).catch(() => { resourcesRemoved = false })
709
+ if (freshState && !keepFreshState && resourcesRemoved) await cleanupDocker(() => options.docker.removeVolume(state.volume, cleanupOptions())).catch(() => { resourcesRemoved = false })
681
710
  if (runTracked && resourcesRemoved) await options.stateRegistry.completeRun(runId, cleanupOperation.signal).catch(() => {})
682
711
  emergencyCleanup?.close()
683
712
  }
713
+ if (cleanupFailure !== undefined) throw cleanupFailure
714
+ if (runError !== undefined) throw runError
715
+ return workerResult
684
716
  }
685
717
 
686
718
  async function cleanupDocker(operation) {
687
719
  return operation()
688
720
  }
689
721
 
722
+ async function stopAndRemoveWorkerContainer(docker, containerId, requestOptions) {
723
+ let inspection = await docker.inspectContainer(containerId, requestOptions)
724
+ if (typeof inspection?.State?.Running !== "boolean") throw new Error("Worker container state unavailable")
725
+ if (inspection.State.Running) {
726
+ await docker.stopContainer(containerId, WORKER_STOP_GRACE_SECONDS, requestOptions)
727
+ inspection = await docker.inspectContainer(containerId, requestOptions)
728
+ if (typeof inspection?.State?.Running !== "boolean") throw new Error("Worker container state unavailable")
729
+ if (inspection.State.Running) {
730
+ await docker.killContainer(containerId, "KILL", requestOptions)
731
+ inspection = await docker.inspectContainer(containerId, requestOptions)
732
+ if (inspection?.State?.Running !== false) throw new ContainerCleanupConfirmationError()
733
+ }
734
+ }
735
+ await docker.removeContainer(containerId, requestOptions)
736
+ }
737
+
738
+ class ContainerCleanupConfirmationError extends Error {
739
+ /** @param {{cause?: unknown}} [options] */
740
+ constructor(options = {}) {
741
+ super("Worker container cleanup could not be confirmed", options)
742
+ this.name = "ContainerCleanupConfirmationError"
743
+ }
744
+ }
745
+
690
746
  export async function prepareStateVolume(docker, state, fresh, requestOptions) {
691
747
  if (fresh) await docker.createVolume(state.volume, state.labels, requestOptions)
692
748
  const volume = await docker.inspectVolume(state.volume, requestOptions).catch(() => undefined)
@@ -1069,6 +1125,84 @@ async function readJson(request, capacity, signal) {
1069
1125
  }
1070
1126
  }
1071
1127
 
1128
+ function readRunLifecycle(request, capacity, signal) {
1129
+ const cancellation = new AbortController()
1130
+ /** @type {Buffer[]} */
1131
+ const chunks = []
1132
+ let bytes = 0
1133
+ let parsed = false
1134
+ let settled = false
1135
+ const cancel = (reason) => cancellation.abort(reason instanceof Error ? reason : new Error("Isolated runtime caller cancelled"))
1136
+ const close = () => {
1137
+ request.off("data", onData)
1138
+ request.off("end", onEnd)
1139
+ request.off("aborted", onAborted)
1140
+ request.off("error", onError)
1141
+ signal?.removeEventListener("abort", onSignal)
1142
+ }
1143
+ /** @type {(value: {value: unknown, signal: AbortSignal, close: () => void}) => void} */
1144
+ let resolveLifecycle
1145
+ /** @type {(error: Error) => void} */
1146
+ let rejectLifecycle
1147
+ const fail = (error) => {
1148
+ if (parsed) {
1149
+ cancel(error)
1150
+ return
1151
+ }
1152
+ if (settled) return
1153
+ settled = true
1154
+ close()
1155
+ rejectLifecycle(error)
1156
+ }
1157
+ const onData = (chunk) => {
1158
+ if (parsed) {
1159
+ fail(new ClientError("Invalid request"))
1160
+ return
1161
+ }
1162
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
1163
+ bytes += buffer.length
1164
+ if (bytes > capacity) {
1165
+ fail(new ClientError("Request exceeded capacity"))
1166
+ return
1167
+ }
1168
+ chunks.push(buffer)
1169
+ const combined = Buffer.concat(chunks, bytes)
1170
+ const newline = combined.indexOf(0x0a)
1171
+ if (newline < 0) return
1172
+ if (newline !== combined.length - 1) {
1173
+ fail(new ClientError("Invalid request"))
1174
+ return
1175
+ }
1176
+ let value
1177
+ try {
1178
+ value = JSON.parse(combined.subarray(0, newline).toString("utf8"))
1179
+ } catch {
1180
+ fail(new ClientError("Invalid request"))
1181
+ return
1182
+ }
1183
+ parsed = true
1184
+ settled = true
1185
+ resolveLifecycle({value, signal: cancellation.signal, close})
1186
+ }
1187
+ const onEnd = () => parsed
1188
+ ? cancel(new Error("Isolated runtime caller cancelled"))
1189
+ : fail(new ClientError("Invalid request"))
1190
+ const onAborted = () => fail(new Error("Isolated runtime caller disconnected"))
1191
+ const onError = (error) => fail(error)
1192
+ const onSignal = () => fail(signal.reason instanceof Error ? signal.reason : new Error("Isolated runtime operation aborted"))
1193
+ const lifecycle = new Promise((resolve, reject) => {
1194
+ resolveLifecycle = resolve
1195
+ rejectLifecycle = reject
1196
+ })
1197
+ request.on("data", onData)
1198
+ request.once("end", onEnd)
1199
+ request.once("aborted", onAborted)
1200
+ request.once("error", onError)
1201
+ if (signal?.aborted) onSignal()
1202
+ else signal?.addEventListener("abort", onSignal, {once: true})
1203
+ return lifecycle
1204
+ }
1205
+
1072
1206
  function requireControl(value, expected) {
1073
1207
  if (value !== `Bearer ${expected}`) throw new ClientError("Request denied")
1074
1208
  }
@@ -0,0 +1,178 @@
1
+ // @ts-check
2
+
3
+ import {closeSync, mkdtempSync, openSync, readFileSync, rmSync, unlinkSync, writeSync} from "node:fs"
4
+ import {tmpdir} from "node:os"
5
+ import {isAbsolute, join, normalize} from "node:path"
6
+
7
+ /**
8
+ * Single-owner byte-oriented spool for one worker's pending JSONL record.
9
+ *
10
+ * Worker stdout is consumed as raw bytes. A pending record (bytes not yet
11
+ * terminated by `\n`) is kept in bounded memory while it stays below
12
+ * `memoryBytes`; beyond that threshold it spills exactly once to one private
13
+ * run-scoped file, writing every byte segment exactly once. Newline scanning
14
+ * happens on raw bytes: `0x0a` can never appear inside a multibyte UTF-8
15
+ * sequence, so newline boundaries are preserved across arbitrary chunk splits
16
+ * and completed records always decode as well-formed UTF-8.
17
+ *
18
+ * `maxBytes` is a separate absolute pending-record cap: unterminated or
19
+ * malformed output can never consume unbounded memory, disk, or JSON.parse
20
+ * allocation. `close()` is idempotent and removes every spool artifact; the
21
+ * owner calls it on every settlement path.
22
+ */
23
+ export class JsonlRecordSpool {
24
+ /** @param {{memoryBytes: number, maxBytes: number, directory?: string}} options */
25
+ constructor(options) {
26
+ this.memoryBytes = positiveSafeInteger(options.memoryBytes, "memoryBytes")
27
+ this.maxBytes = positiveSafeInteger(options.maxBytes, "maxBytes")
28
+ const directory = options.directory ?? tmpdir()
29
+ if (!isAbsolute(directory) || normalize(directory) !== directory) throw new Error("directory must be a normalized absolute path")
30
+ this.directory = directory
31
+ /** @type {Buffer[]} */
32
+ this.segments = []
33
+ this.pendingBytes = 0
34
+ /** @type {{fd: number, path: string} | null} */
35
+ this.file = null
36
+ /** @type {string | null} */
37
+ this.ownedDirectory = null
38
+ this.closed = false
39
+ }
40
+
41
+ /**
42
+ * Consume one stdout chunk and yield each completed record in order, decoded
43
+ * as UTF-8 without the terminating newline. Scanning advances only as the
44
+ * owner consumes records, so terminal protocol completion can leave trailing
45
+ * bytes in the same chunk untouched.
46
+ * @param {Buffer} chunk
47
+ * @returns {Generator<string>}
48
+ * @yields {string}
49
+ */
50
+ *push(chunk) {
51
+ if (this.closed) throw new Error("JSONL record spool is closed")
52
+ let segmentStart = 0
53
+ for (let index = 0; index < chunk.length; index += 1) {
54
+ if (chunk[index] !== 0x0a) continue
55
+ const record = this.completeRecord(chunk.subarray(segmentStart, index))
56
+ segmentStart = index + 1
57
+ try {
58
+ yield record
59
+ } finally {
60
+ this.removeOwnedDirectory()
61
+ }
62
+ }
63
+ if (segmentStart < chunk.length) this.append(chunk.subarray(segmentStart))
64
+ }
65
+
66
+ /**
67
+ * End of stream: return the pending record ("" when none) and release it.
68
+ * @returns {string}
69
+ */
70
+ end() {
71
+ if (this.closed || this.pendingBytes === 0) return ""
72
+ let record
73
+ if (this.file === null) {
74
+ record = Buffer.concat(this.segments, this.pendingBytes).toString("utf8")
75
+ this.segments = []
76
+ } else {
77
+ const file = this.file
78
+ this.file = null
79
+ closeSync(file.fd)
80
+ record = readFileSync(file.path, "utf8")
81
+ unlinkSync(file.path)
82
+ this.removeOwnedDirectory()
83
+ }
84
+ this.pendingBytes = 0
85
+ return record
86
+ }
87
+
88
+ /** Idempotent: close the spool file and remove every run-scoped artifact. */
89
+ close() {
90
+ if (this.closed) return
91
+ this.closed = true
92
+ this.segments = []
93
+ const file = this.file
94
+ this.file = null
95
+ if (file !== null) {
96
+ closeSync(file.fd)
97
+ rmSync(file.path, {force: true})
98
+ }
99
+ this.removeOwnedDirectory()
100
+ }
101
+
102
+ /**
103
+ * Finish one record: combine the pending bytes with the final segment and
104
+ * decode exactly once. A spilled record is closed, read back, and unlinked;
105
+ * the next oversized record spills into a freshly created file, so at most
106
+ * one private file ever exists for the run.
107
+ * @param {Buffer} finalSegment @returns {string}
108
+ */
109
+ completeRecord(finalSegment) {
110
+ const recordBytes = this.pendingBytes + finalSegment.length
111
+ if (recordBytes > this.maxBytes) throw new StdoutRecordTooLargeError()
112
+ if (this.file === null && recordBytes <= this.memoryBytes) {
113
+ const record = this.pendingBytes === 0
114
+ ? finalSegment.toString("utf8")
115
+ : Buffer.concat([...this.segments, finalSegment], recordBytes).toString("utf8")
116
+ this.segments = []
117
+ this.pendingBytes = 0
118
+ return record
119
+ }
120
+ if (this.file === null) this.append(finalSegment)
121
+ else {
122
+ this.pendingBytes = recordBytes
123
+ writeSync(this.file.fd, finalSegment)
124
+ }
125
+ const file = this.file
126
+ if (file === null) throw new Error("JSONL record spool file is unavailable")
127
+ this.file = null
128
+ closeSync(file.fd)
129
+ const record = readFileSync(file.path, "utf8")
130
+ unlinkSync(file.path)
131
+ this.pendingBytes = 0
132
+ return record
133
+ }
134
+
135
+ /** @param {Buffer} segment */
136
+ append(segment) {
137
+ this.pendingBytes += segment.length
138
+ if (this.pendingBytes > this.maxBytes) throw new StdoutRecordTooLargeError()
139
+ if (this.file !== null) {
140
+ writeSync(this.file.fd, segment)
141
+ return
142
+ }
143
+ if (this.pendingBytes <= this.memoryBytes) {
144
+ this.segments.push(segment)
145
+ return
146
+ }
147
+ // First threshold crossing: create the private run-scoped file and write
148
+ // each buffered segment exactly once, in order.
149
+ this.ownedDirectory = mkdtempSync(join(this.directory, "threadwire-record-"))
150
+ const path = join(this.ownedDirectory, "pending-record")
151
+ const fd = openSync(path, "wx", 0o600)
152
+ this.file = {fd, path}
153
+ for (const buffered of this.segments) writeSync(fd, buffered)
154
+ this.segments = []
155
+ writeSync(fd, segment)
156
+ }
157
+
158
+ /** Remove the current record's private spill directory, if one exists. */
159
+ removeOwnedDirectory() {
160
+ const directory = this.ownedDirectory
161
+ if (directory === null) return
162
+ rmSync(directory, {recursive: true, force: true})
163
+ this.ownedDirectory = null
164
+ }
165
+ }
166
+
167
+ export class StdoutRecordTooLargeError extends Error {
168
+ constructor() {
169
+ super("Worker stdout record exceeded the configured byte capacity")
170
+ this.name = "StdoutRecordTooLargeError"
171
+ }
172
+ }
173
+
174
+ /** @param {number} value @param {string} name */
175
+ function positiveSafeInteger(value, name) {
176
+ if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`${name} must be a positive safe integer`)
177
+ return value
178
+ }
@@ -7,6 +7,7 @@ import {dirname, isAbsolute, join} from "node:path"
7
7
 
8
8
  const MAX_CREDENTIAL_BYTES = 65_536
9
9
  const MAX_TOKEN_BYTES = 8_192
10
+ const CURRENT_TOKEN_SAFETY_MARGIN_SECONDS = 30
10
11
  const CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098"
11
12
  const TOKEN_URL = "https://auth.kimi.com/api/oauth/token"
12
13
  const EXACT_KEYS = ["access_token", "expires_at", "expires_in", "refresh_token", "scope", "token_type"]
@@ -117,6 +118,19 @@ export async function openKimiOAuthStore(options) {
117
118
  refreshInFlight = operation
118
119
  return operation
119
120
  },
121
+ /**
122
+ * Non-mutating read for observational callers: returns the already-loaded
123
+ * access token only when it stays valid beyond a fixed 30-second safety
124
+ * margin. It never fetches, refreshes, persists, renames, chmods, or
125
+ * tombstones; logged-out credentials fail with the fixed needs-login error
126
+ * and expired or near-expiry credentials fail with the fixed
127
+ * refresh-required error.
128
+ */
129
+ async getCurrentAccessToken() {
130
+ if (credential.access_token.length === 0) throw needsLogin()
131
+ if (credential.expires_at - now() <= CURRENT_TOKEN_SAFETY_MARGIN_SECONDS) throw refreshRequired()
132
+ return credential.access_token
133
+ },
120
134
  async health() {
121
135
  return credential.access_token.length === 0
122
136
  ? {ready: false, needsLogin: true}
@@ -263,5 +277,7 @@ function record(value) { return typeof value === "object" && value !== null && !
263
277
  function unavailable() { return new Error("Kimi OAuth credential file is unavailable") }
264
278
  /** @returns {Error} */
265
279
  function needsLogin() { return new Error("Kimi OAuth credential was rejected; re-login required") }
280
+ /** @returns {Error} */
281
+ function refreshRequired() { return new Error("Kimi OAuth credential requires refresh") }
266
282
  /** @param {unknown} [cause] @returns {Error} */
267
283
  function refreshUnavailable(cause) { return new Error("Kimi OAuth refresh is unavailable", cause === undefined ? undefined : {cause}) }