Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,13 @@ Written acceptance procedures:
check. Also records that a repeat `hyp github backfill` re-appends by design
(LLP 0374), so a row-count check is not read as a regression. See
`docs/ACCEPTANCE.md`.
- `cursor_editor_cli_capture`: opt-in/manual, needs a real Cursor editor and
Cursor CLI. The adapter reads a private, version-specific saved-session
format, so only a real client can say whether the recovered rows match what
the user saw. Proves editor Agent Chat, interactive CLI and headless CLI
independently, plus live hook delivery, WAL-committed recovery, replay
stability, and that inherited Claude hooks are not misattributed. See
`docs/ACCEPTANCE.md`.

Good acceptance smoke candidates (no written procedure yet):

Expand Down Expand Up @@ -308,6 +315,7 @@ hyp dev smoke local_only_export_withhold
hyp dev smoke source_optout_export_withhold
hyp dev smoke github_local_only_withhold
hyp dev smoke opencode_capture
hyp dev smoke cursor_capture
hyp dev smoke github_local_capture
hyp dev smoke otel_loopback_capture
hyp dev smoke local_parquet_export
Expand Down
6 changes: 6 additions & 0 deletions bin/hypaware.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ import { useColor } from '../src/core/cli/stdio.js'

const argv = process.argv.slice(2)

// @ref LLP 0399#coexistence: Cursor inherits these Claude hook commands even
// when Claude is inactive. Skip before config/dispatch can reject the command.
if (process.env.CURSOR_VERSION && argv[0] === 'claude-hook' &&
(argv[1] === 'session-context' || argv[1] === 'classify-cwd') &&
!argv.includes('--help') && !argv.includes('-h')) process.exit(0)

const stderr = colorizeStderr(process.stderr, process.env)
const color = useColor(process.stderr, process.env)

Expand Down
78 changes: 78 additions & 0 deletions docs/ACCEPTANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1945,6 +1945,84 @@ over unchanged history by design too.

---

## `cursor_editor_cli_capture`

**What it proves:** native session recovery and hook delivery in editor Agent
Chat, interactive CLI and headless CLI, independently. Required before a
release claims compatibility. See [implementation evidence](cursor-capture-evidence.md).
This remains a version-specific Draft integration.

Use a disposable OS account or isolated Cursor installation and HypAware home.
Keep activity in a disposable workspace and retain sanitized evidence only.
Authenticate using the supported client flow with the operator's authorization.
Record exact editor/CLI versions, OS, config paths and hook settings.

1. Configure the candidate with `hyp setup --source cursor`, start the isolated
daemon and run `hyp client attach cursor --json` twice. The second attach
reports `changed: false`. Preserve and exercise an unrelated hook. Test CLI
config overrides: native chats follow `CURSOR_CONFIG_DIR`/`XDG_CONFIG_HOME`,
while user hooks remain in `~/.cursor/hooks.json`. Confirm generated commands
carry host and port without `http://`, which the tested CLI comment parser
would corrupt inside a quoted string.
2. In each client mode, create text turns and repeated identical assistant
segments separated by tools. Include Read, Grep, Glob, Shell, dynamic tool
discovery, missing-file errors and rejected Shell calls. Compare the actual
UI or stream with native rows, including every human prompt, intermediate
assistant segments, arguments, complete results, native tool IDs and correct
error status. A missing prompt is the signal to check the typed user-message
suppression field: a fixture pins the predicate, but only a real store can
say what the field means.
`entrypoint` must identify the actual native editor/CLI store.
3. Query `hyp query sql "select session_id, message_id, part_type, content_text,
tool_call_id, status, entrypoint from ai_gateway_messages where client_name = 'cursor'"`.
Verify identical text in different steps remains distinct. Redeliver hooks,
run `hyp backfill cursor --json`, restart the daemon and repeat backfill.
Native message/part IDs and counts must stay stable. Separately repeated
`beforeReadFile` deliveries may add observations, never completed tool calls.
4. Keep Cursor's store open in WAL mode. Recover committed data before a
checkpoint to the main database. Exercise recovery during streaming and
pending tools: partial text must not take a completed response's identity.
Verify final CLI text lacking a completion timestamp still recovers. Test
interruption, cancellation, resume and regeneration, and compare IDs/content
before and after. A contradiction keeps the decision Draft and blocks a
compatibility claim until resolved.
5. Stop HypAware, generate another turn with transcript export disabled, then
restart. Scheduled recovery must find native saved history, and a second
unchanged sweep must report unchanged roots with zero graph/materialization
work. Test `backfill.on_join: false`, a short `window_days`, and manual
backfill. Out-of-window dated messages must not be reintroduced.
6. Verify `beforeReadFile` is a system/hook observation, with no invented tool
ID. Both observations and native Read results must drop unresolved success,
out-of-workspace, symlink-escape and stricter file policies. Test directory
ignore, explicit `hyp session ignore <id>`, and local-only export withholding.
A queued recovery must respect an ignore applied before persistence.
7. Exercise malformed/missing blobs and an unsupported metadata version on
copies of disposable stores. Recovery reports fixed incomplete diagnostics,
does not expose payloads, and leaves other valid sessions recoverable.
Test native discovery/graph bounds and document unsupported subagent,
standalone shell and external-user-text cases. Do not claim full capture.
8. Enable inherited HypAware Claude hooks. Cursor must not append Claude
context or inject classification instructions. Verify the candidate in
the actual editor and CLI, not only the environment-guard fixture.
Then check the other direction, which the fixture cannot: run attached
Claude Code from Cursor's integrated terminal and from a plain terminal,
print the hook process environment, and confirm Claude still records
session context and still sweeps its body spool in both. The skip keys on
`CURSOR_VERSION` alone, so a Cursor shell that exports it would silently
disable Claude's own lane. Record whether Cursor exports that variable to
terminals it launches.
9. Measure hook latency, idle CPU, heap over more than 1,024 callbacks, queue
saturation, large graphs and waiting-spool growth. Confirm bounded retries,
clean shutdown and no raw payload spool. Billed token usage must remain
absent; context occupancy and hook counts are not billing semantics.
10. Unload and detach. Owned hooks disappear; unrelated or edited hooks remain.
Record editor, interactive CLI and headless CLI as pass/fail/blocked.

