@code-yeongyu/senpi 2026.9.27-4 → 2026.9.28
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 +32 -0
- package/dist/bundle/chunks/{app-server-command-3GVJZFFH.js → app-server-command-TOPTGVQB.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-4SUOOZZ4.js → azure-openai-responses-FCZNBRWE.js} +1 -1
- package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
- package/dist/bundle/chunks/{chunk-PSCNHY7D.js → chunk-3GMFUEU7.js} +1 -1
- package/dist/bundle/chunks/{chunk-HBG6SQU5.js → chunk-62XHQXCZ.js} +1 -1
- package/dist/bundle/chunks/{chunk-SLL375O7.js → chunk-7DY2JPUX.js} +1 -1
- package/dist/bundle/chunks/chunk-7YUGCTEV.js +15 -0
- package/dist/bundle/chunks/{chunk-FWBSGMAE.js → chunk-A4UMPXQF.js} +1 -1
- package/dist/bundle/chunks/chunk-BBK6SSYT.js +2 -0
- package/dist/bundle/chunks/{chunk-C2425O2Q.js → chunk-CNQ4JB45.js} +1 -1
- package/dist/bundle/chunks/{chunk-27IFXX3B.js → chunk-DRN4FQCE.js} +1 -1
- package/dist/bundle/chunks/{chunk-22MGY5JQ.js → chunk-EHSZ7UCW.js} +1 -1
- package/dist/bundle/chunks/chunk-FHJYIU3U.js +5 -0
- package/dist/bundle/chunks/chunk-GJUCSTAG.js +19 -0
- package/dist/bundle/chunks/{chunk-VMKAYXHL.js → chunk-KIOTFUG6.js} +25 -20
- package/dist/bundle/chunks/{chunk-2BMD37YV.js → chunk-LUPPSEQW.js} +1 -1
- package/dist/bundle/chunks/chunk-OAIVTS5X.js +8 -0
- package/dist/bundle/chunks/{chunk-NRGMT74E.js → chunk-QOVUQZO4.js} +1 -1
- package/dist/bundle/chunks/chunk-SN4ZHPMN.js +135 -0
- package/dist/bundle/chunks/{chunk-IKXPBYSO.js → chunk-UJFLZ2M2.js} +1 -1
- package/dist/bundle/chunks/{chunk-4T4GFAGK.js → chunk-VFSXUT7K.js} +1 -1
- package/dist/bundle/chunks/{cli-main-66NFBKVH.js → cli-main-ARR54ET6.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-ZTGKZTGU.js → google-generative-ai-LFCBHG3Y.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-CCZ376YK.js → google-vertex-C474FXU7.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-NZ3I2UEN.js → help-fast-path-3U3YDEZ6.js} +1 -1
- package/dist/bundle/chunks/{host-command-RORPAC5X.js → host-command-6CJ7YM6W.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-UDMJGLOR.js → host-lifecycle-RZIMOFDM.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +6 -6
- package/dist/bundle/chunks/interactive-host-runtime-A3GEIFTU.js +4 -0
- package/dist/bundle/chunks/{interactive-mode-LOAADKLH.js → interactive-mode-ITSFLGF6.js} +1 -1
- package/dist/bundle/chunks/{list-tips-S5CANO5W.js → list-tips-FPXDXN7R.js} +1 -1
- package/dist/bundle/chunks/multi-session-host-B4WMZE2G.js +21 -0
- package/dist/bundle/chunks/{openai-codex-responses-YSGZZDD5.js → openai-codex-responses-2LDIFL6Z.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-KWHRWCBN.js → openai-completions-7KZLPKYP.js} +1 -1
- package/dist/bundle/chunks/{openai-images-6U5FCN5S.js → openai-images-I7DR7ERP.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-7PWMBGYX.js → openai-responses-LCHS3AF3.js} +1 -1
- package/dist/bundle/chunks/{openrouter-images-QIKWQZUD.js → openrouter-images-IPNDRK6C.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-5EEDA35I.js → package-manager-cli-DXHQ6RKA.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-KZG4OVR7.js → rotation-stream-ZVJEGA2G.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-HOEA5NGQ.js → rpc-mode-PFPNP65F.js} +1 -1
- package/dist/bundle/chunks/session-picker-KOPOV6Q4.js +2 -0
- package/dist/bundle/chunks/session-worker.js +311 -302
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/cli/args.js +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/host-command.d.ts +8 -1
- package/dist/cli/host-command.d.ts.map +1 -1
- package/dist/cli/host-command.js +66 -14
- package/dist/cli/host-command.js.map +1 -1
- package/dist/core/agent-session.d.ts +18 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +119 -3
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/retry-fallback/candidates.d.ts +26 -0
- package/dist/core/retry-fallback/candidates.d.ts.map +1 -0
- package/dist/core/retry-fallback/candidates.js +41 -0
- package/dist/core/retry-fallback/candidates.js.map +1 -0
- package/dist/core/retry-fallback/circuit-probes.d.ts +30 -0
- package/dist/core/retry-fallback/circuit-probes.d.ts.map +1 -0
- package/dist/core/retry-fallback/circuit-probes.js +60 -0
- package/dist/core/retry-fallback/circuit-probes.js.map +1 -0
- package/dist/core/retry-fallback/circuit.d.ts +101 -0
- package/dist/core/retry-fallback/circuit.d.ts.map +1 -0
- package/dist/core/retry-fallback/circuit.js +175 -0
- package/dist/core/retry-fallback/circuit.js.map +1 -0
- package/dist/core/retry-fallback/controller-types.d.ts +58 -0
- package/dist/core/retry-fallback/controller-types.d.ts.map +1 -0
- package/dist/core/retry-fallback/controller-types.js +2 -0
- package/dist/core/retry-fallback/controller-types.js.map +1 -0
- package/dist/core/retry-fallback/controller.d.ts +21 -49
- package/dist/core/retry-fallback/controller.d.ts.map +1 -1
- package/dist/core/retry-fallback/controller.js +88 -42
- package/dist/core/retry-fallback/controller.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +25 -17
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-failure-report.d.ts +33 -0
- package/dist/core/session-failure-report.d.ts.map +1 -0
- package/dist/core/session-failure-report.js +62 -0
- package/dist/core/session-failure-report.js.map +1 -0
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +4 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/index.d.ts +1 -0
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js +2 -0
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/help-content.d.ts.map +1 -1
- package/dist/modes/interactive/help-content.js +3 -0
- package/dist/modes/interactive/help-content.js.map +1 -1
- package/dist/modes/interactive/interactive-host-attach.d.ts +10 -0
- package/dist/modes/interactive/interactive-host-attach.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-host-attach.js +14 -0
- package/dist/modes/interactive/interactive-host-attach.js.map +1 -0
- package/dist/modes/interactive/interactive-host-runtime.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-host-runtime.js +3 -6
- package/dist/modes/interactive/interactive-host-runtime.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +7 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +66 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/session-failure-info.d.ts +3 -0
- package/dist/modes/interactive/session-failure-info.d.ts.map +1 -0
- package/dist/modes/interactive/session-failure-info.js +21 -0
- package/dist/modes/interactive/session-failure-info.js.map +1 -0
- package/dist/modes/interactive/tips/catalog/model-tips.d.ts +7 -7
- package/dist/modes/interactive/tips/catalog/model-tips.d.ts.map +1 -1
- package/dist/modes/interactive/tips/catalog/model-tips.js +7 -1
- package/dist/modes/interactive/tips/catalog/model-tips.js.map +1 -1
- package/dist/modes/interactive/tips/catalog/session-tips.d.ts.map +1 -1
- package/dist/modes/interactive/tips/catalog/session-tips.js +1 -1
- package/dist/modes/interactive/tips/catalog/session-tips.js.map +1 -1
- package/dist/modes/rpc/host-attach-hold.d.ts +16 -0
- package/dist/modes/rpc/host-attach-hold.d.ts.map +1 -0
- package/dist/modes/rpc/host-attach-hold.js +9 -0
- package/dist/modes/rpc/host-attach-hold.js.map +1 -0
- package/dist/modes/rpc/host-cleanup-paths.d.ts +20 -0
- package/dist/modes/rpc/host-cleanup-paths.d.ts.map +1 -0
- package/dist/modes/rpc/host-cleanup-paths.js +13 -0
- package/dist/modes/rpc/host-cleanup-paths.js.map +1 -0
- package/dist/modes/rpc/host-client-occupancy.d.ts +24 -0
- package/dist/modes/rpc/host-client-occupancy.d.ts.map +1 -0
- package/dist/modes/rpc/host-client-occupancy.js +53 -0
- package/dist/modes/rpc/host-client-occupancy.js.map +1 -0
- package/dist/modes/rpc/host-daemon-paths.d.ts +61 -8
- package/dist/modes/rpc/host-daemon-paths.d.ts.map +1 -1
- package/dist/modes/rpc/host-daemon-paths.js +128 -11
- package/dist/modes/rpc/host-daemon-paths.js.map +1 -1
- package/dist/modes/rpc/host-daemon-registration.d.ts +11 -1
- package/dist/modes/rpc/host-daemon-registration.d.ts.map +1 -1
- package/dist/modes/rpc/host-daemon-registration.js +28 -15
- package/dist/modes/rpc/host-daemon-registration.js.map +1 -1
- package/dist/modes/rpc/host-endpoints.d.ts +12 -0
- package/dist/modes/rpc/host-endpoints.d.ts.map +1 -0
- package/dist/modes/rpc/host-endpoints.js +59 -0
- package/dist/modes/rpc/host-endpoints.js.map +1 -0
- package/dist/modes/rpc/host-ensure-lock.d.ts +11 -0
- package/dist/modes/rpc/host-ensure-lock.d.ts.map +1 -0
- package/dist/modes/rpc/host-ensure-lock.js +49 -0
- package/dist/modes/rpc/host-ensure-lock.js.map +1 -0
- package/dist/modes/rpc/host-ensure.d.ts +7 -0
- package/dist/modes/rpc/host-ensure.d.ts.map +1 -1
- package/dist/modes/rpc/host-ensure.js +42 -65
- package/dist/modes/rpc/host-ensure.js.map +1 -1
- package/dist/modes/rpc/host-gc-evidence.d.ts +18 -0
- package/dist/modes/rpc/host-gc-evidence.d.ts.map +1 -0
- package/dist/modes/rpc/host-gc-evidence.js +128 -0
- package/dist/modes/rpc/host-gc-evidence.js.map +1 -0
- package/dist/modes/rpc/host-gc.d.ts +28 -0
- package/dist/modes/rpc/host-gc.d.ts.map +1 -0
- package/dist/modes/rpc/host-gc.js +110 -0
- package/dist/modes/rpc/host-gc.js.map +1 -0
- package/dist/modes/rpc/host-generations.d.ts +10 -2
- package/dist/modes/rpc/host-generations.d.ts.map +1 -1
- package/dist/modes/rpc/host-generations.js +12 -7
- package/dist/modes/rpc/host-generations.js.map +1 -1
- package/dist/modes/rpc/host-handoff.d.ts +6 -0
- package/dist/modes/rpc/host-handoff.d.ts.map +1 -1
- package/dist/modes/rpc/host-handoff.js +31 -4
- package/dist/modes/rpc/host-handoff.js.map +1 -1
- package/dist/modes/rpc/host-lifecycle-policy.d.ts +53 -0
- package/dist/modes/rpc/host-lifecycle-policy.d.ts.map +1 -0
- package/dist/modes/rpc/host-lifecycle-policy.js +64 -0
- package/dist/modes/rpc/host-lifecycle-policy.js.map +1 -0
- package/dist/modes/rpc/host-lifecycle.d.ts +1 -44
- package/dist/modes/rpc/host-lifecycle.d.ts.map +1 -1
- package/dist/modes/rpc/host-lifecycle.js +23 -85
- package/dist/modes/rpc/host-lifecycle.js.map +1 -1
- package/dist/modes/rpc/host-observe-request.d.ts +17 -0
- package/dist/modes/rpc/host-observe-request.d.ts.map +1 -0
- package/dist/modes/rpc/host-observe-request.js +30 -0
- package/dist/modes/rpc/host-observe-request.js.map +1 -0
- package/dist/modes/rpc/host-probe.d.ts +12 -0
- package/dist/modes/rpc/host-probe.d.ts.map +1 -1
- package/dist/modes/rpc/host-probe.js +28 -1
- package/dist/modes/rpc/host-probe.js.map +1 -1
- package/dist/modes/rpc/host-protocol-info.d.ts +2 -0
- package/dist/modes/rpc/host-protocol-info.d.ts.map +1 -1
- package/dist/modes/rpc/host-protocol-info.js +1 -0
- package/dist/modes/rpc/host-protocol-info.js.map +1 -1
- package/dist/modes/rpc/host-readiness.d.ts +22 -0
- package/dist/modes/rpc/host-readiness.d.ts.map +1 -0
- package/dist/modes/rpc/host-readiness.js +44 -0
- package/dist/modes/rpc/host-readiness.js.map +1 -0
- package/dist/modes/rpc/host-runner.d.ts +13 -0
- package/dist/modes/rpc/host-runner.d.ts.map +1 -1
- package/dist/modes/rpc/host-runner.js +23 -4
- package/dist/modes/rpc/host-runner.js.map +1 -1
- package/dist/modes/rpc/host-status-all.d.ts +31 -0
- package/dist/modes/rpc/host-status-all.d.ts.map +1 -0
- package/dist/modes/rpc/host-status-all.js +78 -0
- package/dist/modes/rpc/host-status-all.js.map +1 -0
- package/dist/modes/rpc/host-status-rows.d.ts +35 -0
- package/dist/modes/rpc/host-status-rows.d.ts.map +1 -0
- package/dist/modes/rpc/host-status-rows.js +42 -0
- package/dist/modes/rpc/host-status-rows.js.map +1 -0
- package/dist/modes/rpc/host-status.d.ts +32 -1
- package/dist/modes/rpc/host-status.d.ts.map +1 -1
- package/dist/modes/rpc/host-status.js +33 -27
- package/dist/modes/rpc/host-status.js.map +1 -1
- package/dist/modes/rpc/host-successor.d.ts +3 -1
- package/dist/modes/rpc/host-successor.d.ts.map +1 -1
- package/dist/modes/rpc/host-successor.js +105 -42
- package/dist/modes/rpc/host-successor.js.map +1 -1
- package/dist/modes/rpc/host-watchdog.d.ts +6 -0
- package/dist/modes/rpc/host-watchdog.d.ts.map +1 -1
- package/dist/modes/rpc/host-watchdog.js +12 -13
- package/dist/modes/rpc/host-watchdog.js.map +1 -1
- package/dist/modes/rpc/loop-lag-watchdog.d.ts +16 -0
- package/dist/modes/rpc/loop-lag-watchdog.d.ts.map +1 -1
- package/dist/modes/rpc/loop-lag-watchdog.js +34 -2
- package/dist/modes/rpc/loop-lag-watchdog.js.map +1 -1
- package/dist/modes/rpc/multi-session-host.d.ts +2 -1
- package/dist/modes/rpc/multi-session-host.d.ts.map +1 -1
- package/dist/modes/rpc/multi-session-host.js +33 -6
- package/dist/modes/rpc/multi-session-host.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts +2 -2
- package/dist/modes/rpc/rpc-mode.js +2 -2
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +16 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/modes/rpc/session-command-router.d.ts +9 -1
- package/dist/modes/rpc/session-command-router.d.ts.map +1 -1
- package/dist/modes/rpc/session-command-router.js +4 -1
- package/dist/modes/rpc/session-command-router.js.map +1 -1
- package/dist/modes/rpc/session-path-key.d.ts +3 -1
- package/dist/modes/rpc/session-path-key.d.ts.map +1 -1
- package/dist/modes/rpc/session-path-key.js +3 -1
- package/dist/modes/rpc/session-path-key.js.map +1 -1
- package/docs/rpc.md +186 -19
- package/docs/settings.md +25 -1
- package/docs/usage.md +5 -2
- package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +12 -0
- package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
- package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.d.ts +6 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.js +36 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.d.ts +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +14 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +2 -2
- package/node_modules/@earendil-works/pi-pty/package.json +1 -1
- package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/package.json +7 -7
- package/dist/bundle/chunks/chunk-3DEXCYNN.js +0 -135
- package/dist/bundle/chunks/chunk-5X4KSPK5.js +0 -12
- package/dist/bundle/chunks/chunk-KMD5ZONU.js +0 -5
- package/dist/bundle/chunks/chunk-PEHZYGYA.js +0 -8
- package/dist/bundle/chunks/chunk-PRC5XIIH.js +0 -2
- package/dist/bundle/chunks/chunk-QKY33652.js +0 -18
- package/dist/bundle/chunks/interactive-host-runtime-TIFT2SKR.js +0 -4
- package/dist/bundle/chunks/multi-session-host-GRHKB7Q2.js +0 -21
- package/dist/bundle/chunks/session-picker-FTFCQYHH.js +0 -2
package/docs/rpc.md
CHANGED
|
@@ -166,6 +166,9 @@ parsing a version string:
|
|
|
166
166
|
`core.extensions` (absolute roots, deduplicated and sorted). `profile_id` is the sha256 of the canonical JSON
|
|
167
167
|
of `core` with its keys in that sorted order (`extensions`, `multi_session`, `session_runtime`), so any client
|
|
168
168
|
can recompute it and compare two hosts without comparing paths.
|
|
169
|
+
- `memory_pressure` - multi-session hosts only: `true` while this host's memory sampler reads its RSS above
|
|
170
|
+
`SENPI_RPC_HOST_RSS_WARN_MB`, else `false`. Not identity and never an admission signal; `senpi host status`
|
|
171
|
+
reports it per endpoint. Classic hosts and hosts released before the field omit it.
|
|
169
172
|
|
|
170
173
|
Compatibility is decided from `protocolVersion` + `capabilities`, and "is my build newer?" from `engineOrdinal`.
|
|
171
174
|
A `serverVersion` string comparison is never a compatibility test: two hosts with different version strings can
|
|
@@ -306,6 +309,7 @@ directory can never read each other's state:
|
|
|
306
309
|
<agentDir>/rpc-host-daemon/ flat directory (shared; also a legacy host's own state)
|
|
307
310
|
layout.json { "layout": 2, "dir": "<sha256(canonical socket)[:16]>" }
|
|
308
311
|
<sha256(canonical socket)[:16]>/ 0700
|
|
312
|
+
endpoint.json { layout: 2, socket, created_at } - durable identity
|
|
309
313
|
host.pid POINTER: { layout, instance_id, generation_dir, writer }
|
|
310
314
|
settings.json what the supervisor reads at boot
|
|
311
315
|
daemon.lock stderr.log
|
|
@@ -317,9 +321,15 @@ directory can never read each other's state:
|
|
|
317
321
|
reservations/ cross-generation session-path claims
|
|
318
322
|
```
|
|
319
323
|
|
|
320
|
-
Directories are `0700` and every state file is `0600`. The canonical socket is the socket path
|
|
321
|
-
|
|
322
|
-
|
|
324
|
+
Directories are `0700` and every state file is `0600`. The canonical socket is, on POSIX, the socket path
|
|
325
|
+
with its directory resolved through its deepest existing ancestor (`realpath`; a missing tail is kept
|
|
326
|
+
verbatim) - the same identity the ensure lock is keyed by - and on win32 the normalized lower-cased path, so
|
|
327
|
+
a client recomputes the directory name from the socket alone and every spelling of one socket (`/tmp` vs
|
|
328
|
+
`/private/tmp`, a path through a symlinked directory) shares one directory, one registration and one lock.
|
|
329
|
+
A socket already spelled canonically hashes exactly as it did when the name was taken from the spelling
|
|
330
|
+
itself, so an existing endpoint keeps its directory. A directory an older build named after a
|
|
331
|
+
non-canonical spelling is still listed by `status --all` and collected by `gc`, but new state goes to the
|
|
332
|
+
canonical directory. `generation_dir` is relative to the directory holding the pointer (`generations/<instanceId>`), and
|
|
323
333
|
`instance_id` is the same id the host reports as `instanceId` in `get_protocol_info` - so a pointer that
|
|
324
334
|
names a different id than the socket answers describes a generation that is no longer serving.
|
|
325
335
|
|
|
@@ -335,7 +345,20 @@ names is alive an ensure refuses (`legacy_host`) rather than starting a second h
|
|
|
335
345
|
`ensureHost` fails with a typed `HostDaemonStateError` naming the directory it could not create or write,
|
|
336
346
|
and starts no host in that case.
|
|
337
347
|
|
|
338
|
-
|
|
348
|
+
`endpoint.json` names the socket the directory serves, in the spelling its first ensure used (its name
|
|
349
|
+
is the hash of that spelling's canonical form). It is
|
|
350
|
+
written `0600` when the directory is created and re-asserted under the ensure lock. It is written whole to a
|
|
351
|
+
temporary name and linked into place, so no reader ever sees half a file, and a valid one is never rewritten, so
|
|
352
|
+
`created_at` is the endpoint's first ensure. Under the lock the ensure does replace a file that does not name a
|
|
353
|
+
socket hashing to this directory (torn by a crash of an older build, or foreign): left alone, such a file would
|
|
354
|
+
leave the endpoint listed as `socket: null` and kept by `gc` as `unknown_identity` forever. It is the one file a generation's release
|
|
355
|
+
leaves behind: a supervisor that exits (idle, drained, or after its host child crashed) removes the pointer,
|
|
356
|
+
`settings.json` and its generation directory, and without `endpoint.json` such an endpoint could not even be
|
|
357
|
+
enumerated. `stderr.log` and `crashes.jsonl` stay too. The only thing that ever removes an endpoint directory
|
|
358
|
+
(`endpoint.json` included) is the explicit `senpi host gc` below, and only on proof that nothing runs behind it.
|
|
359
|
+
|
|
360
|
+
The directory is PRUNED of what is no longer running on every registration write and on every single-socket
|
|
361
|
+
`host status` (never by `host status --all`):
|
|
339
362
|
a `generations/<instanceId>/` whose record names a pid nobody is running is removed, the pointer goes with it
|
|
340
363
|
while it still names one, and claims in `reservations/` whose owner is gone are removed too. A record that
|
|
341
364
|
cannot be parsed is left alone - an ensure writing one right now must not be mistaken for a generation that
|
|
@@ -370,10 +393,12 @@ Everything above is reachable from one command, so a terminal, a desktop and a t
|
|
|
370
393
|
daemon the same way instead of each re-implementing the decision:
|
|
371
394
|
|
|
372
395
|
```
|
|
373
|
-
senpi host ensure
|
|
374
|
-
senpi host status
|
|
375
|
-
senpi host stop
|
|
376
|
-
senpi host handoff
|
|
396
|
+
senpi host ensure [--json] [--launch-spec <file>] [--policy upgrade|fallback|never] [--socket <path>]
|
|
397
|
+
senpi host status [--json] [--include-workers] [--all] [--socket <path>]
|
|
398
|
+
senpi host stop [--json] [--drain] [--force] [--socket <path>]
|
|
399
|
+
senpi host handoff [--json] [--launch-spec <file>] [--socket <path>]
|
|
400
|
+
senpi host shard-path --kind <p|i> --owner <id> [--root <dir>] [--json]
|
|
401
|
+
senpi host gc [--json] [--agent-dir <dir>]
|
|
377
402
|
```
|
|
378
403
|
|
|
379
404
|
The contract is machine-first: EXACTLY ONE JSON line on stdout and nothing else, diagnostics on stderr,
|
|
@@ -388,7 +413,8 @@ and an exit code that classifies the outcome without parsing the line.
|
|
|
388
413
|
| `4` | fallback: `{ action: "fallback", reason, host }` - under `--policy fallback`, no host is better than this one |
|
|
389
414
|
|
|
390
415
|
The socket is `--socket`, else `SENPI_RPC_SOCKET`, else `<agentDir>/rpc/rpc.sock`. `--json` is accepted for
|
|
391
|
-
symmetry with other commands; the answer is always JSON
|
|
416
|
+
symmetry with other commands; the answer is always JSON (the one exception is `shard-path` without `--json`,
|
|
417
|
+
which prints the bare socket path).
|
|
392
418
|
|
|
393
419
|
- `ensure` prints `{ action, socket, pid, instanceId, generation, engineVersion, engineOrdinal,
|
|
394
420
|
capabilities, launchProfileId, reused, upgradeable }`. `--policy upgrade` (the default) allows a
|
|
@@ -400,7 +426,8 @@ symmetry with other commands; the answer is always JSON.
|
|
|
400
426
|
`senpi host handoff` command.
|
|
401
427
|
- `status` prints `{ reachable, socket, pid, instanceId, generation, engineVersion, capabilities,
|
|
402
428
|
launchProfile, sessions: { total, interactive, worker, retained, foreign_attached, foreign_retained },
|
|
403
|
-
zombies, rss_mb, host_rss_mb, open_fds, env_keys, generations
|
|
429
|
+
zombies, rss_mb, host_rss_mb, open_fds, memory_pressure, env_keys, generations, crashes, shard, session_rows,
|
|
430
|
+
claims_live, claims }` and exits 3 when nothing answers - with the same
|
|
404
431
|
field set, so a caller parses one shape and branches on one boolean. `sessions` is what `list_sessions`
|
|
405
432
|
reports under the same flag, so `worker` stays `0` without `--include-workers`; `foreign_*` is the same
|
|
406
433
|
count from the point of view of a client holding none of those sessions itself. `rss_mb`, `open_fds` and
|
|
@@ -413,12 +440,100 @@ symmetry with other commands; the answer is always JSON.
|
|
|
413
440
|
still claims in `reservations/` - the one occupancy number that is observable for a generation which no
|
|
414
441
|
longer answers on the socket. Records of generations that ended are pruned by the read itself, so a status
|
|
415
442
|
never lists a dead pid.
|
|
443
|
+
|
|
444
|
+
The last five fields and `memory_pressure` are additive (a client that does not know them ignores them):
|
|
445
|
+
- `memory_pressure`: whether the ANSWERING generation's memory sampler currently reads its RSS above
|
|
446
|
+
`SENPI_RPC_HOST_RSS_WARN_MB` (the state its `host_memory_pressure` records announce), read from its
|
|
447
|
+
`get_protocol_info`; `null` when nothing answers or the host predates the field. It lives in the host process
|
|
448
|
+
alone, so a draining predecessor's state is not reported, and it is observability only: a pressured endpoint
|
|
449
|
+
admits every open.
|
|
450
|
+
- `crashes`: records in the endpoint's `crashes.jsonl` - supervised host children that died rather than
|
|
451
|
+
stopped; `0` when the file is absent.
|
|
452
|
+
- `shard`: `{ kind: "p" | "i", key }` when the socket's basename is `<kind>-<16 hex>.sock` (the naming
|
|
453
|
+
contract below), else `null`.
|
|
454
|
+
- `session_rows`: under `--include-workers` only (else `[]`), every row of that same `list_sessions
|
|
455
|
+
{ include_workers: true }` reply as `{ id, kind, session_path, attachments, context }`. `session_path` is
|
|
456
|
+
the host's canonical path, the key a client matches a session by; `context` is the published labels
|
|
457
|
+
including the host's own `host_socket`/`host_instance`, `null` where none were published.
|
|
458
|
+
- `claims_live`: session-path claims in `reservations/` whose owner process is still running, `0` when the
|
|
459
|
+
directory is absent.
|
|
460
|
+
- `claims`: under `--include-workers` only (else `[]`), every claim in `reservations/` whichever generation
|
|
461
|
+
wrote it, as `{ session_path, owner_pid, instance_id, generation, attached, live }` - so a path a
|
|
462
|
+
draining predecessor still holds is visible although the answering generation no longer lists it.
|
|
463
|
+
`generation` is the owner's ordinal while its generation record exists, else `null`; `attached` is `null`
|
|
464
|
+
for a claim written before that flag existed.
|
|
465
|
+
- `status --all` reports every endpoint the agent directory holds state for and ignores `--socket`:
|
|
466
|
+
`{ endpoints: [<status row>, ...] }`, each row the single-socket report above plus `dir` (the endpoint's
|
|
467
|
+
daemon directory) and `identity` (what named its socket: `endpoint` = `endpoint.json`, `settings` = the
|
|
468
|
+
boot `settings.json`, `generation-settings` = a generation's own `settings.json`; each accepted only when
|
|
469
|
+
that socket hashes to the directory it was found in). A directory none of them names is still listed with
|
|
470
|
+
`socket: null` and `identity: "unknown"`, built from the directory alone - the ensure lock is keyed by a
|
|
471
|
+
longer hash of the socket's transport address and cannot be rebuilt from the 16-hex name, so it can be
|
|
472
|
+
shown but never addressed. Before layout 2 (no `layout.json`) the answer is `{ "endpoints": [] }`. Unlike
|
|
473
|
+
the single-socket form, `--all` REMOVES NOTHING: a dead generation is a row with `alive: false`, an endpoint
|
|
474
|
+
whose host exited stays listed through its `endpoint.json` with `reachable: false` and `generations: []`,
|
|
475
|
+
and reclaiming what ended is left to the explicit, evidence-gated `gc`. Every endpoint is read at
|
|
476
|
+
once, each under its own budget (10 s per read), and a socket that does not answer the identity probe is
|
|
477
|
+
not asked for its session listing, so hung hosts cost about one budget in total; rows stay in directory order. Exit `0` while at least one endpoint answers, `3` when none does or none exists.
|
|
478
|
+
- `shard-path` computes a shard socket without contacting any host, so a client that does not link senpi
|
|
479
|
+
can check its own copy of the naming contract against the engine: `{ kind, key, socket }` under `--json`,
|
|
480
|
+
the socket path otherwise. `--root` defaults to `<agentDir>/rpc/shards`.
|
|
481
|
+
- `gc` removes the state of endpoints whose host is PROVABLY gone, in the agent directory (`--agent-dir`, else
|
|
482
|
+
the current one; `--socket` is ignored), and answers `{ removed: [{ socket, dir, reason }], kept: [{ socket,
|
|
483
|
+
dir, reason }] }` with exit `0` whatever it found (`2` for a bad flag). It never signals a process and never
|
|
484
|
+
runs implicitly - not inside `ensure`, not inside `status`. For each endpoint whose socket is known it takes
|
|
485
|
+
that socket's ENSURE lock (`<tmp>/senpi-rpc-host-locks/<sha256(transport address)[:32]>.lock`, the one
|
|
486
|
+
`ensureHost` serializes on - not `daemon.lock`; on POSIX the address is the socket path with its directory
|
|
487
|
+
realpath-canonicalized, so every spelling of one socket shares one lock) for at most 2 s, and inside it requires all three: (a) no
|
|
488
|
+
`generations/*/host.pid` names a live process (pid live and start time matching; a live pid whose identity
|
|
489
|
+
cannot be read counts as live; the generation the pointer names is one of them), (b) no claim in
|
|
490
|
+
`reservations/` has a live owner, and (c) connecting to the socket fails with `ENOENT`, or with `ECONNREFUSED`
|
|
491
|
+
on an entry that is a socket (a regular file where the socket should be is kept as `reachable`) - and so does
|
|
492
|
+
every `<socket>.next-*` successor bind beside it. Only then does it unlink the `<socket>.next-*` /
|
|
493
|
+
`<socket>.shield-*` siblings, then the socket, then remove the endpoint directory LAST, still under the lock;
|
|
494
|
+
`reason` is `socket_refused` or `socket_absent`. A sibling that is a directory is not unlinked: it stays and
|
|
495
|
+
the removed entry lists it as `skipped: [{ path, type: "directory" }]`. Because the directory goes last, a
|
|
496
|
+
removal that fails part-way leaves the endpoint listed for the next gc. Everything else is kept with its
|
|
497
|
+
reason: `live_generation`, `live_claim`, `reachable`, `locked` (an ensure held the lock for 2 s),
|
|
498
|
+
`legacy_layout` (a flat directory without `layout.json` - a legacy host's, never touched), `unknown_identity`
|
|
499
|
+
(nothing names the socket, so its lock cannot be taken), or `failed` (reading the evidence or removing threw;
|
|
500
|
+
the entry carries `error`) - and a `failed` endpoint never stops gc from going on to the others.
|
|
501
|
+
An `unknown_identity` directory is never removed by gc; remove it by hand only after checking that no pid in
|
|
502
|
+
its `generations/*/host.pid` or `reservations/*.json` is running and that no process holds files under it.
|
|
503
|
+
An ensure that raced a gc simply re-creates `endpoint.json` under the lock after gc released it.
|
|
416
504
|
- `stop` is the I1 carve-out: a plain stop needs a validated pidfile AND `foreign_attached +
|
|
417
505
|
foreign_retained == 0`, or it refuses with exit 3 and prints the counts it refused on; `--force`
|
|
418
506
|
overrides after printing the same counts; `--drain` (SIGUSR1) is always permitted, because it ends no
|
|
419
507
|
work.
|
|
420
508
|
- `handoff` forces a generation handoff from THIS binary. A host that cannot drain and a platform that
|
|
421
|
-
cannot rename answer alike: exit 3 `{ reason: "upgrade_unsupported", detail }`.
|
|
509
|
+
cannot rename answer alike: exit 3 `{ reason: "upgrade_unsupported", detail }`. The handoff runs inside the
|
|
510
|
+
endpoint's ensure lock (the one `ensure` and `gc` take), so an `ensure` arriving while the successor comes up
|
|
511
|
+
waits and then attaches to the successor, and `gc` reports the endpoint `locked` rather than judging it
|
|
512
|
+
halfway. An `ensure` waits for the lock as long as the longest holder can keep it: a probe plus either a
|
|
513
|
+
stop-and-restart or a handoff (probe, successor start, 30 s readiness). A REFUSED handoff leaves the endpoint
|
|
514
|
+
as it found it: the successor it started is killed and, once it has exited (up to 5 s), its generation record
|
|
515
|
+
and directory are released, so `status --all` never lists it; the boot `settings.json` it rewrote before
|
|
516
|
+
spawning is restored byte for byte. A cleanup that fails is reported in the refusal's `detail`.
|
|
517
|
+
|
|
518
|
+
#### Shard naming contract (`shardKey`, `shardSocketPath`)
|
|
519
|
+
|
|
520
|
+
A client may run many endpoints under one agent directory - omo one per parent session (`p`), the Desktop
|
|
521
|
+
one per thread (`i`), under `<agentDir>/rpc/shards/`. Every client derives the socket identically:
|
|
522
|
+
|
|
523
|
+
```
|
|
524
|
+
key = sha256("<kind>:<ownerId>") in hex, first 16 characters
|
|
525
|
+
socket = <root>/<kind>-<key>.sock
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
`packages/coding-agent/src/modes/rpc/host-daemon-paths.ts` exports `shardKey(kind, ownerId)`,
|
|
529
|
+
`shardSocketPathForKey(root, kind, key)` (no hashing) and `shardSocketPath(root, kind, ownerId)`. Fixed
|
|
530
|
+
vectors every implementation must reproduce:
|
|
531
|
+
|
|
532
|
+
| kind | owner | key | socket under `/r` |
|
|
533
|
+
|---|---|---|---|
|
|
534
|
+
| `p` | `01a0e28d-40e4-7402-bac7-8de6e76ad84c` | `6d410ba846ba1550` | `/r/p-6d410ba846ba1550.sock` |
|
|
535
|
+
| `i` | `thread-0001` | `da99f196e11b1cf9` | `/r/i-da99f196e11b1cf9.sock` |
|
|
536
|
+
| `p` | (empty) | `3ba7290d74188485` | `/r/p-3ba7290d74188485.sock` |
|
|
422
537
|
|
|
423
538
|
#### Launch spec (`--launch-spec <file>`)
|
|
424
539
|
|
|
@@ -547,6 +662,15 @@ auth, model, extension or resource resolution, and they are never merged into th
|
|
|
547
662
|
`^[a-z][a-z0-9_]*$`, every value at most 16 KiB, and at most 32 KiB of JSON in total. Anything else is refused with
|
|
548
663
|
`invalid_session_context: <detail>`, where the detail names the cap that was broken.
|
|
549
664
|
|
|
665
|
+
A socket host also stamps its own identity into every session's context, overwriting any client-supplied
|
|
666
|
+
value for the same keys, because the host is the authority: `host_socket` is the PUBLIC endpoint clients
|
|
667
|
+
address (the supervisor's path for a supervised host, the bound path for a bare one), realpath-canonicalized
|
|
668
|
+
through the deepest existing ancestor of its directory (so the first generation of a shard, which starts before
|
|
669
|
+
`rpc/shards/` exists, and every successor stamp the same string), and stays the same across a generation handoff; `host_instance` is the answering generation's `instanceId`
|
|
670
|
+
(`get_protocol_info`) and changes with every handoff. So an extension can tell which endpoint and generation
|
|
671
|
+
it runs behind without an environment variable. `host_socket` is omitted where no public path exists (an
|
|
672
|
+
abstract socket, a supervised win32 host); a stdio host adds neither key.
|
|
673
|
+
|
|
550
674
|
One shared host therefore loads ONE extension set and still lets an extension recognize the session it was loaded for
|
|
551
675
|
(`pi.sessionKind`, `pi.sessionContext` - see
|
|
552
676
|
[ExtensionAPI session identity](extensions.md#pisessionkind--pisessioncontext--pisharedhostenabled)). Probe
|
|
@@ -614,17 +738,34 @@ Environment overrides beat the file, and invalid values fall through to the next
|
|
|
614
738
|
(`transient`|`persistent`) and `SENPI_RPC_HOST_IDLE_EXIT_MS` (positive integer milliseconds).
|
|
615
739
|
|
|
616
740
|
The host exits only after the window elapses with NO attached client connections and NO active turns — continuously.
|
|
617
|
-
Any connection or agent turn resets the window, so a busy host never exits.
|
|
741
|
+
Any attached connection or agent turn resets the window, so a busy host never exits. A connection attaches with its
|
|
742
|
+
first request line, unless that line is an OBSERVING read: `get_protocol_info` or `list_sessions` carrying
|
|
743
|
+
`"observe": true`. A connection that only ever sends observing reads never counts and never resets the window, so a
|
|
744
|
+
poller (`senpi host status [--all]`, which marks both of its reads, a doctor loop, a runtime panel) can look at every
|
|
745
|
+
endpoint as often as it likes without keeping any of them alive; the first request that is anything else attaches the
|
|
746
|
+
connection from then on, and `observe` on any other command is ignored. While a connection has not sent its first
|
|
747
|
+
line yet the host does not exit, and the window keeps running. Hosts that predate the field ignore it, so a marked read
|
|
748
|
+
works against every generation. The supervisor learns about turns through
|
|
618
749
|
its observer connection to the host; while that connection is unhealthy it cannot see turns, so it treats activity as
|
|
619
750
|
unknown and keeps the host open as if a turn were running — but only for one idle window, during which it keeps
|
|
620
751
|
reconnecting. An observer that stays unhealthy for longer than the window stops counting as busy, and the connection
|
|
621
752
|
count alone decides from there. A `persistent` host has an infinite window and so keeps its infinite benefit of the
|
|
622
753
|
doubt. The exit itself is clean: the RPC host
|
|
623
|
-
receives SIGTERM first, flushes pending output, removes its socket, and the supervisor then removes `
|
|
624
|
-
`
|
|
754
|
+
receives SIGTERM first, flushes pending output, removes its socket, and the supervisor then removes `settings.json`,
|
|
755
|
+
its generation directory and, last, the registration pointer (`host.pid`); the stderr log stays for diagnostics. The
|
|
756
|
+
crash-path cleanup the host runs when its supervisor dies uses the same order, so a missing pointer always means the
|
|
757
|
+
teardown is complete. After an idle exit, the next `ensureHost()` transparently
|
|
625
758
|
starts a fresh host. `get_protocol_info` over the public socket behaves exactly as before; the supervisor is
|
|
626
759
|
wire-transparent.
|
|
627
760
|
|
|
761
|
+
`ensureHost()` hands its caller an attach hold: the connection its readiness answer arrived on stays open and counts
|
|
762
|
+
as an attached client, so the idle window cannot close between the ensure and the caller's own attach, however slow
|
|
763
|
+
that attach is. Every caller must call `release()` on the returned `EnsuredHost` once its own client is
|
|
764
|
+
attached, or right away when it attaches later on its own connections; the idle window starts from there. Until then
|
|
765
|
+
the host counts the calling process as an attached client, so a transient host whose ensuring process is still running
|
|
766
|
+
never starts its idle window: a long-lived caller that ensures and never releases keeps the host up for its whole
|
|
767
|
+
lifetime. The hold never keeps the calling process alive, and the operating system ends it when that process exits.
|
|
768
|
+
|
|
628
769
|
The RPC host can never outlive its supervisor. It is spawned with an extra inherited pipe on fd 3 whose write end the
|
|
629
770
|
supervisor holds and never writes to; the kernel closes that end whenever the supervisor dies — including `SIGKILL`, an
|
|
630
771
|
OOM kill, or a crash, where no signal handler runs — so the host reads EOF, shuts down cleanly and removes its private
|
|
@@ -729,7 +870,9 @@ What the host does enforce are lifecycle windows, and they only ever return memo
|
|
|
729
870
|
- **Empty-host exit**: when the registry holds zero sessions AND no client is connected, continuously for
|
|
730
871
|
`SENPI_RPC_HOST_EMPTY_EXIT_MS` (default 15 minutes), the host exits through its clean shutdown path (flush, socket
|
|
731
872
|
removal), for stdio and `--listen` hosts alike. A connected client counts as occupancy even with no session open,
|
|
732
|
-
so the host never drops a live socket under itself
|
|
873
|
+
so the host never drops a live socket under itself - except a connection whose every request so far was an
|
|
874
|
+
observing read (`"observe": true`, the same rule as the idle-exit window above), which never holds the host open.
|
|
875
|
+
A connection that has not sent its first request yet still counts. Supervised hosts stay clean either way: a supervisor reads a
|
|
733
876
|
child exit of 0 without a signal as an intentional idle stop and exits 0 with the same cleanup, not as a crash.
|
|
734
877
|
|
|
735
878
|
Values are positive integers; invalid values fall through to the defaults. These lifecycle windows run inside the host process,
|
|
@@ -754,7 +897,11 @@ REPORT: nothing here aborts a turn, kills a session, or refuses an `open_session
|
|
|
754
897
|
deadline (a busy host was measured answering after 57 s), and a timeout after it names the
|
|
755
898
|
queue position instead of a bare deadline. A lost transport still rejects at once.
|
|
756
899
|
`SENPI_RPC_LOOP_LAG_ERROR_MS` (default 5000) additionally broadcasts a `host_stalled` record
|
|
757
|
-
(`{ type, driftMs, sessionId?, tool? }`) to every connection, like the other content-free lifecycle records.
|
|
900
|
+
(`{ type, driftMs, sessionId?, tool?, processCpuMs?, heapDeltaMb? }`) to every connection, like the other content-free lifecycle records.
|
|
901
|
+
`processCpuMs` is the process CPU time spent during the stalled window and `heapDeltaMb` the JS heap change across it, so a
|
|
902
|
+
stall explains itself: CPU close to `driftMs` means the host was busy (a large heap drop in the same window points at a
|
|
903
|
+
collection), and CPU close to zero means the process did not run at all (the machine starved it, or it sat in a blocking
|
|
904
|
+
wait). The stderr line carries the same two numbers as `cpu=<ms> heap=<+/-MB>`.
|
|
758
905
|
- **Stall attribution**: each routed command is dispatched inside an `AsyncLocalStorage` scope carrying its routing
|
|
759
906
|
`sessionId`, and an in-process session's tool executions open a span carrying `{ sessionId, tool }` for as long as
|
|
760
907
|
the tool runs. A stall is blamed on the synchronous work that finished inside the measured window, or on the tool
|
|
@@ -766,7 +913,15 @@ REPORT: nothing here aborts a turn, kills a session, or refuses an `open_session
|
|
|
766
913
|
minutes, and HALVES the idle-eviction window above while the host stays above the threshold, so idle sessions return
|
|
767
914
|
their memory sooner. It is released as soon as RSS falls back under the threshold. Memory never refuses an open: the
|
|
768
915
|
shared host has no resource caps, so every `open_session` is admitted whatever the host holds (#2207). Hosts released
|
|
769
|
-
before #2207
|
|
916
|
+
before #2207 had a second admission watermark; current hosts have no such admission path.
|
|
917
|
+
- **Per-endpoint pressure under sharding**: an agent directory may contain many independent hosts, so each endpoint
|
|
918
|
+
samples and reports its own memory pressure. `host_memory_pressure` describes only the host that emitted it; pressure
|
|
919
|
+
on one `p-*` or `i-*` endpoint does not gate opens on another endpoint. In `host status --all`, `rss_mb` is the
|
|
920
|
+
endpoint generation's whole process tree, `host_rss_mb` is the supervisor plus host processes, and `memory_pressure`
|
|
921
|
+
is whether the answering generation is above the threshold right now. Read them on the endpoint row (and the RSS
|
|
922
|
+
pair on its generation rows) rather than treating an aggregate agent-directory number as an admission
|
|
923
|
+
signal. The warning threshold remains observability-only: every endpoint continues to admit worker `open_session`
|
|
924
|
+
requests while pressured.
|
|
770
925
|
- **Stall-proof dead-peer detection**: the socket dead-peer budget (30 s, `socket-event-fanout.ts`) counts only time
|
|
771
926
|
the host loop actually SERVED. The loop-lag watchdog deposits each measured drift into a process-wide ledger
|
|
772
927
|
(`loop-blocked-time.ts`) and the deadline re-arms for whatever blocked time landed inside its window, so a host that
|
|
@@ -798,7 +953,7 @@ absolute: **no blocking primitive, and no unbounded synchronous filesystem read.
|
|
|
798
953
|
sleeps synchronously, or reads a large file synchronously inside an event handler freezes every other client's
|
|
799
954
|
session on that host. Use the async API, and give genuinely CPU-bound work its own worker or child process.
|
|
800
955
|
- The rule is observable rather than enforced at runtime: the stall watchdog above is what names the offender.
|
|
801
|
-
`host_stalled { driftMs, sessionId, tool }` and the matching stderr line are how a blocking call in a session or a
|
|
956
|
+
`host_stalled { driftMs, sessionId, tool, processCpuMs, heapDeltaMb }` and the matching stderr line are how a blocking call in a session or a
|
|
802
957
|
tool becomes a report instead of an unexplained freeze.
|
|
803
958
|
|
|
804
959
|
### Worker ownership and flow control
|
|
@@ -889,7 +1044,7 @@ containment, or containment of arbitrary native code. They are not an extension
|
|
|
889
1044
|
|
|
890
1045
|
| Command | Params | Success data | Notes |
|
|
891
1046
|
| --- | --- | --- | --- |
|
|
892
|
-
| `get_protocol_info` | - | `{ protocolVersion: 1, serverVersion: string, capabilities: string[], mode: "classic"\|"multi", instanceId: string, generation: number, engineVersion: string, engineOrdinal: [y, m, d, n, epoch], launch_profile: { profile_id, core } }` | Answered in BOTH modes; side-effect-free; the capability probe. Multi-session hosts include `multi_session`, `retain_on_disconnect`, `session_kind`, `session_context` and `auto_title_per_session` plus the negotiated launch capabilities. Those are HOST capabilities (a client never sends them) and are advertised only in multi-session mode, where the host owns the attachment refcount and the per-session launch profile. The identity fields are described under "Host identity" above; compatibility is decided from `protocolVersion`, `capabilities` and `engineOrdinal`, NEVER from `serverVersion`. |
|
|
1047
|
+
| `get_protocol_info` | - | `{ protocolVersion: 1, serverVersion: string, capabilities: string[], mode: "classic"\|"multi", instanceId: string, generation: number, engineVersion: string, engineOrdinal: [y, m, d, n, epoch], launch_profile: { profile_id, core }, memory_pressure?: boolean }` | Answered in BOTH modes; side-effect-free; the capability probe. Multi-session hosts include `multi_session`, `retain_on_disconnect`, `session_kind`, `session_context` and `auto_title_per_session` plus the negotiated launch capabilities. Those are HOST capabilities (a client never sends them) and are advertised only in multi-session mode, where the host owns the attachment refcount and the per-session launch profile. The identity fields are described under "Host identity" above; compatibility is decided from `protocolVersion`, `capabilities` and `engineOrdinal`, NEVER from `serverVersion`. |
|
|
893
1048
|
| `open_session` | `sessionPath?`, `cwd?`, `provider?`, `modelId?`, `thinkingLevel?`, `permissionPreset?`, `retain_on_disconnect?`, `kind?`, `context?`, `auto_title?` (all optional; paths MUST be absolute) | `{ sessionId, state: RpcSessionState, attached?: true }` | `sessionPath` = today's `--session` semantics (open-if-exists else create persisting there, `session-manager.ts:926-940`); `provider`/`modelId` applied only on create (resume restores the session's model — mirrors `SenpiSessionRuntime.ts:198-200`); params form the immutable launch profile (D8). When the path is already held by a fully-open session, the open ATTACHES to it: same routing handle, `attached: true`, one more attachment counted; the runtime is torn down only when the last attachment closes. Idle sessions past the eviction window are closed by the host itself. `retain_on_disconnect: true` (default false) makes a dropped connection DETACH from this session instead of closing it — see "Retained sessions" below. `kind` (default `interactive`) and the opaque `context` map are described under "Session kind and context" above; both are stored frozen for the session's life and never influence auth, model or resource resolution. |
|
|
894
1049
|
| `close_session` | `sessionId` | `{}` | Refused with `unknown_session` when the requesting connection never attached to that handle (a close releases the CALLER's attachment, and `list_sessions` publishes every handle). Otherwise aborts active work, awaits agent idle + settled persistence for up to the host grace window (default 10s), then quarantines any worker that has not exited without releasing its path reservation; its response is the LAST record tagged with that handle for the first closer — no events after (test-pinned). An admitted concurrent close joins the same teardown and receives its own successful response; output saturation rejects admission with the bounded close-overflow/resync notice described above. |
|
|
895
1050
|
| `list_sessions` | `include_workers?` (default false) | `{ sessions: [{ sessionId, durableSessionId, sessionPath, cwd, name, status, attachments, kind, context? }] }` | Includes `opening`/`closing` entries. Internally quarantined workers remain externally `closing` until exit. `attachments` is the session's live client attachment count; `0` on an `open` row is a retained session with no client attached. Every row carries `kind`. Rows with `kind: "worker"` are omitted unless `include_workers: true`, and `context` is published ONLY on that listing — a default listing carries no `context` at all. |
|
|
@@ -1638,11 +1793,23 @@ Response:
|
|
|
1638
1793
|
"tokens": 60000,
|
|
1639
1794
|
"contextWindow": 200000,
|
|
1640
1795
|
"percent": 30
|
|
1796
|
+
},
|
|
1797
|
+
"failures": {
|
|
1798
|
+
"requests": 6,
|
|
1799
|
+
"erroredRequests": 1,
|
|
1800
|
+
"abortedRequests": 0,
|
|
1801
|
+
"failureShare": 0.16666666666666666,
|
|
1802
|
+
"failedDurationMs": 4200,
|
|
1803
|
+
"postFailureRequests": 1,
|
|
1804
|
+
"postFailureFullMissRequests": 1,
|
|
1805
|
+
"postFailureFullMissInputTokens": 48000
|
|
1641
1806
|
}
|
|
1642
1807
|
}
|
|
1643
1808
|
}
|
|
1644
1809
|
```
|
|
1645
1810
|
|
|
1811
|
+
`failures` reports what failed provider requests cost the session. Every assistant response is one request; `erroredRequests` and `abortedRequests` count responses that ended with stop reason `error` or `aborted`, and `failedDurationMs` sums their time from request start to the recorded response. `postFailureRequests` counts the first successful response after one or more failed ones within the same user turn (a user message resets it); `postFailureFullMissRequests` counts those that read nothing from the prompt cache on a prompt of at least 2048 tokens, and `postFailureFullMissInputTokens` sums their uncached prompt tokens (input plus cache writes). The token counts are the provider's usage report; attributing the cache miss to the preceding failure is an inference from the turn's sequence. Hosts older than this field omit it.
|
|
1812
|
+
|
|
1646
1813
|
`tokens` and `cost` include assistant messages, usage reported by tools, and compaction/branch-summary generation across the full session. `contextUsage` contains the actual current context-window estimate used for compaction and footer display.
|
|
1647
1814
|
|
|
1648
1815
|
`contextUsage` is omitted when no model or context window is available. `contextUsage.tokens` and `contextUsage.percent` are `null` immediately after compaction until a fresh post-compaction assistant response provides valid usage data.
|
package/docs/settings.md
CHANGED
|
@@ -254,6 +254,8 @@ See [compaction.md](compaction.md) for trigger and summarization behavior.
|
|
|
254
254
|
| `retry.fallbackChains` | `Record<string, string[]>` | `{}` | Ordered exact model-selector to fallback-selector chains |
|
|
255
255
|
| `retry.fallbackRevertPolicy` | `"cooldown-expiry"` \| `"never"` | `"cooldown-expiry"` | Automatic primary-model restoration policy |
|
|
256
256
|
| `retry.abortServerSideFallback` | boolean | `true` | Abort a turn when the provider substitutes a different model after a classifier decline |
|
|
257
|
+
| `fallback.circuitCooldownMs` | number | `60000` | First circuit-breaker cooldown for a chain entry that failed out of its chain; doubles on each consecutive failure; `0` disables the breaker |
|
|
258
|
+
| `fallback.circuitMaxCooldownMs` | number | `1800000` | Ceiling for the doubled circuit-breaker cooldown (30 minutes) |
|
|
257
259
|
| `retry.maxAgentDelayMs` | number | `60000` | Hard ceiling on agent-level retry delay (60s), applied after the retry profile and jitter |
|
|
258
260
|
| `retry.provider.timeoutMs` | number | `300000` | Provider/SDK request timeout and stream idle timeout in milliseconds |
|
|
259
261
|
| `retry.provider.streamStartTimeoutMs` | number | `300000` | Maximum wait for the first provider stream event; `0` disables |
|
|
@@ -319,6 +321,28 @@ Billing-class failures — Anthropic's 400 *credit balance is too low*, OpenAI's
|
|
|
319
321
|
|
|
320
322
|
Anthropic streaming refusals are identified from typed `stopDetails`. A configured candidate receives an immediate **pinned** fallback switch with a user-visible fallback notice: Senpi does not retry the refusing model and a pinned fallback never auto-reverts. Set `retry.fallbackRevertPolicy` to `"cooldown-expiry"` (the default) to return an unpinned fallback to its primary after the primary's cooldown expires, or `"never"` to keep the fallback until you change models.
|
|
321
323
|
|
|
324
|
+
#### Fallback circuit breaker
|
|
325
|
+
|
|
326
|
+
When a chain entry fails with a provider-health failure, Senpi opens a circuit for that entry: a transient failure (timeout, overload, 429, 5xx, transport drop) once its retry budget is spent, or a billing, credit, quota, or budget exhaustion at once, including on the last entry of the chain. Authentication (401/403) and request-shape rejections do not open a circuit. The circuit is shared by every session in the process that uses the same agent directory - sessions started with `/new`, `/resume`, or `/fork`, and in-process subagents - so they skip the entry without sending it a request or spending its retry budget:
|
|
327
|
+
|
|
328
|
+
- A session whose current model has an open circuit moves to the next chain entry with a closed circuit at the turn boundary (shown as a `transient` model fallback). Mid-turn fallbacks skip open entries the same way.
|
|
329
|
+
- The cooldown starts at `fallback.circuitCooldownMs` and doubles on each consecutive failure, up to `fallback.circuitMaxCooldownMs`. A provider `Retry-After` (seconds or HTTP-date, on 429 and 503 responses) keeps the circuit open until that time when it is longer, and a later failure without a hint never shortens it. An accepted response from the entry closes the circuit and resets the escalation.
|
|
330
|
+
- After the cooldown the circuit is half-open: the first request to use the entry again - a session's turn or a background 429 probe-back - holds its only probe until the probe settles, and every other request keeps skipping it, including the other request lane of the same session. While the circuit tracks an entry, its clock (monotonic, like every cooldown) decides when a fallback returns to it. The probe closes the circuit as soon as the entry streams a response; its first provider-health failure re-opens the circuit with the doubled cooldown and falls back immediately, without same-model retries. A user abort, a request-shaped error, or disposal hands the probe back. A probe that never answers is aborted as a provider failure after the stream-start guard (5 minutes when `retry.provider.streamStartTimeoutMs` is 0).
|
|
331
|
+
- 429 probe-back probes of a demoted primary respect the circuit: none is sent before the provider's retry-after or the cooldown elapses, or while another session holds the probe.
|
|
332
|
+
- The chain never refuses a turn: if every remaining entry is open, the request still goes to the current entry (or the first open candidate) as a probe.
|
|
333
|
+
- Refusals and request-shaped hard errors do not open circuits. Selecting a model yourself closes its circuit.
|
|
334
|
+
|
|
335
|
+
Circuits are process-local like the rest of the fallback state and are never persisted. Set `fallback.circuitCooldownMs` to `0` to turn the breaker off:
|
|
336
|
+
|
|
337
|
+
```json
|
|
338
|
+
{
|
|
339
|
+
"fallback": {
|
|
340
|
+
"circuitCooldownMs": 60000,
|
|
341
|
+
"circuitMaxCooldownMs": 1800000
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
322
346
|
#### Provider-substituted models
|
|
323
347
|
|
|
324
348
|
Anthropic's server-side fallback betas can retry a classifier-declined request on a substitute model *inside the same response*, marking the handoff with a `fallback` content block; a gateway may enable this on your behalf. Honoring that response means paying for a model you did not select, and after the first handoff Anthropic routes later turns of the conversation straight to the substitute with no marker at all — reported only as a `fallback_message` entry in `usage.iterations`.
|
|
@@ -331,7 +355,7 @@ Set it to `false` to keep the substituted response instead. If a gateway in fron
|
|
|
331
355
|
|
|
332
356
|
Fallback decisions are process-local. A `senpi-task` or subagent child process reads its own settings and maintains its own in-memory suppression state; it does not affect its parent process. Disable fallback for one run without changing settings with `--no-model-fallback` or `SENPI_NO_FALLBACK=1`.
|
|
333
357
|
|
|
334
|
-
For diagnostics, Senpi writes sanitized NDJSON records for candidate skips, cooldowns, switches, reverts, manual clears, and validation warnings to `<agentDir>/logs/fallback.log`. The file is mode `0600` and rotates at 5 MB (`fallback.log.1`).
|
|
358
|
+
For diagnostics, Senpi writes sanitized NDJSON records for candidate skips, cooldowns, circuit opens (`circuit_opened`) and turn-boundary circuit skips (`circuit_open_skip`), switches, reverts, manual clears, and validation warnings to `<agentDir>/logs/fallback.log`. `/session` reports what failed requests cost the session: failed-request counts and share, time spent in failed requests, and retries that succeeded after a failure in the same user turn but read nothing from the prompt cache, with their uncached input tokens. Prompts under 2,048 tokens are never counted as cache misses, because providers do not cache prompts that small; the token counts come from the provider's usage report, while tying the miss to the failure is the report's reading of the turn. The file is mode `0600` and rotates at 5 MB (`fallback.log.1`).
|
|
335
359
|
|
|
336
360
|
### Message Delivery
|
|
337
361
|
|
package/docs/usage.md
CHANGED
|
@@ -43,13 +43,13 @@ Type `/` in the editor to open command completion. Extensions can register custo
|
|
|
43
43
|
| `/login`, `/logout` | Manage OAuth or API-key credentials |
|
|
44
44
|
| [`/llama`](llama-cpp.md) | Download, load, and unload llama.cpp router models |
|
|
45
45
|
| `/model` | Switch models; Ctrl+S in the picker saves the startup default |
|
|
46
|
-
| `/thinking` |
|
|
46
|
+
| `/thinking [level]` | Set the thinking level for this session (`/thinking high`), or open the picker with no argument; Ctrl+S in the picker saves it as the startup default |
|
|
47
47
|
| `/scoped-models` | Enable/disable models for Ctrl+P cycling |
|
|
48
48
|
| `/reasoning [on\|off]` | Show or toggle reasoning for the current model |
|
|
49
49
|
| `/efforts [level]` | Show or set reasoning effort (graded models only) |
|
|
50
50
|
| `/fast [on\|off]` | Toggle fast mode (ChatGPT Subscription models, persisted per model) |
|
|
51
51
|
| `/settings` | Theme, message delivery, transport, and other preferences |
|
|
52
|
-
| `/resume` | Pick from previous sessions |
|
|
52
|
+
| `/resume`, `/sessions` | Pick from previous sessions (`/sessions` is an alias) |
|
|
53
53
|
| `/new` | Start a new session |
|
|
54
54
|
| `/rename [name]` | Rename the current session (`/name` is an alias) |
|
|
55
55
|
| `/session` | Show session file, ID, messages, tokens, and cost |
|
|
@@ -71,6 +71,8 @@ Type `/` in the editor to open command completion. Extensions can register custo
|
|
|
71
71
|
|
|
72
72
|
### Reasoning and Fast Mode Commands
|
|
73
73
|
|
|
74
|
+
**Changing the thinking level.** `/thinking <level>` sets it for this session and `/thinking` alone opens a picker. Shift+Tab cycles through the levels the model supports, and `/efforts <level>` sets the reasoning effort for graded models; both remember the level for the current model. The footer shows the active level after the model name.
|
|
75
|
+
|
|
74
76
|
**`/reasoning [on|off]`** shows or toggles reasoning. Behavior adapts to the active model:
|
|
75
77
|
|
|
76
78
|
- Models without reasoning support are told plainly.
|
|
@@ -113,6 +115,7 @@ senpi --fork <path|id> # Fork a session into a new session file
|
|
|
113
115
|
|
|
114
116
|
Useful session commands:
|
|
115
117
|
|
|
118
|
+
- `/resume` (or its alias `/sessions`) opens the same picker as `senpi -r` without leaving the TUI.
|
|
116
119
|
- `/session` shows the current session file and ID.
|
|
117
120
|
- `/tree` navigates the in-file session tree and can summarize abandoned branches. Ctrl+E on an assistant entry edits that response in place of the original (the original stays in the file on an abandoned branch; tool calls in the edited response are dropped).
|
|
118
121
|
- `/fork` creates a new session from an earlier user message.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.28",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Source-only senpi extension package for codemode evaluation tools",
|
|
6
6
|
"type": "module",
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@babel/parser": "8.0.4",
|
|
34
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.
|
|
34
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.28",
|
|
35
35
|
"typebox": "1.3.34"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
38
|
+
"@code-yeongyu/senpi": "2026.9.28"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
41
|
+
"@code-yeongyu/senpi": "2026.9.28"
|
|
42
42
|
},
|
|
43
43
|
"keywords": [
|
|
44
44
|
"senpi",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@earendil-works/pi-agent-core",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.28",
|
|
4
4
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"@earendil-works/chord": "0.85.1",
|
|
60
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.
|
|
61
|
-
"@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.9.
|
|
60
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.28",
|
|
61
|
+
"@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.9.28",
|
|
62
62
|
"diff": "9.0.0",
|
|
63
63
|
"ignore": "7.0.9",
|
|
64
64
|
"typebox": "1.3.34",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"schemaVersion":3,"generatedAt":"2026-09-
|
|
1
|
+
{"schemaVersion":3,"generatedAt":"2026-09-28T01:49:01.161Z","structureHash":"de13fd32a0f46b302e3edd0d4d50c72e20019642cf25e876b706b4edc97089e5","files":{"alibaba-token-plan.json":"3a9c1be76d47459371e9fa2feeffc31e7edbbfec65fa9cc41236988efc4cf6d9","amazon-bedrock.json":"848e38fd1ade9c6e94c3037c1c6104ac515809a6f190d264eafd9f48ab0ae1ae","ant-ling.json":"4979fe79d99ed97382d7ce40170c6132e932e77906de1eb54464c042e7d63633","anthropic.json":"f9db437d92449fb860a2d457a6019cdfd51c11180c08df2b5f14d75ade773953","azure-openai-responses.json":"d70ff10410e6dd8eacb4a53b9fe887be836562cbb4b74eaee4f28bdbc4ac0732","bai.json":"2b12b15bd43462e60711944dd7283db8676611d9d76c82c2a01f64913e16a67e","baseten.json":"d155c434294b5167244cffb6dea6793f9669e91e8ab65b564f9510d81b330e37","cerebras.json":"3be228c613905c215c9da3d0b3535e10cc594ff2351b05390744c7c34de89b6c","chatgpt-subscription.json":"421c6ad932de4259c8f0c08589da278bed99ec80c042f2490d8ffd2a86f4da99","cloudflare-ai-gateway.json":"b64cc5c7c0df4bfce0c3c2b14fe5290636553914b09718634668ad98276a9bc5","cloudflare-workers-ai.json":"8ee15540a01cf79d0e4063c493ae3c436af2fc1d456f9faf1b066418247183e9","deepseek.json":"b5f9693ec641e9a957dc5f7a22042b3bb928bac3d7268406619b6f18e21c4b83","fireworks.json":"bbb8149854a2658f221afa47ae8e2617e6b58a8e2784aa822faaf54f0c0f18c3","github-copilot.json":"5c9fc5b1b553a26aa7bc843a3e36fa14cf220d528ecd89528c601c5090c88a2d","google-vertex.json":"97d80ac909d256cc1bf19ac54d9d8131c492e7c0c102ad89f68ade528544e0ad","google.json":"68f6cf6283cc0d69b77b0fa79e9c94774e96524f61ebba616319f05d5c9db56c","groq.json":"e4728fd72e358cd8f25e9a518e9b49a521ef44523d3debe38a7db9e0663c4077","huggingface.json":"2f93082144a7b2a1cbf79148244ce7559e0428d6310c5eb0648bca89b15a8b4b","minimax-cn.json":"4f636b47b28a507ad33b5eaba33fc4889e5956504d10f2a0d9c61167ae01ff1d","minimax.json":"9fd1764ffeefebded8579039cf0c48854a43e4c5b724f62361fc2ac54f705aa1","mistral.json":"be522597b22bceeb0bb9a0ba1781647649ffd19637d0c8e67b056d7b0ec33d52","moonshotai-cn.json":"326de79b46f6fdbf0879f005b3040d1b14049e81fa678de407b73ed1bd3bce9c","moonshotai.json":"4b917a375e9c880b903927cf5fe1ae1052c74b64d5027a007bb1e67ad0410755","nvidia.json":"6493e09b5db8b8fe9c8fae5bc15dc287645f37f83a4ed3a1ad1181e5830b5ff7","openai.json":"4c18b28b168f56d621670866a62d2d435da3a8eee55a204bdbabe02c48e6513e","opencode-go.json":"00b2c0154285829e8db1e7b9ad492771b098cb88a61b036e51a6861b2f2f6dc1","opencode.json":"1655e248e76f919fd14ca4b8a6201d1dd89b2190b295028c5d7a4a2a03809946","opengateway.json":"121f544154757cfbccd3bc3d7344986a5822886c12e1c00b39ebf20ef609f239","openrouter.json":"ac1c0d449b1fbaa6cd36c55408789ac6e91a861af525045ab0168214f18c8164","qwen-token-plan-cn.json":"4bf520ed38e5c3045680d2ec2816fa8a69079dbe97f7b929767a63172288da6b","qwen-token-plan-individual.json":"eb040e78595853e980011308b1bf9caacae6dc8a91abbac5ffebd402515dd2a6","qwen-token-plan.json":"a5c67213745329ec3ba79f712f31c1c32a781d7ffb8c04f9819f36c27672531f","together.json":"200fa2cc0b4666e3818c260e41b9925b925180e2d0931d7c900e16494b2314b4","venice.json":"d34b566ee6e7a2ed31abd9099a1c0448f1d521fc696382d03d86c90634270cf1","vercel-ai-gateway.json":"95a697cd6f9eba46972dbfa301e80f18f45c701e6068c0326d2f36da10b7ce7e","xai.json":"9b23faa5218e966a17108c05d2498a617ade1eae9b0d0684284c77d67949a3f1","xiaomi-token-plan-ams.json":"7df74a255259fc1d5be8e682c3ffb0908eaaf0af711f6433dedcf345ce3e8393","xiaomi-token-plan-cn.json":"7dc672723d5b37e5b92605752f74f46f44eba10be2b0cb959790260b9e0c2a4e","xiaomi-token-plan-sgp.json":"69b333b158be038459a511a673f4536a5b03d4931c3119339f1ac741bc03d502","xiaomi.json":"333299657f211ad9edac0f3d4298b7e5776626914c9c80e22305a87533f23966","zai-coding-cn.json":"69c572aa75104f16ccbddbaace60c267f4bcc74f86d34891ca43110d312fb1ad","zai.json":"15c8182a88e58bdc1b1c3eb0603c690475342c8a826ceddfe9af1b36b28345ee"}}
|