Skip to content

Commit 7839201

Browse files
sahrizviclaude
andauthored
chore(release): v0.12.5 (#1416)
* release: v0.12.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fJ3X7pcGT4R9yzjsJnqsV * 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fJ3X7pcGT4R9yzjsJnqsV * 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fJ3X7pcGT4R9yzjsJnqsV --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c78b284 commit 7839201

4 files changed

Lines changed: 113 additions & 2 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,48 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.12.5] - 2026-10-06
9+
10+
**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`.
11+
12+
**Heads-up for support:**
13+
- **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.
14+
- **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).
15+
- **`ALTIMATE_WORKSPACE` is retired**; setting it does nothing.
16+
17+
### Changed
18+
19+
- **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)
20+
- **Workspace pages open in the Altimate web app's `/workspaces` section** (`https://<tenant>.app.myaltimate.com/workspaces`) instead of `<tenant>.ws.myaltimate.com`, which is being retired. `ALTIMATE_WORKSPACE_WEB_URL` must now include the mount path. (#1360, #1393)
21+
- **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)
22+
- **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)
23+
24+
### Added
25+
26+
- **Workspace commands for scripts, devcontainers and CI.** `altimate-code workspace status|refresh|sync|unlink [--json] [--directory]` and a non-interactive `altimate-code link --workspace <name|id>` 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 <id>` 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)
27+
- **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)
28+
- **`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)
29+
- **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 <path>] [--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)
30+
- **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)
31+
32+
### Fixed
33+
34+
- **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)
35+
- **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)
36+
- **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)
37+
- **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)
38+
- **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)
39+
- **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)
40+
- **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)
41+
- **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.
42+
- **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)
43+
44+
### Known limitations
45+
46+
- **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.
47+
- **A block whose cloud record was deleted** is re-created only when it is next edited, not on the next load. (#1380)
48+
- **`learn` retrieval is keyword-based**, and a second unrelated request in a session gets only part of the lessons it needs. (#1405)
49+
850
## [0.12.4] - 2026-09-29
951

1052
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.

‎packages/opencode/src/altimate/workspace/memory-sync.ts‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ export function resetEnablementMemoForTests(): void {
213213

214214
/** Warn once per workspace, not once per write. */
215215
const missingFieldWarned = new Set<number>()
216+
/** Workspaces already reported missing from the list, so a lag of a few minutes warns once, not on every call. */
217+
const missingFromListWarned = new Set<number>()
216218

217219
/** Whether the bound workspace has memory switched on.
218220
*
@@ -244,15 +246,28 @@ async function memoryStatus(
244246
try {
245247
const workspaces = await WorkspaceApi.listDatamates()
246248
const match = workspaces.find((w) => w.id === binding.datamateId)
247-
if (match && match.memoryEnabled === undefined && !missingFieldWarned.has(binding.datamateId)) {
249+
if (!match) {
250+
// Missing from the list is not a confirmed toggle. The list lags a workspace created moments ago (one
251+
// service replica serves it stale for a few minutes), so this is unknown, like a failed request: neither
252+
// verdict is kept, so cache-only readers say unknown too, and the write path still fails closed on it.
253+
memoryEnabledCache.delete(binding.datamateId)
254+
memoryDisabledMemo.delete(binding.datamateId)
255+
if (!missingFromListWarned.has(binding.datamateId)) {
256+
missingFromListWarned.add(binding.datamateId)
257+
log.warn("workspace missing from the workspace list; memory setting unknown", { workspace: binding.datamateId })
258+
}
259+
return "error"
260+
}
261+
missingFromListWarned.delete(binding.datamateId)
262+
if (match.memoryEnabled === undefined && !missingFieldWarned.has(binding.datamateId)) {
248263
// Fail-closed is right, but a backend that has not shipped the field
249264
// turns the whole feature into a silent no-op. Say so once.
250265
missingFieldWarned.add(binding.datamateId)
251266
log.warn("workspace has no memory_enabled field; treating memory as disabled", {
252267
workspace: binding.datamateId,
253268
})
254269
}
255-
const value = match?.memoryEnabled === true
270+
const value = match.memoryEnabled === true
256271
if (value) {
257272
memoryEnabledCache.set(binding.datamateId, { checkedAt: Date.now() })
258273
memoryDisabledMemo.delete(binding.datamateId)

‎packages/opencode/test/altimate/workspace/manage.test.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,21 @@ afterAll(() => {
105105
;(AltimateApi as unknown as { getCredentials: typeof originalGetCreds }).getCredentials = originalGetCreds
106106
})
107107

108+
/** The workspace list answering that workspace 42 has memory switched off. A workspace merely missing from the
109+
* list is "unknown", not off (the list lags a workspace created moments ago). */
110+
function memoryOffList() {
111+
const prior = globalThis.fetch
112+
globalThis.fetch = (async (input: any, init?: any) => {
113+
const url = typeof input === "string" ? input : input.url
114+
if (url.endsWith("/datamates/"))
115+
return new Response(JSON.stringify({ datamates: [{ id: 42, name: "Growth", memory_enabled: false }] }), {
116+
status: 200,
117+
headers: { "content-type": "application/json" },
118+
})
119+
return prior(input, init)
120+
}) as typeof fetch
121+
}
122+
108123
async function bind(dir: string, datamateId = 42) {
109124
// Awaited, so the bind's skill sync and memory backfill finish inside this
110125
// test's stubbed `fetch` and its `requests` log. Detached, they straddled
@@ -238,6 +253,7 @@ describe("status", () => {
238253
// nothing at all would hide the first fact to protect the second.
239254
await bind(projectDir)
240255
resetPollMemoForTests()
256+
memoryOffList()
241257

242258
const report = await status(projectDir, { poll: true })
243259

@@ -343,11 +359,22 @@ describe("what the status line is allowed to claim", () => {
343359
expect(report.memory?.unsynced).toBeNull()
344360
})
345361

362+
test("does not report '0 not synced' for a workspace the list does not show yet", async () => {
363+
// The list lags a workspace created moments ago. That is not "memory off", so the count of what is
364+
// outstanding is unknown, not 0.
365+
await bind(projectDir)
366+
resetPollMemoForTests()
367+
const report = await status(projectDir, { poll: true })
368+
expect(report.memory).not.toBeNull()
369+
expect(report.memory?.unsynced).toBeNull()
370+
})
371+
346372
test("still reports 0 outstanding when memory is genuinely off", async () => {
347373
// The contrast that gives the test above its meaning: "disabled" IS an
348374
// answer, and 0 is the truth for it.
349375
await bind(projectDir)
350376
resetPollMemoForTests()
377+
memoryOffList()
351378
const report = await status(projectDir, { poll: true })
352379
expect(report.memory?.unsynced).toBe(0)
353380
})

‎packages/opencode/test/altimate/workspace/memory-sync.test.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,28 @@ describe("memory_enabled", () => {
887887
await mirrorBlock(block({ id: "unknown-ws", scope: "global" }))
888888
expect(callsTo("/datamates/memory/", "POST").length).toBe(0)
889889
})
890+
891+
test("a workspace missing from the list is unknown, not memory off", async () => {
892+
// The list lags a workspace created moments ago. Reading that as "memory off" made sync say so, made the bind
893+
// seed report "off" instead of asking for a retry, and memoized "disabled" so status counted nothing unsent.
894+
workspaces = []
895+
const swept = await backfill([block({ id: "lagging" })], BINDING as any)
896+
expect(swept.gated).toBe(true)
897+
expect(swept.gateReason).toBe("error")
898+
expect(memoryEnabledCached(BINDING as any)).toBe("unknown")
899+
expect(callsTo("/datamates/memory/", "POST").length).toBe(0)
900+
})
901+
902+
test("a workspace last seen with memory off that drops out of the list reads as unknown, not off", async () => {
903+
// The cache-only readers (`workspace status` without a poll, the identity section) must not keep serving the
904+
// earlier "disabled" while the list cannot say anything about the workspace.
905+
workspaces = [{ id: 42, name: "acme", memory_enabled: false }]
906+
await backfill([block({ id: "was-off" })], BINDING as any)
907+
expect(memoryEnabledCached(BINDING as any)).toBe("disabled")
908+
workspaces = []
909+
await backfill([block({ id: "now-missing" })], BINDING as any)
910+
expect(memoryEnabledCached(BINDING as any)).toBe("unknown")
911+
})
890912
})
891913

892914
// ── read path ───────────────────────────────────────────────────────────────
@@ -1646,6 +1668,11 @@ describe("truncated reads", () => {
16461668
workspaces = [{ id: 42, name: "acme", memory_enabled: false }]
16471669
expect((await seedOnBind(dir, BINDING as any)).status).toBe("off")
16481670

1671+
// A workspace the list does not show yet is a retry, not "memory is off".
1672+
resetOverlay()
1673+
workspaces = []
1674+
expect((await seedOnBind(dir, BINDING as any)).status).toBe("incomplete")
1675+
16491676
// A failed enablement lookup gates the sweep too, but is not "memory is off".
16501677
resetOverlay()
16511678
workspaces = []

0 commit comments

Comments
 (0)