This gate does not prove cloud agents, Tab, Cmd+K, complete subagents,
rewind revision history, deleted data recovery or canonical usage.

---

## Other candidates

`CLAUDE.md` lists further acceptance candidates that have no written
Expand Down
164 changes: 164 additions & 0 deletions docs/cursor-capture-evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Cursor capture implementation evidence

Status: **Native recovery implemented; release acceptance still required.**
Date: 2026-09-10. Design: [LLP 0399](../llp/0399-cursor-native-session-recovery.decision.md).

Cursor's local saved sessions now supply user messages, assistant segments,
full tool results and tool outcomes. Native hooks trigger bounded recovery and
retain the extra `beforeReadFile` observations requested by the operator.
Scheduled/manual backfill uses the same identities, so missed hooks and daemon
downtime can be recovered without adding a second conversation copy.

## Source evidence

Inspected editor 3.19.19 and isolated CLI 2026.09.08-6caf4ff. The installed old
CLI 2025.10.28-0a91dc2 was not upgraded. Controlled calls and workspace probes
were authorized; sign-in used Cursor's supported login flow. No credentials
were extracted or staged, and the global HypAware installation is unchanged.

Detailed research, source links and reproducible probes:
[Cursor capture gaps](../research/2026-09-10-cursor-capture-gaps/REPORT.md).
Sanitized hook fixtures remain under `test/fixtures/cursor/`.

The hooks deliberately summarize tiny Read/Grep results, mislabel Glob as
Grep, omit intermediate assistant segments and dynamic tool discovery, and
can report successful-looking completions for rejected CLI Shell calls.
Cursor's JSONL transcript serializer can merge thinking into ordinary text
and omit stable identities, so the adapter does not read those transcripts.
The native SQLite graph preserves richer typed and model-context records.

The candidate JavaScript reader was run read-only against exactly three
previously authorized disposable sessions, with ordinary WAL-aware SQLite:

| Store | User turns | Assistant segments | Tools | Native errors |
| --- | ---: | ---: | ---: | ---: |
| Editor `a04677b4-01ac-478d-a4ef-37ab5ddd0d9a` | 5 | 8 | 12 | 0 |
| CLI `7909a4ec-49bf-48af-bf2e-8a38a369bd6b` | 1 | 2 | 1 | 0 |
| CLI `ec2e6c5a-9456-463f-b917-7fd3f2f3a416` | 1 | 3 | 5 | 3 |

