@brandry/claude-jsonl-compressor 1.0.0-rc.1 → 1.0.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/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  Notable public changes are recorded here. This project follows Semantic Versioning.
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [1.0.0] - 2026-08-11
8
+
9
+ ### Added
10
+
11
+ - Simplified Chinese and Japanese documentation for the public project.
12
+ - Stable machine-readable `reasonCode` values for resume-path rejection.
13
+
14
+ ### Fixed
15
+
16
+ - Enumerate Claude project sessions correctly beneath Windows 8.3 short-name roots.
17
+ - Bound public diagnostics for malformed JSONL metadata without changing the
18
+ underlying validation or compression decisions.
19
+ - Preflight file hard-link capability in the target directory and any explicit
20
+ backup directory before a live session is staged, backed up, or moved.
21
+ - Retain and report live-transaction temporary paths whose observed identity or
22
+ bytes change before cleanup, including cleanup residue after an earlier
23
+ transaction failure.
24
+
25
+ ### Changed
26
+
27
+ - Run the hard-link and transaction suite on Windows, Linux, and macOS CI
28
+ runner volumes and print their filesystem type as diagnostic evidence.
29
+ - Promote the public npm package from release candidate to stable `1.0.0` on
30
+ the `latest` dist-tag.
31
+
5
32
  ## [1.0.0-rc.1] - 2026-07-28
6
33
 
7
34
  Initial public release candidate.
@@ -24,4 +51,6 @@ Initial public release candidate.
24
51
  - Live replacement handles one closed session at a time and requires Python 3.10 or newer.
25
52
  - Parent-directory durability is best effort where the platform does not support directory fsync.
26
53
 
54
+ [Unreleased]: https://github.com/brandrylabs/claude-jsonl-compressor/compare/v1.0.0...HEAD
55
+ [1.0.0]: https://github.com/brandrylabs/claude-jsonl-compressor/releases/tag/v1.0.0
27
56
  [1.0.0-rc.1]: https://github.com/brandrylabs/claude-jsonl-compressor/releases/tag/v1.0.0-rc.1
package/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # Claude JSONL Compressor
2
2
 
