patch-cc 0.3.2__tar.gz → 0.4.0__tar.gz
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.
- patch_cc-0.4.0/.github/workflows/release-watch.yml +164 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/PKG-INFO +11 -9
- {patch_cc-0.3.2 → patch_cc-0.4.0}/README.md +10 -8
- {patch_cc-0.3.2 → patch_cc-0.4.0}/docs/INTERNALS.md +85 -45
- {patch_cc-0.3.2 → patch_cc-0.4.0}/docs/PLAYBOOK.md +99 -11
- patch_cc-0.4.0/docs/corpus.md +97 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/__init__.py +1 -1
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/bun/blob.py +83 -30
- patch_cc-0.4.0/src/patch_cc/bun/container.py +208 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/cli.py +11 -1
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/js.py +388 -135
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/patcher.py +21 -22
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/patches/agents.py +9 -3
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/patches/chrome.py +19 -10
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/patches/codex.py +1 -3
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/patches/streaming.py +319 -78
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/patches/thinking.py +4 -4
- patch_cc-0.3.2/docs/corpus.md +0 -86
- patch_cc-0.3.2/src/patch_cc/bun/container.py +0 -183
- {patch_cc-0.3.2 → patch_cc-0.4.0}/.github/workflows/ci.yml +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/.github/workflows/release.yml +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/.gitignore +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/AGENTS.md +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/CLAUDE.md +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/LICENSE +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/docs/CONDUCT.md +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/docs/demo.gif +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/pyproject.toml +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/bun/__init__.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/bun/elf.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/bun/errors.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/bun/macho.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/cache.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/codex/__init__.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/codex/gateway.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/codex/models.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/codex/oauth.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/codex/translate.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/doctor.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/locate.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/menu.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/patches/__init__.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/patches/base.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/patches/output.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/src/patch_cc/ui.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.4.0}/uv.lock +0 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
name: Release watch
|
|
2
|
+
|
|
3
|
+
# Fires the "patch-cc release watch" cloud routine the moment Anthropic ships a
|
|
4
|
+
# new Claude Code build, instead of leaving it to the routine's own daily cron.
|
|
5
|
+
#
|
|
6
|
+
# The routine already knows how to decide whether anything moved -- it compares
|
|
7
|
+
# the upstream version and the checkout HEAD against the ledger in issue #1 and
|
|
8
|
+
# exits silently when neither did. Its schedule was never the interesting part;
|
|
9
|
+
# it was just a poll, once a day, which meant a release that broke a patch could
|
|
10
|
+
# sit unnoticed for up to 24 hours. This workflow moves the poll here, where a
|
|
11
|
+
# tick is one curl instead of a cloud session, and only wakes the routine when
|
|
12
|
+
# the version actually changes.
|
|
13
|
+
#
|
|
14
|
+
# The routine keeps its daily schedule as a backstop: if this workflow breaks or
|
|
15
|
+
# GitHub skips the schedule, the watch degrades to its old once-a-day behaviour
|
|
16
|
+
# rather than going silent. That is why a failed poll below is a warning and not
|
|
17
|
+
# a red run -- a flaky fetch must not page anyone.
|
|
18
|
+
#
|
|
19
|
+
# SETUP (one time, by hand -- there is no API for token creation):
|
|
20
|
+
# claude.ai/code/routines -> "patch-cc release watch" -> pencil icon
|
|
21
|
+
# -> Add another trigger -> API -> Generate token (shown once)
|
|
22
|
+
# -> store it as the repo secret ROUTINE_FIRE_TOKEN.
|
|
23
|
+
|
|
24
|
+
on:
|
|
25
|
+
schedule:
|
|
26
|
+
# Every 10 minutes. GitHub delays scheduled runs under load, so read this as
|
|
27
|
+
# "within ~10-25 min of a release", not as a guarantee.
|
|
28
|
+
- cron: "*/10 * * * *"
|
|
29
|
+
workflow_dispatch:
|
|
30
|
+
|
|
31
|
+
concurrency:
|
|
32
|
+
group: release-watch
|
|
33
|
+
cancel-in-progress: false
|
|
34
|
+
|
|
35
|
+
permissions:
|
|
36
|
+
contents: read
|
|
37
|
+
issues: read
|
|
38
|
+
|
|
39
|
+
env:
|
|
40
|
+
# The channel file the Claude Code installer itself reads -- the same source
|
|
41
|
+
# the routine treats as authoritative, so the two never disagree about what
|
|
42
|
+
# "the current release" means.
|
|
43
|
+
CHANNEL_URL: https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest
|
|
44
|
+
# Not a secret: firing needs the bearer token, and that is one.
|
|
45
|
+
ROUTINE_ID: trig_01WrgxdCZahJEHD3NNYcetqS
|
|
46
|
+
LEDGER_ISSUE: "1"
|
|
47
|
+
|
|
48
|
+
jobs:
|
|
49
|
+
watch:
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
steps:
|
|
52
|
+
- name: Read the release channel
|
|
53
|
+
id: upstream
|
|
54
|
+
run: |
|
|
55
|
+
set -uo pipefail
|
|
56
|
+
if ! v=$(curl -fsSL --max-time 30 --retry 3 --retry-delay 5 "$CHANNEL_URL"); then
|
|
57
|
+
echo "::warning::could not reach the release channel; skipping this tick"
|
|
58
|
+
echo "skip=1" >> "$GITHUB_OUTPUT"
|
|
59
|
+
exit 0
|
|
60
|
+
fi
|
|
61
|
+
v=$(printf '%s' "$v" | tr -d '[:space:]')
|
|
62
|
+
case "$v" in
|
|
63
|
+
[0-9]*.[0-9]*.[0-9]*) ;;
|
|
64
|
+
*)
|
|
65
|
+
echo "::warning::channel returned something that is not a version: '$v'"
|
|
66
|
+
echo "skip=1" >> "$GITHUB_OUTPUT"
|
|
67
|
+
exit 0
|
|
68
|
+
;;
|
|
69
|
+
esac
|
|
70
|
+
echo "version=$v" >> "$GITHUB_OUTPUT"
|
|
71
|
+
echo "upstream: $v"
|
|
72
|
+
|
|
73
|
+
# First guard: did an earlier tick already fire for this version? The
|
|
74
|
+
# routine takes several minutes to run (it downloads a ~275 MB bundle), so
|
|
75
|
+
# without this every tick during that window would fire it again.
|
|
76
|
+
- name: Already fired for this version?
|
|
77
|
+
id: marker
|
|
78
|
+
if: steps.upstream.outputs.skip != '1'
|
|
79
|
+
uses: actions/cache/restore@v4
|
|
80
|
+
with:
|
|
81
|
+
path: .fired
|
|
82
|
+
key: fired-${{ steps.upstream.outputs.version }}
|
|
83
|
+
lookup-only: true
|
|
84
|
+
|
|
85
|
+
# Second guard, and the authoritative one: the ledger records the version
|
|
86
|
+
# the routine has actually finished processing. This is what catches the
|
|
87
|
+
# case where the cache entry was evicted but the work is long done.
|
|
88
|
+
- name: Read the ledger's last-version
|
|
89
|
+
id: ledger
|
|
90
|
+
if: steps.upstream.outputs.skip != '1' && steps.marker.outputs.cache-hit != 'true'
|
|
91
|
+
env:
|
|
92
|
+
GH_TOKEN: ${{ github.token }}
|
|
93
|
+
run: |
|
|
94
|
+
set -uo pipefail
|
|
95
|
+
if ! body=$(gh issue view "$LEDGER_ISSUE" --repo "$GITHUB_REPOSITORY" --json body -q .body); then
|
|
96
|
+
echo "::warning::could not read the ledger; skipping this tick rather than guessing"
|
|
97
|
+
echo "skip=1" >> "$GITHUB_OUTPUT"
|
|
98
|
+
exit 0
|
|
99
|
+
fi
|
|
100
|
+
last=$(printf '%s\n' "$body" | sed -n 's/^last-version:[[:space:]]*//p' | head -1 | tr -d '[:space:]')
|
|
101
|
+
echo "last=$last" >> "$GITHUB_OUTPUT"
|
|
102
|
+
echo "ledger: $last"
|
|
103
|
+
|
|
104
|
+
- name: Fire the routine
|
|
105
|
+
id: fire
|
|
106
|
+
if: >-
|
|
107
|
+
steps.upstream.outputs.skip != '1' &&
|
|
108
|
+
steps.marker.outputs.cache-hit != 'true' &&
|
|
109
|
+
steps.ledger.outputs.skip != '1' &&
|
|
110
|
+
steps.ledger.outputs.last != steps.upstream.outputs.version
|
|
111
|
+
env:
|
|
112
|
+
TOKEN: ${{ secrets.ROUTINE_FIRE_TOKEN }}
|
|
113
|
+
VERSION: ${{ steps.upstream.outputs.version }}
|
|
114
|
+
LAST: ${{ steps.ledger.outputs.last }}
|
|
115
|
+
run: |
|
|
116
|
+
set -uo pipefail
|
|
117
|
+
if [ -z "${TOKEN:-}" ]; then
|
|
118
|
+
echo "::error::ROUTINE_FIRE_TOKEN is not set -- see the setup note at the top of this file"
|
|
119
|
+
exit 1
|
|
120
|
+
fi
|
|
121
|
+
body=$(printf 'Claude Code %s is on the release channel; the ledger last recorded %s. This line is a hint for the run log only -- re-derive the version yourself from the channel, per your step 2.' "$VERSION" "${LAST:-(none)}")
|
|
122
|
+
payload=$(jq -nc --arg t "$body" '{text: $t}')
|
|
123
|
+
out=$(mktemp)
|
|
124
|
+
code=$(curl -sS -o "$out" -w '%{http_code}' -X POST \
|
|
125
|
+
"https://api.anthropic.com/v1/claude_code/routines/$ROUTINE_ID/fire" \
|
|
126
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
127
|
+
-H "anthropic-version: 2023-06-01" \
|
|
128
|
+
-H "anthropic-beta: experimental-cc-routine-2026-04-01" \
|
|
129
|
+
-H "Content-Type: application/json" \
|
|
130
|
+
-d "$payload") || code=000
|
|
131
|
+
echo "HTTP $code"
|
|
132
|
+
cat "$out"
|
|
133
|
+
echo
|
|
134
|
+
if [ "$code" != "200" ]; then
|
|
135
|
+
# Deliberately not marking the version as fired, so the next tick
|
|
136
|
+
# retries. A 429 here just means the daily run cap was reached.
|
|
137
|
+
echo "::error::fire failed with HTTP $code"
|
|
138
|
+
exit 1
|
|
139
|
+
fi
|
|
140
|
+
echo "url=$(jq -r '.claude_code_session_url // empty' "$out")" >> "$GITHUB_OUTPUT"
|
|
141
|
+
: > .fired
|
|
142
|
+
|
|
143
|
+
# Only reached on a 200, so the marker never claims a fire that failed.
|
|
144
|
+
- name: Mark this version as fired
|
|
145
|
+
if: steps.fire.outcome == 'success'
|
|
146
|
+
uses: actions/cache/save@v4
|
|
147
|
+
with:
|
|
148
|
+
path: .fired
|
|
149
|
+
key: fired-${{ steps.upstream.outputs.version }}
|
|
150
|
+
|
|
151
|
+
- name: Summary
|
|
152
|
+
if: always()
|
|
153
|
+
run: |
|
|
154
|
+
{
|
|
155
|
+
echo "### Release watch"
|
|
156
|
+
echo
|
|
157
|
+
echo "- upstream: \`${{ steps.upstream.outputs.version || 'unreadable' }}\`"
|
|
158
|
+
echo "- ledger last-version: \`${{ steps.ledger.outputs.last || 'not read' }}\`"
|
|
159
|
+
if [ -n "${{ steps.fire.outputs.url }}" ]; then
|
|
160
|
+
echo "- **fired the routine** -> ${{ steps.fire.outputs.url }}"
|
|
161
|
+
else
|
|
162
|
+
echo "- no fire (nothing moved, or already fired for this version)"
|
|
163
|
+
fi
|
|
164
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: patch-cc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Interactive patcher for the Claude Code native binary
|
|
5
5
|
Project-URL: Homepage, https://github.com/anfreire/patch-cc
|
|
6
6
|
Project-URL: Repository, https://github.com/anfreire/patch-cc
|
|
@@ -198,14 +198,16 @@ startup name / `--version` marker are visible tells too.
|
|
|
198
198
|
## Why native-only, and why it stays small
|
|
199
199
|
|
|
200
200
|
Claude Code now ships only as a Bun single-file executable; the npm package is a
|
|
201
|
-
wrapper that downloads it. patch-cc edits the JavaScript
|
|
202
|
-
binary's `.bun` section in place
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
201
|
+
wrapper that downloads it. patch-cc edits the JavaScript modules embedded in the
|
|
202
|
+
binary's `.bun` section in place — since 2.1.242 the app is code-split across
|
|
203
|
+
~1,300 of them, and patch-cc treats every one as a single surface. It also drops
|
|
204
|
+
the stale precompiled bytecode of the modules it edits — editing a module's
|
|
205
|
+
source invalidates its bytecode anyway — so on Linux, where the ELF section is
|
|
206
|
+
rewritten in place, a patched binary is *smaller* than the original, not larger
|
|
207
|
+
(83 MB smaller on 2.1.243). (On macOS the freed bytes are not yet reclaimed, so
|
|
208
|
+
the file keeps its size; it still runs correctly.)
|
|
209
|
+
[docs/INTERNALS.md](docs/INTERNALS.md#the-bytecode-and-why-we-drop-it) has the
|
|
210
|
+
measurements; `patch-cc status` has yours.
|
|
209
211
|
|
|
210
212
|
See [docs/INTERNALS.md](docs/INTERNALS.md) for the container format and
|
|
211
213
|
[docs/PLAYBOOK.md](docs/PLAYBOOK.md) for repairing a patch after an update.
|
|
@@ -167,14 +167,16 @@ startup name / `--version` marker are visible tells too.
|
|
|
167
167
|
## Why native-only, and why it stays small
|
|
168
168
|
|
|
169
169
|
Claude Code now ships only as a Bun single-file executable; the npm package is a
|
|
170
|
-
wrapper that downloads it. patch-cc edits the JavaScript
|
|
171
|
-
binary's `.bun` section in place
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
170
|
+
wrapper that downloads it. patch-cc edits the JavaScript modules embedded in the
|
|
171
|
+
binary's `.bun` section in place — since 2.1.242 the app is code-split across
|
|
172
|
+
~1,300 of them, and patch-cc treats every one as a single surface. It also drops
|
|
173
|
+
the stale precompiled bytecode of the modules it edits — editing a module's
|
|
174
|
+
source invalidates its bytecode anyway — so on Linux, where the ELF section is
|
|
175
|
+
rewritten in place, a patched binary is *smaller* than the original, not larger
|
|
176
|
+
(83 MB smaller on 2.1.243). (On macOS the freed bytes are not yet reclaimed, so
|
|
177
|
+
the file keeps its size; it still runs correctly.)
|
|
178
|
+
[docs/INTERNALS.md](docs/INTERNALS.md#the-bytecode-and-why-we-drop-it) has the
|
|
179
|
+
measurements; `patch-cc status` has yours.
|
|
178
180
|
|
|
179
181
|
See [docs/INTERNALS.md](docs/INTERNALS.md) for the container format and
|
|
180
182
|
[docs/PLAYBOOK.md](docs/PLAYBOOK.md) for repairing a patch after an update.
|
|
@@ -13,7 +13,11 @@ binary:
|
|
|
13
13
|
- **Windows**: a PE `.bun` section (not supported here)
|
|
14
14
|
|
|
15
15
|
Inside that section is a *Bun module graph*: a flat arena of payloads, a module
|
|
16
|
-
table describing them, and a trailer.
|
|
16
|
+
table describing them, and a trailer. Before 2.1.242 the graph was the app in one
|
|
17
|
+
module plus a few asset modules; since then the app is **code-split** across
|
|
18
|
+
~1,300 `chunk-*.js` modules that the entry lazily imports (see
|
|
19
|
+
[the split](#the-21242-split-and-the-patchable-surface)). Either way patch-cc
|
|
20
|
+
treats every module the container declares to be JavaScript as one surface.
|
|
17
21
|
|
|
18
22
|
```
|
|
19
23
|
.bun section
|
|
@@ -35,52 +39,85 @@ A module record (new 52-byte format) is six such pairs — `name`, `contents`,
|
|
|
35
39
|
`sourcemap`, `bytecode`, `moduleInfo`, `bytecodeOriginPath` — followed by four
|
|
36
40
|
`u8` flags (`encoding`, `loader`, `moduleFormat`, `side`).
|
|
37
41
|
|
|
38
|
-
The module we patch is the entrypoint, which the offsets struct names by index
|
|
39
|
-
(`entry_point_id`) — the same index Bun itself resolves it by. Its `contents` is
|
|
40
|
-
the JS we edit. Its *name* is upstream's to change and we never read it:
|
|
41
|
-
2.1.229 renamed it `/$bunfs/root/src/entrypoints/cli.js` → `/$bunfs/root/cli`.
|
|
42
|
-
|
|
43
42
|
Code: `src/patch_cc/bun/blob.py`.
|
|
44
43
|
|
|
45
|
-
## The
|
|
46
|
-
|
|
47
|
-
The entry module also carries precompiled Bun **bytecode** — more than half the
|
|
48
|
-
binary. Every other module has none.
|
|
44
|
+
## The 2.1.242 split, and the patchable surface
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
Through 2.1.241 the entrypoint module *was* the app: one ~28 MB `contents`
|
|
47
|
+
carrying every line patch-cc anchors on. 2.1.242 turned on Bun code-splitting
|
|
48
|
+
with lazy loading, and the shape changed under the tool:
|
|
52
49
|
|
|
53
|
-
|
|
|
50
|
+
| build | modules | entrypoint `contents` |
|
|
54
51
|
|---|---|---|
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
`
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
52
|
+
| 2.1.241 | 11 | 28,249,679 bytes (the whole app) |
|
|
53
|
+
| 2.1.243 | 1,385 | 19,952 bytes (an argv shim) |
|
|
54
|
+
|
|
55
|
+
The entrypoint is now a ~20 KB shim that parses argv and lazily
|
|
56
|
+
`import()`s the app across ~1,300 `/$bunfs/root/chunk-*.js` modules; ~46 MB of
|
|
57
|
+
JS, the largest chunk 7.3 MB. The code did not disappear — every anchor is still
|
|
58
|
+
in the binary — but it left the one module patch-cc used to read, and it does not
|
|
59
|
+
concentrate in a single chunk (`branding` spans a dozen modules, `org-label` ten,
|
|
60
|
+
`spinner-tips` six).
|
|
61
|
+
|
|
62
|
+
So the patchable surface is **every module the container declares to be
|
|
63
|
+
JavaScript**, discovered the way the entrypoint itself is discovered — off the
|
|
64
|
+
artifact, never hardcoded. A module's *loader* (the second trailing flag) is how
|
|
65
|
+
Bun decides whether to compile it as source or hand it over as opaque bytes, and
|
|
66
|
+
the entrypoint is by definition the JS Bun runs, so its loader *is* the JS loader
|
|
67
|
+
(`Blob.js_modules`). The asset modules (the native addons, the bundled
|
|
68
|
+
`mermaid`/`hljs`, the HTML template) carry other loaders and are left alone. A
|
|
69
|
+
pre-split build is the one-module case of this — `js_modules()` returns just the
|
|
70
|
+
monolith — so [`js.Source`](PLAYBOOK.md#the-many-module-surface) spans one module
|
|
71
|
+
or a thousand through the same code.
|
|
72
|
+
|
|
73
|
+
The entrypoint still matters for one thing: it is where the manifest lives and
|
|
74
|
+
what `status` reads, named by the offsets struct's `entry_point_id` — the same
|
|
75
|
+
index Bun resolves it by. Its *name* is upstream's to change and we never read
|
|
76
|
+
it: 2.1.229 renamed it `/$bunfs/root/src/entrypoints/cli.js` → `/$bunfs/root/cli`.
|
|
77
|
+
|
|
78
|
+
## The bytecode, and why we drop it
|
|
79
|
+
|
|
80
|
+
Modules carry precompiled Bun **bytecode** — most of the binary. Before the
|
|
81
|
+
split only the entry module had any (~half of it); the code-split builds carry it
|
|
82
|
+
on nearly every chunk (232 MB of 377 on 2.1.243, across ~1,375 modules).
|
|
83
|
+
|
|
84
|
+
Any edit to a module's `contents` invalidates *that module's* bytecode; Bun
|
|
85
|
+
detects the mismatch and recompiles that module from source at launch. So keeping
|
|
86
|
+
a stale copy buys nothing — the recompile is paid either way — and dropping it
|
|
87
|
+
reclaims the space and guarantees our edits are what runs. patch-cc drops the
|
|
88
|
+
bytecode of exactly the modules it edited (`rebuild` over `changed_modules`) and
|
|
89
|
+
leaves every untouched module its bytecode and its fast start. On Linux, where
|
|
90
|
+
the ELF section is rewritten in place, the binary is smaller by exactly the
|
|
91
|
+
edited modules' bytecode.
|
|
92
|
+
|
|
93
|
+
Measured on 2.1.243, the full patch set:
|
|
94
|
+
|
|
95
|
+
| binary | size | bytecode | startup |
|
|
96
|
+
|---|---|---|---|
|
|
97
|
+
| pristine | 378 MB | 232 MB (every module) | ~13 ms `--version` |
|
|
98
|
+
| patched (edited modules' bytecode dropped) | **295 MB** | 150 MB (untouched modules) | ~15 ms |
|
|
99
|
+
|
|
100
|
+
The 83 MB reclaimed is the ~45 edited modules' bytecode; the rest stays, which is
|
|
101
|
+
why a split-build patched binary is smaller but not the *half* a patched monolith
|
|
102
|
+
was. The recompile is now per lazily-imported edited module rather than the whole
|
|
103
|
+
app at once, so startup barely moves. Read the current figures off any binary
|
|
104
|
+
with `patch-cc status` rather than off this table — the bytecode total grows every
|
|
105
|
+
few builds.
|
|
106
|
+
|
|
107
|
+
The size story is the **ELF** path: the `.bun` section is rewritten in place, so
|
|
108
|
+
the dropped bytecode is genuinely reclaimed (`container.verify` refuses a Linux
|
|
109
|
+
write that did not shrink by about that much). On **macOS** the file keeps its
|
|
110
|
+
original size: `macho.py` grows a segment but never shrinks one, so the freed
|
|
111
|
+
bytes stay as dead space. The binary still runs correctly (the bytecode is gone),
|
|
112
|
+
it is just not smaller — reclaiming it means shrinking the Mach-O segment and
|
|
113
|
+
re-laying `__LINKEDIT`, which is not done yet.
|
|
114
|
+
|
|
115
|
+
Every write asserts each **edited** module carries `bytecode == 0` in the binary
|
|
116
|
+
it produced (`container.verify`, beside the round-trip check), and `patch-cc
|
|
117
|
+
status` reports the total for an installed one. `doctor` cannot: a dry run is
|
|
118
|
+
handed a *clean* bundle, which still has all its bytecode by definition. If a
|
|
119
|
+
future Bun build makes bytecode authoritative over source, that assert is the
|
|
120
|
+
tripwire — every edit would silently no-op otherwise.
|
|
84
121
|
|
|
85
122
|
## Writing it back without ballooning
|
|
86
123
|
|
|
@@ -108,8 +145,10 @@ edit is followed by an ad-hoc `codesign` (mandatory on Apple Silicon).
|
|
|
108
145
|
|
|
109
146
|
## The manifest
|
|
110
147
|
|
|
111
|
-
Every patched bundle
|
|
112
|
-
|
|
148
|
+
Every patched bundle carries a single comment line — appended to the **entry
|
|
149
|
+
module**, the one module always present and always re-extracted, and the one
|
|
150
|
+
`status` reads — describing its shape; [PLAYBOOK.md](PLAYBOOK.md) covers what it
|
|
151
|
+
means for matcher health:
|
|
113
152
|
|
|
114
153
|
```
|
|
115
154
|
//patch-cc {"v":1,"tool":"<version>","patches":[...],"brand":...,"suffix":...,
|
|
@@ -157,7 +196,8 @@ rather than from a store of their own.
|
|
|
157
196
|
and dropped rather than aborting the run. See
|
|
158
197
|
[PLAYBOOK.md](PLAYBOOK.md#the-syntax-gate).
|
|
159
198
|
- Every write is verified: patch-cc re-extracts the JS from the binary it just
|
|
160
|
-
wrote and asserts
|
|
199
|
+
wrote and asserts every module equals what it meant to write, and that each
|
|
200
|
+
module it edited carries no leftover bytecode to run instead of the edit.
|
|
161
201
|
- Patching a binary that is already marked, when no pristine backup exists, is
|
|
162
202
|
refused outright — there is nothing clean to start from, and our edits change
|
|
163
203
|
lengths, so a second pass would corrupt rather than update. `restore` or a
|
|
@@ -152,6 +152,56 @@ gone green, and every one buys a branch against a single build. The counting
|
|
|
152
152
|
below is the net for drift; the rule above is the only thing standing between a
|
|
153
153
|
repair and the next repair.
|
|
154
154
|
|
|
155
|
+
## The many-module surface
|
|
156
|
+
|
|
157
|
+
Through 2.1.241 the bundle was one module — the entrypoint carried the whole app
|
|
158
|
+
— and every matcher searched that one buffer. 2.1.242 code-split it: the
|
|
159
|
+
entrypoint became a ~20 KB argv shim that lazily imports the app across ~1,300
|
|
160
|
+
`chunk-*.js` modules ([INTERNALS](INTERNALS.md#the-21242-split-and-the-patchable-surface)).
|
|
161
|
+
The anchors did not move — they scattered. So `js.Source` **spans every
|
|
162
|
+
JavaScript module the container declares** (`Blob.js_modules`, discovered off the
|
|
163
|
+
entrypoint's own loader), and the one-module world is exactly the many-module
|
|
164
|
+
world with one module: `find`/`literals`/`count` sweep every module, `apply`
|
|
165
|
+
routes each edit to the module its node came from, and a patch says
|
|
166
|
+
`source.find(name)` / `source.apply(edits)` unchanged. This is representation
|
|
167
|
+
change of the crudest kind — the same code in more files — so it *moved* no
|
|
168
|
+
invariant and abolished none; the surface widened, the rules held. Three things
|
|
169
|
+
the move made explicit, each a rule the single-module world could leave unsaid
|
|
170
|
+
because the module was the bundle:
|
|
171
|
+
|
|
172
|
+
- **A node carries its module; an offset does not.** An `Edit` is built from a
|
|
173
|
+
node, and the node's program root (`node.id`, distinct across parses) names the
|
|
174
|
+
module the edit belongs to, so a batch spanning a dozen chunks routes each
|
|
175
|
+
splice home and reparses only the modules it touched. It is why dedup keys are
|
|
176
|
+
the node's `id`, never its `start_byte`: a minified offset repeats in a
|
|
177
|
+
thousand modules, and two different sites at the same local offset are two
|
|
178
|
+
sites, not one to skip.
|
|
179
|
+
|
|
180
|
+
- **A minified local is scoped to its module.** Resolving one — a hoisted
|
|
181
|
+
`agentType:uu` constant read through its value — searches *its own* module
|
|
182
|
+
(`Source.find_local`), never the bundle, where `uu` binds a string in a dozen
|
|
183
|
+
unrelated chunks and `js.only` would rightly refuse to choose. Before the split
|
|
184
|
+
the module *was* the bundle, so a bundle-wide `find` was a module-wide one; the
|
|
185
|
+
two only came apart when the app was dealt across files. The same lesson the
|
|
186
|
+
playbook already states for identifiers within a build — "a name is a spelling
|
|
187
|
+
until its scope is said" — now reaches the module boundary too. A scope search
|
|
188
|
+
that climbs outward (`live-thinking`'s state and memo resolution) reads the
|
|
189
|
+
module wrapper by what it *is* — the pre-split monolith's whole-body IIFE is a
|
|
190
|
+
module of one top-level statement; a split module's `program` is dozens, and
|
|
191
|
+
its top-level components are real scopes to search — never by a fixed position,
|
|
192
|
+
which read every split component as the un-searchable wrapper and found nothing.
|
|
193
|
+
|
|
194
|
+
- **The gate reads code, not linkage.** tree-sitter's JavaScript grammar does not
|
|
195
|
+
model a reserved word as an import/export alias (`export{x as if}`,
|
|
196
|
+
`import{if as a}` — legal ES2015, which the split minifier emits when its
|
|
197
|
+
two-letter alias generator lands on `if`/`in`/`do`), so it plants a localized
|
|
198
|
+
`ERROR` inside the clause. patch-cc never locates in or edits module linkage —
|
|
199
|
+
every anchor is executable code — so `Source.defect` reads *past* an error
|
|
200
|
+
confined to an import/export statement and returns the first one that is not,
|
|
201
|
+
while a splice that broke real code still lands outside linkage and is still
|
|
202
|
+
caught. It is the gate's scope stated precisely, not a tolerance bolted on: the
|
|
203
|
+
eight such modules on 2.1.243 parse clean for every purpose the tool has.
|
|
204
|
+
|
|
155
205
|
## Discovery instead of hardcoding
|
|
156
206
|
|
|
157
207
|
Anything the binary can enumerate for us, it does:
|
|
@@ -313,7 +363,9 @@ re-deriving it.
|
|
|
313
363
|
|
|
314
364
|
Both numbers above are counts, and a count cannot tell a rewrite that landed
|
|
315
365
|
from a rewrite that landed *one prop-name to the left*. So the bundle is parsed,
|
|
316
|
-
and any `ERROR` or `MISSING` node aborts
|
|
366
|
+
and any `ERROR` or `MISSING` node **outside module linkage** aborts
|
|
367
|
+
(`src/patch_cc/js.py`; the linkage read-past is
|
|
368
|
+
[above](#the-many-module-surface)).
|
|
317
369
|
|
|
318
370
|
The parse is not a separate pass any more: locating already needs it, and an
|
|
319
371
|
edit costs one incremental reparse — ~75 ms on a 25 MB bundle against ~3 s for a
|
|
@@ -455,7 +507,11 @@ Each sub-step records its own `candidates`/`applied`:
|
|
|
455
507
|
reducer whose setter threading applied while every arm had drifted once
|
|
456
508
|
reported hits and streamed nothing.
|
|
457
509
|
- **`final-summary`**, optional: redacted thinking in the closing summary is a
|
|
458
|
-
refinement, not the feature.
|
|
510
|
+
refinement, not the feature. Its guard is whichever enclosing `if` *tests the
|
|
511
|
+
block* — 2.1.236 nested an experiment gate between the test and the summary
|
|
512
|
+
it guards, and "the nearest `if`" read a shape that had merely moved as one
|
|
513
|
+
that was gone. The climb is bounded by the function that declares the block,
|
|
514
|
+
because a receiver's name is only a spelling until its scope is said.
|
|
459
515
|
|
|
460
516
|
An *optional* sub-step that finds nothing is reported as absent, not broken —
|
|
461
517
|
it is just a shape this build doesn't have. A sub-step that finds a shape but
|
|
@@ -489,6 +545,10 @@ that broke a patch along with every note.
|
|
|
489
545
|
patch-cc extract ~/.local/share/claude/versions/<ver> > clean.js
|
|
490
546
|
```
|
|
491
547
|
|
|
548
|
+
Since 2.1.242 that is every JavaScript module concatenated, each behind a
|
|
549
|
+
`// ==== patch-cc module <n> ====` header, so `rg` still works over one file
|
|
550
|
+
and the header says which chunk a hit lives in.
|
|
551
|
+
|
|
492
552
|
2. Run `patch-cc doctor`. Note which patch dropped to `candidates == 0`, or —
|
|
493
553
|
for `live-thinking` — which sub-step.
|
|
494
554
|
|
|
@@ -788,6 +848,28 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
|
|
|
788
848
|
*initialised* to is deliberately not asked: `useState(null)` is every
|
|
789
849
|
build's spelling and `useState(void 0)` would be the same state, while the
|
|
790
850
|
setter is the identity that matters.
|
|
851
|
+
- **The state has two homes, and the handed setter names both.** Through
|
|
852
|
+
2.1.235 the scope that hands `onStreamingThinking` declared the state
|
|
853
|
+
itself, and the state is the array pattern binding the handed setter —
|
|
854
|
+
`useState`'s pair. 2.1.236 moved it into an external stream store
|
|
855
|
+
(`subscribe`/`getSnapshot`/`_publish` — the `useSyncExternalStore` shape)
|
|
856
|
+
and hands `<store>.setStreamingThinking` instead; the same scope reads the
|
|
857
|
+
store back by destructuring the hook call it hands the store to
|
|
858
|
+
(`{streamingToolUses:…}=useX(<store>)`), so the state is that pattern's own
|
|
859
|
+
`streamingThinking` binding — upstream's the day it takes one, the goal
|
|
860
|
+
achieved, and until then ours, inserted at the front of the pattern. The
|
|
861
|
+
pattern is proven the snapshot read by the store expression itself: the
|
|
862
|
+
call's *only* argument is the very expression the setter was read off, one
|
|
863
|
+
answer or none (`js.only`). Sole argument is deliberate — a second is a
|
|
864
|
+
selector whose result is no longer the snapshot, and extending a pattern of
|
|
865
|
+
unknowable provenance binds `undefined` with every count green. The
|
|
866
|
+
insertion also pays for a witness the way `thinking-summaries` does: the
|
|
867
|
+
field it binds must still be named by the bundle's own objects (the store's
|
|
868
|
+
snapshot initialiser, its publish call), so a store that renames the field
|
|
869
|
+
reads as the step reporting the store rather than threading `undefined`.
|
|
870
|
+
Two things come free with the store: its setter takes functional updaters —
|
|
871
|
+
React's own contract, which the reducer splices already speak — and it
|
|
872
|
+
hides a finished block itself after 30 s, upstream's own linger.
|
|
791
873
|
|
|
792
874
|
A render was once selected by *position* — the observation that the real sites
|
|
793
875
|
fall after the state's `useState` declaration. That is worth recording as a
|
|
@@ -830,11 +912,17 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
|
|
|
830
912
|
than carried over from whichever renderer the first step happened to see
|
|
831
913
|
last: a second component with the same four props donated its local to the
|
|
832
914
|
first one's memo, spliced an identifier that component cannot see, and
|
|
833
|
-
reported both steps green. The memo itself is found by what it computes
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
915
|
+
reported both steps green. The memo itself is found by what it computes — an
|
|
916
|
+
arrow whose whole body is a `flatMap` that wraps each streaming block as a
|
|
917
|
+
message, and the call that takes that arrow — never by the memo hook's name.
|
|
918
|
+
The hook is one more minified spelling: the monolith reaches it as
|
|
919
|
+
`<React>.useMemo(...)` (the property name survives a member read), but a
|
|
920
|
+
code-split module imports it under a local (`import{useMemo as te}` → `te(...)`),
|
|
921
|
+
so keying on `useMemo` read a build that memoizes exactly the same way as one
|
|
922
|
+
with no memo at all, and the rewrite reuses the memo's *own* callee rather than
|
|
923
|
+
re-spelling `.useMemo`. What a claim about the *text* around the block
|
|
924
|
+
(`.contentBlock]}`) only approximated is the computation itself: one sibling
|
|
925
|
+
property beside `content` read as the whole thing being gone.
|
|
838
926
|
|
|
839
927
|
**`display-mode`** defaults the request's thinking display to `"summarized"`;
|
|
840
928
|
without it the API only streams summary text when the `showThinkingSummaries`
|
|
@@ -1245,10 +1333,10 @@ enumerated with their hashes in [corpus.md](corpus.md). Each removed matcher hit
|
|
|
1245
1333
|
can still be running; each was carried as "kept for older builds", which is a
|
|
1246
1334
|
claim the corpus disproves. The verbatim matchers were re-run from `HEAD` over
|
|
1247
1335
|
the whole corpus after the move, and they are still zero everywhere. The
|
|
1248
|
-
measurements this section quotes were taken during the move across
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1336
|
+
measurements this section quotes were taken during the move across
|
|
1337
|
+
2.1.210 → 2.1.233 (2.1.230 was never published) — a span the corpus has since
|
|
1338
|
+
grown to hold in full, so they are re-checkable rather than historical, and
|
|
1339
|
+
re-running `doctor` over it is one command (below).
|
|
1252
1340
|
|
|
1253
1341
|
- **`live-thinking` / `reducer-legacy`** — the pre-2.1.138 reducer with
|
|
1254
1342
|
positional parameters. Its removal collapsed the `reducer` variant *group*
|