The editor includes four Read, three Glob, one Grep, two Shell and two
GetDynamicTools calls. The last CLI session includes a missing-file Read and
two genuinely rejected Shell calls. File text and matching Grep lines are
recovered. These counts agree with the independent research probes.
The final assistant timestamp is absent in exited CLI sessions; the candidate
requires matching saved model-context text for those segments.

These read-only probes verify native decoding, not a globally installed live
handler or every interactive client lifecycle.

## Implemented and verified

- Endpoint-free activation, picker, native hook attach, idempotent reattach,
foreign hook preservation, dry run and marker-owned disk detach.
- Read-only editor format 18 and CLI SQLite version 1, committed WAL visibility,
content hashes, bounded wire traversal and omission of system/reasoning/key data.
- Native user/step/tool identity, repeated identical messages, correct Shell
rejection, unknown future outcomes, archive overlap and incomplete checkpoints.
- Live recovery retries, restart dedupe, queue bounds, shutdown cancellation,
session ignore and serialization with backfill.
- Scheduled unchanged-root skips; failed/dry runs do not advance fingerprints.
Manual backfill bypasses fingerprints and uses the existing materializer.
- Extra file observations retain delivery identity and file-scope checks.
Native Read results receive equivalent scope checks. Local-only rows remain
queryable and are withheld through the actual export seam.
- Inherited Claude hooks skip Cursor before configuration loading, without
injecting context or misattributing activity to Claude.

The `cursor_capture` smoke activates Cursor without the gateway, seeds both
native store formats with open WAL connections, sends hooks, queries 18 rows,
and repeats recovery through the registered backfill command without duplicates.
It checks receive, storage, recovery and privacy telemetry. Passing run:
`smoke-cursor_capture-2026-09-10T23-22-23-505Z-52999`.
Final verification: `npm test` passed 6,570 tests with three skips. Type
checking, the `cursor_capture` smoke, file hygiene and LLP reference checks
passed. Synthetic fixtures pin the researched schema; they do not replace
the real-client acceptance procedure.

## Fresh candidate CLI acceptance, September 10

A new live probe exposed a configuration incompatibility missed by the synthetic
smoke: CLI `HooksConfigLoader.parseJSONC` removes `//` comments before parsing
JSON, even inside strings. Generated `http://127.0.0.1:...` command arguments
therefore invalidated the complete hooks file. Both the ordinary and trusted
comparison runs delivered zero callbacks. A manually triggered recovery of the
first saved session succeeded, isolating the failure to hook configuration.

The generated command now passes `127.0.0.1:<port>`; the standalone sender
restores the HTTP scheme. A regression reproducing the inspected parser failed
before the fix and passes after it. The sender also retains its full-URL input
for existing callers. This is constant bounded work with no new CPU or memory
concern.

With corrected temporary project hooks, fresh CLI session
`791944d7-dfec-4390-80a2-399226b48227` delivered eight callbacks. The real candidate
listener and storage recovered one user message, all three assistant segments,
five tools, and one extra file observation: ten distinct stored parts. The
Read result contained the complete 32-byte fixture; Grep retained matching text,
Glob retained file names, and both rejected Shell attempts became error rows.
The native assistant/tool counts match the independent filtered CLI stream.

During execution, incomplete roots and pending tools produced bounded retries.
Recovery later added the final assistant segment and settled with no pending
work or last error. Replaying saved native records added zero rows, both before
and after restarting the candidate source. Data stayed in a disposable cache;
this was a temporary project-hook test, not a global installation or proof of
all editor/interactive behavior. Editor verification remains pending.

## Additional editor history recovery

At 23:25 UTC, the latest saved turn in the scoped disposable editor session
had checkpoint time 23:15:15 UTC. It contained thirteen tools: three Read,
two Grep, two Glob, two Shell, three GetDynamicTools and one CallDynamicTool,
plus three assistant segments. A manual local recovery trigger persisted the
whole six-turn session as 42 unique parts: six user messages, eleven assistant
segments and twenty-five tools. Replay wrote zero additional rows. The combined
editor/CLI test cache contained 52 unique parts and no duplicate identities.
Full native results were retained; unverified editor Shell/dynamic-call outcome
variants correctly remained unknown rather than being labeled successful.

The operator confirmed that the 23:15 UTC (4:15 p.m. local) turn was the
intended test batch. All thirteen tools and three assistant segments in that
batch were recovered. No new editor callback had reached the candidate
listener: this verifies saved-session recovery, while live editor delivery
still requires a fresh run after attachment. The temporary listener was stopped
and the disposable workspace hook file restored byte-for-byte afterward.

