Skip to content

Commit dd7d1e5

Browse files
feat(workspace): attach the bound workspace's integration engine
Integrations are served by the local datamate engine — the same process the VS Code extension spawns as `datamate start-stdio`. Altimate Code could reuse an entry an IDE had already written, but could not acquire an engine on its own: with no entry present it fell through to the hosted SSE endpoint, which runs in multi-user mode and serves a DIFFERENT tool set (no connection validation, no extension-bridge tools, server-side cwd). A terminal session in a bound project therefore had either the IDE's tools or the wrong ones. `workspace/engine-sync.ts` closes that gap with `ensure(sessionID)`, idempotent per session and gated on the workspace pilot flag. Its rules, in order: **Reuse.** A connected `datamate` MCP entry wins — that is an IDE-written or previously persisted entry, and attaching to it is free. If it is down, what it is decides what happens next. A URL entry is an IDE's in-process engine or the hosted endpoint; neither can be revived from here, so with a binding and a usable engine on PATH we spawn locally and report what was replaced. The IDE's own config is never touched. A command entry that failed is retried once, then reported — spawning a second engine beside a failing one is the duplicate-process problem the single-gateway design exists to avoid. **Opportunistic use, never an install.** A `datamate` on PATH whose `--version` clears the floor is spawned as `datamate start-stdio --datamate <id>`, pinned to the bound workspace and persisted to the project config so later sessions start it at boot. With no engine present the user is told which workspace tools are unavailable and how to install one; the CLI ships as a self-contained binary with no Node runtime, so it must not pull one in. **Never fall back to hosted on failure.** The local and hosted tool sets diverge in both directions, so a silent fallback would change the workspace's declared contract. A failed engine is reported, not routed around. **Report what was declared but not delivered.** The engine intersects the workspace allowlist with what it managed to build and says nothing about the difference; this diffs declared keys against the tools that actually arrived and surfaces the gap. **First-turn readiness.** A turn resolves its tool list before the per-turn work that starts the attach, so a session that spawned its own engine listed the engine's tools one turn late — the model saw `datamate_manager` alone on the first turn and the integration tools only from the second. The attach now starts ahead of tool resolution and `whenAttached` gives it a bounded window, so those tools make the first tool list. A cold attach measures ~6.5s (≈1s to probe `--version`, ≈1s for the declared allowlist, ≈4.5s for the engine to boot, handshake and build its tools), against a 15s cap set well clear of that and far below MCP's own 30s connect timeout. Past the cap the turn proceeds and `tools/list_changed` delivers the tools when they land. Unbound and disabled sessions settle without I/O and wait for nothing. `datamate_manager list-integrations` now hides extension-type integrations, which are RPC into a live VS Code host and have no meaning on the CLI surface, and reports how many it hid rather than pretending they do not exist. Inert without a local binding. 21 unit tests cover the decision logic through the `syncInternals` seams.
1 parent 8e76c90 commit dd7d1e5

5 files changed

Lines changed: 814 additions & 3 deletions

File tree

‎packages/opencode/src/altimate/api/client.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ const DatamateSummary = z.object({
3737
const IntegrationSummary = z.object({
3838
id: z.coerce.string(),
3939
name: z.string().optional(),
40+
// altimate_change start — catalog `type` (tool | mcp | code | api | extension).
41+
// Extension-type integrations are RPC into a live VS Code host and have no
42+
// meaning on the CLI surface; callers filter on this.
43+
type: z.string().optional(),
44+
// altimate_change end
4045
description: z.string().nullable().optional(),
4146
tools: z
4247
.array(

‎packages/opencode/src/altimate/tools/datamate.ts‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,18 @@ async function handleList() {
138138

139139
async function handleListIntegrations() {
140140
try {
141-
const integrations = await AltimateApi.listIntegrations()
141+
const catalog = await AltimateApi.listIntegrations()
142+
// altimate_change start — extension-type integrations need a live VS Code
143+
// bridge and cannot work from the CLI. Hide them from this surface (the
144+
// workspace UI still offers them), but say how many were hidden rather than
145+
// pretending they don't exist.
146+
const integrations = catalog.filter((i) => i.type !== "extension")
147+
const hidden = catalog.length - integrations.length
148+
// altimate_change end
142149
if (integrations.length === 0) {
143150
return {
144151
title: "Integrations: none found",
145-
metadata: { count: 0 },
152+
metadata: { count: 0, hidden },
146153
output: "No integrations available.",
147154
}
148155
}
@@ -151,9 +158,17 @@ async function handleListIntegrations() {
151158
const tools = i.tools?.map((t) => t.key).join(", ") ?? "none"
152159
lines.push(`${i.id} | ${i.name} | ${tools}`)
153160
}
161+
// altimate_change start
162+
if (hidden > 0) {
163+
lines.push(
164+
"",
165+
`(${hidden} extension-type integration${hidden === 1 ? "" : "s"} omitted — they require a live VS Code bridge and are not available from the CLI.)`,
166+
)
167+
}
168+
// altimate_change end
154169
return {
155170
title: `Integrations: ${integrations.length} available`,
156-
metadata: { count: integrations.length },
171+
metadata: { count: integrations.length, hidden },
157172
output: lines.join("\n"),
158173
}
159174
} catch (e) {

0 commit comments

Comments
 (0)