pi-claude-native 0.0.0-stage → 0.9.1
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 +111 -0
- package/LICENSE +22 -0
- package/README.md +115 -2
- package/docs/adr/0001-replay-input-contract.md +29 -0
- package/docs/adr/0002-per-turn-effort-placement.md +43 -0
- package/docs/architecture.md +182 -0
- package/docs/maintaining.md +88 -0
- package/docs/startup.md +25 -0
- package/docs/testing.md +146 -0
- package/docs/transport-measurements.md +296 -0
- package/docs/troubleshooting.md +77 -0
- package/docs/usage.md +104 -0
- package/extensions/claude-native/index.ts +33 -0
- package/package.json +56 -4
- package/scripts/trace-native.mjs +66 -0
- package/scripts/verify.py +223 -0
- package/scripts/verify_warm.py +255 -0
- package/src/admission.ts +743 -0
- package/src/billing.ts +87 -0
- package/src/child-cwd.ts +38 -0
- package/src/completion.ts +445 -0
- package/src/discovery.ts +159 -0
- package/src/docs-section.ts +64 -0
- package/src/errors.ts +89 -0
- package/src/inert-mcp.ts +69 -0
- package/src/lazy-stream.ts +41 -0
- package/src/measurements.ts +149 -0
- package/src/model-compat.ts +9 -0
- package/src/models.ts +28 -0
- package/src/native-context.ts +491 -0
- package/src/native-output.ts +279 -0
- package/src/native-rewrite.ts +70 -0
- package/src/process.ts +334 -0
- package/src/provider.ts +82 -0
- package/src/replay.ts +527 -0
- package/src/request-plan.ts +79 -0
- package/src/request.ts +480 -0
- package/src/setup.ts +205 -0
- package/src/stream.ts +441 -0
- package/src/transcript.ts +193 -0
- package/src/turn-effort.ts +138 -0
- package/src/types.ts +125 -0
- package/src/upstream-agent.ts +22 -0
- package/src/web-catalog.ts +253 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Maintaining the repository
|
|
2
|
+
|
|
3
|
+
## Triage issues
|
|
4
|
+
|
|
5
|
+
Use these issue searches:
|
|
6
|
+
|
|
7
|
+
- New reports: `is:issue is:open label:needs-triage`
|
|
8
|
+
- Waiting for a reporter: `is:issue is:open label:needs-info`
|
|
9
|
+
- Verified bugs: `is:issue is:open label:bug label:confirmed`
|
|
10
|
+
|
|
11
|
+
Remove `needs-triage` after reviewing an issue. Use `needs-info` only when specific information is missing.
|
|
12
|
+
Remove `needs-info` and `awaiting-response` when the requested information arrives. Use `confirmed` for verified problems.
|
|
13
|
+
Link duplicates to the existing issue before closing them.
|
|
14
|
+
|
|
15
|
+
The daily cleanup workflow only handles `needs-info` issues. It reminds after 30 inactive days and closes after another 14.
|
|
16
|
+
Confirmed issues and milestone issues are exempt. The workflow does not clean up pull requests.
|
|
17
|
+
|
|
18
|
+
Closing an issue automatically locks its discussion. Cleanup also locks the issues it closes.
|
|
19
|
+
Existing closed issues are not backfilled. To continue a discussion after reopening an issue, manually unlock it in GitHub.
|
|
20
|
+
Reporters can request reopening through a new help issue that links the closed issue.
|
|
21
|
+
See [contribution guidance](../CONTRIBUTING.md) for the reporter-facing policy.
|
|
22
|
+
|
|
23
|
+
Public contributions are issue-only. If an unsolicited pull request arrives, explain that policy and close it.
|
|
24
|
+
Maintainer pull requests remain available for branch review and CI.
|
|
25
|
+
|
|
26
|
+
## Watch the upstream tracker
|
|
27
|
+
|
|
28
|
+
`.github/workflows/upstream-watch.yml` runs daily and appends upstream
|
|
29
|
+
[anthropics/claude-code](https://github.com/anthropics/claude-code) issues to a single
|
|
30
|
+
`Upstream Claude Code watch` issue. It looks for two kinds of report:
|
|
31
|
+
|
|
32
|
+
- A literal identifier this transport writes or reads: the billing identity label, the
|
|
33
|
+
`CLAUDE_CODE_*` variables the child receives, `isUsingOverage`, `unifiedWindows`, or the
|
|
34
|
+
setting that decides whether replayed thinking is sent (`CLAUDE_CODE_RUSTLING_PIXEL`,
|
|
35
|
+
`tengu_rustling_pixel`).
|
|
36
|
+
- A report about the SDK, `claude -p`, or headless use that also discusses metering. Reports
|
|
37
|
+
about the desktop app, VS Code, Cowork, and other clients stay out of this group.
|
|
38
|
+
|
|
39
|
+
Search hits are only a recall step, because GitHub search treats hyphens and underscores as
|
|
40
|
+
word breaks. Relevance is decided on the raw issue text. The tracker issue body holds the last
|
|
41
|
+
scan date and the numbers already reported, so a rerun never repeats an item, and that issue
|
|
42
|
+
doubles as the archive. Deleting the state marker stops the job rather than duplicating the
|
|
43
|
+
archive.
|
|
44
|
+
|
|
45
|
+
The same job reports when the published `@anthropic-ai/claude-code` version leaves
|
|
46
|
+
`QUALIFIED_CLI_RANGE`. That is the signal to repeat the verification in [testing.md](testing.md).
|
|
47
|
+
|
|
48
|
+
Run the workflow manually from the Actions tab to preview a report with `dry_run`. Set
|
|
49
|
+
`lookback_days` to widen the window, which applies only while no previous report exists.
|
|
50
|
+
|
|
51
|
+
## Develop on branches
|
|
52
|
+
|
|
53
|
+
Push work to a branch. CI checks types and compilation on branch pushes and pull requests targeting `main`.
|
|
54
|
+
These checks do not exercise the installed Pi runtime or make paid model requests.
|
|
55
|
+
Use the [verification guidance](testing.md) for local runtime checks.
|
|
56
|
+
|
|
57
|
+
Merge the branch into `main` with a squash merge once CI passes. Git installations that follow `main` pick the change up with `pi update git:github.com/yamatakeru/pi-claude-native`; npm installations get it with the next release.
|
|
58
|
+
|
|
59
|
+
To release, open a pull request that:
|
|
60
|
+
|
|
61
|
+
- bumps `version` in `package.json` and both `version` fields at the top of `package-lock.json`;
|
|
62
|
+
- adds a `## [x.y.z] - YYYY-MM-DD` section at the top of `CHANGELOG.md`.
|
|
63
|
+
|
|
64
|
+
Pi reads `CHANGELOG.md` and, on the first fresh launch after `pi update`, shows the sections newer than the version it last saw under "Extension Updates" (see Pi's `docs/packages.md`, "Show a changelog"). Pi compares against `version` in `package.json`, so a change merged without a version bump is never shown. Pi ends a section at the next `## ` line, so headings inside a section use `###`. Keep each section short and written for users; it is shown in full.
|
|
65
|
+
|
|
66
|
+
The release notes start from that section and add what does not belong in Pi's notice, such as maintenance notes and how to pin the release. The README's pinned-install example names the new tag, so create it right after the merge, on the squash commit the merge put on `main`. Push the tag first, then publish the release on it:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
git tag v<x.y.z> <squash-commit-sha> && git push origin v<x.y.z> &&
|
|
70
|
+
gh release create v<x.y.z> --verify-tag --title v<x.y.z> --notes-file <notes>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`git push` refuses a tag that already exists on the remote and fails when the remote is unreachable, so the release is never published on another commit. `--verify-tag` publishes only on the pushed tag.
|
|
74
|
+
|
|
75
|
+
The pushed tag also starts the Release workflow (`.github/workflows/release.yml`). It refuses a tag that does not match `version` in `package.json` and `package-lock.json`, runs the same checks as CI, and stages the package on npm with `npm stage publish` through trusted publishing. Approve the staged version with 2FA on npmjs.com (Staged Packages) or with `npm stage approve <stage-id>`; until then it is not installable. The workflow skips a version that is already on npm.
|
|
76
|
+
|
|
77
|
+
The trusted publisher is configured on npmjs.com under the package's Settings, Trusted publishing: GitHub Actions, user `yamatakeru`, repository `pi-claude-native`, workflow `release.yml`, with only `npm stage publish` allowed. A new configuration expires unless a publish succeeds within 2 days, so add it just before a release. The first version on npm, 0.9.1, is published by hand, because a trusted publisher can only be added to a package that already exists on npm. Run `npm publish` from a clean checkout of the squash commit before pushing the tag, so the tag's run finds the version on npm and skips it.
|
|
78
|
+
|
|
79
|
+
Installations pinned to a tag stay on it until the user changes the ref.
|
|
80
|
+
|
|
81
|
+
## Security and dependencies
|
|
82
|
+
|
|
83
|
+
Dependabot vulnerability alerts remain enabled. Automatic security-update and version-update pull requests are disabled.
|
|
84
|
+
Review alerts in GitHub's Security tab and update affected dependencies on a maintenance branch.
|
|
85
|
+
Review CodeQL and secret-scanning alerts there as well. See the [security policy](../SECURITY.md) for private reports.
|
|
86
|
+
|
|
87
|
+
Workflow actions are pinned to full commit SHAs. When updating one, verify the upstream release and commit before changing the pin.
|
|
88
|
+
Branch CI has read-only repository access.
|
package/docs/startup.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Startup
|
|
2
|
+
|
|
3
|
+
Claude Native registers synchronously with an empty catalog. The extension factory starts no processes, sockets, timers, or web requests. The streaming API is registered independently of the catalog, so an empty catalog does not disable dispatch. The request transport remains lazy.
|
|
4
|
+
|
|
5
|
+
## Catalog initialization
|
|
6
|
+
|
|
7
|
+
- Cache-only refresh restores a previously verified snapshot from Pi’s model store. It runs no CLI probes or web requests.
|
|
8
|
+
- Restored entries are labeled `last verified snapshot`. A fresh offline install has no Claude Native models.
|
|
9
|
+
- Interactive Pi starts live refresh after the editor is ready. Claude Native uses this lifecycle, not an extension startup hook.
|
|
10
|
+
- Live refresh loads discovery modules asynchronously. It uses a supervised child for the Claude Code picker and asynchronous requests for public Anthropic documentation.
|
|
11
|
+
- Refresh has a 12-second deadline and honors Pi’s abort signal. Documents have a 2 MiB limit. At most four model pages load concurrently.
|
|
12
|
+
- Successful refresh persists verified metadata. Repeated refreshes reuse it for 15 minutes unless forced.
|
|
13
|
+
- Missing metadata excludes the affected model with a warning. A refresh failure reports the error and deletes the snapshot. There is no pinned fallback.
|
|
14
|
+
|
|
15
|
+
On a fresh installation, start interactive Pi, open `/model`, and wait for Claude Native catalog refresh. Only then select a Claude Native model from the command line. `pi update --models` refreshes catalogs without loading extensions, so it does not initialize Claude Native (checked in Pi 1.3.0 and in the Pi 1.5.0 source). A saved snapshot supports later offline and non-interactive invocations.
|
|
16
|
+
|
|
17
|
+
## Lazy loading
|
|
18
|
+
|
|
19
|
+
The stream wrapper imports `@earendil-works/pi-ai/compat`, which Pi resolves for extensions. It does not import the incompatible `@earendil-works/pi-ai/api/lazy` subpath. Discovery and transport modules stay outside the extension load path.
|
|
20
|
+
|
|
21
|
+
## Verification
|
|
22
|
+
|
|
23
|
+
The installed-Pi harness checks offline model listing and verifies that no Claude child starts. Paid scenarios verify fresh requests and complete-session forks through the user’s real Pi executable.
|
|
24
|
+
|
|
25
|
+
Offline listing is not an interactive editor-readiness benchmark. The harness does not promise zero extension overhead. See [testing.md](testing.md).
|
package/docs/testing.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Verification
|
|
2
|
+
|
|
3
|
+
Verification has two layers. The offline regression suite runs in seconds on Node 22 and covers transport contracts with a fake `claude` child and loopback endpoints. The installed-Pi harness runs the operator’s real `pi` executable with real settings, credentials, tools, and extensions. It uses a private copy of the user’s configuration and replaces only the Claude Native extension source with the local checkout. This prevents the installed package from overwriting the local code during extension loading.
|
|
4
|
+
|
|
5
|
+
## Offline regression suite
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm test
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Run it on Node 22. The suite uses `node:test` and needs no subscription credentials. It covers contracts that the installed-Pi harness cannot guard cheaply:
|
|
12
|
+
|
|
13
|
+
- The admission relay forwards one upstream Messages request and rejects a second locally, including the tool-change beta and request rewrite.
|
|
14
|
+
- The signed-thinking carrier restores only while the visible projection matches.
|
|
15
|
+
- Backend overrides are rejected and API credentials are left out of the Claude Code environment without the test bypass.
|
|
16
|
+
- The usage-credit guard stops an overage turn, permits it with `PI_CLAUDE_NATIVE_ALLOW_USAGE_CREDITS=1`, and marks the error non-retryable.
|
|
17
|
+
- Discovery reads the served picker first-party with Claude Code's feature flags on, keeps a user's opt-out and the loopback guard behind another backend, and fails on a model-turn record.
|
|
18
|
+
- A thinking-level change states each replayed turn's effort where Claude Code places it in its own sessions, keeps the previous request as the prefix of the next, and forwards any other request shape as built.
|
|
19
|
+
- The tool-change plan emits by-value additions (including redefinitions), removals, and assistant-boundary injections only when model metadata supports them.
|
|
20
|
+
|
|
21
|
+
The tests use a temporary fake executable in the stream tests. They do not prove real Claude generation, native cache reuse, or upstream acceptance of the beta fields. The installed-Pi harness remains the end-to-end gate.
|
|
22
|
+
|
|
23
|
+
## Harness requirements
|
|
24
|
+
|
|
25
|
+
- Python 3 and the user’s installed Pi on `PATH`.
|
|
26
|
+
- Claude Code installed and authenticated for paid scenarios.
|
|
27
|
+
- A discovered model in the user’s Pi configuration or verified model snapshot.
|
|
28
|
+
|
|
29
|
+
Use `--pi /absolute/path/to/pi` or `PI_VERIFY_PI` to select another installed executable. The harness excludes repository `node_modules` directories from automatic executable lookup.
|
|
30
|
+
|
|
31
|
+
## Verified environments
|
|
32
|
+
|
|
33
|
+
The current maintainer installation is Pi 1.5.2 with Claude Code 2.1.322 on macOS arm64, confirmed with `pi --version` and `claude --version`. On 2.1.322, these checks passed:
|
|
34
|
+
|
|
35
|
+
- Offline startup (`npm run verify`): no stderr and no observed Claude child starts.
|
|
36
|
+
- Fresh model request (`fresh --paid`): a successful response with the user's full extension set.
|
|
37
|
+
- Model discovery from the served picker, as a temporary check. The real CLI handshake with discovery's own arguments returned 14 picker rows, and stdout carried only `control_response`. Models built from those rows by discovery and the catalog were then called directly through `streamClaudeNative`, twice each, with a one-tool system message: Haiku 4.5 and Sonnet 5 both replied `OK` with status 200.
|
|
38
|
+
- Complete existing-session replay (`session --paid`): a session written by `openai-codex/gpt-6.1-sol` (18 assistant turns) continued on Haiku 5.5, with 17 replay acknowledgments.
|
|
39
|
+
- Thinking-level change (direct calls through `streamClaudeNative`, Haiku 5.5, one session at high, high, medium, with a 15K-token system prompt): two runs. In each, the third request read everything the second request had read and written, and `nativeContext` reported `turnEffortsPlaced`.
|
|
40
|
+
- First run: 15,137 tokens read and 12 written; the replies had no thinking.
|
|
41
|
+
- Second run, with replayed signed thinking: 15,233 tokens read and 402 written. Every reply carried signed thinking, and the third, at medium, was accepted with the first two turns' thinking replayed. It answered from both earlier results (346 + 427 = 773).
|
|
42
|
+
- Warm cache (`warm --paid`, no prior session): three successful requests, one upstream request each, stable Pi prompt and tool hashes, and cache reads of 64.26%, 98.76% and 99.85%. The first request read a prefix that an earlier run had written within the cache lifetime; on a cold cache earlier the same day it read 0%.
|
|
43
|
+
|
|
44
|
+
Replayed thinking was last checked on Claude Code 2.1.318, as a temporary check: signed thinking from an earlier turn was still sent and accepted; conversations started on 2.1.286 or earlier were not tested (see [Replayed thinking and the assistant model](transport-measurements.md#replayed-thinking-and-the-assistant-model)).
|
|
45
|
+
|
|
46
|
+
After the replacement `<docs>` section was updated for Pi 1.5.0, a navigation check ran on Claude Code 2.1.316 as temporary checks with `--tools read,bash`. Asked how a script calls a classifier and whether Pi can generate images, Haiku 5.5 opened `docs/docs.json`, then `codemode.md`, and answered correctly. Both requests were accepted as plan usage.
|
|
47
|
+
|
|
48
|
+
The replacement `<docs>` section depends on Anthropic's undocumented classification (see [Plan usage metering](usage.md#plan-usage-metering)). Repeat the fresh request after each Pi or Claude Code update, and after any change to Pi's default system prompt or to the replacement text.
|
|
49
|
+
|
|
50
|
+
Before v0.1.7 was released, both compiled-Pi fixes passed these checks on macOS arm64 with compiled Pi 1.2.0 / Bun 1.3.14 and Claude Code 2.1.281:
|
|
51
|
+
|
|
52
|
+
- The emitted MCP helper command completed initialization and tool listing, and denied native tool execution. The pre-fix command started Pi instead and failed in an empty offline configuration.
|
|
53
|
+
- The real CLI discovery flow returned seven picker models under Node 26.10.0 and compiled Pi. The pre-fix compiled runtime failed during server cleanup.
|
|
54
|
+
- The compiled Pi model picker completed live catalog refresh in a private configuration. A separate offline process listed 13 Claude Native entries, including aliases.
|
|
55
|
+
- Six paid calls to `claude-haiku-4-5-20251001` covered a fresh response and a four-turn conversation. The conversation remembered a marker, read a real file through Pi's `read` tool, recalled both values, and recalled them again after restarting Pi and resuming.
|
|
56
|
+
- Each model call in the four-turn conversation admitted one upstream request with none blocked. The conversation produced no unintended Pi sessions or stderr errors.
|
|
57
|
+
|
|
58
|
+
The paid conversation checks used both fixes together. The protocol, discovery, model-picker, and multi-turn conversation checks used temporary verification scripts, not permanent harness scenarios. They do not add repeatable tool-round-trip coverage to the repository. The short paid conversations reported no cache reads and do not verify warm-cache reuse. Separate long-session cache measurements are recorded in [transport-measurements.md](transport-measurements.md).
|
|
59
|
+
|
|
60
|
+
The two compiled-Pi fixes have not been verified locally on Linux. Earlier Linux lifecycle and cache measurements remain separate evidence; they do not cover these fixes.
|
|
61
|
+
|
|
62
|
+
## Scenarios
|
|
63
|
+
|
|
64
|
+
### Offline startup
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
npm run verify
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Equivalent command:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
python3 scripts/verify.py startup
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The harness runs offline model listing through the installed Pi. It checks the process result and verifies that no Claude child starts. This is not an interactive editor-readiness benchmark.
|
|
77
|
+
|
|
78
|
+
### Fresh model request
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
python3 scripts/verify.py fresh --paid
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The harness starts a real Pi session and requests a short greeting from Haiku 5.5. It requires a successful terminal response, nonempty visible text, and output usage. It does not require exact model wording. Pi can exit successfully after a model error.
|
|
85
|
+
|
|
86
|
+
### Complete existing-session replay
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
python3 scripts/verify.py session --paid --session /absolute/path/to/session.jsonl
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The harness copies every session entry into a private temporary snapshot. Pi forks that snapshot and submits the verification prompt. It does not filter errors, replace tools, remove thinking, shorten history, or write to the original session. The user’s working directory, credential values, tools, and other extensions remain in effect. Configuration and credentials are copied privately, not written back.
|
|
93
|
+
|
|
94
|
+
This is the primary regression check for provider switching and accumulated session history. A fresh request passing does not establish that an existing session works.
|
|
95
|
+
|
|
96
|
+
### Same-process warm-cache measurement
|
|
97
|
+
|
|
98
|
+
Check RPC setup without model requests:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
python3 scripts/verify.py warm --dry-run
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
After approving three paid model requests, run:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
python3 scripts/verify.py warm --paid --session /absolute/path/to/session.jsonl
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Omit `--session` to start without existing history. The scenario keeps one Pi RPC process alive for exactly three greeting prompts. It disables Pi auto-retry, auto-compaction, and idle cache warming. It snapshots an existing session once and retains the original tool declarations and other extensions.
|
|
111
|
+
|
|
112
|
+
A private benchmark extension blocks tool execution and allows at most one agent provider request per prompt and three in total. An unexpected provider call stops Pi with exit code `78` before the provider runs. This guard applies only to the benchmark, not normal provider behavior. Do not use extensions that independently make model calls outside Pi's agent request path.
|
|
113
|
+
|
|
114
|
+
Each turn must produce one successful assistant response and admit exactly one upstream request. The harness rejects unexpected tools, retries, compaction, changed Pi prompt/tool hashes, or mismatched native-child counts. Model-discovery children are counted separately because their initialize handshake does not make a model request.
|
|
115
|
+
|
|
116
|
+
The summary reports wall time, token usage, cache-read share, phase timings, replay counts, request bytes, connection reuse, and Pi prefix hashes. It does not save model response text or native request bodies. It reports reuse without asserting a universal percentage: cache size thresholds, lifetime, model, and actual prompt changes affect results. The hashes cover Pi inputs, not CLI-added context.
|
|
117
|
+
|
|
118
|
+
`--dry-run` initializes RPC, checks the model and guard, and sends no model prompts. A discovery child can still start. Dry-run success does not establish successful generation or warm-cache reuse. For this scenario, `--timeout` bounds each prompt; initialization commands have a maximum of 30 seconds, followed by bounded shutdown.
|
|
119
|
+
|
|
120
|
+
## Options and safety
|
|
121
|
+
|
|
122
|
+
- `--model provider/model` selects the model. The default is `claude-native/claude-haiku-5-5`.
|
|
123
|
+
- `--extension PATH` selects the extension. The default is this checkout’s entry point.
|
|
124
|
+
- `--timeout SECONDS` bounds the scenario. The default is 120 seconds.
|
|
125
|
+
- `--paid` explicitly approves real model requests. Without it, paid scenarios refuse to run.
|
|
126
|
+
|
|
127
|
+
Run from the same project directory as the original session. Existing tool permissions and extension behavior still apply. The verification prompt asks the model not to call tools, but the harness does not replace the user’s tools.
|
|
128
|
+
|
|
129
|
+
Private artifacts go to `/tmp/pi-claude-native-verify-*`. The harness removes copied configuration, credentials, input snapshots, and Pi-generated test sessions after the run. It retains summaries and bounded native error diagnostics. It does not save model responses, credentials, or successful native transcript records. Native errors can quote input fragments; inspect the diagnostics before sharing them.
|
|
130
|
+
|
|
131
|
+
The observer records real Claude process starts, replay acknowledgments, terminal result errors, and exits. It does not substitute native responses or alter the provider request. For the fresh and session scenarios, automatic Pi retries can consume additional allowance if enabled in the user's settings. The warm scenario explicitly disables them and installs its request-budget guard.
|
|
132
|
+
|
|
133
|
+
## Build checks and CI
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
npm run check
|
|
137
|
+
npm run build
|
|
138
|
+
npm test
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The first two commands check types and compilation. `npm test` runs the offline regression suite on Node 22, including fixture tests for the request rewrites and relay failure handling, grouped by issue. End-to-end provider tests run each call through a fake `claude` child and an optional loopback upstream from `tests/support/native.ts`. Branch and pull request CI run all three checks. Paid runtime verification stays local because CI is not the user’s Pi installation and has no approved subscription credentials.
|
|
142
|
+
|
|
143
|
+
The permanent harness does not yet exercise an in-process `/reload`, forced subprocess stalls, cancellation, or an explicit tool round trip. Temporary checks described above do not extend the supported scenarios.
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
Separate temporary Linux supervisor checks verified forced write/read/exit stalls and cancellation using real subprocesses and process groups. A three-request installed-Pi check verified cache reuse after restarting Pi. These checks are recorded in [transport-measurements.md](transport-measurements.md#linux-lifecycle-and-cross-restart-verification); they are not additional scenarios in the supported harness.
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# Long-session transport measurement baseline
|
|
2
|
+
|
|
3
|
+
The historical cache-anchor repair achieved **99.59% and 99.71% cache-read share on two warm turns** in a three-request same-process verification. Both warm turns also reused the upstream HTTPS connection.
|
|
4
|
+
|
|
5
|
+
This report records the original timing baseline, the cache investigation, and the post-fix verification. Workloads differ between batches, so their timings do not establish a transport speedup.
|
|
6
|
+
|
|
7
|
+
## Original 30-run timing result
|
|
8
|
+
|
|
9
|
+
The median provider-call completion time was **2,345.663 ms**. The nearest-rank p95 was **4,037.668 ms**. The upstream request-to-first-data interval had a median of **1,415.731 ms** and a p95 of **3,328.727 ms**.
|
|
10
|
+
|
|
11
|
+
| Measurement | Median | p95, nearest rank |
|
|
12
|
+
|---|---:|---:|
|
|
13
|
+
| Whole Pi process wall time | 3,704 ms | 5,631 ms |
|
|
14
|
+
| Provider call to completion | 2,345.663 ms | 4,037.668 ms |
|
|
15
|
+
| History preparation | 1.785 ms | 2.827 ms |
|
|
16
|
+
| Setup after history | 18.849 ms | 27.932 ms |
|
|
17
|
+
| Child spawn to replay complete | 436.408 ms | 704.098 ms |
|
|
18
|
+
| Replay complete to upstream request | 28.647 ms | 42.274 ms |
|
|
19
|
+
| Upstream request to first response data | 1,415.731 ms | 3,328.727 ms |
|
|
20
|
+
| First response data to upstream response end | 274.013 ms | 332.143 ms |
|
|
21
|
+
| Upstream request to response end | 1,743.929 ms | 3,478.182 ms |
|
|
22
|
+
| Upstream response end to child exit | 79.919 ms | 94.986 ms |
|
|
23
|
+
| Child exit to Pi completion | 0.427 ms | 0.711 ms |
|
|
24
|
+
|
|
25
|
+
The nearest-rank p95 for 30 samples is the 29th value, or the second-slowest sample. Treat it as an exploratory estimate from this one workload.
|
|
26
|
+
|
|
27
|
+
## Workload and method
|
|
28
|
+
|
|
29
|
+
- Model: `claude-directsdk/claude-haiku-5-5`.
|
|
30
|
+
- Runs: 30 sequential replays of the same saved session; all 30 succeeded.
|
|
31
|
+
- Session: 32 tool calls and 467,954 bytes on disk.
|
|
32
|
+
- Per replay: 69 prepared frames, including 68 historical frames and 34 historical user acknowledgments.
|
|
33
|
+
- Upstream request body: 347,888 bytes in every run.
|
|
34
|
+
- Percentiles: median and nearest-rank p95, calculated separately for each measurement.
|
|
35
|
+
|
|
36
|
+
The batch ran with opt-in prefix hashing enabled. The measured system-prompt hash differed in all 30 runs; the tool-definition hash was the same in all 30. This shows that the Pi-supplied prompt input was not byte-identical across runs. The hashes do not identify the cause or which prompt portion changed. This report does not include hashes, prompts, or tool arguments.
|
|
37
|
+
|
|
38
|
+
## Cache counters
|
|
39
|
+
|
|
40
|
+
The median cache-read share was **2.2028%**; its nearest-rank p95 was **2.2030%**. Across all runs, reported usage counters summed to 60 input tokens, 377 output tokens, 88,530 cache-read tokens, and 3,930,250 cache-write tokens.
|
|
41
|
+
|
|
42
|
+
These counters describe token usage and reuse. They do not state subscription allowance deductions or billed cost. Because the system-prompt hash changed between runs, do not treat this result as a fixed-prefix cache benchmark without first identifying the changing prompt input.
|
|
43
|
+
|
|
44
|
+
## Limits
|
|
45
|
+
|
|
46
|
+
This is one long-session scenario for one model. It is not a before-and-after comparison, and it does not show that a proposed transport change would improve latency.
|
|
47
|
+
|
|
48
|
+
The upstream request-to-first-data interval combines transport setup and upstream response delay. The measurement does not isolate DNS, connection establishment, response headers, or model processing. First response data is not necessarily the first generated token.
|
|
49
|
+
|
|
50
|
+
A separate single fresh-request check is not part of these N=30 statistics and is not a short-session cohort. This report also does not cover image-heavy requests, failed-call diagnostics, or end-to-end multi-turn task duration.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## Same-process cache investigation
|
|
54
|
+
|
|
55
|
+
Three additional paid requests ran sequentially through one Pi RPC process, using a private fork of an existing session and a fixed agent directory. All three succeeded, each admitted one upstream request, and the process produced no stderr. Auto-retry and auto-compaction were disabled. This measurement ran before connection pooling was implemented.
|
|
56
|
+
|
|
57
|
+
The starting session was 746,001 bytes, not the 467,954-byte session used in the earlier 30-run baseline. Do not compare their timings as a transport improvement.
|
|
58
|
+
|
|
59
|
+
| Turn | RPC prompt to settled | Provider completion | Cache-read tokens | Cache-write tokens | Cache-read share |
|
|
60
|
+
|---|---:|---:|---:|---:|---:|
|
|
61
|
+
| 1 | 3,620.861 ms | 3,510.288 ms | 2,951 | 196,674 | 1.4783% |
|
|
62
|
+
| 2 | 3,097.597 ms | 3,082.145 ms | 7,298 | 192,399 | 3.6545% |
|
|
63
|
+
| 3 | 2,365.867 ms | 2,350.713 ms | 7,298 | 192,460 | 3.6534% |
|
|
64
|
+
|
|
65
|
+
The Pi system prompt, native system blocks, and native tool definitions had identical content hashes across all three calls. Request bodies were 530,509, 530,844, and 531,134 bytes. Replay required 59, 60, and 61 historical user acknowledgments. History preparation took 0.930–2.542 ms, child spawn to replay completion took 531.599–602.012 ms, and replay completion to upstream request took 33.959–52.514 ms.
|
|
66
|
+
|
|
67
|
+
The earlier fresh-process benchmark changed paths that Pi includes in its system prompt. The same-process run removes that confound, but cache reuse remains low. Stable system and tool content alone does not explain the missing history reuse.
|
|
68
|
+
|
|
69
|
+
### Native prefix observations
|
|
70
|
+
|
|
71
|
+
A private observer recorded content hashes, byte counts, and cache-control positions at the native request boundary. It did not save prompt text, tool arguments, or authentication headers.
|
|
72
|
+
|
|
73
|
+
- Native requests contained 121, 123, and 125 messages.
|
|
74
|
+
- Two system text blocks had explicit one-hour cache controls.
|
|
75
|
+
- There were no tool breakpoints or top-level cache controls.
|
|
76
|
+
- Each request had one message breakpoint, on the first block of its final system-role message. That block had identical content across the three calls.
|
|
77
|
+
- For each consecutive pair, earlier history was identical. Differences began at the former final user message: its replay lost tail blocks, and the former final system-message position became an assistant message.
|
|
78
|
+
|
|
79
|
+
Thus, the conversation cache breakpoint was after native tail content that the next replay did not reproduce. There was no explicit conversation anchor in the unchanged historical prefix. The official [prompt-caching guidance](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) requires exact matching prefixes and describes the distinction between cache-writing breakpoints and the limited backward lookup window. A stable earlier prefix does not, by itself, establish that the API cached that prefix.
|
|
80
|
+
|
|
81
|
+
These observations support adding an explicit anchor to stable historical replay content. Do not remove legitimate Pi instructions or freeze the system prompt to improve cache reuse.
|
|
82
|
+
|
|
83
|
+
### Cache-anchor repair
|
|
84
|
+
|
|
85
|
+
`src/replay.ts` now places one explicit cache control on the last nonempty block of the latest historical user/tool-result frame. It does not mark the final user frame: the CLI inserts transient reminder content inside that frame. The historical anchor lets the API write a reusable prefix before that transient content.
|
|
86
|
+
|
|
87
|
+
The change affects replay metadata only. It does not edit Pi messages, tool arguments, tool-result associations, system instructions, or signed thinking. If there is no historical user frame, it adds no anchor. The native completion gate and one-request admission limit remain unchanged.
|
|
88
|
+
|
|
89
|
+
Replay and native breakpoints use matching cache lifetimes. The transport defaults to `1h`, matching the original measured subscription requests. Set `CLAUDE_CODE_PROMPT_CACHE_TTL=5m` for a five-minute lifetime. `FORCE_PROMPT_CACHING_5M` takes precedence. Invalid TTL values produce an error that names the setting. Global and model-family cache-disable flags suppress the replay anchor.
|
|
90
|
+
|
|
91
|
+
The existing benchmark was run against a loopback-only, refused upstream to inspect real CLI request generation without paid requests. The CLI preserved the historical anchor before the final user and system-tail messages. Requests had four breakpoints: two system blocks, the historical anchor, and the native tail. Both one-hour and forced five-minute settings produced compatible cache controls.
|
|
92
|
+
|
|
93
|
+
Those model calls failed as expected because the upstream was blocked. That check verified native marker emission only. The subsequent paid verification below confirmed successful generation and high warm-cache reuse with one-hour controls. Longer gaps or more than the API's backward lookup window between anchors can still prevent reuse.
|
|
94
|
+
|
|
95
|
+
### Replay-stable native context
|
|
96
|
+
|
|
97
|
+
The historical anchor still left the previous call's final frame uncached. On Claude Code 2.1.303, a loopback capture of real CLI requests (structure and hashes only) showed three causes:
|
|
98
|
+
|
|
99
|
+
- The CLI's session-context reminder, which carries the account email in an empty child directory, is added to the final frame. It appears in three places: as text before the user message, as a suffix of a string tool result, or as trailing text after parallel tool results and a user message.
|
|
100
|
+
- From the second request on, today's date moves from the end of the environment system message to a trailing system message. That changes the conversation's second message after call 1, so call 2 missed everything after it.
|
|
101
|
+
- The CLI's message breakpoint sits on that trailing date message, which never recurs.
|
|
102
|
+
|
|
103
|
+
`CLAUDE_CODE_DISABLE_ATTACHMENTS=1` did not change the request. No setting was found that removes the reminder while keeping OAuth.
|
|
104
|
+
|
|
105
|
+
The relay now restores a replay-stable prefix (see [architecture.md](architecture.md#replay-stable-native-context-srcnative-contextts)). This ports the Hermes reference relay's `restore_queried_turn`, `stable_request_context`, `_frame_first`, and `pin_message_breakpoint` ([hermes-official-plugins#83](https://github.com/NousResearch/hermes-official-plugins/pull/83) and earlier changes). After the change, the same capture showed every request's prefix through its message breakpoint replayed unchanged in the next request. This held for user turns, a tool round, and parallel tools followed by text. Each request still had at most four breakpoints.
|
|
106
|
+
|
|
107
|
+
A paid three-request `warm` run then went through installed Pi 1.3.0 with Claude Code 2.1.303 and `claude-haiku-5-5`. It used no prior session and short prompts. All three requests succeeded, each admitted one upstream request, and the Pi prompt and tool hashes stayed stable:
|
|
108
|
+
|
|
109
|
+
| Request | Cache-read tokens | Cache-write tokens | Cache-read share |
|
|
110
|
+
|---|---:|---:|---:|
|
|
111
|
+
| 1 | 19,431 | 9,109 | 66.93% |
|
|
112
|
+
| 2 | 28,540 | 376 | 98.69% |
|
|
113
|
+
| 3 | 28,916 | 56 | 99.79% |
|
|
114
|
+
|
|
115
|
+
Request 1 read the system and tools prefix cached by earlier runs. Requests 2 and 3 read the whole previous request and wrote only the new turn. Before this change, the previous call's final frame was written again on every call, and call 2 re-wrote the opening turn.
|
|
116
|
+
|
|
117
|
+
After the upstream v0.2.0 merge (fork #19, commit `0700cf7`), the same run on installed Pi 1.4.0 with Claude Code 2.1.310 started from an empty cache and read 0%, 98.76% and 99.85% (28,705 / 357 / 41 cache-write tokens). Each request again admitted one upstream request with stable Pi prompt and tool hashes. After the upstream v0.3.0 merge, with Claude Code 2.1.312 and the CLI's own `sdk-cli` label ([fork #23](https://github.com/yamatakeru/pi-claude-directsdk/issues/23)), it read 0%, 98.64% and 99.87% (25,953 / 353 / 30 cache-write tokens), again with stable hashes. After the upstream v0.3.1 merge, with upstream's default `pi-claude-directsdk` label and its replacement `<docs>` section, it read 0%, 98.64% and 99.87% (25,588 / 349 / 30 cache-write tokens), again with stable hashes. On installed Pi 1.5.0 with Claude Code 2.1.316, after v0.5.0 and with the replacement `<docs>` section updated for Pi 1.5.0, it read 0%, 98.68% and 99.85% (26,607 / 353 / 37 cache-write tokens), again with stable hashes.
|
|
118
|
+
|
|
119
|
+
Three options were considered for the reminder:
|
|
120
|
+
|
|
121
|
+
- **Drop it after the opening call (adopted, as in Hermes).** Native Claude Code attaches this context only when the history lacks it, so a continuous session carries it once. Dropping it keeps every replay in the context its signed thinking was generated against. In this transport it carries only the account email.
|
|
122
|
+
- **Move it after the frame on every call (rejected).** The model would see it on every call, but each call's thinking would be generated with context that its replay lacks.
|
|
123
|
+
- **Pin it after the conversation's first frame (not adopted).** This is closest to native behaviour, but it has no reference measurements, and it requires normalizing CLI text that differs by placement.
|
|
124
|
+
|
|
125
|
+
Limits:
|
|
126
|
+
|
|
127
|
+
- Routes without a mid-conversation environment message, such as Haiku 4.5, still miss once on call 2: the CLI changes its opening-turn text between the first and second request.
|
|
128
|
+
- After a tool loop that contains thinking, the next user turn can still miss entries written after the first thinking block. That is a separate API-side behaviour. The relay now ports the Hermes turn-start marker ([hermes-official-plugins#36](https://github.com/NousResearch/hermes-official-plugins/issues/36), `_keep_turn_mark`), where the next turn's read rose from 54% to 94% in Hermes' live run. A loopback capture with Claude Code 2.1.316 after a 14-round thinking loop showed the marker on the opening frame within four breakpoints; this transport has no paid measurement of it yet. Until replayed thinking reached the request (see below), this miss did not apply here.
|
|
129
|
+
- A later CLI that changes these placements disables the affected step and shows up in `nativeContext`, not as a failed call.
|
|
130
|
+
|
|
131
|
+
#### Fable batching reminder
|
|
132
|
+
|
|
133
|
+
On Claude Code 2.1.322, the same kind of loopback capture ran through the provider and a real CLI, with no upstream contact. It covered an opening request, a tool-result step and a later user turn on Fable 5.1, Fable 5.2 and Opus 5.5. On Fable 5.1 only, the tool-result step's trailing system message held the date line, with the CLI's marker, followed by a batching reminder (`tengu_toasty_thimble`). Before the change for [#41](https://github.com/yamatakeru/pi-claude-native/issues/41), the environment message lacked the date on that step and had it on the user turn, so the user turn did not replay the step's prefix. After it, every request's prefix through its message breakpoint was replayed unchanged on all three models.
|
|
134
|
+
|
|
135
|
+
The CLI enables the reminder by default only for models with the `fable_5_1_prompt_bundle` capability, which Fable 5.2 lacks. Anthropic can still enable it, and a second reminder (`tengu_gentle_parasol`), for any model through client data. With both forced on Fable 5.2 through their environment variables (`CLAUDE_CODE_TOASTY_THIMBLE`, `CLAUDE_CODE_GENTLE_PARASOL`), the CLI joined them into one block after the date, and the result matched Fable 5.1 before and after the change.
|
|
136
|
+
|
|
137
|
+
Two options were considered:
|
|
138
|
+
|
|
139
|
+
- **Move only the leading date block (adopted, as in Hermes).** It extends the existing step to the layout observed, and leaves the opening request and every other route as the CLI builds them. Hermes merged the same change ([hermes-official-plugins#253](https://github.com/NousResearch/hermes-official-plugins/pull/253)).
|
|
140
|
+
- **Move the date out of the environment message instead (deferred).** First proposed in [hermes-official-plugins#243](https://github.com/NousResearch/hermes-official-plugins/issues/243), where Hermes did not adopt it and gave no reason. It would also keep the prefix across date changes and any later trailing layout, but changes the opening request and departs from Hermes; it is tracked in [#69](https://github.com/yamatakeru/pi-claude-native/issues/69).
|
|
141
|
+
|
|
142
|
+
### Artifact scratchpad line under the custom label
|
|
143
|
+
|
|
144
|
+
A loopback server recorded `/v1/messages` bodies from Claude Code 2.1.312 and 2.1.316 on macOS, run with this extension's child arguments and environment. No paid requests were made.
|
|
145
|
+
|
|
146
|
+
| Child environment | Scratchpad line |
|
|
147
|
+
|---|---|
|
|
148
|
+
| As `buildChildEnv` sets it (`CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1`), label `pi-claude-native` | Absent; the body matches the `sdk-cli` one apart from the label and `session_id` |
|
|
149
|
+
| Without `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC`, label `pi-claude-native` or `claude-desktop` | Present, in the Environment reminder at `messages[1]`, with a per-process UUID path |
|
|
150
|
+
| Same, plus `CLAUDE_CODE_DISABLE_ARTIFACT=1` | Absent; the body matches the `sdk-cli` one apart from the label and `session_id` |
|
|
151
|
+
| Label `sdk-cli` | Absent in every case |
|
|
152
|
+
|
|
153
|
+
The gate the CLI applies before adding the line was not identified, so other accounts or CLI versions may add it even with `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1`. The child therefore also sets `CLAUDE_CODE_DISABLE_ARTIFACT=1`, which left these bodies unchanged where the line was absent. The extension does not use native tools, so turning off native artifacts removes nothing it relies on.
|
|
154
|
+
|
|
155
|
+
### Replayed thinking and the assistant model
|
|
156
|
+
|
|
157
|
+
Through v0.4.0 the replay restored signed thinking from the carrier, but Claude Code did not send it. A loopback capture with Claude Code 2.1.312 and 2.1.316, Opus 5.5 at effort `high`, and a signature taken from a real session replayed a 14-round tool loop. All 15 thinking blocks were in the replayed frames, and none were in the `/v1/messages` body.
|
|
158
|
+
|
|
159
|
+
The cause is the assistant frame's `model`. `prepareHistory` sent only `role` and `content`. In the 2.1.316 binary, an assistant message without a string `model` counts as written by another model. The CLI keeps such thinking only when its "foreign thinking" setting allows it. That setting is the `tengu_rustling_pixel` feature flag, overridable with `CLAUDE_CODE_RUSTLING_PIXEL` (`all`, `upgrade` or `none`). It is `none` when `ANTHROPIC_BASE_URL` is not a first-party URL, as with this transport's relay. With the carrier's `model` on each restored frame, the same capture sent all 15 blocks.
|
|
160
|
+
|
|
161
|
+
Prompt caching was not affected: the stripping was the same on every call, so the warm-turn measurements above still read the previous request. The model, however, did not see its earlier reasoning, as in [hermes-official-plugins#40](https://github.com/NousResearch/hermes-official-plugins/issues/40). Hermes replays the stored native message, `model` included, so it does not hit this.
|
|
162
|
+
|
|
163
|
+
A paid run through the real API then checked the fix: Claude Code 2.1.316, `claude-haiku-5-5` at effort `high`, and a two-round tool loop calling `streamClaudeNative` directly. A logging proxy recorded only status and counts.
|
|
164
|
+
|
|
165
|
+
| Call | Status | Earlier assistant messages | Thinking blocks sent | Cache-read tokens | Cache-write tokens |
|
|
166
|
+
|---|---:|---:|---:|---:|---:|
|
|
167
|
+
| 1 | 200 | 0 | 0 | 0 | 0 |
|
|
168
|
+
| 2 | 200 | 1 | 1 | 0 | 901 |
|
|
169
|
+
| 3 | 200 | 2 | 2 | 901 | 360 |
|
|
170
|
+
|
|
171
|
+
The API accepted the replayed signed thinking, and call 3 read call 2's whole request. Call 1 wrote nothing, which suggests its prompt was below the model's minimum cacheable length.
|
|
172
|
+
|
|
173
|
+
Claude Code 2.1.318 "Removed the 2.1.288 fix that kept the model's earlier thinking when resuming a conversation started on 2.1.286 or earlier; that thinking can now be dropped." The paid check was repeated on 2.1.318 on 2026-11-05, with the same model, effort and proxy. A fourth call asked a follow-up question after the tool loop.
|
|
174
|
+
|
|
175
|
+
| Call | Status | Earlier assistant messages | Thinking blocks sent | Thinking blocks returned | Cache-read tokens | Cache-write tokens |
|
|
176
|
+
|---|---:|---:|---:|---:|---:|---:|
|
|
177
|
+
| 1 | 200 | 0 | 0 | 0 | 545 | 0 |
|
|
178
|
+
| 2 | 200 | 1 | 0 | 0 | 919 | 0 |
|
|
179
|
+
| 3 | 200 | 2 | 0 | 1 | 979 | 0 |
|
|
180
|
+
| 4 | 200 | 3 | 1 | 1 | 979 | 175 |
|
|
181
|
+
|
|
182
|
+
Calls 1 and 2 returned no thinking, so calls 2 and 3 had none to send. The thinking returned by call 3 was sent with call 4 and accepted. Call 1 read a prefix that an earlier attempt with the same transcript had written.
|
|
183
|
+
|
|
184
|
+
So on 2.1.318, replayed signed thinking is still sent and accepted. The transcript was built for this check, not started on Claude Code 2.1.286 or earlier, so the case the change names was not tested. This transport replays history over stdin on every call instead of resuming a Claude Code session file, so the change most likely does not apply to it; that is an inference, not a measurement.
|
|
185
|
+
|
|
186
|
+
### Mid-conversation system messages (earlier fork rewrite)
|
|
187
|
+
|
|
188
|
+
These measurements used the fork's former opt-in relay rewrite (`PI_DIRECTSDK_MID_CONVERSATION_SYSTEM=1`, fork #12). It was removed in fork #18 in favour of upstream v0.2.0's tool changes, which send the same kind of request: the initial prompt and tools, a deferred placeholder, and later changes as `role: system` messages. The numbers below were not re-measured with upstream's implementation.
|
|
189
|
+
|
|
190
|
+
Without either, a later Pi prompt or tool change rewrites the leading prompt and tool list, and the next call misses the whole cache. On Claude Code 2.1.303, the CLI ignored `type: "system"` lines on stream-json input and rejected `role: "system"` inside a user line, so the change cannot reach the API through the CLI, and the admitted request has to be rewritten in the relay.
|
|
191
|
+
|
|
192
|
+
A loopback capture covered three changes after the first call: a section update with an added tool, a removed tool, and a redefined tool. For additions and removals, the leading system blocks and the tool-list prefix stayed identical across calls, and each request's prefix through its message breakpoint replayed unchanged. A redefined tool fell back to the current tool list, as designed. Haiku 4.5, which has no mid-conversation system messages, was forwarded collapsed.
|
|
193
|
+
|
|
194
|
+
Three approved paid requests went through the provider path with `claude-haiku-5-5`. A prompt section change and an added tool came between calls 1 and 2:
|
|
195
|
+
|
|
196
|
+
| Request | What happened | Cache-read tokens | Cache-write tokens | Uncached input |
|
|
197
|
+
|---|---|---:|---:|---:|
|
|
198
|
+
| 1 | Initial tools | 0 | 7,795 | 493 |
|
|
199
|
+
| 2 | Section changed, tool added; the model called the added tool | 7,795 | 350 | 151 |
|
|
200
|
+
| 3 | Tool result returned; final answer | 8,145 | 215 | 2 |
|
|
201
|
+
|
|
202
|
+
The added tool was declared with `defer_loading` and activated by `tool_addition`; the model called it through the inert MCP server. Each request admitted one upstream request. Opus 5.5 and Sonnet 5.5 produced the same request shape in the loopback capture, but only Haiku 5.5 was measured on the subscription route.
|
|
203
|
+
|
|
204
|
+
### Tools defined by value (`inline-tools-2026-09-15`)
|
|
205
|
+
|
|
206
|
+
Since #56, later tools are defined by value in `tool_addition` blocks instead of being declared up front with `defer_loading`, and the admitted request carries the `inline-tools-2026-09-15` beta value. The tool list is the initial tools plus the placeholder for the whole conversation, so a tool redefined under the same name no longer falls back to the current tool list.
|
|
207
|
+
|
|
208
|
+
A loopback capture with Claude Code 2.1.318 showed that the CLI sends `mid-conversation-tool-changes-2026-07-01` on its own and forwards the request body unchanged; the relay appends `inline-tools-2026-09-15`.
|
|
209
|
+
|
|
210
|
+
Three approved paid requests ran on 2026-11-05 with `claude-haiku-5-5` and Claude Code 2.1.318, through `streamClaudeNative`, first on this change and then with the same transcript on `main`. The initial tools were `alpha` and `gamma`. Before request 2, `alpha` was redefined with a new required `mode` field and `beta` was added; before request 3, `gamma` was removed.
|
|
211
|
+
|
|
212
|
+
| Request | What happened | This change: cache read / write | `main`: cache read / write |
|
|
213
|
+
|---|---|---:|---:|
|
|
214
|
+
| 1 | Initial tools | 0 / 623 | 623 / 0 |
|
|
215
|
+
| 2 | `alpha` redefined, `beta` added; the model called `alpha` with `mode` | 623 / 344 | 0 / 984 |
|
|
216
|
+
| 3 | Tool result returned, `gamma` removed; final answer | 967 / 546 | 0 / 996 |
|
|
217
|
+
|
|
218
|
+
The model's call used the new required field, so the redefinition reached it. On `main`, request 1 read the prefix the first run wrote, and requests 2 and 3 each missed the whole cache. With this change, each request read everything the previous one wrote. The prompts were small, so the token counts are small; the read/miss pattern is the result.
|
|
219
|
+
|
|
220
|
+
### Interactive sessions after the trimmed-result fix
|
|
221
|
+
|
|
222
|
+
At commit `1fa1c48`, two fresh interactive Pi 1.3.0 sessions ran with the earlier mid-conversation opt-in enabled and Claude Code 2.1.303, driven through the Herdr terminal multiplexer. Across 31 requests, `nativeContext` reported no skipped restoration and `midConversation` reported no fallback. The first user turn after a tool loop read 99.4% from cache after 12 Haiku 5.5 tool calls, and 99.0% after 10 Sonnet 5.5 tool calls.
|
|
223
|
+
|
|
224
|
+
These sessions did not reproduce the long thinking/tool-loop miss ([#10](https://github.com/yamatakeru/pi-claude-directsdk/issues/10)), but they were too short to rule it out. They were one-off checks and are not part of the permanent harness.
|
|
225
|
+
|
|
226
|
+
### Paid verification of the repair
|
|
227
|
+
|
|
228
|
+
Three approved requests ran sequentially through one Pi RPC process with the historical anchor and shared connection pools enabled. All three succeeded, each admitted one upstream request, and the process produced no stderr. Auto-retry and auto-compaction remained disabled.
|
|
229
|
+
|
|
230
|
+
| Turn | RPC prompt to settled | Provider completion | Cache-read tokens | Cache-write tokens | Cache-read share | HTTPS connection reused |
|
|
231
|
+
|---|---:|---:|---:|---:|---:|---|
|
|
232
|
+
| 1 | 3,312.022 ms | 3,192.764 ms | 2,951 | 111,010 | 2.59% | No |
|
|
233
|
+
| 2 | 3,154.486 ms | 3,139.958 ms | 113,576 | 464 | 99.59% | Yes |
|
|
234
|
+
| 3 | 1,731.962 ms | 1,714.062 ms | 113,772 | 332 | 99.71% | Yes |
|
|
235
|
+
|
|
236
|
+
The Pi system prompt, native system blocks, and native tool definitions had identical content hashes across these three calls. Native requests contained 93, 95, and 97 messages. Each had four one-hour breakpoints, including the historical anchor before the final user frame and native tail.
|
|
237
|
+
|
|
238
|
+
The first turn wrote the large prefix. The following turns read almost all input from cache and wrote only a small suffix. This confirms that the repaired request path can reuse historical content, rather than only system instructions and tool definitions.
|
|
239
|
+
|
|
240
|
+
The starting session file had grown to 1,520,559 bytes. Its replay prepared 91, 93, and 95 frames, compared with 119, 121, and 123 frames in the earlier cache investigation. Request bodies were 264,370, 264,730, and 265,028 bytes, not the earlier approximately 530 KB bodies. These are different replay workloads, not a controlled before-and-after latency comparison.
|
|
241
|
+
|
|
242
|
+
The earlier warm cache-read share was approximately 3.65%; the repaired workload achieved more than 99.5% on both warm turns. Treat this as evidence of successful warm reuse, not an exact workload-matched improvement ratio. Output counts also differed: 210, 235, and 21 tokens. Neither cache reuse nor connection pooling alone explains the timing differences.
|
|
243
|
+
|
|
244
|
+
### Connection reuse and remaining lifecycle verification
|
|
245
|
+
|
|
246
|
+
The relay now uses shared HTTP and HTTPS agents, with at most two idle sockets per destination and no shared admission state or request headers. Request cleanup destroys an active upstream request, not pooled sockets from completed requests. Downstream-close listeners are removed when upstream forwarding settles, and the stream removes its relay-abort listener on failure as well as success.
|
|
247
|
+
|
|
248
|
+
Diagnostics now report whether Node reused an upstream socket and separate response-header arrival from first response data. These measurements do not isolate DNS or TLS time, and headers-to-first-data is not necessarily time to the first generated token.
|
|
249
|
+
|
|
250
|
+
TypeScript checking, whitespace checking, and the existing offline startup verification passed. The paid batch verified normal successful HTTPS reuse across request-scoped child processes. Dedicated cancellation and late-cleanup checks have not run. The post-fix batch used exactly the three additional approved paid requests; no further paid calls were made.
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
## Subsequent lifecycle and benchmark changes
|
|
254
|
+
|
|
255
|
+
Subprocess writes and child-closure waits now have request-timeout bounds. Read timeouts remove the correct waiter and terminate the child. Cancellation rejects pending reads and writes, and discovery cleanup releases its server and temporary directory even after an exit-observation failure. The completion gate before executable tool calls is unchanged.
|
|
256
|
+
|
|
257
|
+
Model-request children now use a persistent, empty, private cache directory rather than a new temporary directory per Pi process. See [architecture.md](architecture.md#native-working-directory-srcchild-cwdts) for paths and validation. This removes a changing directory path from native context. The Linux cross-restart verification below measured successful reuse.
|
|
258
|
+
|
|
259
|
+
The supported `warm` scenario now reproduces the three-turn same-process measurement through installed Pi. It has an explicit three-request budget, private configuration and session cleanup, no-tool enforcement, and a no-model-request dry run. See [testing.md](testing.md#same-process-warm-cache-measurement).
|
|
260
|
+
|
|
261
|
+
TypeScript checking, compilation, Python and observer syntax checks, offline startup, and RPC dry-run preparation passed. A complete-session loopback-only refusal probe completed 86 historical acknowledgments and reached the deliberately blocked upstream. Both native children exited, and the persistent child directory stayed empty with mode `0700`. That probe verifies replay and failure cleanup, not successful generation. Its model call failed as expected; no paid request was made.
|
|
262
|
+
|
|
263
|
+
The earlier measured 99.59% and 99.71% warm-cache shares are from before these subsequent changes. Subsequent Linux verification covered deadlines, cancellation, and successful cache reuse across a Pi restart, as described below.
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
## Linux lifecycle and cross-restart verification
|
|
267
|
+
|
|
268
|
+
The background verification completed successfully in 19.7 seconds. It used temporary verification programs outside the repository and exactly three approved paid requests.
|
|
269
|
+
|
|
270
|
+
### Forced stalls and cancellation
|
|
271
|
+
|
|
272
|
+
Temporary Node child programs exercised the public subprocess supervisor with real Linux pipes and process groups. They did not emulate model responses or contact an API.
|
|
273
|
+
|
|
274
|
+
- Stalled stdin writes, silent stdout reads, and child-exit waits timed out with a 300 ms supervisor limit.
|
|
275
|
+
- Whole-case durations, including child startup and cleanup, were 409, 393, and 405 ms, respectively.
|
|
276
|
+
- Cancellation during a blocked write, pending read, and exit wait completed each whole case in 103, 101, and 103 ms.
|
|
277
|
+
- Each case confirmed that the parent and its descendant were no longer running.
|
|
278
|
+
- The pre-fix supervisor lacked the write and exit deadlines. Those two regression controls reached the independent 800 ms safety guard instead, then were terminated.
|
|
279
|
+
|
|
280
|
+
These checks verify the Linux supervisor's timeout and cancellation behavior. They are not a complete Pi tool-execution cancellation test or a dedicated relay late-cleanup test.
|
|
281
|
+
|
|
282
|
+
### Cache reuse after restarting Pi
|
|
283
|
+
|
|
284
|
+
One installed Pi RPC process forked a private snapshot and completed two requests. It then exited. A second Pi RPC process resumed the saved private session and completed the third request. Both processes used the same copied agent directory, working folder, model, and empty native working directory. Auto-retry, auto-compaction, idle warming, and tool execution were disabled for verification.
|
|
285
|
+
|
|
286
|
+
| Request | Phase | Cache-read tokens | Cache-write tokens | Cache-read share | HTTPS connection reused | RPC prompt to settled |
|
|
287
|
+
|---|---|---:|---:|---:|---|---:|
|
|
288
|
+
| 1 | Cold | 2,951 | 194,728 | 1.49% | No | 3,347.356 ms |
|
|
289
|
+
| 2 | Same-process warm | 197,137 | 610 | 99.69% | Yes | 5,193.948 ms |
|
|
290
|
+
| 3 | After Pi restart | 197,491 | 310 | 99.84% | No | 3,134.302 ms |
|
|
291
|
+
|
|
292
|
+
All three requests succeeded and admitted exactly one upstream request each. Pi system-prompt and tool-definition hashes were identical across the restart. Replay prepared 205, 207, and 209 frames, with 102, 103, and 104 historical acknowledgments. Request bodies were 492,536, 492,868, and 493,155 bytes.
|
|
293
|
+
|
|
294
|
+
The restarted process opened a new HTTPS connection but still read 99.84% of input from the API cache. This verifies that prompt-cache reuse survives a Pi restart under these stable inputs; it does not depend on keeping the old connection or Pi process alive.
|
|
295
|
+
|
|
296
|
+
Cache hits do not guarantee lower latency. The same-process warm request waited 3,944.553 ms for response headers, despite 99.69% cache-read share. These three timings are not a controlled latency comparison. Legitimate instruction or tool changes, cache lifetime expiry, and upstream eviction can still prevent reuse.
|