feat(workspace): post a sanitized session attach report when the outcome settles
Issue for this PR
Closes #1309
Type of change
- Bug fix
- New feature
- Refactor / code improvement
- Documentation
What does this PR do?
Stacked on the client half of the unfulfilled-keys work (base branch feat/unfulfilled-keys-meta); merge that first, then retarget this to main.
When a workspace attach settles, the CLI is the only place that knows what the session actually received. This PR posts that as a session attach report to the backend's POST /datamates/{id}/attach-reports (a new endpoint, not yet deployed anywhere — see verification), so the workspace page can show "your last session received …" and whether a linked project is ready.
attach-report.ts(new): the report shape,buildAttachReport(one report per settled outcome:attached,engine-missing,engine-too-old,connect-failed; nothing fordisabled/unbound),attachReportSignature, andpostAttachReport(throughAltimateApi.postAttachReport, guarded: a failure logs once at debug and never throws).- Sanitized by construction. Engine
detailstrings are raw error text and can name paths and hosts. They never leave the machine:sanitizeDetailreduces each to{code}(ENOENT,EACCES,ETIMEDOUT,ECONNREFUSED,invalid-url, elseother) plus, for spawn failures only, the basename of the spawned command (spawn /Users/x/bin/docker ENOENT→{code: "ENOENT", command: "docker"}). The binding is identified by the git remote or project path the server binding row already holds. engine-overlay.ts: the probed engine version now rides on the overlay state;reportOutcomebuilds and posts the report at the four terminal sites, once per distinct report per session (signature excludes the timestamp), fire-and-forget — never awaited by the turn.bridge_connectedis true when a live IDE bridge served extension keys.engine-seams.ts:reportAttachseam so tests capture posts;api/client.ts:postAttachReport.
Claims
- C1 — Nothing free-text leaves the machine. Every
detailis{code, command?}; unit tests feed representative engine strings with paths, Windows paths and hosts and assert only the code and a basename survive; the E2E asserts the stored row contains nospawntext. - C2 — One post per distinct report. Same outcome twice → one post; a changed reason or version → another (test).
- C3 — Failed attaches post too, with the version the CLI saw (
engine-too-oldcarriesfound;engine-missingcarries null) (tests). - C4 — Never on the turn's path. The post is
voided; a sink that throws leaves the outcome and the toast untouched (test). - C5 — No post without a binding, the workspace flag, or credentials.
bindingKeynull → no report; unbound → no report (test); production path checksAltimateApi.isConfigured().
Residuals
- R1 —
connect-failedposts the outcome but not an error code: the backend row has no field for it yet. - R2 — The report is per session signature; a second session on the same project posts its own report, which the backend upserts into the same row (by design).
How did you verify your code works?
bun run typecheckclean; prettier clean; Marker Guard: no upstream-shared files touched.- Unit:
test/altimate/workspace/attach-report.test.ts(11) and six new attach-report cases inengine-overlay.test.ts;test/altimate/workspace— 81 pass. - Real chain, locally. The backend endpoint exists only on a local
altimate-backendbranch, so per the ticket every E2E runs against that local stack:uvicornon:5001with the new migration applied, SuperTokens local, a demo workspace declaring six integrations chosen to produce every reason (an in-house integration served, Jira and a GitHub MCP without connections, an extension integration with no VS Code, an integration removed from the catalog, and a custom MCP pointing at a missing binary). The probe drives the CLI's production attach path (bootstrap+beforeTurn) with the 0.7.2 release-candidate engine on PATH, then reads the row back throughGET …/attach-reports/latest. 14/14 checks, 8063 ms:- toast:
2 of 7 declared integration tools available. Declared but not available — no usable connection: jira_get_issue, jira_create_issue, create_branch; no longer in the catalog: retired_tool; server failed to start (spawn altimate-demo-missing-mcp ENOENT): demo_tool. - stored: outcome
attached, engine0.7.2, delivered['altimate_analyze_snowflake_query', 'altimate_analyze_snowflake_table'], unfulfilled 7 entries incl.demo_tool→spawn-failedwith detail{"code": "ENOENT", "command": "altimate-demo-missing-mcp"}and no raw text; one row per binding.
- toast:
- This run found and fixed a real defect on both sides: a custom integration's id arrives from the engine as a number, and the client parser rejected the whole report as malformed (the attach then announced no gaps at all). The engine now stringifies ids (AltimateAI/altimate-mcp-engine#248,
64e5bb4) and the client parser accepts a number (82531540con the base branch).
Screenshots / recordings
Not a UI change; the toast text and the stored row are asserted above.
Checklist
- I have tested my changes locally
- I have not included unrelated changes in this PR
🤖 Generated with Claude Code
https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
Appendix — complexity delta (altimate-code: attach report post)
82531540c7 → 2cd250d2fe · only functions this diff touches · advisory, not a gate.
| Function | File | Cognitive | CCN | Status |
|---|---|---|---|---|
overlay L213 | packages/opencode/src/altimate/workspace/engine-overlay.ts | 9 → 10 ▲ | 13 → 14 ▲ | easy |
buildAttachReport L91 | packages/opencode/src/altimate/workspace/attach-report.ts | new → 4 | new → 11 | new — trivial |
reconcile L516 | packages/opencode/src/altimate/workspace/engine-overlay.ts | 64 = | 47 → 50 ▲ | needs decomposition |
Summary: 5 touched · 2 rose · 0 improved · 10 new (max cognitive 4) · net cognitive Δ +16
ℹ️ How to read these numbers
Cognitive (Sonar spec) counts breaks in linear reading flow — each if/loop/catch/ternary/boolean-operator switch adds 1, and nesting makes every further break cost more. It approximates how much you must hold in your head to follow the function: 0–5 trivial · 6–10 easy · 11–15 moderate (15 = Sonar's recommended per-function cap) · 16–25 hard to follow · >25 needs decomposition.
CCN (cyclomatic) counts independent paths — also the minimum number of test cases for full branch coverage of the function.
Only functions this diff touches are measured, as deltas — pre-existing complexity is not counted against this change. Rising numbers aren't automatically wrong; they're where review attention should go. Test files excluded.
Summary by cubic
The CLI now posts a sanitized session attach report to POST /datamates/{id}/attach-reports when an attach settles; previously, attach details stayed local. This lets the workspace page show what the session received without exposing raw engine errors or changing attach behavior.
- Reports cover
attached,engine-missing,engine-too-old, andconnect-failed;disabledandunboundproduce no report. - Reports include binding identity, CLI and engine versions, bridge status, declared and delivered keys, and sanitized unfulfilled entries.
- Engine errors become codes, with only the command basename retained for spawn failures.
- Identical reports post once per session; failures log at debug level without affecting the outcome or toast.
Rollout
- Merge
feat/unfulfilled-keys-metafirst, then retarget this branch tomain; verify against the local backend until the endpoint is deployed. - Closes #1309.