Commit 3e2e482
* fix(cursor): migrator path-priority + ambiguity surface (closes #844 regression)
The legacy-to-ACP migrator's `findLegacyChatStore()` walks
`~/.cursor/chats/<workspace-hash>/<cursorSessionId>/store.db` via
`readdirSync()` and returns the FIRST match. When the same cursor
session id exists in more than one workspace-hash drawer (operator
opened the session from a worktree, an old workspace clone, etc.)
the readdir order picks an arbitrary candidate. The migrator then
transplants alien content into the ACP target, deletes the source
drawer, and reports success - because the verify probe only checks
"loads cleanly", not "loaded the right content". Operator session
resurrects with no recall of its real history.
Four-part fix (all four must land together):
1. Path-priority discovery in `findLegacyChatStore(id, home, cwd?)`:
- Optional 3rd arg = canonical workspace path (caller passes
`session.metadata.path`).
- Compute md5(cwd) and check that drawer FIRST.
- Fall back to readdir scan only if the canonical drawer is empty.
- If 2+ candidates remain after fallback, throw
`AmbiguousLegacyStoreError` listing all of them
(workspaceHash, sizeBytes, mtimeMs).
2. Ambiguity surface in `maybeAutoMigrateLegacyCursorSession`:
- Catch `ambiguous_legacy_store` / `size_mismatch` refusals and
promote `cursorMigrationState` from 'in_progress' to a new
'ambiguous' state instead of silently clearing the banner.
Operator sees an actionable web-banner.
3. Size sanity check before transplant:
- Compare HAPI's known message count (new `MessageStore.countMessages`
+ `CursorLegacyMigratorDeps.getHapiMessageCount` dep) against
the candidate `store.db`'s blob count. If message count > 100
AND blob count < messageCount/4, refuse with `size_mismatch`.
- Skipped when message count is 0 (brand-new session) or the dep
is unwired (unit tests, CLI direct callers).
4. Diagnostic logging on every successful transplant:
- `[migrator] transplanted` info log capturing cursorSessionId,
picked workspaceHash, candidate count discovered, sourceBytes,
sourceBlobCount, targetAcpPath, sourceRemoved, canonical-path
md5. Future regressions of this bug shape are diagnosable from
`journalctl -u hapi-hub` without blob-overlap forensics.
Tests added in `hub/src/cursor/cursorLegacyMigrator.test.ts`:
- regression guard for single-drawer discovery
- canonical-path wins over readdir order
- ambiguity throws with all candidates listed (3-drawer + 2-drawer
no-canonical-arg variants)
- canonical-path resolves ambiguity cleanly
- listLegacyChatStoreCandidates enumeration
- workspaceHashFromPath shape
- migrateOne happy path with canonical workspace + 3 sibling decoys
- migrateOne refuses with ambiguous_legacy_store (3 drawers, no
canonical match) and leaves all sources untouched
- migrateOne proceeds when canonical path resolves
- size_mismatch refuses tiny candidate when messageCount=6000
- size_mismatch passes when candidate blob count meets the floor
- size sanity skipped on messageCount=0, missing dep, throwing dep,
boundary (messageCount=100)
- countLegacyStoreBlobs returns counts / null on bad path
And in `hub/src/sync/syncEngineAutoMigrate.test.ts`:
- cursorMigrationState promoted to 'ambiguous' on
ambiguous_legacy_store / size_mismatch refusals.
Schema:
- `shared/src/schemas.ts`: cursorMigrationState enum gains 'ambiguous'.
- `shared/src/apiTypes.ts`: CursorMigrateRefusalReason gains
'ambiguous_legacy_store' + 'size_mismatch'.
Real-world repro (operator's tooling session, 2026-06-09): three legacy
drawers contained one cursor session id - one with the real 21k-blob
history, two with stale 19/568-blob diagnostic snapshots. Migrator
silently transplanted the 568-blob alien content; resurrected session
had no memory of prior history. Manual rescue completed; this fix
prevents recurrence and surfaces the ambiguity to the operator instead.
* fix(cursor): address cold review on migrator path-priority fix
Self-review against the cold-PR rubric surfaces four polish items on
the previous commit; all four addressed in-loop before push.
- Major: `migrator:transplanted` candidate count was captured AFTER
the source rm, so for the dominant single-candidate happy path the
log reported `candidateCount=0, sourceRemoved=true`. Useless for
diagnosing a future regression of the bug shape this PR is fixing.
Snapshot candidates + source-side size + source-side blob count
BEFORE any destructive step and use those for the log.
- Minor: `sourceBytes` and `sourceBlobCount` were read from the
destination path (acpSessionDir/store.db). The cp guarantees they
match, but the field names imply source-side measurement. Now they
measure the source directly.
- Minor: `setCursorMigrationStateAmbiguous` silently returned false on
cache miss / repeated version mismatch / write failure, letting the
finally{} block clear the banner without any log. Now emits a
warn-level log so the gap is diagnosable from journalctl.
- Minor: `findLegacyChatStore` is exported public API and used as a
free function in unit tests. An out-of-band caller bypassing
preflightSession could pass `..` or `/etc/passwd` and have the inner
`join(chatsRoot, wsh, id, 'store.db')` resolve to an arbitrary on-
disk path. The probe is read-only `statSync` so blast radius is
small, but enforce the same CURSOR_SESSION_ID_RE at the function
boundary as a defence-in-depth. New unit test locks the behaviour.
Hub test suite: 414 pass, 0 fail. Typecheck clean across cli/web/hub.
* fix(cursor): cold-review polish on migrator path-priority (#873)
- Web `CursorMigrationBanner` now renders a "Manual review needed"
state for `cursorMigrationState === 'ambiguous'` (Major #1: caller
was promoting the metadata flag but no UI surfaced it).
- Pin the md5-fixture contract for `workspaceHashFromPath`: raw,
no-normalization, trailing-slash-distinct hashes computed via
`printf '%s' <path> | md5sum` (Major #2: prevents algorithm drift
that would silently revert path-priority discovery to fallback).
- Snapshot full candidate set BEFORE the canonical fast-path resolves
a single drawer so the `migrator:transplanted` log reports the
decision-time count, not a post-rm undercount (Minor #1).
- Warn log when canonical-path drawer is missing but readdir hands
back exactly one candidate - regression-equivalent behaviour, but
the size mismatch warrants a journalctl trail (path-normalization
corner case the maintainer can grep for).
- Boundary test: `messageCount = 101` (first value above the skip
threshold) engages the size sanity check, pinning the cutoff
contract (Nit).
- Schema docstring on `cursorMigrationState` enum spelling out the
banner contract per value (Nit).
- syncEngine `getHapiMessageCount` warn-logs `countMessages` throws
instead of silently downgrading to 0 (would chronically disable
the floor).
Drafted with claude-4.6-sonnet-thinking via Cursor; reviewed and
tested by the operator. #873.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cursor): correct log-search strings in ambiguous banner copy
The en/zh-CN locale strings told users to grep for
'migrator:ambiguous_legacy_store' and 'migrator:size_mismatch'
but the hub emits '[migrator] ambiguous legacy store; refusing
transplant' and '[migrator] size sanity check refused transplant'.
Fix both locale files to quote the actual log prefix so the
journalctl grep the operator is directed to actually hits.
Addresses #877 bot finding (Minor).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cursor): address #877 bot Minor findings (trim + boundary guard)
- Remove .trim() from canonical path before hashing: Cursor hashes
raw workspace-path bytes; trimming a POSIX path with leading/
trailing spaces would hash to the wrong drawer, causing a false
canonical miss and potential ambiguity refusal.
- Add CURSOR_SESSION_ID_RE guard to listLegacyChatStoreCandidates:
the function was exported without the same traversal-ID boundary
check present in findLegacyChatStore. A future direct caller
bypassing findLegacyChatStore could stat paths outside the intended
<wsh>/<cursorSessionId>/store.db shape.
- Move CURSOR_SESSION_ID_RE declaration above both functions that
reference it so there is no temporal-dead-zone hazard.
Addresses #877 bot review Minor findings.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 434cd90 commit 3e2e482
12 files changed
Lines changed: 930 additions & 41 deletions
File tree
- hub/src
- cursor
- store
- sync
- web/src
- components
- lib/locales
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
| |||
82 | 83 | | |
83 | 84 | | |
84 | 85 | | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
85 | 90 | | |
86 | 91 | | |
87 | 92 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
299 | 299 | | |
300 | 300 | | |
301 | 301 | | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
302 | 317 | | |
303 | 318 | | |
304 | 319 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
576 | 576 | | |
577 | 577 | | |
578 | 578 | | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
579 | 600 | | |
580 | 601 | | |
581 | 602 | | |
| |||
872 | 893 | | |
873 | 894 | | |
874 | 895 | | |
| 896 | + | |
| 897 | + | |
| 898 | + | |
| 899 | + | |
| 900 | + | |
| 901 | + | |
| 902 | + | |
| 903 | + | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
| 912 | + | |
| 913 | + | |
| 914 | + | |
| 915 | + | |
| 916 | + | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
875 | 931 | | |
876 | 932 | | |
877 | 933 | | |
| |||
894 | 950 | | |
895 | 951 | | |
896 | 952 | | |
| 953 | + | |
| 954 | + | |
| 955 | + | |
| 956 | + | |
| 957 | + | |
| 958 | + | |
| 959 | + | |
| 960 | + | |
| 961 | + | |
| 962 | + | |
| 963 | + | |
| 964 | + | |
| 965 | + | |
| 966 | + | |
| 967 | + | |
| 968 | + | |
| 969 | + | |
| 970 | + | |
| 971 | + | |
| 972 | + | |
| 973 | + | |
| 974 | + | |
| 975 | + | |
| 976 | + | |
| 977 | + | |
| 978 | + | |
| 979 | + | |
| 980 | + | |
| 981 | + | |
| 982 | + | |
| 983 | + | |
| 984 | + | |
897 | 985 | | |
898 | 986 | | |
899 | 987 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
273 | 273 | | |
274 | 274 | | |
275 | 275 | | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
276 | 310 | | |
277 | 311 | | |
278 | 312 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
179 | 179 | | |
180 | 180 | | |
181 | 181 | | |
| 182 | + | |
| 183 | + | |
182 | 184 | | |
183 | 185 | | |
184 | 186 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
42 | | - | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
43 | 49 | | |
44 | 50 | | |
45 | 51 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
86 | 113 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
33 | 47 | | |
34 | 48 | | |
35 | | - | |
36 | | - | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
37 | 70 | | |
38 | | - | |
39 | | - | |
40 | | - | |
41 | | - | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
53 | 88 | | |
54 | 89 | | |
55 | 90 | | |
56 | | - | |
57 | | - | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
58 | 94 | | |
0 commit comments