threadwire 0.1.11 → 0.1.13

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 CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.13 - 2026-08-01
6
+
7
+ - Remove the default one-hour full-run deadline from the isolated runtime: when `THREADWIRE_ISOLATED_RUNTIME_CLIENT_TIMEOUT_MS` (client) and `THREADWIRE_WORKER_TIMEOUT_MS` (supervisor) are unset or blank, a healthy running worker continues until natural completion, caller cancellation/disconnect, supervisor shutdown, or concrete failure. Setting either to a positive millisecond value enables the existing single absolute preflight+run deadline with unchanged validation and caps. Compose no longer inserts numeric timeout defaults. Preflight admission, per-operation Docker/network calls, state collection (30 seconds), and emergency cleanup stay independently bounded.
8
+ - Add authenticated broker grant-lease renewal (Kimi and Codex): grants are issued as a bounded lease (default 60 seconds, `THREADWIRE_GRANT_LEASE_MS`, capped at one hour) that the supervisor renews via `POST /admin/grants/{token}/renew` while the run is active. Cleanup still revokes explicitly; if the supervisor dies or loses contact, renewals stop and the broker expires and aborts the grant within one lease. Malformed renewals (unknown/expired token, invalid ttlMs, missing admin auth) fail closed without extending the grant, and no renewal route exists on worker endpoints.
9
+ - Add Kimi-only Threadwire binding v1 named-volume admission: task-labelled source/context/lease validation, immutable context manifest validation, offline validator, dynamic task UID/GID workers with context RO/state RW mounts and an exact binding-declared source mount mode, and v5 authenticated binding state. Kimi path/worktree preflights and supervisor host workspace/socket mounts are no longer accepted.
10
+ - Fail closed on Kimi context manifest task/image identity mismatch: the worker itself revalidates the mounted manifest against the binding task attestation before launch, so a mismatched manifest task identity can never produce a successful client run.
11
+ - Support explicitly read-only Kimi review sources: binding v1 `source.readOnly: true` pins a read-only lease/worker source mount for Docker-enforced reviewers, while writable implementation sources keep the exact read-write mode.
12
+ - Add a credential-free per-run cross-daemon relay: the central Kimi broker splits into an internal admin/control endpoint and a stable worker endpoint, and each run creates a tiny mount-free, credential-free relay on the separate task DinD that forwards only the fixed chat shape to the worker endpoint. The worker URL and published bind have no insecure default and must be explicitly task-DinD-reachable; the configured worker URL is the stable worker origin, which the relay normalizes to the fixed `/v1/chat/completions` path (rejecting any other path fail-closed). Kimi-specific values are load-time optional so a genuine Codex-only relay-write profile renders and starts without them, while the Kimi runtime and client still fail closed at startup or job launch when they are absent. Outer-Compose DNS and outer-host loopback are never assumed, and admin/worker authority stay distinct.
13
+ - Stream Kimi output incrementally over bounded framed NDJSON: the supervisor follows the worker's multiplexed Docker logs, validates each stdout envelope as it arrives, and relays `record` frames to the client before worker completion, followed by exactly one `terminal` exit-code frame. Session envelopes are buffered and published only once the run succeeds, so a resume hint followed by a nonzero exit never exposes an unusable resume session. Byte caps, redaction/admission rules, exact session validation, cancellation/deadline behavior, and cleanup are preserved; stderr is never relayed.
14
+ - Reconcile exact relay/worker/network/state/grant identity on restart: genuine resources are adopted and cleaned up by their exact sealed spec, while image, mount, network, or environment tampering fails closed and retains authenticated cleanup evidence. The sealed Kimi worker/validator/relay image is the resolved image content Id (the worker/validator pin the binding's `contextImageId`; the relay pins its inspected image Id), matching how Docker normalizes a created container's `Config.Image`, so a genuine crash survivor is recognized rather than retained as a cleanup failure. The validator and relay environments accept legitimate non-sensitive keys inherited from the image while every owned key must match its sealed value exactly once and any injected sensitive or controlled key fails closed.
15
+ - Qualify genuine separate-daemon fresh/resume/denial/read-only/concurrency operation with zero residue: adversarial split-daemon E2E proves exact resume, read-only review mounts, binding/argument denial, concurrent task isolation, and complete relay/network/state cleanup on the task DinD.
16
+
17
+ - Remove workspace profiles from normal provider execution. `--cwd` now passes
18
+ any caller-accessible directory directly to Codex, Claude, Kimi, and
19
+ OpenCode; no Git, source identity, task binding, or root-allowlist lookup is
20
+ performed. `--workspace-profile` is rejected—migrate invocations to
21
+ `--cwd <directory>`. Native Kimi is the default path, supports
22
+ `THREADWIRE_KIMI_BIN`, translates exact `--resume-session` values to Kimi's
23
+ native `--session` option, and passes compatible provider arguments while
24
+ Threadwire retains prompt and stream-output ownership. The optional isolated
25
+ Kimi path remains explicit and fail-closed without native fallback.
26
+ - Preserve only the explicit caller-owned native Kimi configuration contract,
27
+ report exactly one completed or failed terminal lifecycle outcome, and reject
28
+ provider option terminators that could neutralize Threadwire-owned arguments.
29
+
5
30
  ## 0.1.10 - 2026-07-28
6
31
 
7
32
  - Accept official slash-qualified Kimi model aliases through workspace-free
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Threadwire
2
2
 
3
- Threadwire is a Node.js CLI that launches a Codex, Claude, or OpenCode worker and streams a concise, human-readable activity feed to one explicitly selected Telegram destination: a direct message, normal chat, or forum topic. The default feed includes the assistant's response prose, lifecycle outcomes, and diagnostics. Provider tool-status messages are disabled by default.
3
+ Threadwire is a Node.js CLI that launches a Codex, Claude, Kimi, or OpenCode worker and streams a concise, human-readable activity feed to one explicitly selected Telegram destination: a direct message, normal chat, or forum topic. The default feed includes the assistant's response prose, lifecycle outcomes, and diagnostics. Provider tool-status messages are disabled by default.
4
4
 
5
5
  For the container-native webhook deployment, including its named-volume worktree,
6
6
  non-root Codex provider, Docker secrets, loopback TLS proxy, verification, and
@@ -8,10 +8,18 @@ rollback path, see [Container runtime](docs/container-runtime.md). The legacy
8
8
  host launcher below remains supported and is separate from that deployment.
9
9
 
10
10
  Relay-originated Codex writes are explicit and default off. `--relay-write`
11
- requires a reviewed `--workspace-profile` plus the root-owned isolated runtime
12
- and credential broker. The worker gets one writable task worktree, no upstream
11
+ selects the optional root-owned isolated runtime and credential broker. The worker gets one writable task worktree, no upstream
13
12
  or Git credential, and no route except its run-scoped broker. Missing
14
13
  prerequisites fail before prompt or credential reads with no native fallback.
14
+ The isolated runtime imposes no default full-run deadline: a healthy running
15
+ worker is never stopped merely because time elapsed. An operator may set
16
+ `THREADWIRE_WORKER_TIMEOUT_MS` (supervisor) and
17
+ `THREADWIRE_ISOLATED_RUNTIME_CLIENT_TIMEOUT_MS` (client) to a positive
18
+ millisecond value to enable one absolute preflight+run deadline; empty or
19
+ unset means unlimited. Broker grants use a bounded lease that the supervisor
20
+ renews while the run is active and that expires within one lease if the
21
+ supervisor dies; unrelated safety timeouts (preflight slice, per-operation
22
+ Docker/network calls, state collection, emergency cleanup) are unchanged.
15
23
  See [Isolated provider runtime](docs/isolated-provider-runtime.md).
16
24
 
17
25
  ## Run with npx
@@ -62,11 +70,11 @@ Use the local launcher. Install or refresh it after updating Threadwire with `np
62
70
 
63
71
  The target always has the form `telegram:<chat-id>:<thread-id>` for topics, or `telegram:<chat-id>` for direct messages (DMs) and ordinary chats. The chat ID is always required and must be a nonzero signed integer. The thread ID is optional and valid only as the explicit topic suffix: when present it must be a positive safe integer, and a DM target simply omits it — a missing thread ID is never an error. `--provider` must be exactly one of `codex`, `claude`, `kimi`, or `opencode`. Malformed or missing provider, chat ID, or thread ID arguments are hard failures (exit code 2) reported through Threadwire's normal error path with fixed messages; they fail before the Telegram token is read, before any transport is created, and before any provider is launched, and the messages never echo the bot token or the untrusted argument value. `--process-number` accepts an explicit positive safe integer for automation; when omitted, Threadwire uses its launcher PID. Every Telegram message is labeled consistently, such as `[P42] …`, including chunked continuations.
64
72
 
65
- Pass `--workspace-profile <name>` only when invoking Threadwire inside the container-native runtime, for example with `node bin/threadwire.js run` as shown in [Container runtime](docs/container-runtime.md). A profile selects a reviewed entry from the checked-in [threadwire.workspace-profiles.json](threadwire.workspace-profiles.json). It supplies the absolute in-container repository root, the exact in-container working directory, and the allowlisted providers for that workspace. Threadwire validates the selected profile, rejects malformed or unknown names, verifies the configured workspace exists and is still a healthy Git worktree, and records the selected profile plus immutable revision/worktree identity in the local activity log before provider spawn. The checked-in `container-runtime` default targets `/workspace/threadwire` inside the container-native runtime and allowlists `codex` and isolated `kimi`.
73
+ `--cwd` is the complete workspace contract for every provider. It may name any caller-accessible directory, including a non-Git directory. Threadwire passes it unchanged to the native provider and performs no profile, repository, source-identity, task-binding, or root-allowlist lookup. `--workspace-profile` has been removed and is rejected; migrate invocations by replacing it with `--cwd <directory>`.
66
74
 
67
- Provider tool-start and tool-finish events do not produce Telegram messages by default. Pass the boolean `--tool-messages` flag to opt in, as shown above. When enabled, each tool is one concise, silent status line rather than a pair of noisy start/finish notices: when a tool starts, Threadwire sends `🛠 [P42] Tool: <safe command or tool description>` (for example `🛠 [P42] Tool: command — git status --short`), and when that same tool finishes, it edits the original message in place so it begins `✅` instead — no second completion message is sent, and Telegram edits raise no notification. The command/description shows a safe CLI-style preview with credential-bearing values (tokens, passwords, keys, authorization/cookie forms, credentials in URLs) redacted and control characters stripped; only normalized Codex command-completion output may also be appended to that same edited status as a Telegram-native expandable HTML blockquote with the visible label `Output →`; it is credential-redacted, HTML-escaped, and Unicode-safe bounded. Stderr, reasoning, and every other raw provider result remain excluded. Use `--max-output-length <positive-integer>` to truncate only that preview Unicode-safely; omit the option for unlimited tool detail. A prompt may instead come from `--prompt-file` or piped stdin, and every source rejects blank/whitespace content. Arguments following `--` pass to the selected provider, except output-stream options owned by Threadwire. Kimi is narrower: it always uses the dedicated isolated subscription runtime and accepts only one server-approved model alias through `--model <alias>` (or `-m <alias>`); Threadwire owns its prompt, output, session, permissions, configuration, tools, skills, plugins, MCP, and directories.
75
+ Provider tool-start and tool-finish events do not produce Telegram messages by default. Pass the boolean `--tool-messages` flag to opt in, as shown above. When enabled, each tool is one concise, silent status line rather than a pair of noisy start/finish notices: when a tool starts, Threadwire sends `🛠 [P42] Tool: <safe command or tool description>` (for example `🛠 [P42] Tool: command — git status --short`), and when that same tool finishes, it edits the original message in place so it begins `✅` instead — no second completion message is sent, and Telegram edits raise no notification. The command/description shows a safe CLI-style preview with credential-bearing values (tokens, passwords, keys, authorization/cookie forms, credentials in URLs) redacted and control characters stripped; only normalized Codex command-completion output may also be appended to that same edited status as a Telegram-native expandable HTML blockquote with the visible label `Output →`; it is credential-redacted, HTML-escaped, and Unicode-safe bounded. Stderr, reasoning, and every other raw provider result remain excluded. Use `--max-output-length <positive-integer>` to truncate only that preview Unicode-safely; omit the option for unlimited tool detail. A prompt may instead come from `--prompt-file` or piped stdin, and every source rejects blank/whitespace content. Arguments following `--` pass to the selected provider, except output-stream and session options owned by Threadwire. Native Kimi uses the caller's compatible configuration. Threadwire maps its public `--resume-session <exact-id>` option to Kimi's native `--session <exact-id>` option and owns `--prompt` plus `--output-format stream-json`. The caller environment or image must provide the Kimi executable at `/usr/local/bin/kimi` or set `THREADWIRE_KIMI_BIN`; a missing executable fails normally.
68
76
 
69
- Threadwire directly spawns the structurally separate terminal adapters `/opt/data/libexec/threadwire/codex`, `/opt/data/libexec/threadwire/claude`, or `/opt/data/libexec/threadwire/opencode-local-fleet` with an argv array and `shell: false`. These libexec adapters hold the real provider-CLI logic and are never on a worker's `PATH`, so relaying can never recurse back into the front-door shims. Each spawn also sets `THREADWIRE_ACTIVE=1` in the provider child environment. The executable can be overridden per provider with `THREADWIRE_CODEX_BIN`, `THREADWIRE_CLAUDE_BIN`, or `THREADWIRE_OPENCODE_BIN` for staged cutover and rollback. It does not impose a worker timeout. `SIGINT` and `SIGTERM` are forwarded to the worker as explicit cancellation.
77
+ Threadwire directly spawns the structurally separate terminal adapters `/opt/data/libexec/threadwire/codex`, `/opt/data/libexec/threadwire/claude`, `/usr/local/bin/kimi`, or `/opt/data/libexec/threadwire/opencode-local-fleet` with an argv array and `shell: false`. These libexec adapters hold the real provider-CLI logic and are never on a worker's `PATH`, so relaying can never recurse back into the front-door shims. Each spawn also sets `THREADWIRE_ACTIVE=1` in the provider child environment. The executable can be overridden per provider with `THREADWIRE_CODEX_BIN`, `THREADWIRE_CLAUDE_BIN`, `THREADWIRE_KIMI_BIN`, or `THREADWIRE_OPENCODE_BIN` for staged cutover and rollback. It does not impose a worker timeout. `SIGINT` and `SIGTERM` are forwarded to the worker as explicit cancellation.
70
78
 
71
79
  ## Provider front-door shims
72
80
 
@@ -74,11 +82,11 @@ Threadwire directly spawns the structurally separate terminal adapters `/opt/dat
74
82
 
75
83
  The relay contract: the caller supplies `THREADWIRE_TARGET` (`telegram:<chat-id>` or `telegram:<chat-id>:<thread-id>`) and, optionally, `THREADWIRE_PROCESS_NUMBER`. The prompt arrives on a piped stdin; all native provider flags pass through after `--`. A positional prompt is never parsed. A missing `THREADWIRE_TARGET` fails closed (exit code 2) and a malformed one is rejected by the launcher's token-free preflight — the shim never invents a default destination and never silently downgrades a requested relay to a direct run. A shim relays only for a worker mode (`codex exec …`, `opencode-local-fleet run …`, or any non-interactive `claude …`); it drops the worker subcommand before relaying and Threadwire re-adds it around the prompt. Version and help queries, non-worker subcommands, interactive TTY sessions, and any invocation already inside a relayed run (`THREADWIRE_ACTIVE=1`, so nested provider workers stay local/native) run the real CLI directly through the libexec adapter.
76
84
 
77
- Pass `--resume-session <exact-provider-session-id>` to continue a stored Codex, Claude, or OpenCode session. Threadwire preserves the provider's native resume ordering and streaming options and never uses implicit latest-session behavior.
85
+ Pass `--resume-session <exact-provider-session-id>` to continue a stored Codex, Claude, Kimi, or OpenCode session. Threadwire preserves the provider's native resume ordering and streaming options and never uses implicit latest-session behavior.
78
86
 
79
87
  Pass `--transcript <path>` to capture the exact normalized, caller-facing JSONL protocol written to run stdout. The file is created privately (mode `0600`), must not already exist, and contains the same records in the same byte order as stdout. It never contains provider JSONL, reasoning, tool payloads, prompts, stderr, or credentials.
80
88
 
81
- `--activity-log <path>` is different: it retains its append contract and writes only minimal safe lifecycle facts—the selected workspace profile and immutable provenance, provider start (`provider`, child `pid`), and available native session ID. It is local-only and never enters Telegram or normalized stdout.
89
+ `--activity-log <path>` is different: it retains its append contract and writes only minimal safe lifecycle facts—provider start (`provider`, child `pid`) and available native session ID. It is local-only and never enters Telegram or normalized stdout.
82
90
 
83
91
  Provider stdout is decoded as JSONL with a finite 1 MiB UTF-8 byte limit for each pending unterminated record. Exceeding it rejects the run, removes listeners and signal handlers, and terminates the provider without parsing or rendering the oversized content.
84
92
 
@@ -1,10 +1,10 @@
1
1
  # Telegram Ingress for Threadwire
2
2
 
3
3
  Standalone Threadwire webhook service that accepts Telegram Bot API updates.
4
- Codex, Claude, and OpenCode retain the existing worker behavior. Kimi always
5
- uses its dedicated isolated preflight/run authority with no native fallback;
6
- a missing Kimi runtime fails before provider or transport construction. Hermes
7
- is not involved.
4
+ Codex, Claude, Kimi, and OpenCode use their native provider adapter by default.
5
+ An operator may explicitly configure the Kimi isolated client; that selected
6
+ path remains fail-closed and never falls back to native execution. Hermes is
7
+ not involved.
8
8
 
9
9
  ## Important bot ownership
10
10
 
@@ -83,8 +83,10 @@ Optional:
83
83
  | `THREADWIRE_TELEGRAM_REQUEST_TIMEOUT_MS` | Deadline for each outbound Telegram request, including response parsing (default `30000`, positive safe integer no greater than `2147483647`) |
84
84
  | `THREADWIRE_UPDATE_ID_CAPACITY` | Max retained completed `update_id` entries (default `10000`) |
85
85
  | `THREADWIRE_UPDATE_ID_TTL_MS` | Retention TTL for completed `update_id` entries in ms (default `86400000`) |
86
- | `THREADWIRE_KIMI_ISOLATED_RUNTIME_URL` | Dedicated Kimi supervisor URL; Compose defaults to `http://kimi-isolated-runtime:8790` |
87
- | `THREADWIRE_KIMI_ISOLATED_RUNTIME_CONTROL_TOKEN` | Dedicated Kimi control token. When absent, `/code kimi` remains recognized but fails closed before launch. |
86
+ | `THREADWIRE_KIMI_ISOLATED_RUNTIME_URL` | Optional dedicated Kimi supervisor URL, used only with an explicit control token. |
87
+ | `THREADWIRE_KIMI_ISOLATED_RUNTIME_CONTROL_TOKEN` | Selects the optional dedicated Kimi isolation client. Without it, `/code kimi` runs natively. |
88
+ | `THREADWIRE_KIMI_TASK_BINDING` | Optional trusted binding used only by the explicitly selected Kimi isolation client; native Kimi never reads it. |
89
+ | `THREADWIRE_ISOLATED_RUNTIME_CLIENT_TIMEOUT_MS` | Optional absolute preflight+run deadline in ms for isolated (Codex relay-write and Kimi) runs. Empty/unset means no full-run deadline: a healthy running worker is never stopped merely because time elapsed. The supervisor-side equivalent is `THREADWIRE_WORKER_TIMEOUT_MS`. Broker grants always use a bounded lease renewed by the supervisor while the run is active, expiring within one lease if the supervisor dies; unrelated safety timeouts are unchanged. |
88
90
 
89
91
  ## Run
90
92
 
@@ -84,16 +84,13 @@ docker compose exec -T threadwire sh -c 'test "$(id -u):$(id -g)" = 10001:10001
84
84
 
85
85
  Telegram commands derive their explicit destination from the authenticated
86
86
  update. A credentialed direct smoke test must also name a target explicitly and
87
- keeps both credentials mounted as secret files. The checked-in
88
- `container-runtime` workspace profile points to the bootstrapped
89
- `/workspace/threadwire` worktree inside the current runtime, so `threadwire run`
90
- selects that reviewed in-container workspace directly rather than deriving a
91
- path from ambient host state or Telegram input:
87
+ keeps both credentials mounted as secret files. Direct execution supplies the
88
+ caller-accessible working directory explicitly:
92
89
 
93
90
  ```sh
94
91
  node bin/threadwire.js run \
95
92
  --provider codex --target telegram:-1001234567890:42 \
96
- --workspace-profile container-runtime \
93
+ --cwd /workspace/threadwire \
97
94
  --prompt 'Inspect the container worktree.'
98
95
  ```
99
96
 
@@ -114,27 +111,11 @@ permissions that deny the provider, or move it to a separate container or
114
111
  sandbox. The same-UID, same-container, capability-free design cannot truthfully
115
112
  provide that stronger boundary.
116
113
 
117
- With `--workspace-profile`, `threadwire run` emits one caller-facing
118
- `execution_environment` JSONL record before provider startup. It includes only
119
- the resolved profile name, in-container repository path, exact Git revision,
120
- and immutable worktree identity; it never includes Telegram or provider
121
- credentials.
122
-
123
- The same reviewed profile file also governs authenticated webhook launches. The
124
- default profile is selected server-side; Telegram message text never supplies a
125
- raw path, container identifier, executable path, Git URL, or environment
126
- assignment. Before every spawn, Threadwire verifies that the configured
127
- workspace still exists, is still a Git worktree at the configured root, and
128
- still has a stable revision/worktree identity. If the worktree is removed or
129
- unhealthy, spawn fails before the provider starts so Telegram can retry after
130
- remediation.
131
-
132
- Provision profile changes and revision updates entirely inside the container
133
- worktree. Edit `threadwire.workspace-profiles.json` in `/workspace/threadwire`,
134
- commit the reviewed change in that worktree, and let the existing bootstrap and
135
- rollback flow move the whole repository plus its `.threadwire-source-identity`
136
- forward or backward together. Do not introduce host-only paths, Docker CLI
137
- arguments, or Compose-generated container names into the profile file.
114
+ `--workspace-profile` is removed and rejected. Threadwire does not inspect Git,
115
+ source identity, task binding, or workspace allowlists for normal provider
116
+ launches. Telegram ingress uses its own process working directory unless its
117
+ operator supplies another caller-accessible directory through the embedding
118
+ configuration; Telegram message text never supplies a path.
138
119
 
139
120
  ## Verification and leakage audit
140
121
 
@@ -203,12 +184,20 @@ Never export or archive mounted secret files with workspace/provider backups.
203
184
 
204
185
  ## Kimi subscription containers
205
186
 
206
- Kimi is intentionally excluded from the same-UID container-native provider
207
- launch described above. Never mount `threadwire-kimi-oauth`, its access/refresh
208
- tokens, or the auth home into the Threadwire runtime, supervisor, worker,
209
- workspace, or session-state volume. Kimi uses four immutable build targets:
210
- `kimi-auth`, `kimi-model-broker`, `kimi-relay-worker`, and `isolated-runtime`.
211
- The first two share UID/GID 10003 and only the OAuth volume; the worker uses
187
+ Kimi source is not a host worktree. The task controller creates one labelled source named volume (including its `.git` directory), one labelled immutable context named volume, and a running labelled lease container. The trusted Kimi supervisor reaches only the owning task-DinD API through `THREADWIRE_KIMI_TASK_DOCKER_HOST`; it has no workspace bind or outer Docker socket. It verifies the closed binding, exact labels/volume identities, lease image/user/workdir/mounts, references, Git revision, and manifest before issuing a broker grant. The worker has exactly the binding-declared source mount at `/workspace` (read-write for implementation sources, read-only for review sources), context RO at `/context`, and lineage state RW at `/state`. The worker entrypoint independently revalidates the mounted context manifest task/image identity and the exact source mount mode before Kimi starts; a mismatched manifest task identity or mount mode fails the run closed.
188
+
189
+ The checked-in standard `runtime` image installs Codex only: it does not install
190
+ Kimi or provide Kimi credentials. Native Kimi therefore requires a caller image
191
+ or environment that provides `/usr/local/bin/kimi` (or sets
192
+ `THREADWIRE_KIMI_BIN`); a missing executable fails normally. Never mount
193
+ `threadwire-kimi-oauth`, its access/refresh tokens, or the auth home into the
194
+ Threadwire runtime, supervisor, worker, workspace, or session-state volume.
195
+ The optional isolated Kimi flow uses five immutable build targets:
196
+ `kimi-auth`, `kimi-model-broker`, `kimi-model-relay`, `kimi-relay-worker`, and
197
+ `isolated-runtime`.
198
+ The first two share UID/GID 10003 and only the OAuth volume; the credential-free
199
+ relay also uses UID/GID 10003 with no mounts and no credential environment; the
200
+ worker uses
212
201
  UID/GID 10002 with only the selected worktree and provider-bound state; the
213
202
  root supervisor alone receives the Docker socket. The broker/auth service uses
214
203
  Node `22.19.0` and exactly `@moonshot-ai/kimi-code@0.29.2` with the checked npm
@@ -216,8 +205,39 @@ integrity and archive SHA-512 in the Dockerfile.
216
205
 
217
206
  Enable the checked-in override with `--profile kimi` only after the OAuth/model
218
207
  operator flow in [Isolated provider runtime](isolated-provider-runtime.md).
219
- The default workspace profile allowlists Kimi because isolation is mandatory;
220
- without the dedicated Kimi control token/client, both CLI and Telegram Kimi
221
- jobs fail closed rather than using the native child path. Keep broker egress
208
+ The per-run relay is created on the separate task DinD, so the deployment has
209
+ no default central worker route: set
210
+ `THREADWIRE_KIMI_MODEL_BROKER_WORKER_URL` to an explicit address the task DinD
211
+ can reach (for example the outer host's task-DinD gateway) and
212
+ `THREADWIRE_KIMI_MODEL_BROKER_WORKER_BIND` to an explicit bind reachable by that
213
+ daemon. The URL is the stable worker origin; the relay normalizes a bare origin
214
+ to the fixed `/v1/chat/completions` path and rejects any other path fail-closed.
215
+ Outer-Compose DNS and outer-host loopback are unreachable from the task
216
+ daemon. Both values are load-time optional so a Codex-only relay-write profile
217
+ renders without them; the Kimi runtime then fails closed at startup when they
218
+ are empty, so the Kimi profile never becomes functional without both explicit
219
+ values. An unset bind renders the published worker port on a non-task-reachable
220
+ loopback sentinel (`127.0.0.1`) so the file loads without publishing on every
221
+ interface; the broker receives the raw setting and refuses to open either
222
+ listener until an explicit nonempty bind is configured, so the sentinel is
223
+ never an operational default.
224
+ Only the worker port is published, on that bind — the admin endpoint stays on
225
+ the internal control network.
226
+ The optional Kimi supervisor is selected only by its dedicated control
227
+ token/client. Without that explicit selection, CLI and Telegram Kimi jobs use
228
+ the native child path and caller/provider configuration. Keep broker egress
222
229
  restricted to official Kimi Code subscription/auth endpoints and do not provide
223
230
  a Moonshot API key, custom base URL, Docker socket, or ingress secret.
231
+
232
+ Neither the Kimi nor the Codex isolated runtime imposes a default full-run
233
+ deadline: `THREADWIRE_WORKER_TIMEOUT_MS` (supervisor) and
234
+ `THREADWIRE_ISOLATED_RUNTIME_CLIENT_TIMEOUT_MS` (client) pass through empty
235
+ when unset, and an empty/unset value means a healthy running worker is never
236
+ stopped merely because time elapsed. Setting either to a positive millisecond
237
+ value enables one absolute preflight+run deadline. Broker grants are bounded
238
+ leases (default 60 seconds, `THREADWIRE_GRANT_LEASE_MS`, maximum one hour;
239
+ invalid, zero, or excessive values fail startup) that the supervisor renews
240
+ over the authenticated admin endpoint while the run is active; the grant
241
+ expires within one lease if
242
+ the supervisor dies. Unrelated safety timeouts (preflight slice, per-operation
243
+ Docker/network calls, state collection, emergency cleanup) are unchanged.
@@ -1,7 +1,7 @@
1
1
  # Isolated provider runtime
2
2
 
3
3
  Relay-originated Codex writes are disabled unless the caller supplies both
4
- `--relay-write` and a reviewed `--workspace-profile`. The write path never
4
+ `--relay-write` and an explicitly selected supervisor. The write path never
5
5
  falls back to the native provider launcher.
6
6
  When the Telegram service is configured with isolated-runtime authority, Codex
7
7
  webhook jobs use the same preflight/run boundary exclusively; an unavailable
@@ -30,7 +30,11 @@ container-local tmp/run, resource limits, and exactly one writable
30
30
  worktree mount. It has no host home, common Git directory, sibling worktree,
31
31
  host temp, shared provider state, secret, Docker/SSH socket, or external route. Its
32
32
  short-lived broker grant is revoked during cleanup.
33
- The grant owns one absolute deadline and every accepted socket. Expiry,
33
+ The grant is issued as a bounded lease (default 60 seconds) that the
34
+ supervisor renews over the authenticated admin endpoint while its run is
35
+ active; cleanup still revokes it explicitly, and if the supervisor dies or
36
+ loses contact the broker's own expiry timer fires within one lease. The grant
37
+ owns every accepted socket. Expiry,
34
38
  explicit revoke, credential reload, or broker shutdown aborts an incomplete
35
39
  upload or upstream request, destroys accepted sockets, and closes the listener;
36
40
  authorization is checked again after the bounded request body is acquired.
@@ -69,15 +73,19 @@ expire without requiring a matching `/run`.
69
73
  Consumed capabilities enter a separate global/per-task active-run admission
70
74
  before validation can yield. The lineage reservation is acquired synchronously
71
75
  at that boundary, preventing periodic GC from deleting resume state during
72
- revalidation. Docker and broker operations have bounded deadlines, and each
73
- worker has a bounded operator-configured lifetime; timeout cleanup revokes the
76
+ revalidation. Docker and broker operations have bounded deadlines. There is no
77
+ default full-run deadline: a healthy running worker is never stopped merely
78
+ because time elapsed. An operator may set `THREADWIRE_WORKER_TIMEOUT_MS` to a
79
+ positive millisecond value to enable one absolute preflight+run deadline;
80
+ timeout cleanup revokes the
74
81
  grant and removes the exactly labelled worker and network before capacity is
75
82
  released.
76
- The relay creates one absolute launch deadline before preflight. The same
77
- deadline and cancellation signal span prompt/stdin and file-secret reads,
83
+ When the operator enables it, the relay creates one absolute launch deadline
84
+ before preflight. That
85
+ deadline and the cancellation signal span prompt/stdin and file-secret reads,
78
86
  evidence and Telegram setup, Docker setup, broker readiness, execution,
79
87
  response/log reads, upstream work and exact cleanup. A disconnected caller or
80
- supervisor shutdown aborts its run. Independent per-step timeouts cannot extend
88
+ supervisor shutdown aborts its run regardless. Independent per-step timeouts cannot extend
81
89
  the overall budget, and response bodies are stream-limited to 2 MiB.
82
90
 
83
91
  Codex bypasses its nested sandbox only inside this mandatory outer container.
@@ -105,10 +113,20 @@ credential. The supervisor alone receives `/var/run/docker.sock`. Restrict
105
113
  broker egress at the host/firewall to provider endpoints.
106
114
 
107
115
  The supervisor defaults to 16 active runs globally, two per reviewed task, a
108
- 30-second maximum preflight slice, and a one-hour end-to-end launch deadline. Operators may
109
- lower these finite limits with `THREADWIRE_PREFLIGHT_TIMEOUT_MS`,
116
+ 30-second maximum preflight slice, and no full-run deadline: an unset or empty
117
+ `THREADWIRE_WORKER_TIMEOUT_MS` (supervisor) or
118
+ `THREADWIRE_ISOLATED_RUNTIME_CLIENT_TIMEOUT_MS` (client) means a run may
119
+ continue until natural completion, caller cancellation/disconnect, supervisor
120
+ shutdown, or concrete failure. Setting either to a positive millisecond value
121
+ enables one absolute preflight+run deadline. Operators may
122
+ lower the finite limits with `THREADWIRE_PREFLIGHT_TIMEOUT_MS`,
110
123
  `THREADWIRE_ACTIVE_RUN_CAPACITY`, `THREADWIRE_ACTIVE_TASK_CAPACITY`, and
111
124
  `THREADWIRE_WORKER_TIMEOUT_MS`; invalid, zero, or excessive values fail startup.
125
+ Broker grants always use a bounded lease (default 60 seconds, tunable with
126
+ `THREADWIRE_GRANT_LEASE_MS`, maximum one hour) that the supervisor renews while
127
+ the run is active; the grant expires within one lease if the supervisor dies.
128
+ An invalid, zero, or excessive `THREADWIRE_GRANT_LEASE_MS` value fails startup
129
+ rather than being silently coerced.
112
130
 
113
131
  The authenticated registry persists through file fsync, atomic rename, and
114
132
  parent-directory fsync. Startup removes only strictly named abandoned temp
@@ -137,7 +155,19 @@ by that script. E2E uses host-volume canaries and fake-upstream authorization
137
155
  logs; worker self-report is not the sole assertion.
138
156
 
139
157
 
140
- ## Native Kimi Code subscription boundary
158
+ ## Optional isolated Kimi Code subscription boundary
159
+
160
+ Normal `threadwire run --provider kimi` is native execution: it directly runs
161
+ the caller-provided Kimi executable at `/usr/local/bin/kimi`, or the path in
162
+ `THREADWIRE_KIMI_BIN`. It does not select this isolated supervisor, broker, or
163
+ model allowlist. The checked-in standard `runtime` image installs Codex only,
164
+ so native Kimi needs a caller-supplied executable and fails normally when it is
165
+ missing. The remaining section documents the separately configured, optional
166
+ isolated Kimi supervisor.
167
+
168
+ Kimi binding v1 is separate from the Codex worktree contract. A Kimi preflight contains `binding`, never `repositoryRoot` or `cwd`. The supervisor queries the task-DinD daemon for exactly one labelled source volume, context volume, and running lease, rejects ambiguity/RW context references, validates the lease and worker image, then uses an offline two-volume validator to check the real Git revision and complete immutable context manifest. Only after that does it create the per-run data plane on the task daemon: a private internal worker network, a task-owned non-internal egress network, one credential-free relay attached to both, and a three-volume Kimi worker attached only to the internal network. OAuth remains broker-only; workers receive a synthetic per-run grant and no raw binding, volume, lease, control, or OAuth value. Kimi v6 state seals include the binding digest, source/context identities, source mount mode, dynamic UID/GID/workdir, validator, worker, relay, relay image, both networks, the central endpoint identity, state, and image. Old path-based and pre-relay Kimi state is intentionally not adopted.
169
+
170
+ The binding's `source.readOnly` selects the exact admitted mount mode: `false` is a writable implementation source, `true` an explicitly read-only review source for Docker-enforced reviewers. Admission and revalidation pin the lease and worker source mounts to that exact mode, one-writer isolation is unchanged, and the worker entrypoint independently asserts the mounted mode from `/proc/self/mountinfo` before launch. Both the validator and the worker compare the mounted context manifest's task and image identity against the binding attestation; a mismatch fails the run closed and can never produce a successful client run.
141
171
 
142
172
  Kimi uses the same proven supervisor mechanics but a distinct protocol and
143
173
  credential service. `THREADWIRE_ISOLATED_PROVIDER=kimi` makes the supervisor
@@ -147,14 +177,54 @@ provider-bound state in the separate Kimi namespace. Codex requests, sessions,
147
177
  workers, grants, and volumes cannot be adopted by this service, and the Codex
148
178
  broker remains unchanged.
149
179
 
180
+ The central Kimi broker runs on an outer daemon, not the task daemon, so the
181
+ supervisor never resolves a broker container by name. It splits the broker into
182
+ an admin/control endpoint (grant lifecycle only) and a stable worker endpoint
183
+ (grant-auth chat proxy). For each run it creates a tiny credential-free relay
184
+ on the task daemon from the immutable `THREADWIRE_KIMI_MODEL_RELAY_IMAGE`
185
+ digest. The relay knows only the configured central worker URL
186
+ (`THREADWIRE_KIMI_MODEL_BROKER_WORKER_URL`) and its own listen port, receives
187
+ no grant, OAuth, admin token, binding, mount, or Docker socket, and is attached
188
+ to both the internal worker network and a dedicated egress network. The worker
189
+ reaches only the relay's internal address; the relay forwards the fixed
190
+ `POST /v1/chat/completions` shape to the central worker endpoint, preserving
191
+ the grant bearer. The configured worker URL is the stable worker origin: a bare
192
+ origin (for example `http://<gateway>:8792`) or the already-exact accepted path
193
+ is normalized to the fixed `/v1/chat/completions` path, and any other path is
194
+ rejected fail-closed at relay startup rather than forwarded verbatim. There is
195
+ no DNS or host-gateway fallback, no container-name resolution, and TLS
196
+ verification is never disabled.
197
+
198
+ Because the relay runs on the separate task daemon, the stable worker URL is
199
+ not defaulted: `THREADWIRE_KIMI_MODEL_BROKER_WORKER_URL` must be set to an
200
+ explicit address the task daemon can reach (for example the outer host's
201
+ task-DinD gateway), and the broker's published worker bind
202
+ (`THREADWIRE_KIMI_MODEL_BROKER_WORKER_BIND`) must likewise be chosen explicitly
203
+ to be reachable by that daemon. Outer-Compose DNS and outer-host loopback are
204
+ unreachable from the task daemon and are never assumed. These values are
205
+ load-time optional so a Codex-only relay-write profile renders and starts
206
+ without them; the Kimi runtime and client then fail closed at startup or job
207
+ launch when they are empty, so a Kimi deployment that does not supply an
208
+ explicit task-reachable URL and bind never becomes functional. When the bind
209
+ is unset, Compose renders the published worker port on a non-task-reachable
210
+ loopback sentinel (`127.0.0.1`) purely so the file loads — never on every
211
+ interface — and passes the raw setting through to the broker, which requires a
212
+ nonempty explicit bind before opening either listener. The sentinel is not an
213
+ operational default: without the explicit bind the broker exits instead of
214
+ serving. The
215
+ admin/control endpoint is never published — only the worker
216
+ port is, on the explicit bind — so admin and worker authority stay distinct.
217
+
150
218
  The Kimi broker is not a generic credential proxy. It accepts only
151
219
  `POST /v1/chat/completions`, validates a closed request schema and one approved
152
220
  wire model, substitutes OAuth immediately before the fixed official
153
221
  `https://api.kimi.com/coding/v1/chat/completions` request, and streams the
154
222
  bounded response. A grant is pending until the inspected worker is ready and is
155
223
  bound to provider, task hash, exact resume session (or fresh lineage), run,
156
- Docker network, approved alias, and wire model. Revocation, timeout, shutdown,
157
- or cleanup closes its private listener and active sockets.
224
+ Docker network, approved alias, and wire model. The grant is a bounded lease
225
+ renewed by the supervisor while the run is active. Revocation, lease expiry,
226
+ shutdown,
227
+ or cleanup rejects the grant and closes its active sockets.
158
228
 
159
229
  The immutable Kimi worker image pins `@moonshot-ai/kimi-code@0.29.2` and verifies
160
230
  the npm archive SHA-512 before installation. It receives no OAuth file, Docker
@@ -164,7 +234,8 @@ Its fixed entrypoint owns `--model`, optional exact `--session`, `--prompt`,
164
234
  `--output-format stream-json`, and an empty skills directory. Static deny rules,
165
235
  workspace/config ancestry checks, a sanitized private home, read-only root,
166
236
  and the existing mount/inode checks remain mandatory. There is no native unsafe
167
- fallback and no `THREADWIRE_KIMI_BIN` control.
237
+ fallback within the explicitly selected isolated path. Normal Kimi execution
238
+ uses the native adapter and may use `THREADWIRE_KIMI_BIN`.
168
239
 
169
240
  Only exact pinned stream records are normalized: assistant string content,
170
241
  validated tool name/key start and finish, and one exact
@@ -174,10 +245,26 @@ and OAuth material are discarded. The worker is the only reader of native Kimi
174
245
  stdout and Docker receives only sanitized envelopes and fixed lifecycle events;
175
246
  the supervisor returns `rawChunks: []` for every Kimi run.
176
247
 
248
+ The supervisor does not wait for worker completion before relaying output. It
249
+ follows the worker's multiplexed Docker logs, demultiplexes and validates each
250
+ stdout envelope as it arrives, and streams it to the relay caller over a single
251
+ framed NDJSON run response: `record` frames are written incrementally, then
252
+ exactly one `terminal` frame carries the final exit code. The client parses the
253
+ same NDJSON stream and forwards each validated event to `WorkerControl` as it
254
+ arrives, so long runs relay assistant/tool/lifecycle output to Telegram
255
+ incrementally. Session envelopes are the one exception: they are buffered and
256
+ only published once the run's success is known, so a resume hint followed by a
257
+ nonzero exit never exposes an unusable resume session. Stderr frames are
258
+ dropped, the same byte caps and conflicting-session rejection apply as the
259
+ buffered path, and a mid-stream
260
+ failure or cancellation still revokes the grant and removes the exact worker,
261
+ relay, and both networks before capacity is released.
262
+
177
263
  ### Operator OAuth and model approval
178
264
 
179
- Build and publish the `kimi-auth`, `kimi-model-broker`, `kimi-relay-worker`, and
180
- existing `isolated-runtime` targets as immutable digests. Set distinct random
265
+ Build and publish the `kimi-auth`, `kimi-model-broker`, `kimi-model-relay`,
266
+ `kimi-relay-worker`, and existing `isolated-runtime` targets as immutable
267
+ digests. Set distinct random
181
268
  Kimi runtime and broker admin tokens of at least 32 characters, the Kimi image digests, and an allowlist
182
269
  such as:
183
270
 
@@ -185,7 +272,8 @@ such as:
185
272
  export THREADWIRE_ALLOWED_KIMI_MODELS='{"default":{"model":"kimi-for-coding","protocol":"kimi"},"kimi-code/kimi-for-coding":{"model":"kimi-for-coding","protocol":"kimi"}}'
186
273
  ```
187
274
 
188
- `default` is the server-owned selection for no-argument and Telegram jobs.
275
+ `default` is the server-owned selection for isolated supervisor jobs without a
276
+ model argument.
189
277
  `kimi-code/kimi-for-coding` is the exact official provider-qualified alias for
190
278
  explicit callers. Each key is an opaque allowlisted value, not a path, and
191
279
  Threadwire never normalizes or strips its prefix. The `model` field is the
@@ -194,8 +282,8 @@ alias.
194
282
 
195
283
  OAuth writes only the named `threadwire-kimi-oauth` volume at the official
196
284
  `credentials/kimi-code.json` location. Stop the broker during interactive auth
197
- so the CLI and broker cannot race a refresh, then run the opt-in, workspace-free
198
- auth service:
285
+ so the auth CLI and broker cannot race a refresh, then run the opt-in,
286
+ workspace-free auth service:
199
287
 
200
288
  ```sh
201
289
  docker compose -f compose.yaml -f compose.relay-write.yaml stop kimi-isolated-runtime kimi-model-broker
@@ -210,20 +298,12 @@ The auth service has no workspace, ingress network, Docker socket, Threadwire
210
298
  control token, or Telegram credential and uses Docker logging driver `none`.
211
299
  `verify <provider-qualified-alias>` discards native output and prints only a
212
300
  fixed success/failure; use it to confirm each exact official alias before
213
- adding that alias and its independently verified wire model to the server
214
- allowlist. An explicit CLI job selects the exact key as one argv value:
215
-
216
- ```sh
217
- node bin/threadwire.js run \
218
- --provider kimi \
219
- --target telegram:-1001234567890:42 \
220
- --workspace-profile container-runtime \
221
- --prompt 'Inspect the project.' \
222
- -- --model 'kimi-code/kimi-for-coding'
223
- ```
301
+ adding that alias and its independently verified wire model to the isolated
302
+ supervisor allowlist. This operator configuration does not configure or select
303
+ the native CLI Kimi model.
224
304
 
225
- API keys, Moonshot platform mode, custom base URLs, discovery by ordinary jobs,
226
- and arbitrary aliases are unsupported.
305
+ For the isolated supervisor, API keys, Moonshot platform mode, custom base
306
+ URLs, discovery by ordinary jobs, and arbitrary aliases are unsupported.
227
307
 
228
308
  For logout or account rotation, stop the Kimi runtime and broker, run
229
309
  `--profile kimi-auth run --rm kimi-auth logout` or `login`, verify approved
@@ -244,7 +324,8 @@ payload, or synthetic grant.
244
324
 
245
325
  Rollback by stopping `kimi-isolated-runtime` and `kimi-model-broker`, removing
246
326
  the `kimi` Compose profile/control variables, and reverting the Threadwire image.
247
- Kimi commands then fail closed; Codex behavior is unaffected. Preserve the OAuth
327
+ Isolated Kimi requests then fail closed; native Kimi remains independent of
328
+ that optional deployment. Codex behavior is unaffected. Preserve the OAuth
248
329
  and Kimi state volumes for a recoverable rollback. Remove them only as an
249
330
  explicit credential/session teardown after OAuth revocation; never archive them
250
331
  with workspace backups.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadwire",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Stream Codex, Claude, Kimi Code, and OpenCode worker progress to an explicit Telegram destination",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -20,8 +20,7 @@
20
20
  "docs/",
21
21
  "scripts/",
22
22
  "src/",
23
- "TELEGRAM-INGRESS.md",
24
- "threadwire.workspace-profiles.json"
23
+ "TELEGRAM-INGRESS.md"
25
24
  ],
26
25
  "bin": {
27
26
  "threadwire-isolated-runtime": "bin/isolated-runtime.js",
@@ -58,15 +58,11 @@ threadwire_relay_arguments=(run --provider "$THREADWIRE_PROVIDER" --target "$THR
58
58
  case "${THREADWIRE_RELAY_WRITE:-0}" in
59
59
  0) ;;
60
60
  1)
61
- if [[ ! "${THREADWIRE_WORKSPACE_PROFILE:-}" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ ]]; then
62
- printf '%s\n' 'threadwire: relay write capability requires a valid THREADWIRE_WORKSPACE_PROFILE' >&2
63
- exit 2
64
- fi
65
61
  if [[ "$THREADWIRE_PROVIDER" != codex ]]; then
66
62
  printf '%s\n' 'threadwire: relay write capability is only supported for codex' >&2
67
63
  exit 2
68
64
  fi
69
- threadwire_relay_arguments+=(--workspace-profile "$THREADWIRE_WORKSPACE_PROFILE" --relay-write)
65
+ threadwire_relay_arguments+=(--relay-write)
70
66
  ;;
71
67
  *)
