pig-claude-bridge 0.4.0 → 0.6.0
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/README.md +6 -9
- package/UPSTREAM.md +14 -8
- package/docs/compatibility.md +18 -15
- package/extensions/pig-claude-bridge/extension.go +16 -3
- package/extensions/pig-claude-bridge/internal/bridge/askclaude.go +6 -6
- package/extensions/pig-claude-bridge/internal/bridge/consume.go +78 -59
- package/extensions/pig-claude-bridge/internal/bridge/hooks.go +39 -28
- package/extensions/pig-claude-bridge/internal/bridge/host.go +13 -5
- package/extensions/pig-claude-bridge/internal/bridge/isolated.go +5 -1
- package/extensions/pig-claude-bridge/internal/bridge/provider.go +258 -100
- package/extensions/pig-claude-bridge/internal/bridge/query.go +71 -11
- package/extensions/pig-claude-bridge/internal/bridge/ratelimit.go +13 -2
- package/extensions/pig-claude-bridge/internal/bridge/recording.go +672 -0
- package/extensions/pig-claude-bridge/internal/bridge/recordtap.go +211 -0
- package/extensions/pig-claude-bridge/internal/bridge/sessionstore.go +183 -0
- package/extensions/pig-claude-bridge/internal/bridge/state.go +130 -103
- package/extensions/pig-claude-bridge/internal/bridge/stream.go +38 -14
- package/extensions/pig-claude-bridge/internal/bridge/sync.go +192 -150
- package/extensions/pig-claude-bridge/internal/ccproto/args.go +28 -3
- package/extensions/pig-claude-bridge/internal/ccsession/records.go +8 -0
- package/extensions/pig-claude-bridge/internal/ccsession/repair.go +22 -4
- package/extensions/pig-claude-bridge/internal/ccsession/session.go +26 -5
- package/extensions/pig-claude-bridge/internal/ccsession/transcript.go +287 -0
- package/extensions/pig-claude-bridge/internal/config/config.go +12 -16
- package/extensions/pig-claude-bridge/internal/convert/convert.go +53 -84
- package/extensions/pig-claude-bridge/internal/digest/digest.go +300 -0
- package/extensions/pig-claude-bridge/internal/models/catalog_gen.go +5 -4
- package/extensions/pig-claude-bridge/internal/models/models.go +4 -2
- package/extensions/pig-claude-bridge/internal/models/pending.go +21 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,9 +30,9 @@ Use `/model` to select any Claude model in the catalog, e.g. `claude-bridge/clau
|
|
|
30
30
|
|
|
31
31
|
Behind the scenes, PiG's tools are bridged to Claude Code but everything works like normal in PiG. Bash commands get Claude Code's 120-second default timeout since PiG's bash has none. Skills are forwarded to Claude Code's system prompt, and steering mid-turn reaches Claude at the next tool boundary.
|
|
32
32
|
|
|
33
|
-
The model list is a static table generated from the Anthropic catalog of `@earendil-works/pi-ai` 1.
|
|
33
|
+
The model list is a static table generated from the Anthropic catalog of `@earendil-works/pi-ai` 1.5.0 (`scripts/dev/gen-catalog.mjs`) and registered when the extension loads; it does not follow later catalog updates. Models Claude Code ships before pi-ai does (currently Fable 5.2) are listed by hand in `internal/models/pending.go` until a regenerated catalog carries them. Dated snapshot ids are not shown. See [docs/compatibility.md](docs/compatibility.md) (D5).
|
|
34
34
|
|
|
35
|
-
**1M context:** Fable 5/5.1, Opus 5.5/5/4.8/4.7, and Sonnet 5.5/5 get 1M context. Opus 4.6 gets 1M on Max, Ultra, Team (Standard and Premium seats) and Enterprise plans or with Extra Usage, and Sonnet 4.6 only with Extra Usage ([Claude Code docs](https://code.claude.com/docs/en/model-config#extended-context)): the extension detects the plan by itself (see `provider.plan` in [Configuration](#configuration)); set `provider.longContextExtraUsage` for Extra Usage.
|
|
35
|
+
**1M context:** Fable 5/5.1/5.2, Opus 5.5/5/4.8/4.7, and Sonnet 5.5/5 get 1M context. Opus 4.6 gets 1M on Max, Ultra, Team (Standard and Premium seats) and Enterprise plans or with Extra Usage, and Sonnet 4.6 only with Extra Usage ([Claude Code docs](https://code.claude.com/docs/en/model-config#extended-context)): the extension detects the plan by itself (see `provider.plan` in [Configuration](#configuration)); set `provider.longContextExtraUsage` for Extra Usage.
|
|
36
36
|
|
|
37
37
|
**Fast mode:** `/claude-fast` turns Claude Code's fast mode on or off for the rest of the session (`/claude-fast on`, `/claude-fast off`, or no argument to toggle); set `provider.fastMode` to `true` to start every session with it on. It takes effect from the next turn, on models that have fast mode (Opus 5.5, for example); other models ignore it. On subscription plans fast mode is billed to usage credits, not to the plan's limits, and on Team and Enterprise an Owner has to enable it for the organization first ([Claude Code docs](https://code.claude.com/docs/en/fast-mode)). While it is on, the footer shows `fast` (see [Footer status](#footer-status)).
|
|
38
38
|
|
|
@@ -52,12 +52,9 @@ The plan shows from session start, fast mode as soon as `/claude-fast` changes i
|
|
|
52
52
|
|
|
53
53
|
## Configuration
|
|
54
54
|
|
|
55
|
-
Config file `claude-bridge.json
|
|
55
|
+
Config file `claude-bridge.json` in PiG's agent directory, `~/.pig/agent` by default (`$PIG_HOME/agent`, or `PIG_CODING_AGENT_DIR`). With `PIG_USE_PI_DIRS=1` PiG shares Pi's tree and this is `~/.pi/agent` (or `PI_CODING_AGENT_DIR`). There is no project config (pi-claude-bridge #198): a cloned repo could otherwise point the bridge at its own Claude Code executable or turn on metered billing. For the same reason Claude Code loads only `~/.claude/settings.json` on the provider path, never a repo's `.claude/settings.json` or `settings.local.json`.
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
- project: `<project>/.pig/claude-bridge.json`; under `PIG_USE_PI_DIRS=1`, `<project>/.pi/claude-bridge.json`.
|
|
59
|
-
|
|
60
|
-
The extension asks PiG for both directories; nothing is hard-coded. A missing or unparseable file counts as empty; with `CLAUDE_BRIDGE_DEBUG` set, a parse failure is written to the debug log. A key with a value of the wrong type is ignored, except `askClaude.allowFullMode` and `askClaude.defaultMode`, which fail closed (full mode denied, mode `none`).
|
|
57
|
+
The extension asks PiG for the directory; nothing is hard-coded. A missing or unparseable file counts as empty; with `CLAUDE_BRIDGE_DEBUG` set, a parse failure is written to the debug log. A key with a value of the wrong type is ignored, except `askClaude.allowFullMode` and `askClaude.defaultMode`, which fail closed (full mode denied, mode `none`).
|
|
61
58
|
|
|
62
59
|
```json
|
|
63
60
|
{
|
|
@@ -184,9 +181,9 @@ Claude Code sends a `rate_limit_event` when a usage window changes. The TypeScri
|
|
|
184
181
|
|
|
185
182
|
## Known issues
|
|
186
183
|
|
|
187
|
-
**
|
|
184
|
+
**Session sync.** The bridge compares PiG's history with what Claude Code's session file covers, message by message. An unchanged history resumes the session; a rewind, edit, abort, failed turn or restart forks or trims it at the last point both agree and imports only what follows; a full import happens only when the model, the system prompt or the working directory changed. The pointer from a PiG session to its Claude Code session lives under `<agent dir>/claude-bridge/sessions/` (user-only files), so a restart or `/reload` forks the earlier session instead of re-sending the conversation, and a `/fork` or clone continues from its parent's. Each import or trim still costs that turn a prompt-cache miss for what it rewrote.
|
|
188
185
|
|
|
189
|
-
**Files Claude Code edits are not carried across
|
|
186
|
+
**Files Claude Code edits are not carried across an import.** The edit itself survives in the history as a tool call and result; what is lost is the post-edit file snapshot. `@file` expansions and tool results Claude Code saved to a file *are* carried.
|
|
190
187
|
|
|
191
188
|
**System prompt changes mid-session may not reach the model.** Project context (AGENTS.md/CLAUDE.md), skills, and extension-written instructions are captured on the first request and reused on resume. This keeps the cached prefix stable, but later changes may not take effect until a rebuild or compaction. Start a new session if updated instructions must take effect immediately.
|
|
192
189
|
|
package/UPSTREAM.md
CHANGED
|
@@ -6,13 +6,13 @@ This extension is a Go port of [pi-claude-bridge](https://github.com/elidickinso
|
|
|
6
6
|
|
|
7
7
|
| what | pin |
|
|
8
8
|
|---|---|
|
|
9
|
-
| pi-claude-bridge | `
|
|
10
|
-
| cc-session-io | 0.4.
|
|
9
|
+
| pi-claude-bridge | `acbe45b` (0.11.0). The AskClaude package split and `src/cache-trace.ts` are not ported (docs/compatibility.md D8, D9); `src/index.ts` line ranges below are those of 6aa276a, where the code moved they name the 0.11.0 function instead |
|
|
10
|
+
| cc-session-io | 0.4.1 (MIT), the subset the bridge imports |
|
|
11
11
|
| `@anthropic-ai/claude-agent-sdk` | 0.3.310 (Claude Code 2.1.310). The protocol client was written from the SDK's public type definitions and live probes ([protocol reference](docs/reference/agent-sdk-protocol-0.3.310.md)); the SDK itself is not vendored or depended on |
|
|
12
12
|
| Claude Code | 2.1.310 (`ccproto.MinVersion`) |
|
|
13
13
|
| PiG SDK | `github.com/MichaelKinsy/PiG/extensions/sdk` v0.7.0 (PiG 0.7.0); the [signatures](docs/reference/pig-sdk-signatures-v0.5.3.md) recorded at v0.5.3 still match every symbol the bridge uses |
|
|
14
14
|
| `@modelcontextprotocol/sdk` | 1.29.0, only as the reference for the JSON-RPC responses the in-process MCP server must produce |
|
|
15
|
-
| `@earendil-works/pi-ai` | 1.
|
|
15
|
+
| `@earendil-works/pi-ai` | 1.5.0, source of the generated model catalog |
|
|
16
16
|
| `golang.org/x/text` | v0.43.0, for NFC normalisation of project paths |
|
|
17
17
|
|
|
18
18
|
## File map
|
|
@@ -26,7 +26,10 @@ Paths are relative to `extensions/pig-claude-bridge/`. TypeScript paths are `src
|
|
|
26
26
|
| `internal/askclaude/tool.go` | `index.ts` 2662-2780 and the AskClaude call handler near 1750 | tool definition, remembered/isolated sessions, notices |
|
|
27
27
|
| `internal/askclaude/ui.go` | `askclaude-ui.ts` | action summary and tool-name mapping, rendered as lines (D6) |
|
|
28
28
|
| `internal/bridge/askclaude.go` | `index.ts` `promptAndWait` (2263) | AskClaude query path |
|
|
29
|
-
| `internal/bridge/consume.go` | `index.ts` 1028-1100, 1486-1790 (`consumeQuery`, steering, tool-result delivery, abort, discard) | goroutines and channels instead of the event loop (D3) |
|
|
29
|
+
| `internal/bridge/consume.go` | `index.ts` 1028-1100, 1486-1790 (`consumeQuery`, steering, tool-result delivery, abort, discard; 0.11.0 `discardRewrittenQuery`, `dropParkedQuery`, `deliverToolResults`) | goroutines and channels instead of the event loop (D3) |
|
|
30
|
+
| `internal/bridge/recording.go` | `session-sync.ts` (`Recording`, `compareHistory`, `importHistory`, `LiveRecording`), `session-transcript.ts` (`resolveRecordingCut`, `recordedToolResults`) | mutex-guarded |
|
|
31
|
+
| `internal/bridge/recordtap.go` | 0.11.0 `index.ts` `tapRecording`, `incorporate`, `recordAssistantEmission`, `completeAssistantRecording` | |
|
|
32
|
+
| `internal/bridge/sessionstore.go` | `session-store.ts` | `""` stands for a null uuid in memory; the file keeps JSON null |
|
|
30
33
|
| `internal/bridge/debug.go` | `index.ts` 36-150 (`debug`, `makeCliDebugOptions`, `diagDump`) | |
|
|
31
34
|
| `internal/bridge/hooks.go` | `index.ts` 2463-2660 (event handlers) | `session_start`, `before_agent_start`, `agent_start`, `turn_start`, compaction/tree, shutdown |
|
|
32
35
|
| `internal/bridge/host.go` | new (Go-only) | `Host` interface and `Deps`, so the PiG SDK stays confined to `extension.go` and the bridge is testable with a fake host |
|
|
@@ -37,12 +40,14 @@ Paths are relative to `extensions/pig-claude-bridge/`. TypeScript paths are `src
|
|
|
37
40
|
| `internal/bridge/query.go` | `query-state.ts`, `prompt-stream.ts`, `index.ts` 480-494 (`newAssistantOutput`) | |
|
|
38
41
|
| `internal/bridge/ratelimit.go` | `rate-limit.ts`, `index.ts` 510-550 | headers go to the debug log only (D1) |
|
|
39
42
|
| `internal/bridge/sessioncwd.go` | `session-cwd.ts` | most-recent-pending summary slot (D7) |
|
|
40
|
-
| `internal/bridge/state.go` | `index.ts`
|
|
43
|
+
| `internal/bridge/state.go` | 0.11.0 `index.ts` "Session persistence" (`sessionStateFor`, `inheritSessionState`, `setSessionStateFor`, `markRebuildForSession`, `armStaleContexts`, `waitForSessionWriter`) | `sponsorMarkRebuildForSession` not ported (D4) |
|
|
41
44
|
| `internal/bridge/stream.go` | `index.ts` 1101-1216, 1215-1485 (`processStreamEvent`, `processAssistantMessage`, finalisation) | |
|
|
42
|
-
| `internal/bridge/sync.go` | `index.ts`
|
|
45
|
+
| `internal/bridge/sync.go` | 0.11.0 `index.ts` `syncSharedSession`, `turnStart`, `verifyWrittenSession`, `safeRealpath`, `debugSessionPaths` | `importForAskClaude` is the port's (D8) |
|
|
43
46
|
| `internal/bridge/toolargs.go` | `index.ts` 914-1018 (`piToolNameFor`, `mapToolArgs`) | |
|
|
44
47
|
| `internal/bridge/usage.go` | `usage.ts`, `index.ts` `recordUsage` | |
|
|
45
|
-
| `internal/ccproto/args.go` | `
|
|
48
|
+
| `internal/ccproto/args.go` | `cc-child.ts` (`CC_CHILD_ENV`, `PARENT_SESSION_VARS`, `SESSION_STORAGE_VARS`, `ccChildEnv`), `index.ts` query options; the SDK's argv builder (`sdk.mjs`) | argv pinned by `testdata/sdk-argv-snapshot.json`; `CLAUDE_CODE_DISABLE_FEATURE_FLAGS` is the port's |
|
|
49
|
+
| `internal/ccsession/transcript.go` | `session-transcript.ts` (`readTranscript`, `findBoundary`, `resolveCut`, `savePrefixSession`, the tool-result index of `recordedToolResults`) | |
|
|
50
|
+
| `internal/digest/digest.go` | `message-digests.ts` | pinned by `tests/fixtures/message-digests.json` (`scripts/dev/gen-digests.mjs`) |
|
|
46
51
|
| `internal/ccproto/control.go` | the SDK's control protocol (`sdk.mjs`), reference sections 2 to 4 | requests, responses, inbound `mcp_message`, interrupt |
|
|
47
52
|
| `internal/ccproto/exec.go` | `index.ts` executable options; the SDK's executable handling | `claude` from `PATH` and version check (D2) |
|
|
48
53
|
| `internal/ccproto/messages.go` | the SDK's `SDKUserMessage` type, reference section 4 | |
|
|
@@ -63,8 +68,9 @@ Paths are relative to `extensions/pig-claude-bridge/`. TypeScript paths are `src
|
|
|
63
68
|
| `internal/convert/transcript.go` | `transcript.ts` | |
|
|
64
69
|
| `internal/mcptools/schema.go` | `mcp-server.ts` (root-schema flattening) | |
|
|
65
70
|
| `internal/mcptools/server.go` | `mcp-server.ts`; `@modelcontextprotocol/sdk` 1.29.0 for response shapes | |
|
|
66
|
-
| `internal/models/catalog_gen.go` | new (Go-only), generated by `scripts/dev/gen-catalog.mjs` from `@earendil-works/pi-ai` 1.
|
|
71
|
+
| `internal/models/catalog_gen.go` | new (Go-only), generated by `scripts/dev/gen-catalog.mjs` from `@earendil-works/pi-ai` 1.5.0 | static catalog (D5); do not edit |
|
|
67
72
|
| `internal/models/models.go` | `models.ts` | |
|
|
73
|
+
| `internal/models/pending.go` | new (Go-only) | models Claude Code ships before pi-ai's catalog does (Fable 5.2); an entry leaves when a regenerated catalog carries it |
|
|
68
74
|
| `internal/promptcapture/capture.go` | `prompt-capture.ts` | |
|
|
69
75
|
|
|
70
76
|
## Bump procedure
|
package/docs/compatibility.md
CHANGED
|
@@ -10,11 +10,11 @@ What this port is pinned to, and every place where it differs from the TypeScrip
|
|
|
10
10
|
| PiG Go SDK | `github.com/MichaelKinsy/PiG/extensions/sdk` v0.7.0, Go 1.26 floor |
|
|
11
11
|
| Claude Code | 2.1.310 minimum (the port warns when the binary is older); verified against 2.1.319 |
|
|
12
12
|
| Agent SDK protocol | `@anthropic-ai/claude-agent-sdk` 0.3.310 (Claude Code 2.1.310); [reference](reference/agent-sdk-protocol-0.3.310.md). 0.3.308 to 0.3.310 changes nothing on the wire that the bridge uses; the one new control subtype, survey feedback, is never sent |
|
|
13
|
-
| pi-claude-bridge | `
|
|
14
|
-
| cc-session-io | 0.4.
|
|
13
|
+
| pi-claude-bridge | `acbe45b` (0.11.0), https://github.com/elidickinson/pi-claude-bridge; see "Caught up to 0.11.0" below for what is not ported |
|
|
14
|
+
| cc-session-io | 0.4.1 (MIT), the subset the bridge imports (`addToolResultBlocks` as `AddToolResultBlocks`; the 0.4.1 timestamp stamping is approximated by the port's monotonic clock) |
|
|
15
15
|
| Pi compatibility surface | pi 1.4.0 (the Pi that PiG 0.5.3 tracks) |
|
|
16
16
|
| `@modelcontextprotocol/sdk` | 1.31.0 (the TypeScript declares `^1.29.0`), only as the reference for the JSON-RPC responses the in-process MCP server produces |
|
|
17
|
-
| Model catalog | `@earendil-works/pi-ai` 1.
|
|
17
|
+
| Model catalog | `@earendil-works/pi-ai` 1.5.0, generated into `internal/models/catalog_gen.go` |
|
|
18
18
|
|
|
19
19
|
## Principal differences (D1 to D7)
|
|
20
20
|
|
|
@@ -26,12 +26,18 @@ What this port is pinned to, and every place where it differs from the TypeScrip
|
|
|
26
26
|
|
|
27
27
|
**D4. Subagent mark sponsorship is not ported.** TypeScript: `sponsorMarkRebuildForSession` lets a subagent hand a rebuild mark to the module instance that serves the session, because several module instances share one process. Port: PiG subagents are separate processes, so there is nothing to sponsor; the function has no counterpart. The matching test, `unit-session-cwd` "shares the store across module instances", has no Go counterpart either.
|
|
28
28
|
|
|
29
|
-
**D5. The model list is a static catalog registered in the factory.** TypeScript: the list comes from pi-ai's Anthropic catalog at run time and follows package updates. Port: `scripts/dev/gen-catalog.mjs` generates `internal/models/catalog_gen.go` from `@earendil-works/pi-ai` 1.
|
|
29
|
+
**D5. The model list is a static catalog registered in the factory.** TypeScript: the list comes from pi-ai's Anthropic catalog at run time and follows package updates. Port: `scripts/dev/gen-catalog.mjs` generates `internal/models/catalog_gen.go` from `@earendil-works/pi-ai` 1.5.0 and the provider is registered when the extension loads, with nothing registered at `session_start`. Reason: spike S1 showed that registration from `session_start` is too late for `defaultProvider`/`defaultModel` resolution at startup, so the catalog must be known in the factory. A new Claude model needs a regenerated catalog (see [../UPSTREAM.md](../UPSTREAM.md)).
|
|
30
30
|
|
|
31
31
|
**D6. AskClaude renders as lines.** TypeScript: `askclaude-ui.ts` builds pi-tui components (colour, "(to expand)" affordance). Port: PiG's `ToolDefinition.RenderCall`/`RenderResult` return lines, so the action summary is plain lines without colour, with "… (to expand)" as text, and truncation counts runes.
|
|
32
32
|
|
|
33
33
|
**D7. Summary attribution by the most recent pending compaction.** TypeScript: `session-cwd.ts` keys the pending summary session by the identity of the request's `AbortSignal`. Port: Go has no such identity, so a single most-recent-pending slot is set at `session_before_compact` / `session_before_tree` and the isolated summary query reads it; it is not cleared by the read (Pi makes several summary calls per operation, compaction makes two) and `forgetSummarySession` clears it only if no other session has been remembered since. With two sessions summarising at once in one process, the later one wins. A compaction that fails or is aborted never reaches `session_compact`, so the port also forgets the pending session at PiG's `session_compact_failed` (fired after a failed or aborted compaction, PiG `compaction.md`); without it the stale slot would hand that session's cwd to a later summary from another session. The TypeScript needs no such hook because a cancelled compaction's signal is never looked up again. A cancelled tree navigation has no failure event, so its slot stays until the next `session_before_compact` or `session_before_tree` replaces it.
|
|
34
34
|
|
|
35
|
+
**D8. AskClaude stays in this package.** TypeScript 0.11.0 moved the tool to `pi-claude-bridge-askclaude`, its own npm package with its own copy of the conversion and child env. Port: the tool stays in `internal/askclaude`, off unless `askClaude.enabled` is set, and its shared mode imports pi's history into a session of its own (`importForAskClaude`) without touching the provider's mirror. Reason: a second Go package means a second binary, release flow and repo for a tool that is already opt-in.
|
|
36
|
+
|
|
37
|
+
**D9. No cache trace.** TypeScript: `CLAUDE_BRIDGE_TRACE_DIR` writes a per-query trace (`src/cache-trace.ts`) that `diag/audit-cache.mjs` analyses. Port: not ported; `CLAUDE_BRIDGE_DEBUG` and the `syncResult:` decision lines remain. Reason: debug tooling with no user-facing behaviour.
|
|
38
|
+
|
|
39
|
+
**D10. Shutdown kills every live child.** TypeScript: `session_shutdown` closes only the shutting-down session's parked queries, since subagents share the process. Port: the session's own parked queries are closed first with their progress committed, then every Claude Code child this process started is killed (as before). Reason: PiG subagents are separate processes (D4), so another session's live query in the same process does not occur.
|
|
40
|
+
|
|
35
41
|
## Rulings that changed the plan
|
|
36
42
|
|
|
37
43
|
- Work in place on branch `feat/native-port`, no worktree: the repo is new and single-purpose.
|
|
@@ -71,8 +77,10 @@ What this port is pinned to, and every place where it differs from the TypeScrip
|
|
|
71
77
|
|
|
72
78
|
### Session files and logs
|
|
73
79
|
|
|
74
|
-
- `syncSharedSession` takes `host` and `
|
|
75
|
-
- The
|
|
80
|
+
- `syncSharedSession` takes `host`, `modelID` and `promptDigest`; its `syncResult:` line carries the row and reason in one line where the TypeScript logs them across its trace.
|
|
81
|
+
- The persisted state file is the TypeScript's (`s-<sha256 of the UTF-16LE pi session id>.json`, version 1), under PiG's agent dir rather than Pi's, so the two bridges never read each other's pointers.
|
|
82
|
+
- A prefix write re-emits the retained records through a key-order-preserving field rewrite (`sessionId`, and `parentUuid` on the suffix's first record), where the TypeScript spreads the object.
|
|
83
|
+
- The fake Claude Code the unit tests drive echoes completed blocks before closing them and writes session records, as the real one does; the TypeScript tests stub the SDK instead.
|
|
76
84
|
- The `[moduleInstanceId]` bracket is dropped from log lines (the TypeScript `diag/*.mjs` parsers expect it); the deadlock-check regex was adapted. The `imported roles:` debug line and the sanitized-id list are dropped.
|
|
77
85
|
- Stderr tail counts runes where the SDK counts UTF-16 units.
|
|
78
86
|
- An empty `CLAUDE_CONFIG_DIR` counts as unset (`??` in JavaScript would use it), and with `HOME` unset the Claude config dir falls back to the account's home directory instead of a relative `.claude`.
|
|
@@ -84,7 +92,7 @@ What this port is pinned to, and every place where it differs from the TypeScrip
|
|
|
84
92
|
### Provider stream and notices
|
|
85
93
|
|
|
86
94
|
- `Details.executionTime` is an `int64`.
|
|
87
|
-
- The "Claude Code returned an error result" replacement is unreachable, as in the TypeScript (the loop stops at the error result).
|
|
95
|
+
- The "Claude Code returned an error result" replacement is unreachable, as in the TypeScript (the loop stops at the error result). A failed query (error result, process exit, panic) commits its recording dirty, as the TypeScript does; nothing drops the mirror any more.
|
|
88
96
|
- Abort lifetime is proven by the live smoke's tool round trip. Abort follows `ctx.Signal()`, PiG's run-scoped signal, where the plan said `ctx.Done()`: the per-request `Done()` is cancelled when each provider call ends, which would kill a query parked at a tool call between calls; `Done()` is used only when a context carries no signal.
|
|
89
97
|
- Abort is honoured while Claude Code starts: the call's cancellation reaches the spawn until the `initialize` answer arrives (spike S2 saw about 4 s, inferred to be user `SessionStart` hooks), so an abort then ends the turn as aborted instead of waiting; after that, the stream's own abort wiring takes over, so a parked query still survives between calls. The isolated and AskClaude paths spawn under the call's context. `claude --version` is bounded by 10 s and runs once at a time; a probe that times out is logged and does not fail the query, and an abort during the probe ends the turn as aborted without caching a result. Only the leading `x.y.z` of the version output is read.
|
|
90
98
|
- The cost of a turn leaves out pi-ai's `cost.tiers` and `cacheWrite1h`. The registered models carry zero rates and no tiers, so the cost is zero either way.
|
|
@@ -127,18 +135,13 @@ What this port is pinned to, and every place where it differs from the TypeScrip
|
|
|
127
135
|
- The alternative provider for the AskClaude, image and session-resume tests must come from `CLAUDE_BRIDGE_TESTING_ALT_PROVIDER`, `CLAUDE_BRIDGE_TESTING_ALT_MODEL` and that provider's API-key variable, because every live run uses a fresh `PIG_HOME`/`PIG_CODING_AGENT_DIR` (the TypeScript suite used the real agent dir). Without them those checks skip.
|
|
128
136
|
- The deadlock-check log pattern, the shutdown test (a third case for the stdin close) and the subagent test (our own agent definition, run through `@tintinweb/pi-subagents`) are adapted as listed in [verification.md](verification.md).
|
|
129
137
|
|
|
130
|
-
##
|
|
131
|
-
|
|
132
|
-
Upstream pi-claude-bridge moved on to 0.11.0 (acbe45b, 100 commits) after the pin. These were ported on their own, each with the TypeScript commit; the rest is listed below.
|
|
138
|
+
## Caught up to 0.11.0
|
|
133
139
|
|
|
134
|
-
-
|
|
135
|
-
- **Parent-session markers (e93d51d, #198).** When `CLAUDECODE` is set in PiG's environment, the parent Claude Code session's markers are dropped from the child env. The TypeScript also moved `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` off its own `process.env`; the port never set it there.
|
|
136
|
-
- **Default output style (5c4a268, #220).** The provider's `--settings` carries `outputStyle: "default"`. Pinned live by `TestOutputStyleDefaultOverridesAUserLevelOutputStyle`.
|
|
137
|
-
- **Thinking off (0b096a8, #152).** No reasoning level means `--thinking=disabled` on the provider and isolated paths, for reasoning models whose `thinkingLevelMap` does not mark `off` as `null`.
|
|
140
|
+
The port followed pi-claude-bridge from 6aa276a to acbe45b (0.10.0 and 0.11.0, 100 commits). Ported: the content-based session sync (#222) with its live recording, cuts, prefix writes, forks and persisted state; the conversion changes (#227, #207, #242); the child environment (f502f5d, 2edde93, e93d51d), `settingSources: ["user"]` and the end of the project config (#198); the default output style (#220); thinking off (#152); provider env; the Extra Usage notice (#201, #213); same-session routing (#111); parked-query closing at shutdown (#210, as D10); the bash default timeout in the `tool_call` hook (#203); the fallback fix (87f67d1). The argv snapshot predates some of these, so `TestArgvMatchesNodeSdkSnapshot` sets `EnvAddedAfterSnapshot`, `--setting-sources=user`, `--session-id` and `outputStyle` aside, and the unit tests pin them instead.
|
|
138
141
|
|
|
139
142
|
Port-only, with no TypeScript counterpart: **`CLAUDE_CODE_DISABLE_FEATURE_FLAGS=1`** in `CCChildEnv`. In Claude Code 2.1.319 feature-flag evaluation (`Ph()`) requires both no disable variable and telemetry on, and `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` counts as telemetry off, so the TypeScript already runs with flags off; the explicit variable also keeps the disk cache closed should `CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF` be exported, and `TestDisableFeatureFlagsTurnsFlagEvaluationOff` pins the behaviour.
|
|
140
143
|
|
|
141
|
-
Not ported
|
|
144
|
+
Not ported: the AskClaude split (D8), `CLAUDE_BRIDGE_TRACE_DIR` and the `diag/` tooling (D9), the per-session scope of shutdown (D10), the removal of the `provider.plan` startup notice (the port detects the plan instead and never asked), and the `BRIDGE_VERSION`/SDK version log line at load. D1 to D7 above still hold.
|
|
142
145
|
|
|
143
146
|
## Pi-only tests that are not ported
|
|
144
147
|
|
|
@@ -129,8 +129,7 @@ func Extension() *sdk.Extension {
|
|
|
129
129
|
// unread and the logs beside the cwd, and the extension still loads.
|
|
130
130
|
agentDir, _ := sdk.AgentDir()
|
|
131
131
|
configDirName, _ := sdk.ConfigDirName()
|
|
132
|
-
|
|
133
|
-
cfg := config.Load(agentDir, cwd, configDirName)
|
|
132
|
+
cfg := config.Load(agentDir)
|
|
134
133
|
// Detected before the provider registers, since the plan decides which models get 1M.
|
|
135
134
|
// Skipped outside a host, so validate-only installs and tests never spawn claude.
|
|
136
135
|
if agentDir != "" && cfg.Provider.Plan == nil {
|
|
@@ -196,7 +195,8 @@ func Extension() *sdk.Extension {
|
|
|
196
195
|
ext.OnSessionStart(func(ctx sdk.Context, data map[string]any) (any, error) {
|
|
197
196
|
defer func() { _ = recover() }()
|
|
198
197
|
reason, _ := data["reason"].(string)
|
|
199
|
-
|
|
198
|
+
previous, _ := data["previousSessionFile"].(string)
|
|
199
|
+
b.OnSessionStart(sdkHost{ctx}, reason, previous)
|
|
200
200
|
return nil, nil
|
|
201
201
|
})
|
|
202
202
|
forward := func(event string, fn func(h bridge.Host, data map[string]any)) {
|
|
@@ -218,6 +218,19 @@ func Extension() *sdk.Extension {
|
|
|
218
218
|
forward("session_compact_failed", func(h bridge.Host, _ map[string]any) { b.OnSessionCompactFailed(h) })
|
|
219
219
|
forward("session_tree", func(h bridge.Host, _ map[string]any) { b.OnSessionTree(h) })
|
|
220
220
|
forward("session_shutdown", func(h bridge.Host, _ map[string]any) { b.OnSessionShutdown(h) })
|
|
221
|
+
// Claude Code models expect bash to time out (CC's default is 120s); PiG's bash has none.
|
|
222
|
+
// Set on the execution copy PiG hands tool_call handlers, not in the stored call, so PiG's
|
|
223
|
+
// history keeps the arguments CC sent and a rebuild matches CC's records (pi-claude-bridge
|
|
224
|
+
// #203). A `background: true` call (added by an extension) doesn't block the turn, so it
|
|
225
|
+
// gets no default.
|
|
226
|
+
ext.OnEvent("tool_call", func(ctx sdk.Context, data map[string]any) (any, error) {
|
|
227
|
+
defer func() { _ = recover() }()
|
|
228
|
+
input, _ := data["input"].(map[string]any)
|
|
229
|
+
if sdk.IsToolCallEventType("bash", data) && input != nil && input["timeout"] == nil && input["background"] != true && ctx.ModelProvider() == "claude-bridge" {
|
|
230
|
+
input["timeout"] = 120
|
|
231
|
+
}
|
|
232
|
+
return nil, nil
|
|
233
|
+
})
|
|
221
234
|
return ext
|
|
222
235
|
}
|
|
223
236
|
|
|
@@ -65,19 +65,19 @@ func (b *Bridge) PromptAndWait(ctx context.Context, host Host, r AskRequest) (te
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// A non-isolated call continues the session the provider already keeps for this pi
|
|
68
|
-
// session, or
|
|
69
|
-
//
|
|
68
|
+
// session, or imports pi's context into one of its own. The provider's mirror is
|
|
69
|
+
// not touched (D8): its recording does not cover what AskClaude adds, so the next
|
|
70
|
+
// provider turn forks or rewrites from the content comparison as usual.
|
|
70
71
|
resume := ""
|
|
71
72
|
if !r.Isolated && len(r.Context) > 0 {
|
|
72
73
|
if state := b.sessionStateFor(r.PiSessionID); state != nil {
|
|
73
|
-
resume = state.
|
|
74
|
+
resume = state.CCSessionID
|
|
74
75
|
} else {
|
|
75
76
|
withPrompt := append(slices.Clone(r.Context), map[string]any{"role": "user", "content": r.Prompt, "timestamp": b.deps.Now().UnixMilli()})
|
|
76
|
-
|
|
77
|
-
if err != nil {
|
|
77
|
+
var err error
|
|
78
|
+
if resume, err = b.importForAskClaude(host, withPrompt, cwd, cliModel); err != nil {
|
|
78
79
|
return "", err
|
|
79
80
|
}
|
|
80
|
-
resume = synced.SessionID
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -67,13 +67,18 @@ func (b *Bridge) unregisterContext(q *QueryContext) {
|
|
|
67
67
|
delete(b.contexts, q)
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// contextForToolResults finds the
|
|
71
|
-
// tool call ids it parked, queued or emitted. nil when none
|
|
72
|
-
|
|
70
|
+
// contextForToolResults finds the parked query of this pi session that a turn's
|
|
71
|
+
// results answer, by the tool call ids it parked, queued or emitted. nil when none
|
|
72
|
+
// matches. Tool-call ids repeat across sessions whenever one copies another's
|
|
73
|
+
// history (a fork, a worker seeded from its host), so an id alone could deliver one
|
|
74
|
+
// session's turn into another's Claude Code query (pi-claude-bridge #111).
|
|
75
|
+
func (b *Bridge) contextForToolResults(results []convert.McpResult, piSessionID string) *QueryContext {
|
|
73
76
|
b.mu.Lock()
|
|
74
77
|
live := make([]*QueryContext, 0, len(b.contexts))
|
|
75
78
|
for c := range b.contexts {
|
|
76
|
-
|
|
79
|
+
if c.piSessionID == piSessionID {
|
|
80
|
+
live = append(live, c)
|
|
81
|
+
}
|
|
77
82
|
}
|
|
78
83
|
b.mu.Unlock()
|
|
79
84
|
for _, r := range results {
|
|
@@ -195,6 +200,7 @@ func parseRateLimitInfo(m map[string]any) RateLimitInfo {
|
|
|
195
200
|
OverageStatus: str(m["overageStatus"]), OverageResetsAt: num(m["overageResetsAt"]),
|
|
196
201
|
Utilization: num(m["utilization"]), SurpassedThreshold: num(m["surpassedThreshold"]),
|
|
197
202
|
}
|
|
203
|
+
info.IsUsingOverage, _ = m["isUsingOverage"].(bool)
|
|
198
204
|
if windows := obj(m["unifiedWindows"]); windows != nil {
|
|
199
205
|
info.UnifiedWindows = map[string]*UsageWindow{}
|
|
200
206
|
for name, raw := range windows {
|
|
@@ -253,7 +259,14 @@ loop:
|
|
|
253
259
|
b.debugf("consumeQuery: skipping malformed frame: %v", err)
|
|
254
260
|
continue
|
|
255
261
|
}
|
|
262
|
+
if q.isAbandoned() {
|
|
263
|
+
break
|
|
264
|
+
}
|
|
265
|
+
b.tapRecording(q, frame)
|
|
256
266
|
b.reportHeldRateLimit(q)
|
|
267
|
+
if ctx.Err() != nil || q.isAbandoned() {
|
|
268
|
+
break
|
|
269
|
+
}
|
|
257
270
|
if b.consumeFrame(host, q, frame, &res) {
|
|
258
271
|
break
|
|
259
272
|
}
|
|
@@ -300,17 +313,10 @@ func (b *Bridge) consumeFrame(host Host, q *QueryContext, frame map[string]any,
|
|
|
300
313
|
typ := str(frame["type"])
|
|
301
314
|
|
|
302
315
|
// Lifecycle frames come before the currentPiStream gate: everything below it is
|
|
303
|
-
// content, which has nowhere to go once a turn ended on a tool call.
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
// its MCP calls still need stdin until the following result.
|
|
308
|
-
if parent, present := frame["parent_tool_use_id"]; typ == "user" && present && parent == nil && frame["isReplay"] == true {
|
|
309
|
-
q.mu.Lock()
|
|
310
|
-
delete(q.pendingSteerUUIDs, str(frame["uuid"]))
|
|
311
|
-
q.mu.Unlock()
|
|
312
|
-
}
|
|
313
|
-
|
|
316
|
+
// content, which has nowhere to go once a turn ended on a tool call. A replay
|
|
317
|
+
// acknowledgement (tapRecording) clears a queued steer even after its turn's
|
|
318
|
+
// stream ended; it marks incorporation, not completion: the steer's MCP calls
|
|
319
|
+
// still need stdin until the following result.
|
|
314
320
|
switch typ {
|
|
315
321
|
case "result":
|
|
316
322
|
// The failure a result carries is the only record the turn failed at all; it
|
|
@@ -373,7 +379,15 @@ func (b *Bridge) consumeFrame(host Host, q *QueryContext, frame map[string]any,
|
|
|
373
379
|
case "stream_event":
|
|
374
380
|
b.processStreamEvent(q, frame)
|
|
375
381
|
case "assistant":
|
|
376
|
-
|
|
382
|
+
// A completed echo can trail tool-result delivery. tapRecording recorded it for
|
|
383
|
+
// its original step; it is never rendered again into the new pi stream.
|
|
384
|
+
q.mu.Lock()
|
|
385
|
+
step := q.assistantSteps[str(obj(frame["message"])["id"])]
|
|
386
|
+
emitted := step != nil && step.emitted != nil
|
|
387
|
+
q.mu.Unlock()
|
|
388
|
+
if !emitted {
|
|
389
|
+
b.processAssistantMessage(q, frame)
|
|
390
|
+
}
|
|
377
391
|
case "result":
|
|
378
392
|
// The success path: push the result text when no stream event delivered it.
|
|
379
393
|
func() {
|
|
@@ -478,12 +492,10 @@ func (b *Bridge) turnField(q *QueryContext, key string) any {
|
|
|
478
492
|
return q.turnOutput[key]
|
|
479
493
|
}
|
|
480
494
|
|
|
481
|
-
// finishAborted ends an aborted query
|
|
482
|
-
//
|
|
483
|
-
// stream already ended was returned to pi and is left alone.
|
|
495
|
+
// finishAborted ends an aborted query; its final snapshot was committed by the abort
|
|
496
|
+
// itself. A turn whose stream already ended was returned to pi and is left alone.
|
|
484
497
|
func (b *Bridge) finishAborted(q *QueryContext) {
|
|
485
|
-
b.
|
|
486
|
-
b.debugf("provider: abort detected, marked session needsRebuild + forceRotate")
|
|
498
|
+
b.debugf("provider: abort detected")
|
|
487
499
|
q.mu.Lock()
|
|
488
500
|
defer q.mu.Unlock()
|
|
489
501
|
q.heldRateLimit = nil
|
|
@@ -495,17 +507,16 @@ func (b *Bridge) finishAborted(q *QueryContext) {
|
|
|
495
507
|
q.endStreamLocked(map[string]any{"type": "error", "reason": "aborted", "error": deepCopy(q.turnOutput)})
|
|
496
508
|
}
|
|
497
509
|
|
|
498
|
-
// failQuery ends a failed query with exactly one error event on the open stream
|
|
499
|
-
//
|
|
510
|
+
// failQuery ends a failed query with exactly one error event on the open stream,
|
|
511
|
+
// after committing its progress dirty: the next turn compares content and keeps what
|
|
512
|
+
// still matches. A turn whose stream already ended is left unchanged (issue #181).
|
|
500
513
|
func (b *Bridge) failQuery(q *QueryContext, client ccproto.Client, failure error) {
|
|
501
514
|
b.debugf("provider: query error, error=%v", failure)
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
} else {
|
|
508
|
-
b.markSessionNeedsRebuild(q.piSessionID, false)
|
|
515
|
+
q.mu.Lock()
|
|
516
|
+
commit := q.commitRecording
|
|
517
|
+
q.mu.Unlock()
|
|
518
|
+
if commit != nil {
|
|
519
|
+
commit(true)
|
|
509
520
|
}
|
|
510
521
|
q.mu.Lock()
|
|
511
522
|
ps := q.promptStream
|
|
@@ -638,16 +649,23 @@ func (b *Bridge) drainForAbort(q *QueryContext) {
|
|
|
638
649
|
}
|
|
639
650
|
|
|
640
651
|
// discardRewrittenQuery drops a Claude Code query parked at a tool boundary whose
|
|
641
|
-
// conversation pi has since rewritten (/compact, tree navigation
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
//
|
|
652
|
+
// conversation pi has since rewritten or replaced (/compact, tree navigation, a
|
|
653
|
+
// history edit, a fresh prompt from the same session; issue #101). Its progress is
|
|
654
|
+
// committed first, dirty, so the next sync keeps whatever still matches. Delivering
|
|
655
|
+
// the tool result into it would hand Claude Code the context pi just shrank; the
|
|
656
|
+
// caller takes the fresh-query path instead, where the sync imports pi's current
|
|
657
|
+
// history, this tool result included. reason is what the parked handlers are answered with.
|
|
646
658
|
func (b *Bridge) discardRewrittenQuery(q *QueryContext, reason string) {
|
|
659
|
+
q.mu.Lock()
|
|
660
|
+
commit := q.commitRecording
|
|
661
|
+
q.mu.Unlock()
|
|
662
|
+
if commit != nil {
|
|
663
|
+
commit(true)
|
|
664
|
+
}
|
|
647
665
|
q.mu.Lock()
|
|
648
666
|
discarded := q.activeQuery
|
|
649
667
|
q.activeQuery = nil
|
|
650
|
-
q.abandoned =
|
|
668
|
+
q.abandoned = true
|
|
651
669
|
q.turnToolCallIDs = []string{}
|
|
652
670
|
ps := q.promptStream
|
|
653
671
|
q.promptStream = nil
|
|
@@ -668,21 +686,14 @@ func (b *Bridge) discardRewrittenQuery(q *QueryContext, reason string) {
|
|
|
668
686
|
// subprocess never settles.
|
|
669
687
|
q.releasePendingToolCalls(reason)
|
|
670
688
|
b.killClient(discarded)
|
|
671
|
-
// The CLI may still flush a record into the session JSONL and the rebuild is
|
|
672
|
-
// next, so rotate rather than race it. Only this session's mirror.
|
|
673
|
-
b.markSessionNeedsRebuild(q.piSessionID, true)
|
|
674
|
-
b.debugf("provider: history rewritten under a parked query — discarded it, rebuilding from current history")
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// steerMissedSession records a steer that never made it into Claude Code's
|
|
678
|
-
// session. The cursor has already counted it, so count-based sync would skip it
|
|
679
|
-
// forever: rebuild instead, which re-imports the message from pi's context.
|
|
680
|
-
func (b *Bridge) steerMissedSession(q *QueryContext, text string) {
|
|
681
689
|
q.mu.Lock()
|
|
682
|
-
q.
|
|
690
|
+
if q.currentPiStream != nil && q.turnOutput != nil {
|
|
691
|
+
q.turnOutput["stopReason"] = "error"
|
|
692
|
+
q.turnOutput["errorMessage"] = "Conversation replaced"
|
|
693
|
+
q.endStreamLocked(map[string]any{"type": "error", "reason": "error", "error": deepCopy(q.turnOutput)})
|
|
694
|
+
}
|
|
683
695
|
q.mu.Unlock()
|
|
684
|
-
b.
|
|
685
|
-
b.debugf("provider: steer never reached CC, marked query for rebuild: %s", truncateRunes(text, 60))
|
|
696
|
+
b.debugf("provider: discarded parked query; its recorded prefix remains available for a fork")
|
|
686
697
|
}
|
|
687
698
|
|
|
688
699
|
func newUUID() string {
|
|
@@ -705,9 +716,15 @@ func newUUID() string {
|
|
|
705
716
|
// the FIFO ordering are Claude Code internals.
|
|
706
717
|
//
|
|
707
718
|
// Results pair with handlers by tool use id. host may be nil (no stuck-handler notice).
|
|
708
|
-
|
|
719
|
+
// history is pi's conversation without system messages; it registers the results
|
|
720
|
+
// and any steer with the recording.
|
|
721
|
+
func (b *Bridge) deliverToolResults(ctx context.Context, host Host, q *QueryContext, results []convert.McpResult, steer []any, history []map[string]any) {
|
|
722
|
+
contextLength := len(history)
|
|
723
|
+
if err := q.recording.Results(history); err != nil {
|
|
724
|
+
b.debugf("recording: tool results could not be digested: %v", err)
|
|
725
|
+
}
|
|
709
726
|
if len(steer) > 0 {
|
|
710
|
-
b.pushSteer(ctx, q, steer)
|
|
727
|
+
b.pushSteer(ctx, q, steer, history)
|
|
711
728
|
}
|
|
712
729
|
|
|
713
730
|
type release struct {
|
|
@@ -757,8 +774,9 @@ func (b *Bridge) deliverToolResults(ctx context.Context, host Host, q *QueryCont
|
|
|
757
774
|
}
|
|
758
775
|
|
|
759
776
|
// pushSteer writes a steer to Claude Code's stdin and waits for the write. A steer
|
|
760
|
-
// that cannot be written
|
|
761
|
-
|
|
777
|
+
// that cannot be written leaves a coverage gap (no replay acknowledgement ever
|
|
778
|
+
// comes, so the next sync imports it from pi); the tool results are delivered anyway.
|
|
779
|
+
func (b *Bridge) pushSteer(ctx context.Context, q *QueryContext, steer []any, history []map[string]any) {
|
|
762
780
|
parts := make([]string, len(steer))
|
|
763
781
|
for i, raw := range steer {
|
|
764
782
|
if blk := obj(raw); blk["type"] == "text" {
|
|
@@ -775,24 +793,25 @@ func (b *Bridge) pushSteer(ctx context.Context, q *QueryContext, steer []any) {
|
|
|
775
793
|
if ps != nil {
|
|
776
794
|
uuid = newUUID()
|
|
777
795
|
// Registered before the write: a result can arrive during it.
|
|
778
|
-
q.
|
|
796
|
+
q.addPendingSteer(uuid)
|
|
779
797
|
}
|
|
780
798
|
q.mu.Unlock()
|
|
781
799
|
if ps == nil {
|
|
782
800
|
b.debugf("WARNING: steer with no prompt stream, dropping: %s", truncateRunes(text, 60))
|
|
783
|
-
b.steerMissedSession(q, text)
|
|
784
801
|
return
|
|
785
802
|
}
|
|
803
|
+
if err := q.recording.Users(history, turnStart(history), uuid, false, true); err != nil {
|
|
804
|
+
b.debugf("recording: steer could not be digested: %v", err)
|
|
805
|
+
}
|
|
786
806
|
msg := userMessage(steer, "next")
|
|
787
807
|
msg.UUID = uuid
|
|
788
808
|
if err := ps.Push(ctx, msg); err != nil {
|
|
789
|
-
// The query is ending: further input would wedge tool-result delivery.
|
|
790
|
-
//
|
|
809
|
+
// The query is ending: further input would wedge tool-result delivery. No replay
|
|
810
|
+
// acknowledgement means no coverage slot; the next sync imports the steer from pi.
|
|
791
811
|
q.mu.Lock()
|
|
792
|
-
|
|
812
|
+
q.removePendingSteer(uuid)
|
|
793
813
|
q.mu.Unlock()
|
|
794
814
|
b.debugf("provider: steer push rejected, delivering tool result anyway: %v", err)
|
|
795
|
-
b.steerMissedSession(q, text)
|
|
796
815
|
return
|
|
797
816
|
}
|
|
798
817
|
b.debugf("provider: steer written to CC stdin before tool result: %s", truncateRunes(text, 60))
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
package bridge
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
"fmt"
|
|
5
4
|
"runtime/debug"
|
|
6
5
|
"slices"
|
|
7
6
|
"sort"
|
|
@@ -117,15 +116,22 @@ func (b *Bridge) hostSessionID(host Host, event string) string {
|
|
|
117
116
|
return id
|
|
118
117
|
}
|
|
119
118
|
|
|
120
|
-
// OnSessionStart remembers the session's cwd
|
|
121
|
-
// session
|
|
122
|
-
|
|
119
|
+
// OnSessionStart remembers the session's cwd. Mirrors stay: each is keyed by its own
|
|
120
|
+
// pi session id, so switching sessions erases none of them. A fork or clone starts
|
|
121
|
+
// from its parent's mirror, so its first turn forks the parent's Claude Code session.
|
|
122
|
+
func (b *Bridge) OnSessionStart(host Host, reason, previousSessionFile string) {
|
|
123
123
|
defer b.recoverHandler("session_start")
|
|
124
124
|
b.showStatus(host)
|
|
125
|
-
b.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
id := b.hostSessionID(host, "session_start")
|
|
126
|
+
b.rememberSessionCwd(id, host.Cwd())
|
|
127
|
+
b.mu.Lock()
|
|
128
|
+
n := len(b.sessions)
|
|
129
|
+
b.mu.Unlock()
|
|
130
|
+
b.debugf("session_start:%s: retaining %d shared session(s)", reason, n)
|
|
131
|
+
if reason == "fork" && previousSessionFile != "" && id != "" {
|
|
132
|
+
if err := b.inheritSessionState(previousSessionFile, id); err != nil {
|
|
133
|
+
b.debugf("session_start:fork: %v", err)
|
|
134
|
+
}
|
|
129
135
|
}
|
|
130
136
|
}
|
|
131
137
|
|
|
@@ -207,18 +213,39 @@ func (b *Bridge) historyRewrittenBy(host Host, event string) {
|
|
|
207
213
|
b.forgetSummarySession(id)
|
|
208
214
|
}
|
|
209
215
|
|
|
210
|
-
// OnSessionShutdown ends a pi session: its cwd
|
|
211
|
-
//
|
|
216
|
+
// OnSessionShutdown ends a pi session: its cwd and sync lock go, its parked queries
|
|
217
|
+
// are closed with their progress kept for a later resume (a tool that returns
|
|
218
|
+
// terminate: true ends pi's run with Claude Code still parked on its result, issue
|
|
219
|
+
// #210), and then every Claude Code child this process started is killed (D10),
|
|
220
|
+
// even when the host fails. Mirrors stay; they are keyed per pi session and persisted.
|
|
212
221
|
func (b *Bridge) OnSessionShutdown(host Host) {
|
|
213
222
|
defer b.recoverHandler("session_shutdown")
|
|
214
223
|
defer b.killLiveQueries()
|
|
215
224
|
id := b.hostSessionID(host, "session_shutdown")
|
|
216
225
|
b.forgetSessionCwd(id)
|
|
217
226
|
b.mu.Lock()
|
|
218
|
-
delete(b.syncLocks,
|
|
227
|
+
delete(b.syncLocks, id)
|
|
219
228
|
b.mu.Unlock()
|
|
229
|
+
b.closeSessionQueries(id)
|
|
220
230
|
b.reportLeaks("session_shutdown")
|
|
221
|
-
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// closeSessionQueries closes every query the shutting-down pi session still holds.
|
|
234
|
+
func (b *Bridge) closeSessionQueries(piSessionID string) {
|
|
235
|
+
for _, q := range b.liveContexts() {
|
|
236
|
+
if q.piSessionID != piSessionID {
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
q.mu.Lock()
|
|
240
|
+
active, pending, ps := q.activeQuery != nil, len(q.pendingToolCalls), q.promptStream != nil
|
|
241
|
+
q.mu.Unlock()
|
|
242
|
+
if active {
|
|
243
|
+
b.debugf("session_shutdown: closing parked query — pendingToolCalls=%d promptStream=%t", pending, ps)
|
|
244
|
+
} else {
|
|
245
|
+
b.debugf("WARNING: session_shutdown left a context with no query — pendingToolCalls=%d promptStream=%t", pending, ps)
|
|
246
|
+
}
|
|
247
|
+
b.discardRewrittenQuery(q, "Session shut down; this query was closed.")
|
|
248
|
+
}
|
|
222
249
|
}
|
|
223
250
|
|
|
224
251
|
// killLiveQueries aborts every live query, then kills every Claude Code child still
|
|
@@ -235,19 +262,3 @@ func (b *Bridge) killLiveQueries() {
|
|
|
235
262
|
}
|
|
236
263
|
ccproto.Shutdown()
|
|
237
264
|
}
|
|
238
|
-
|
|
239
|
-
// clearSessions drops every mirror: children never emit session_shutdown (only
|
|
240
|
-
// runtime teardown does), so there is no per-entry removal, and a top-level
|
|
241
|
-
// transition takes every mirror with it.
|
|
242
|
-
func (b *Bridge) clearSessions(event string) {
|
|
243
|
-
b.mu.Lock()
|
|
244
|
-
n := len(b.sessions)
|
|
245
|
-
b.sessions = map[string]SessionState{}
|
|
246
|
-
b.historyRewritten = map[string]struct{}{}
|
|
247
|
-
b.mu.Unlock()
|
|
248
|
-
plural := "s"
|
|
249
|
-
if n == 1 {
|
|
250
|
-
plural = ""
|
|
251
|
-
}
|
|
252
|
-
b.debugf("%s", fmt.Sprintf("%s: clearing %d shared session%s", event, n, plural))
|
|
253
|
-
}
|