feat(workspace): post a sanitized session attach report when the outcome settles

Draft
RA
2mo ago
+472-8

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 for disabled/unbound), attachReportSignature, and postAttachReport (through AltimateApi.postAttachReport, guarded: a failure logs once at debug and never throws).
  • Sanitized by construction. Engine detail strings are raw error text and can name paths and hosts. They never leave the machine: sanitizeDetail reduces each to {code} (ENOENT, EACCES, ETIMEDOUT, ECONNREFUSED, invalid-url, else other) 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; reportOutcome builds 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_connected is true when a live IDE bridge served extension keys.
  • engine-seams.ts: reportAttach seam so tests capture posts; api/client.ts: postAttachReport.

Claims

  • C1 — Nothing free-text leaves the machine. Every detail is {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 no spawn text.
  • 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-old carries found; engine-missing carries 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. bindingKey null → no report; unbound → no report (test); production path checks AltimateApi.isConfigured().

Residuals

  • R1 — connect-failed posts 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 typecheck clean; prettier clean; Marker Guard: no upstream-shared files touched.
  • Unit: test/altimate/workspace/attach-report.test.ts (11) and six new attach-report cases in engine-overlay.test.ts; test/altimate/workspace — 81 pass.
  • Real chain, locally. The backend endpoint exists only on a local altimate-backend branch, so per the ticket every E2E runs against that local stack: uvicorn on :5001 with 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 through GET …/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, engine 0.7.2, delivered ['altimate_analyze_snowflake_query', 'altimate_analyze_snowflake_table'], unfulfilled 7 entries incl. demo_tool → spawn-failed with detail {"code": "ENOENT", "command": "altimate-demo-missing-mcp"} and no raw text; one row per binding.
  • 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 (82531540c on 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.

FunctionFileCognitiveCCNStatus
overlay L213packages/opencode/src/altimate/workspace/engine-overlay.ts9 → 10 ▲13 → 14 ▲easy
buildAttachReport L91packages/opencode/src/altimate/workspace/attach-report.tsnew → 4new → 11new — trivial
reconcile L516packages/opencode/src/altimate/workspace/engine-overlay.ts64 =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, and connect-failed; disabled and unbound produce 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-meta first, then retarget this branch to main; verify against the local backend until the endpoint is deployed.
  • Closes #1309.