3
+ [English](https://github.com/brandrylabs/claude-jsonl-compressor/blob/v1.0.0/README.md) | [简体中文](https://github.com/brandrylabs/claude-jsonl-compressor/blob/v1.0.0/docs/README.zh-CN.md) | [日本語](https://github.com/brandrylabs/claude-jsonl-compressor/blob/v1.0.0/docs/README.ja.md)
4
+
3
5
  Strict, model-assisted compression for one Claude Code session transcript, plus an independent byte-preserving compatibility repair for historical `Read.pages` records.
4
6
 
5
- **Release:** [`1.0.0-rc.1`](CHANGELOG.md)<br>
7
+ **Release:** [`1.0.0`](CHANGELOG.md)<br>
6
8
  **Engine:** `v10`<br>
7
9
  **Model-pack schema:** `v11`<br>
8
10
  **License:** GPL-3.0-only<br>
@@ -94,9 +96,22 @@ Excluded records appear in reports only as counts and digests. Their text is not
94
96
  - Python 3.10 or newer
95
97
  - Node.js 22 or newer only when using the npm command wrappers
96
98
  - Claude Code is optional; it is needed only for an explicitly requested runtime `/resume` or `/context` smoke test
99
+ - Hard-link support on the volume holding the target file, and on an explicitly configured backup directory, for `--replace-original` only
97
100
 
98
101
  No Python package installation is required.
99
102
 
103
+ ### Hard-link requirement for live replacement
104
+
105
+ `--replace-original` publishes the candidate with `os.link` so that it never overwrites a concurrent claimant, and the rollback path restores the captured original the same way. The target directory must therefore support file hard links. An explicitly supplied `--backup-dir` also publishes numbered backups with a hard link and must support the same operation.
106
+
107
+ Before live replacement staging, backup payload publication, or target movement, the compressor runs a unique, same-directory hard-link probe on the target volume. The probe briefly creates two dot-prefixed temporary files and removes them; a cleanup failure is reported with the retained probe name and stops the live operation. If the capability probe fails, the target remains at its original path and byte content, and no live replacement stage, backup payload, or target move has started. This probe is a capability check, not a guarantee against a later permission, quota, network, or concurrency failure; late publication, rollback, and cleanup failures are reported with their transaction state and verified recovery assets.
108
+
109
+ Each unique live-transaction temporary path is checked against its recorded filesystem identity and frozen bytes before cleanup. A detected mismatch is retained and reported as `committed-cleanup-failed` after a committed candidate, or alongside the primary failure before commit. Portable Python cannot bind the final pathname `unlink` atomically to that earlier identity check across Windows, Linux, and macOS. Run live replacement only with the session closed and no other writer; hostile same-account directory manipulation is outside this guarantee.
110
+
111
+ NTFS, APFS, ext4, XFS, and btrfs commonly support file hard links, while FAT/exFAT, some network shares, overlay or bind-mounted filesystems, and Windows ReFS may reject them. The probe is authoritative for the actual directory and account; the filesystem name alone is not treated as a guarantee. The repository CI runs the probe and transaction tests on Windows, Linux, and macOS runner volumes.
112
+
113
+ Candidate output is unaffected: it publishes through `os.replace` and has no hard-link dependency.
114
+
100
115
  ## Installation
101
116
 
102
117
  ### Install As A Codex Skill
@@ -134,10 +149,10 @@ The installed directory must contain `SKILL.md`, `scripts/`, `config/`, `templat
134
149
 
135
150
  ### Install The npm CLI
136
151
 
137
- After the RC is published:
152
+ After version 1.0.0 is published:
138
153
 
139
154
  ```bash
140
- npm install --global @brandry/claude-jsonl-compressor@rc
155
+ npm install --global @brandry/claude-jsonl-compressor
141
156
  ```
142
157
 
143
158
  This installs two commands:
@@ -152,7 +167,7 @@ The npm package is a zero-dependency Node shim over the bundled Python implement
152
167
  Upgrade or uninstall the global CLI:
153
168
 
154
169
  ```bash
155
- npm install --global @brandry/claude-jsonl-compressor@rc
170
+ npm install --global @brandry/claude-jsonl-compressor
156
171
  npm update --global @brandry/claude-jsonl-compressor
157
172
  npm uninstall --global @brandry/claude-jsonl-compressor
158
173
  ```
@@ -160,7 +175,7 @@ npm uninstall --global @brandry/claude-jsonl-compressor
160
175
  Local development install and invocation:
161
176
 
162
177
  ```bash
163
- npm install --save-dev @brandry/claude-jsonl-compressor@rc
178
+ npm install --save-dev @brandry/claude-jsonl-compressor
164
179
  npm update @brandry/claude-jsonl-compressor
165
180
  npm exec -- claude-jsonl-compressor --version
166
181
  npm exec -- claude-jsonl-repair-read-pages --version
@@ -170,15 +185,15 @@ npm uninstall @brandry/claude-jsonl-compressor
170
185
  Run without retaining an installation:
171
186
 
172
187
  ```bash
173
- npx --yes --package @brandry/claude-jsonl-compressor@rc claude-jsonl-compressor --version
174
- npx --yes --package @brandry/claude-jsonl-compressor@rc claude-jsonl-repair-read-pages --version
188
+ npx --yes --package @brandry/claude-jsonl-compressor claude-jsonl-compressor --version
189
+ npx --yes --package @brandry/claude-jsonl-compressor claude-jsonl-repair-read-pages --version
175
190
  ```
176
191
 
177
192
  Actual npm/npx operations use the same Python CLI options:
178
193
 
179
194
  ```bash
180
- npx --yes --package @brandry/claude-jsonl-compressor@rc claude-jsonl-compressor --input session.jsonl --write-model-pack run/session.model-pack.md
181
- npx --yes --package @brandry/claude-jsonl-compressor@rc claude-jsonl-repair-read-pages --input session.jsonl --scan-only
195
+ npx --yes --package @brandry/claude-jsonl-compressor claude-jsonl-compressor --input session.jsonl --write-model-pack run/session.model-pack.md
196
+ npx --yes --package @brandry/claude-jsonl-compressor claude-jsonl-repair-read-pages --input session.jsonl --scan-only
182
197
  ```
183
198
 
184
199
  ### Use From Source Without Installing
@@ -209,6 +224,9 @@ python "$skill\scripts\compress_claude_jsonl.py" `
209
224
  ```
210
225
 
211
226
  This is read-only. A nonzero result must be resolved before model-pack generation.
227
+ Use `reasonCode` for the exact machine-readable cause; `status` is only the
228
+ coarse category. The stable value table is in
229
+ [`references/claude-jsonl-compression-format.md`](references/claude-jsonl-compression-format.md#authoritative-last-prompt).
212
230
 
213
231
  ### 2. Generate A Model Evidence Pack
214
232
 
@@ -541,24 +559,24 @@ python -B -I -S scripts/compress_claude_jsonl.py --version
541
559
  python -B -I -S scripts/repair_claude_jsonl.py --version
542
560
  npm test
543
561
  npm pack --dry-run --json
544
- npm publish --dry-run --access public --tag rc
562
+ npm publish --dry-run --access public --tag latest
545
563
  ```
546
564
 
547
565
  The release suite covers active/dead branch partitioning, fixed-seed topology transformations, strict pointer failures, dual model-pack budgets, complete structured token accounting, multilingual semantic ledgers and thinking, handoffs, request/claim digests, mandatory support excerpts, tool pairs, repeated compression, checkpoint policies, transaction races and committed-report states, exact byte repair, BOM/CRLF, npm tarball allowlisting and offline tarball installation.
548
566
 
549
- ### Maintainer RC Release Checklist
567
+ ### Maintainer Stable Release Checklist
550
568
 
551
- 1. Confirm a clean public tree and matching `1.0.0-rc.1` values in `package.json`, Python version output, docs, and tests.
569
+ 1. Confirm a clean public tree and matching `1.0.0` values in `package.json`, Python version output, docs, and tests.
552
570
  2. Run the Python, npm, isolated-Python, tarball, privacy, and offline-install gates above.
553
571
  3. Inspect `npm pack --dry-run --json`; publish only the allowlisted files.
554
- 4. Require a clean worktree, create annotated tag `v1.0.0-rc.1`, and push the commit and tag.
555
- 5. For the first manual RC, publish from an authenticated maintainer machine with npm 2FA:
572
+ 4. Require a clean worktree, create annotated tag `v1.0.0`, and push the commit and tag.
573
+ 5. Publish from an authenticated maintainer environment with permission to publish this package:
556
574
 
557
575
  ```bash
558
- npm publish --access public --tag rc
576
+ npm publish --access public --tag latest
559
577
  ```
560
578
 
561
- 6. Verify the npm version and `rc` dist-tag, then create the GitHub prerelease from the already-pushed tag.
579
+ 6. Verify npm version `1.0.0` and the `latest` dist-tag, then create a GitHub release (not a prerelease) from the already-pushed tag.
562
580
 
563
581
  Do not append `--provenance` to a local publish. npm provenance requires a supported cloud CI runner. For later releases, prefer npm trusted publishing from a public GitHub repository on a GitHub-hosted runner with `id-token: write`, a protected release tag, and a matching protected environment; trusted publishing generates provenance automatically.
564
582
 
package/SKILL.md CHANGED
@@ -7,7 +7,7 @@ description: Compress one Claude Code JSONL session with strict active-branch is
7
7
 
8
8
  Operate on exactly one authoritative JSONL. Never merge another branch or session automatically.
9
9
 
10
- Public package: `1.0.0-rc.1`. Internal engine: `v10`. Model-pack schema: `v11`.
10
+ Public package: `1.0.0`. Internal engine: `v10`. Model-pack schema: `v11`.
11
11
 
12
12
  ## Safety Invariants
13
13
 
@@ -73,6 +73,10 @@ python "$skill\scripts\compress_claude_jsonl.py" `
73
73
  --analyze-resume-path
74
74
  ```
75
75
 
76
+ Use `reasonCode` as the exact machine-readable outcome and `status` only as its
77
+ coarse category. The stable pairs are documented in
78
+ `references/claude-jsonl-compression-format.md`.
79
+
76
80
  Strict active mode stops before writing any pack, candidate, sidecar or backup when the authority is absent, malformed, dangling, cyclic, has unsafe/recurring session lineage, has ordinary-message physical parent inversion, contains a malformed `parentUuid`, or is UUID-ambiguous.
77
81
 
78
82
  After partitioning, run the shared validator on the authoritative logical active chain plus its projected pointer. Old malformed tool exchanges, duplicate tool IDs, or compact-pair metadata on that chain must stop before semantic evidence generation; damage confined to excluded inactive branches remains excluded and does not become summary text.
@@ -336,5 +340,6 @@ Use `--scan-titles` only when the user supplies a title and permits title scanni
336
340
  - A source hash change aborts live replacement.
337
341
  - Live replacement requires the Claude process for that session to be closed. The transaction validates immutable candidate bytes, exclusively creates and verifies a numbered backup, captures the actual old target, verifies its full SHA-256, installs the candidate, and verifies the published bytes and structure. If another process recreates the target during capture, preserve the external target and recovery backups and fail without publishing. Parent-directory fsync is best effort and reported because platform support differs.
338
342
  - A write/fsync/validation/replace failure returns nonzero. The transaction restores the captured original bytes when replacement began; if restoration itself fails, it raises a high-priority error and retains the numbered backup for recovery.
343
+ - Each unique live-transaction temporary path is checked against its recorded filesystem identity and frozen bytes before cleanup. A detected mismatch is retained and reported; a pre-commit failure also includes any cleanup residue in its error. Portable pathname cleanup cannot atomically bind that final delete to the earlier identity check across Windows, Linux, and macOS, so require a closed session and no other writer. Do not claim protection from hostile same-account directory manipulation.
339
344
  - If a valid live replacement commits but final sidecar/report publication fails, return exit code 3 with `committed-report-failed` and the committed hashes/backup labels. Do not rerun blindly or describe that state as an uncommitted failure.
340
345
  - Keep reports and temporary work outside `.claude`; do not leave ad hoc files in live session directories.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandry/claude-jsonl-compressor",
3
- "version": "1.0.0-rc.1",
3
+ "version": "1.0.0",
4
4
  "description": "Strict, model-assisted compression and byte-preserving compatibility repair for one Claude Code JSONL session.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -55,6 +55,6 @@
55
55
  },
56
56
  "publishConfig": {
57
57
  "access": "public",
58
- "tag": "rc"
58
+ "tag": "latest"
59
59
  }
60
60
  }
@@ -1,7 +1,7 @@
1
1
  # Claude Code JSONL Compression Format Notes
2
2
 
3
3
  This document describes the empirical format handled by
4
- `@brandry/claude-jsonl-compressor` package `1.0.0-rc.1` and engine `v10`.
4
+ `@brandry/claude-jsonl-compressor` package `1.0.0` and engine `v10`.
5
5
 
6
6
  Claude Code transcript JSONL is an observed internal format, not a published
7
7
  stable storage API. The rules below are deliberately strict where ambiguity
@@ -14,7 +14,7 @@ The project keeps four independent version domains:
14
14
 
15
15
  | Domain | Current value | Meaning |
16
16
  | --- | --- | --- |
17
- | Package | `1.0.0-rc.1` | GitHub/npm release version |
17
+ | Package | `1.0.0` | GitHub/npm release version |
18
18
  | Compression engine | `v10` | Topology, partition, and rewrite behavior |
19
19
  | Model-pack schema | `v11` | Evidence-pack and model-summary binding protocol |
20
20
  | Report schema | `1` | Compression and repair report fields |
@@ -39,6 +39,38 @@ A transcript can contain records that play very different roles:
39
39
 
40
40
  Physical line order alone is therefore not a definition of current context.
41
41
 
42
+ ### Physical line accounting
43
+
44
+ The JSONL framer recognizes records only at physical LF boundaries, removes a
45
+ single CR before each LF, and skips blank physical lines. The parser keeps a
46
+ trusted side map from each parsed record to its original 1-based physical line
47
+ number. `validate_jsonl` and the `Read.pages` repair report use that map, so a
48
+ blank line or an excluded branch cannot make an error point at the wrong file
49
+ line. Input `_line`, `_mergedLines`, `_mergedUuids`, and
50
+ `_validationContentLines` fields are untrusted and ignored; validation rebuilds
51
+ record- and content-block provenance in an isolated projection.
52
+
53
+ The model evidence pack uses its own `L<number>` logical nonblank-record
54
+ coordinates because it is a reserialized evidence view; those anchors are not
55
+ claims about physical source-file line numbers. Repair `recordLine`, validator
56
+ diagnostic `line` fields, and the source active-chain preflight used by
57
+ compression/model-pack generation, in contrast, refer to the original file's
58
+ physical lines. The active-chain preflight is a logical projection, so it
59
+ carries source-line provenance separately rather than renumbering the chain.
60
+
61
+ Validation/report counters and `type`/`sessionId`/`subtype` metadata carried
62
+ by validator or system-error diagnostics preserve ordinary short strings.
63
+ Synthetic keys use the collision-safe angle-bracket namespace: `<missing>`,
64
+ `<null>`, and `<invalid:json-type>`. A real string beginning with `<` is
65
+ escaped by adding a second leading `<`; an unusually long real string is
66
+ represented by a bounded `<string:length=...;sha256=...>` label that contains
67
+ no original characters. Complex
68
+ malformed values are labeled by JSON type rather than copied into diagnostics
69
+ or model evidence. The same bound is applied recursively to public validator
70
+ samples, nested diagnostic metadata, identifier-valued dictionary keys, and
71
+ oversized source values embedded in error text. Ordinary error wording and
72
+ physical-line locations remain readable when they contain only normal values.
73
+
42
74
  ## Common Observed Record Types
43
75
 
44
76
  | `type` | Observed purpose |
@@ -107,6 +139,39 @@ Strict topology statuses include:
107
139
  - `extension-unsafe`
108
140
  - `valid`
109
141
 
142
+ `status` is the coarse category. `reasonCode` is the stable, machine-readable
143
+ cause. Current producers emit exactly one of these pairs:
144
+
145
+ | `status` | `reasonCode` | Meaning |
146
+ | --- | --- | --- |
147
+ | `absent` | `last-prompt-absent` | No authoritative `last-prompt` exists |
148
+ | `duplicate-uuid` | `duplicate-uuid` | At least one UUID is ambiguous |
149
+ | `malformed` | `leaf-uuid-malformed` | The authoritative `leafUuid` is absent, empty or not a string |
150
+ | `dangling` | `chain-missing-uuid` | The selected chain references an unknown UUID |
151
+ | `loop` | `chain-loop` | The selected chain contains a parent loop |
152
+ | `malformed-parent` | `chain-malformed-parent` | A selected record has an invalid non-null `parentUuid` |
153
+ | `dangling` | `chain-empty` | Defensive outcome when tracing returns no chain and no more specific error |
154
+ | `non-monotonic` | `chain-non-monotonic` | Physical parent order violates the accepted compatibility rule |
155
+ | `session-mismatch` | `lineage-unsafe` | Session ancestry is not an accepted one-way lineage |
156
+ | `extension-limit` | `extension-limit-exceeded` | Post-pointer records exceed the explicit closure limit |
157
+ | `session-mismatch` | `extension-authority-session-missing` | A requested closure has no usable authority session |
158
+ | `extension-unsafe` | `extension-record-missing-uuid` | A closure record has no usable UUID |
159
+ | `extension-branch` | `extension-record-not-linear-descendant` | A closure record is not the next direct child |
160
+ | `session-mismatch` | `extension-record-session-mismatch` | A closure record does not match the authority session |
161
+ | `extension-unsafe` | `extension-record-not-safe-closure` | A closure record is not a tool-result-only safe closure |
162
+ | `extension-unsafe` | `extension-pending-tool-ids` | The requested closure leaves tool calls unresolved |
163
+ | `valid` | `ok` | Strict topology validation succeeded |
164
+
165
+ For `lineage-unsafe`, `lineageReason` carries the lower-level lineage diagnosis.
166
+ Human-readable `errors` text is diagnostic and is not an enum. Normal
167
+ compression/model-pack/repair failures also include `reasonCode=<value>` in the
168
+ raised CLI error.
169
+
170
+ Report-schema v1 objects and `compactMetadata` are open, additive objects:
171
+ consumers must ignore unknown fields. Stable producers include `reasonCode` on
172
+ every strict topology result. A pre-stable v1 artifact may lack it; readers
173
+ should treat that as legacy/unknown rather than infer `ok`.
174
+
110
175
  `--resume-leaf UUID` is an explicit recovery override. It is reported as
111
176
  `active-chain-manual-override`, distinct from default strict `active-chain`.
112
177
  `--preserve-physical-tail` is a separate legacy compatibility
@@ -175,7 +240,7 @@ The engine emits exactly one current Codex-created compact pair:
175
240
  Simplified synthetic shape:
176
241
 
177
242
  ```json
178
- {"type":"system","subtype":"compact_boundary","uuid":"BOUNDARY_UUID","parentUuid":null,"compactMetadata":{"codexOfflineCompression":true,"codexOfflineCompressionVersion":"v10","modelPackSchemaVersion":11,"reportSchemaVersion":1,"summaryUuid":"SUMMARY_UUID","preserveMode":"active-chain","resumeLeafInfo":{"status":"valid","selectedLeafUuid":"ACTIVE_LEAF"}}}
243
+ {"type":"system","subtype":"compact_boundary","uuid":"BOUNDARY_UUID","parentUuid":null,"compactMetadata":{"codexOfflineCompression":true,"codexOfflineCompressionVersion":"v10","modelPackSchemaVersion":11,"reportSchemaVersion":1,"summaryUuid":"SUMMARY_UUID","preserveMode":"active-chain","resumeLeafInfo":{"status":"valid","reasonCode":"ok","selectedLeafUuid":"ACTIVE_LEAF"}}}
179
244
  {"type":"user","uuid":"SUMMARY_UUID","parentUuid":"BOUNDARY_UUID","isCompactSummary":true,"message":{"role":"user","content":"SUMMARY_TEXT"}}
180
245
  ```
181
246
 
@@ -460,6 +525,19 @@ does not overwrite the external target. It preserves the verified numbered
460
525
  backup and, when necessary, places the captured original in another numbered
461
526
  backup, then fails without publishing the candidate.
462
527
 
528
+ Before any live staging, backup payload publication, or target move, the
529
+ implementation probes file hard-link support in the target directory and, when
530
+ configured, the external backup directory. The probe itself briefly creates
531
+ and removes unique dot-prefixed temporary files. Every unique live-transaction
532
+ temporary path records its observed filesystem identity and frozen bytes.
533
+ Cleanup rechecks both and retains a detected mismatch. A mismatch after commit is reported as
534
+ `committed-cleanup-failed`; a mismatch while an earlier transaction failure is
535
+ already active is appended to that failure. This is a best-effort race check,
536
+ not a portable atomic identity-bound delete: standard Python pathname cleanup
537
+ cannot eliminate a replacement between its final check and `unlink()` across
538
+ Windows, Linux, and macOS. Live operation assumes Claude Code is closed and no
539
+ other writer is manipulating the session directory.
540
+
463
541
  Candidate-mode reports are written with the candidate. Live compression delays
464
542
  its final sidecar/report until replacement metadata is available, so it does not
465
543
  leave a stale pre-commit report. If the JSONL commits and validates but final
@@ -36,6 +36,25 @@ def eprint(*parts: object) -> None:
36
36
 
37
37
  configure_stdio()
38
38
 
39
+ MIN_SUPPORTED_PYTHON = (3, 10)
40
+
41
+
42
+ def warn_if_python_too_old() -> Optional[str]:
43
+ """Warn on an unsupported interpreter without blocking the run."""
44
+ if sys.version_info >= MIN_SUPPORTED_PYTHON:
45
+ return None
46
+ running = ".".join(str(part) for part in sys.version_info[:3])
47
+ required = ".".join(str(part) for part in MIN_SUPPORTED_PYTHON)
48
+ message = (
49
+ f"WARNING: running on Python {running}; this project documents Python {required} or newer. "
50
+ "Continuing anyway. Unexpected errors may be caused by the interpreter version."
51
+ )
52
+ eprint(message)
53
+ return message
54
+
55
+
56
+ warn_if_python_too_old()
57
+
39
58
 
40
59
  def read_jsonl_records(path: pathlib.Path) -> List[Dict[str, Any]]:
41
60
  records: List[Dict[str, Any]] = []
@@ -57,13 +76,25 @@ def list_session_files(root: pathlib.Path) -> List[pathlib.Path]:
57
76
  found: List[pathlib.Path] = []
58
77
  for dir_path, dir_names, file_names in os.walk(root, topdown=True, followlinks=False):
59
78
  directory = pathlib.Path(dir_path)
79
+ # Resolve the containing directory once and compare each child against
80
+ # `resolved_directory / name`. Comparing child.resolve() against
81
+ # os.path.abspath(child) instead would reject every entry whenever any
82
+ # component of the root is a Windows 8.3 short name, because resolve()
83
+ # expands short names to their long form and abspath() leaves them as
84
+ # written. A symlink or junction that leaves the directory still
85
+ # resolves somewhere other than resolved_directory / name, so the
86
+ # escape guard is unchanged.
87
+ try:
88
+ resolved_directory = directory.resolve()
89
+ except OSError:
90
+ dir_names[:] = []
91
+ continue
60
92
  safe_dirs: List[str] = []
61
93
  for name in dir_names:
62
94
  child = directory / name
63
95
  try:
64
96
  resolved = child.resolve()
65
- lexical = pathlib.Path(os.path.abspath(str(child)))
66
- if resolved != lexical or not is_same_or_inside(resolved, root_resolved):
97
+ if resolved != resolved_directory / name or not is_same_or_inside(resolved, root_resolved):
67
98
  continue
68
99
  except OSError:
69
100
  continue
@@ -75,8 +106,7 @@ def list_session_files(root: pathlib.Path) -> List[pathlib.Path]:
75
106
  candidate = directory / name
76
107
  try:
77
108
  resolved = candidate.resolve()
78
- lexical = pathlib.Path(os.path.abspath(str(candidate)))
79
- if resolved != lexical or not is_same_or_inside(resolved, root_resolved):
109
+ if resolved != resolved_directory / name or not is_same_or_inside(resolved, root_resolved):
80
110
  continue
81
111
  if not candidate.is_file():
82
112
  continue