72
68
  printf '%s\n' 'threadwire: relay write capability must be enabled with THREADWIRE_RELAY_WRITE=1' >&2
@@ -77,10 +77,9 @@ const EXPECTED_FILES = [
77
77
  "src/telegram-ingress/http.js",
78
78
  "src/telegram-ingress/update-guard.js",
79
79
  "src/telegram-webhook.js",
80
+ "src/threadwire-binding.js",
80
81
  "src/types.js",
81
- "src/worker-control.js",
82
- "src/workspace-profile.js",
83
- "threadwire.workspace-profiles.json"
82
+ "src/worker-control.js"
84
83
  ]
85
84
 
86
85
  /** @returns {Promise<void>} */
@@ -3,7 +3,7 @@
3
3
 
4
4
  export class AbsoluteDeadline {
5
5
  constructor(expiresAt, {signal, request, response, now = Date.now, timeoutMessage = "Operation timeout", disconnectMessage = "Caller disconnected"} = {}) {
6
- if (!Number.isSafeInteger(expiresAt) || expiresAt <= now()) throw new Error(timeoutMessage)
6
+ if (expiresAt !== undefined && (!Number.isSafeInteger(expiresAt) || expiresAt <= now())) throw new Error(timeoutMessage)
7
7
  this.expiresAt = expiresAt
8
8
  this.now = now
9
9
  this.controller = new AbortController()
@@ -26,13 +26,14 @@ export class AbsoluteDeadline {
26
26
  request?.once?.("aborted", this.abortRequest)
27
27
  response?.once?.("close", this.abortResponse)
28
28
  this.socket?.once?.("close", this.abortSocket)
29
- this.timer = setTimeout(() => this.controller.abort(new Error(timeoutMessage)), Math.max(1, expiresAt - now()))
30
- this.timer.unref()
29
+ this.timer = expiresAt === undefined ? undefined : setTimeout(() => this.controller.abort(new Error(timeoutMessage)), Math.max(1, expiresAt - now()))
31
30
  this.signal.addEventListener("abort", () => request?.destroy?.(this.signal.reason), {once: true})
32
31
  if (request?.aborted || (response?.destroyed && !response.writableEnded)) this.abortRequest()
33
32
  }
34
33
  remaining() {
35
34
  this.throwIfAborted()
35
+ if (this.expiresAt === undefined) return undefined
36
+ if (this.expiresAt === undefined) return undefined
36
37
  const remaining = this.expiresAt - this.now()
37
38
  if (remaining <= 0) {
38
39
  this.controller.abort(new Error(this.timeoutMessage))
@@ -41,13 +42,14 @@ export class AbsoluteDeadline {
41
42
  return Math.max(1, remaining)
42
43
  }
43
44
  options() {
44
- return {signal: this.signal, timeoutMs: this.remaining()}
45
+ const timeoutMs = this.remaining()
46
+ return timeoutMs === undefined ? {signal: this.signal} : {signal: this.signal, timeoutMs}
45
47
  }
46
48
  throwIfAborted() {
47
49
  if (this.signal.aborted) throw this.signal.reason instanceof Error ? this.signal.reason : new Error("Operation aborted")
48
50
  }
49
51
  close() {
50
- clearTimeout(this.timer)
52
+ if (this.timer !== undefined) clearTimeout(this.timer)
51
53
  this.abortParent && this.parentSignal?.removeEventListener?.("abort", this.abortParent)
52
54
  this.request?.off?.("aborted", this.abortRequest)
53
55
  this.response?.off?.("close", this.abortResponse)
@@ -89,6 +91,7 @@ export async function readResponseCapped(response, capacity, signal) {
89
91
  }
90
92
  return Buffer.concat(chunks, size)
91
93
  } finally {
94
+ if (signal?.aborted) await reader.cancel(signal.reason).catch(() => {})
92
95
  reader.releaseLock()
93
96
  }
94
97
  }