## Remaining limits

[Release acceptance](ACCEPTANCE.md#cursor_editor_cli_capture) still covers
installed handler behavior, editor/interactive/headless modes, interruption,
resume, regeneration, upstream format drift and sustained resource measurements.
Native storage is an internal version-specific interface, not a public API.
Unknown required formats and incomplete/oversized graphs fail with fixed safe
diagnostics. A session beyond discovery/graph bounds is not fully recoverable.

Assistant identity assumes a request's typed step ordinal remains stable.
Rewrites within the same request/ordinal are not a revision history. CLI saved
model-context corroboration is stronger than typed streaming text alone, but
all upstream finality behavior has not been established. No orphan blobs are
scanned. Deleted/pruned data, standalone shell turns, external user-text blobs,
complete subagent graphs, cloud agents, Tab and Cmd+K are outside coverage.

No normalized token usage is emitted. One observed stream reported input 17,601
and cache reads 18,304, so naive subtraction would be invalid. Hooks no longer
persist response rows or raw token frames. Provider is unknown; native editor
and CLI rows have their actual frontend, while extra file observations keep an
unknown frontend when hooks supply no discriminator.

Explicit session IDs are required for Cursor session ignore/status commands.
Multi-root hooks are refused because the row/export contract has one cwd.

## CPU and memory assessment

The implementation adds no runtime dependency. It bounds hook bodies (1 MiB),
HTTP admission (four requests), pending recovery (64 sessions), attempts (three),
and each native graph (4,096 blobs, 32 MiB total, 1 MiB per record). Discovery
limits and fixed diagnostics prevent unbounded catalog scans. Unchanged roots
skip graph decoding and storage dedupe. Metadata fingerprints retain at most
1,000 scheduled roots and 64 live roots. Writers and policy caches are bounded
or retired between passes; no conversation contents remain in adapter caches.

The CPU/memory pass found no unbounded new queue or lifetime content retention.
Synchronous bounded SQLite/parsing can pause the event loop for large graphs,
and shared dedupe still scans waiting data. Hook process overhead, large-session
latency and sustained heap use remain measurements for release acceptance.
Original file line number Diff line number Diff line change
Expand Up @@ -1415,7 +1415,9 @@ function buildStatus(block, isLastPart, role, finishReason) {
/** @type {Record<string, unknown>} */
const status = {}
const b = isPlainObject(block) ? block : undefined
if (b && (b.type === 'tool_result' || b.type === 'web_search_tool_result')) {
// Explicit null means the producer cannot establish the outcome. Keep
// the legacy missing-field convention for providers that imply success.
if (b && b.is_error !== null && (b.type === 'tool_result' || b.type === 'web_search_tool_result')) {
status.tool_status = b.is_error === true ? 'error' : 'success'
}
if (isLastPart && role === 'assistant' && finishReason) status.finish_reason = finishReason
Expand Down
3 changes: 3 additions & 0 deletions hypaware-core/plugins-workspace/claude/src/classify_hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ export async function runClaudeClassifyHook(argv, ctx, deps = {}) {
return 0
}

// @ref LLP 0399#coexistence: Cursor can inherit Claude hooks; those
// invocations must not inject Claude classification instructions.
if (ctx.env.CURSOR_VERSION || typeof event.cursor_version === 'string') return 0
const cwd = str(event.new_cwd) ?? str(event.cwd)
if (!cwd) return 0

Expand Down
7 changes: 7 additions & 0 deletions hypaware-core/plugins-workspace/claude/src/hook_command.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ export async function runClaudeSessionContextHook(argv, ctx, deps = {}) {
return 0
}

// @ref LLP 0399#coexistence: inherited hooks are not Claude activity and
// must not write Claude context or run its body-spool maintenance. Below the
// help branch, which the binary's own skip also spares, so `--help` still
// answers in a Cursor environment.
if (ctx.env.CURSOR_VERSION) return 0

// The recording half is already internally fault-tolerant, but it is wrapped
// here too so the invariant holds structurally: whatever it does, the hook
// exits 0 and the sweep below still runs.
Expand Down Expand Up @@ -113,6 +119,7 @@ async function recordSessionContext(argv, ctx, deps) {
return
}

if (typeof event.cursor_version === 'string') return
const sessionId = str(event.session_id)
const cwd = str(event.new_cwd) ?? str(event.cwd)
if (!sessionId || !cwd) return
Expand Down
Loading
Loading