From 702846dadb3feabd7acb29ee1c79870bb4447c10 Mon Sep 17 00:00:00 2001 From: Haider Date: Tue, 6 Oct 2026 19:25:56 +0530 Subject: [PATCH 1/3] release: v0.12.5 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018fJ3X7pcGT4R9yzjsJnqsV --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95698429c..b0cf140c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,47 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.12.5] - 2026-10-06 + +**Workspaces are on by default.** Everyone signed in to Altimate now gets project linking, workspace skills and memory sync, the `/workspace` menu, the sidebar tile and a workspace section in the boot box, with no `ALTIMATE_WORKSPACE=1` needed. `ALTIMATE_DISABLE_WORKSPACE=1` turns all of it off. Also in this release: Snowflake connections that reopen instead of failing until restart, passwords added through `warehouse_add` that survive a restart, a crash-safe debug mode with `altimate debug bundle`, headless `workspace` commands, and `altimate-code learn`. + +**Heads-up for support:** +- **Network traffic for signed-in users.** Each process now asks the Altimate service whether the project is linked. That is two requests for an unlinked project (by git remote, then by folder path) and one for a project linked by remote, sending the `origin` URL (credentials removed) and the project's absolute path. The running process keeps the answer and asks again at most every 5 minutes, but every separate `altimate-code run` asks once. A failed lookup is retried on the next turn, and the turn waits at most 2 s for it. Users who are not signed in send nothing. `ALTIMATE_DISABLE_WORKSPACE=1` stops all of it. +- **Startup changes for signed-in users.** The boot box shows a "Workspace mode" section in every project; in an unlinked one it says how to link. After the first-launch scan, a dialog offers to set up or link a workspace (Skip hides it for 7 days per project and account). +- **`ALTIMATE_WORKSPACE` is retired**; setting it does nothing. + +### Changed + +- **Workspaces are on by default, with `ALTIMATE_DISABLE_WORKSPACE` as the kill switch.** The value is trimmed and read without regard to case, and anything other than `0`, `false` or an empty value turns workspaces off. With the switch set, a project that is already linked keeps its link but ignores it: workspace skills already pulled into the project are taken out of service, the agent is not told about the workspace, and the MCP entries the workspace manages can be edited by hand again. `link`, `workspace` and `skill publish` print "Workspaces are turned off because ALTIMATE_DISABLE_WORKSPACE is set", and `serve`'s workspace routes answer 409. (#1381) +- **Workspace pages open in the Altimate web app's `/workspaces` section** (`https://.app.myaltimate.com/workspaces`) instead of `.ws.myaltimate.com`, which is being retired. `ALTIMATE_WORKSPACE_WEB_URL` must now include the mount path. (#1360, #1393) +- **The attach notice is one line.** It gives counts ("2 of 9 integration tools available · 7 need attention. Details: /workspace") instead of listing every missing tool. `/workspace` → **Status** lists each integration with its counts, reasons and tools, and works offline from the last attach; the sidebar tile and the boot box show the same headline. (#1312) +- **Missing integration tools now come with reasons**, read from the workspace engine's report (`@altimateai/datamate` 0.7.3 or later): an expired token, an MCP server that could not start (with the engine's detail), an integration removed from the catalog. Previously every missing tool looked the same. (#1308) + +### Added + +- **Workspace commands for scripts, devcontainers and CI.** `altimate-code workspace status|refresh|sync|unlink [--json] [--directory]` and a non-interactive `altimate-code link --workspace ` or `--create [name]`. Re-running `--create` for the workspace the project is already linked to creates nothing, so it is safe in a setup command. Replacing a link, or unlinking without a terminal, needs `--yes`. On a fresh clone, `workspace sync` does not send this machine's memory until `link --workspace ` confirms the link here. Exit codes: `0` done, `1` failed (including an unreachable service, which is never reported as "not linked"), `2` a request to change, `3` not linked. (#1396) +- **The agent answers "what does this workspace have" from the workspace.** For a linked project, the system prompt lists the workspace's own skills (from the synced copy, sorted, with a skill whose `SKILL.md` cannot be read named as unreadable), its integrations, its memory setting and its knowledge sources. Previously the agent listed every installed skill. (#1397) +- **`altimate-code learn`: the agent learns team conventions from corrections.** Capture is opt-in (`learn enable`). A model proposes lessons, and a person reviews and promotes them (`learn show`, `learn promote`); nothing reaches the agent before that. Promoted lessons are delivered by retrieval, up to 15 core and 15 retrieved per session, and `learn bootstrap` and `learn import-reviews` seed lessons from past sessions and merged GitHub reviews after showing what they will read. Local only; nothing is shared unless you run `learn promote --publish`. `learn.enabled: false` or `ALTIMATE_LEARN=0` turns it off. Projects without approved lessons get no lessons; the only change is a dismissible TUI tip after two corrections in a session, shown at most three times (`learn nudge off` hides it). (#1405) +- **Debug mode that survives a crash, and `altimate debug bundle`.** `ALTIMATE_DEBUG=1` writes every tool call to `opencode.log` as it starts and ends, with a heartbeat every 15 s for calls still running, appended synchronously so the last lines survive a kill. `altimate debug bundle [--output ] [--no-network]` writes a local Markdown report: findings first (runs that ended mid-tool, connections with no retrievable password, unfinished browser sign-ins, unreachable hosts, long stalls), then the evidence. Passwords, keys, tokens, emails, URL queries, the home folder and the user name are removed, and nothing is uploaded. Always on: one `altimate-code started` line per process, and every event-loop stall is logged. (#1409, #1414) +- **Gateway requests carry the IDE extension's trace.** A `traceparent` sent with a prompt is passed to the Altimate gateway as a child span with a matching `x-request-id`, so a failed turn can be followed across the extension, `serve` and the gateway. Altimate providers only. (#1400) + +### Fixed + +- **Closed Snowflake connections are reopened instead of failing until restart.** Once Snowflake closed a session (idle expiry, VPN drop, laptop sleep), every later statement failed with `Unable to perform operation using terminated connection`. The driver now keeps the session alive (`client_session_keep_alive: false` opts out), reopens a dead connection, and replays `USE`, `ALTER SESSION`, `SET` and `UNSET` onto it. A statement is resent only when the error shows it never reached Snowflake, and never one that consumes a sequence value or calls `SYSTEM$`. Temporary tables and an open transaction cannot be restored, so statements that relied on them report it instead of running. (#1395, #1414) +- **Passwords added through `warehouse_add` are kept after a restart.** The credential store needed an optional module that released binaries do not include, so every password, key and token was dropped from the saved connection and the next start failed with `A password must be specified`. Secrets now go to the OS credential store (macOS Keychain, Windows Credential Manager, libsecret on Linux), and a secret that cannot be stored or removed is reported as a warning. (#1395, #1414) +- **Snowflake browser sign-in is visible.** With `authenticator: externalbrowser` the TUI shows **Waiting for sign-in** with the link, headless `run` prints one line, and a sign-in nobody completes tells the agent to ask the user instead of retrying. Other logins fail after 120 s with a network or VPN hint instead of the SDK's 300 s. (#1395) +- **The terminal is restored when the process exits without closing the TUI**, so the shell no longer receives mouse moves and key presses as text. (#1395, #1414) +- **The prompt cache hits on the second model call of each turn in a linked project.** The tool list was reordered between calls, so every turn re-sent the whole prompt uncached. (#1401) +- **The link pickers open on your own workspace that already has the project's name**, instead of creating a second one on a plain Enter. A teammate's workspace of that name is listed but not preselected, and creating a duplicate asks first. (#1379) +- **A project linked to a teammate's private workspace is no longer a dead end in the TUI.** Quick create says to ask the owner to share the workspace or unlink the project, and creating a workspace in the TUI now says it is private. (#1390) +- **Workspace memory:** a block whose cloud record was deleted elsewhere is re-created on its next save instead of failing on every sync, and a block archived elsewhere is removed on this machine when the archive is newer than the block and the block is unchanged. At most 25 blocks are removed per load. (#1380) + +### Known limitations + +- **Workspace skills can add standing instructions.** A synced skill keeps its `alwaysApply` / `applyPaths` frontmatter, so anyone who can upload a skill to a workspace can add instructions to every linked member's prompt. This was approved for the pilot and re-approved for default-on. +- **A block whose cloud record was deleted** is re-created only when it is next edited, not on the next load. (#1380) +- **`learn` retrieval is keyword-based**, and a second unrelated request in a session gets only part of the lessons it needs. (#1405) + ## [0.12.4] - 2026-09-29 One fix for every TUI user, and a round of workspace-pilot work: the IDE extension can drive `/workspace` and `skill publish` over `serve`, a sync says which skills it skipped, and cached workspace state is scoped to the signed-in account. Everything under **Added** is behind `ALTIMATE_WORKSPACE=1`. **Heads-up for support (pilot):** after upgrading, each project's cached workspace link and synced skills are discarded and fetched again on first use (the cache format changed to carry the account), so the first turn in a linked project re-syncs. From 0e7cbc3d17ea413c834a2be63399f3b2aa7762da Mon Sep 17 00:00:00 2001 From: Haider Date: Tue, 6 Oct 2026 20:26:38 +0530 Subject: [PATCH 2/3] fix(workspace): a workspace missing from the list is "unknown", not "memory off" The service's `/datamates/` list can leave out a workspace created moments ago (one replica serves it stale for a few minutes). `memoryStatus` read the absence as memory switched off and memoized it, so `workspace sync` and the bind seed said "Memory is off for this workspace" and sent nothing, and `workspace status` counted everything as synced. Found by this release's live test on freemium. - `memoryStatus` returns "error" for a workspace missing from the list, uncached; the write path still fails closed - The bind seed reports "incomplete" (retry) instead of "off"; status reports the outstanding count as unknown - Two `manage.test.ts` cases that meant "memory genuinely off" now stub `memory_enabled: false` instead of relying on an empty list Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018fJ3X7pcGT4R9yzjsJnqsV --- CHANGELOG.md | 1 + .../src/altimate/workspace/memory-sync.ts | 12 +++++++-- .../test/altimate/workspace/manage.test.ts | 27 +++++++++++++++++++ .../altimate/workspace/memory-sync.test.ts | 16 +++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0cf140c7..c981c8ebb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **The prompt cache hits on the second model call of each turn in a linked project.** The tool list was reordered between calls, so every turn re-sent the whole prompt uncached. (#1401) - **The link pickers open on your own workspace that already has the project's name**, instead of creating a second one on a plain Enter. A teammate's workspace of that name is listed but not preselected, and creating a duplicate asks first. (#1379) - **A project linked to a teammate's private workspace is no longer a dead end in the TUI.** Quick create says to ask the owner to share the workspace or unlink the project, and creating a workspace in the TUI now says it is private. (#1390) +- **A workspace created moments ago no longer reads as "memory off".** The service's workspace list can leave out a just-created workspace for a few minutes, and the client read that as memory switched off: `workspace sync` and linking said "Memory is off for this workspace" and sent nothing, and `workspace status` counted everything as already in the workspace. It is now treated as not yet known, so linking and sync say to retry; nothing is sent until the setting is confirmed. Found in this release's testing. - **Workspace memory:** a block whose cloud record was deleted elsewhere is re-created on its next save instead of failing on every sync, and a block archived elsewhere is removed on this machine when the archive is newer than the block and the block is unchanged. At most 25 blocks are removed per load. (#1380) ### Known limitations diff --git a/packages/opencode/src/altimate/workspace/memory-sync.ts b/packages/opencode/src/altimate/workspace/memory-sync.ts index ea42f506c..6e2f5e826 100644 --- a/packages/opencode/src/altimate/workspace/memory-sync.ts +++ b/packages/opencode/src/altimate/workspace/memory-sync.ts @@ -244,7 +244,15 @@ async function memoryStatus( try { const workspaces = await WorkspaceApi.listDatamates() const match = workspaces.find((w) => w.id === binding.datamateId) - if (match && match.memoryEnabled === undefined && !missingFieldWarned.has(binding.datamateId)) { + if (!match) { + // Missing from the list is not a confirmed toggle. The list lags a workspace created moments ago (one + // service replica serves it stale for a few minutes), so this is unknown, like a failed request: not + // cached either way, and the write path still fails closed on it. + memoryEnabledCache.delete(binding.datamateId) + log.warn("workspace missing from the workspace list; memory setting unknown", { workspace: binding.datamateId }) + return "error" + } + if (match.memoryEnabled === undefined && !missingFieldWarned.has(binding.datamateId)) { // Fail-closed is right, but a backend that has not shipped the field // turns the whole feature into a silent no-op. Say so once. missingFieldWarned.add(binding.datamateId) @@ -252,7 +260,7 @@ async function memoryStatus( workspace: binding.datamateId, }) } - const value = match?.memoryEnabled === true + const value = match.memoryEnabled === true if (value) { memoryEnabledCache.set(binding.datamateId, { checkedAt: Date.now() }) memoryDisabledMemo.delete(binding.datamateId) diff --git a/packages/opencode/test/altimate/workspace/manage.test.ts b/packages/opencode/test/altimate/workspace/manage.test.ts index 91d763c24..1c5a39163 100644 --- a/packages/opencode/test/altimate/workspace/manage.test.ts +++ b/packages/opencode/test/altimate/workspace/manage.test.ts @@ -105,6 +105,21 @@ afterAll(() => { ;(AltimateApi as unknown as { getCredentials: typeof originalGetCreds }).getCredentials = originalGetCreds }) +/** The workspace list answering that workspace 42 has memory switched off. A workspace merely missing from the + * list is "unknown", not off (the list lags a workspace created moments ago). */ +function memoryOffList() { + const prior = globalThis.fetch + globalThis.fetch = (async (input: any, init?: any) => { + const url = typeof input === "string" ? input : input.url + if (url.endsWith("/datamates/")) + return new Response(JSON.stringify({ datamates: [{ id: 42, name: "Growth", memory_enabled: false }] }), { + status: 200, + headers: { "content-type": "application/json" }, + }) + return prior(input, init) + }) as typeof fetch +} + async function bind(dir: string, datamateId = 42) { // Awaited, so the bind's skill sync and memory backfill finish inside this // test's stubbed `fetch` and its `requests` log. Detached, they straddled @@ -238,6 +253,7 @@ describe("status", () => { // nothing at all would hide the first fact to protect the second. await bind(projectDir) resetPollMemoForTests() + memoryOffList() const report = await status(projectDir, { poll: true }) @@ -343,11 +359,22 @@ describe("what the status line is allowed to claim", () => { expect(report.memory?.unsynced).toBeNull() }) + test("does not report '0 not synced' for a workspace the list does not show yet", async () => { + // The list lags a workspace created moments ago. That is not "memory off", so the count of what is + // outstanding is unknown, not 0. + await bind(projectDir) + resetPollMemoForTests() + const report = await status(projectDir, { poll: true }) + expect(report.memory).not.toBeNull() + expect(report.memory?.unsynced).toBeNull() + }) + test("still reports 0 outstanding when memory is genuinely off", async () => { // The contrast that gives the test above its meaning: "disabled" IS an // answer, and 0 is the truth for it. await bind(projectDir) resetPollMemoForTests() + memoryOffList() const report = await status(projectDir, { poll: true }) expect(report.memory?.unsynced).toBe(0) }) diff --git a/packages/opencode/test/altimate/workspace/memory-sync.test.ts b/packages/opencode/test/altimate/workspace/memory-sync.test.ts index 7bc5d0b2b..447f4cf8f 100644 --- a/packages/opencode/test/altimate/workspace/memory-sync.test.ts +++ b/packages/opencode/test/altimate/workspace/memory-sync.test.ts @@ -887,6 +887,17 @@ describe("memory_enabled", () => { await mirrorBlock(block({ id: "unknown-ws", scope: "global" })) expect(callsTo("/datamates/memory/", "POST").length).toBe(0) }) + + test("a workspace missing from the list is unknown, not memory off", async () => { + // The list lags a workspace created moments ago. Reading that as "memory off" made sync say so, made the bind + // seed report "off" instead of asking for a retry, and memoized "disabled" so status counted nothing unsent. + workspaces = [] + const swept = await backfill([block({ id: "lagging" })], BINDING as any) + expect(swept.gated).toBe(true) + expect(swept.gateReason).toBe("error") + expect(memoryEnabledCached(BINDING as any)).toBe("unknown") + expect(callsTo("/datamates/memory/", "POST").length).toBe(0) + }) }) // ── read path ─────────────────────────────────────────────────────────────── @@ -1646,6 +1657,11 @@ describe("truncated reads", () => { workspaces = [{ id: 42, name: "acme", memory_enabled: false }] expect((await seedOnBind(dir, BINDING as any)).status).toBe("off") + // A workspace the list does not show yet is a retry, not "memory is off". + resetOverlay() + workspaces = [] + expect((await seedOnBind(dir, BINDING as any)).status).toBe("incomplete") + // A failed enablement lookup gates the sweep too, but is not "memory is off". resetOverlay() workspaces = [] From 70f842c6da0bb4dc49e1911da332189151b9faa3 Mon Sep 17 00:00:00 2001 From: Haider Date: Tue, 6 Oct 2026 21:06:32 +0530 Subject: [PATCH 3/3] fix(workspace): a missing workspace also clears a cached "memory off", and warns once (review) - `memoryStatus` clears `memoryDisabledMemo` as well when the workspace is missing from the list, so cache-only readers (`workspace status` without a poll, the identity section) say unknown instead of a stale "disabled" - The "missing from the workspace list" warning is logged once per workspace while it stays missing Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018fJ3X7pcGT4R9yzjsJnqsV --- .../opencode/src/altimate/workspace/memory-sync.ts | 13 ++++++++++--- .../test/altimate/workspace/memory-sync.test.ts | 11 +++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/memory-sync.ts b/packages/opencode/src/altimate/workspace/memory-sync.ts index 6e2f5e826..85e115146 100644 --- a/packages/opencode/src/altimate/workspace/memory-sync.ts +++ b/packages/opencode/src/altimate/workspace/memory-sync.ts @@ -213,6 +213,8 @@ export function resetEnablementMemoForTests(): void { /** Warn once per workspace, not once per write. */ const missingFieldWarned = new Set() +/** Workspaces already reported missing from the list, so a lag of a few minutes warns once, not on every call. */ +const missingFromListWarned = new Set() /** Whether the bound workspace has memory switched on. * @@ -246,12 +248,17 @@ async function memoryStatus( const match = workspaces.find((w) => w.id === binding.datamateId) if (!match) { // Missing from the list is not a confirmed toggle. The list lags a workspace created moments ago (one - // service replica serves it stale for a few minutes), so this is unknown, like a failed request: not - // cached either way, and the write path still fails closed on it. + // service replica serves it stale for a few minutes), so this is unknown, like a failed request: neither + // verdict is kept, so cache-only readers say unknown too, and the write path still fails closed on it. memoryEnabledCache.delete(binding.datamateId) - log.warn("workspace missing from the workspace list; memory setting unknown", { workspace: binding.datamateId }) + memoryDisabledMemo.delete(binding.datamateId) + if (!missingFromListWarned.has(binding.datamateId)) { + missingFromListWarned.add(binding.datamateId) + log.warn("workspace missing from the workspace list; memory setting unknown", { workspace: binding.datamateId }) + } return "error" } + missingFromListWarned.delete(binding.datamateId) if (match.memoryEnabled === undefined && !missingFieldWarned.has(binding.datamateId)) { // Fail-closed is right, but a backend that has not shipped the field // turns the whole feature into a silent no-op. Say so once. diff --git a/packages/opencode/test/altimate/workspace/memory-sync.test.ts b/packages/opencode/test/altimate/workspace/memory-sync.test.ts index 447f4cf8f..3b615c6ad 100644 --- a/packages/opencode/test/altimate/workspace/memory-sync.test.ts +++ b/packages/opencode/test/altimate/workspace/memory-sync.test.ts @@ -898,6 +898,17 @@ describe("memory_enabled", () => { expect(memoryEnabledCached(BINDING as any)).toBe("unknown") expect(callsTo("/datamates/memory/", "POST").length).toBe(0) }) + + test("a workspace last seen with memory off that drops out of the list reads as unknown, not off", async () => { + // The cache-only readers (`workspace status` without a poll, the identity section) must not keep serving the + // earlier "disabled" while the list cannot say anything about the workspace. + workspaces = [{ id: 42, name: "acme", memory_enabled: false }] + await backfill([block({ id: "was-off" })], BINDING as any) + expect(memoryEnabledCached(BINDING as any)).toBe("disabled") + workspaces = [] + await backfill([block({ id: "now-missing" })], BINDING as any) + expect(memoryEnabledCached(BINDING as any)).toBe("unknown") + }) }) // ── read path ───────────────────────────────────────────────────────────────