Skip to content

Commit 3aa43a8

Browse files
sahrizviclaude
andauthored
chore(release): v0.12.10 (#1693)
Release v0.12.10: workspace rules state lives outside the project (#1669), a path-linked project can gain its git remote and learn status says why rules are off (#1670), tool commands deliver a large answer whole on a pipe (#1687), and a failed session title is retried on later turns and reported (#1688). Adds the changelog entry, the release adversarial tests, a 60 s stall bound on tool output, and the requested model in title failure reports. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent b06bd16 commit 3aa43a8

6 files changed

Lines changed: 380 additions & 8 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@ 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.10] - 2026-11-07
9+
10+
**Workspace rules are no longer read from the repository, and a session's title no longer sticks on its placeholder.** The workspace rules state (queued proposals, pending approvals, review notices, dismissed topics and the personal learning choice) moves out of the project folder to a private per-user file, so a branch, merge or pull cannot plant a proposal or an approval that a client would then send with the signed-in person's credential, and team rules reach a session only for the account that pulled them. A session whose first title request fails now retries on the next messages and records the provider's error. Also in this release: a project linked by its path can gain its git remote so team rules can sync, `learn status` says why rules are off, and `tool` commands deliver a large answer whole on a pipe.
11+
12+
**Heads-up for support:**
13+
- **Workspace rules state moves to `$XDG_STATE_HOME/altimate-code/workspace-rules/<hash>.json`** (usually `~/.local/state/…`), keyed by the project folder's real path, so two clones or a moved folder keep separate state. Only projects linked to a workspace are affected. There is no migration from the in-project `.altimate-code/learn/workspace-rules/rules.json`, on purpose: that file is no longer read and can be deleted. Lost with it: proposals queued but never sent, review notices, dismissed topics, the personal learning choice (someone who turned learning off in `/rules` is back on the workspace setting until they set it again), and the record of which usage was already reported, so the first report after upgrading can count the last 30 days again. Until the first sync after upgrading (at session start, then every 5 minutes, or `altimate-code learn sync`) a session has no team rules. The in-project `approved.json` copy is still written for display and recurrence but is never used for delivery.
14+
- **Before the first sync, `learn status` distinguishes** not linked, linked by path only (rules need a git remote), not signed in, learning off, and linked but not yet synced, and the `learn sync` skip message says what to do.
15+
- **A path-linked project that now has a git remote gets it added to the link** when `altimate-code link` or the TUI link picker is run again and the same workspace is picked, or with `link --workspace <id>`, instead of stopping at "Already linked".
16+
- **A session keeps `New session - <time>` only while its title request keeps failing.** The title is retried on each of the first five user messages, always from the first message; a session titled on the first message makes no extra request, and one that already has more than five messages (for example resumed from an older version) is left as it is. Each failure writes `failed to generate title` to the log with the provider's error (`errorType`, `message`, `status`, `attempt`) and sends a `title_generation_failed` telemetry event (provider and model that were asked for, attempt, error class and type, HTTP status; no message text). The worst case for a session whose small model never answers is five attempts of up to three HTTP requests each. A `small_model` the provider cannot serve, or does not list, is a likely cause; the models page now describes it.
17+
- **`tool` commands write the whole answer before returning.** Read stdout while the command runs: a reader that stops taking output for 60 seconds makes the command exit 1 with a message on stderr, as does a write that fails (for example a full disk), where it used to exit 0 with the answer lost. A reader that closes early, such as `| head`, still exits 0.
18+
19+
### Fixed
20+
21+
- **Workspace rules state lives outside the project.** Proposals, approvals and notices found in the working tree are no longer trusted or acted on; the state is a private per-user file, and team rules come from it only for the account that pulled them. (#1669)
22+
- **A project linked by its path can gain its git remote**, and `learn status` says why workspace rules are off (not linked, linked by path only, not signed in, learning off, not yet synced) instead of "not linked". The learn docs say that rules need a git remote. (#1670)
23+
- **`tool` commands no longer lose a big answer on a pipe.** A slow reader used to receive only the first 64 KiB of a larger answer (for example 64 KiB of a 354 KB `tool run --json` result). `tool run` in JSON and text mode, `tool list --json`, `tool context` and `tool schema` now write the whole answer before returning. (#1687)
24+
- **A failed session title is retried on later turns and reported.** The log carries the provider's error instead of "No output generated", a failure before the request (a `small_model` the provider does not list) is reported too, and telemetry gets a `title_generation_failed` event. (#1688)
25+
826
## [0.12.9] - 2026-11-04
927

1028
**Claude Code and other coding agents can call altimate-code's data tools directly.** `altimate-code mcp serve` and the new model-free `altimate-code tool` CLI run the same tools without a second model: reads by default, writes only with `--allow-write` (over MCP, `warehouse_add` can save a connection without it). In projects linked to a workspace, what people teach the agent can become a team rule: they confirm it on a card, the workspace owner reviews it, and approved rules reach every teammate's sessions. `dbt_lineage` and `impact_analysis` now return structured lineage, and column impact traces each downstream model's compiled SQL. Also in this release: `auth logout` removes the saved Altimate gateway key, the analyst stays read-only whatever config grants, and the home screen suggests the Claude Code and Codex plugins.

‎docs/docs/configure/models.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ altimate supports models from all configured providers. Use the `model` and `sma
1818

1919
The model format is `provider/model-name`.
2020

21-
`small_model` handles the small background requests that are not part of the conversation, such as the session title and prompt enhancement. When it is not set, Altimate Code picks a cheap model from the same provider (Claude Haiku, Gemini Flash or GPT-5 nano when the provider lists one) and otherwise uses the chat model. A `small_model` the provider cannot serve, or does not list, fails only those requests: chat keeps working, while new sessions keep their `New session - <time>` placeholder title until a retry succeeds. Altimate Code retries the title on each of the first five messages of a session and reports every failure in telemetry.
21+
`small_model` handles the small background requests that are not part of the conversation, such as the session title and prompt enhancement. When it is not set, Altimate Code picks a cheap model from the same provider (Claude Haiku, Gemini Flash or GPT-5 nano when the provider lists one) and otherwise uses the chat model. A `small_model` the provider cannot serve, or does not list, fails only those requests: chat keeps working, while new sessions keep their `New session - <time>` placeholder title until a retry succeeds. Altimate Code retries the title on each of the first five messages of a session and reports every failure in telemetry. A failure shows up as a `failed to generate title` line in the log, with the provider's error, and as a `title_generation_failed` telemetry event naming the model that was asked for; correct or remove the `small_model` value to resolve it.
2222

2323
## Browsing Models
2424

‎packages/opencode/src/cli/cmd/tool.ts‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,18 +303,36 @@ const ToolDaemonCommand = cmd({
303303
*/
304304
function writeAll(text: string) {
305305
const bytes = Buffer.from(text)
306+
let progressAt = performance.now()
306307
for (let at = 0; at < bytes.length; ) {
308+
let wrote = 0
307309
try {
308-
at += writeSync(1, bytes, at)
310+
wrote = writeSync(1, bytes, at)
309311
} catch (error) {
310312
const code = (error as NodeJS.ErrnoException).code
311313
if (code === "EPIPE") return // The reader left, so nobody is waiting for the rest.
312314
if (code !== "EAGAIN") throw error
313-
Bun.sleepSync(2)
314315
}
316+
if (wrote > 0) {
317+
at += wrote
318+
progressAt = performance.now()
319+
continue
320+
}
321+
// Nothing was taken (EAGAIN, or a zero-byte write). A reader that stopped draining the pipe (a
322+
// stopped pager, a wedged log shipper) would otherwise keep this process spinning forever; after
323+
// a minute without progress on a monotonic clock, fail.
324+
if (performance.now() - progressAt > WRITE_STALL_MS) {
325+
throw new Error(
326+
`stdout stopped accepting output for ${WRITE_STALL_MS / 1000} s; ${bytes.length - at} bytes of the answer were not written`,
327+
)
328+
}
329+
Bun.sleepSync(2)
315330
}
316331
}
317332

333+
/** How long `writeAll` waits for a reader that has stopped taking output before it gives up. */
334+
const WRITE_STALL_MS = 60_000
335+
318336
export const ToolCommand = cmd({
319337
command: "tool",
320338
describe: "run altimate-code data tools directly, without a model",

‎packages/opencode/src/session/prompt.ts‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1254,12 +1254,13 @@ export namespace SessionPrompt {
12541254

12551255
step++
12561256
if (step === 1)
1257-
ensureTitle({
1257+
// altimate_change — fire-and-forget, but never an unhandled rejection
1258+
void ensureTitle({
12581259
session,
12591260
modelID: lastUser.model.modelID,
12601261
providerID: lastUser.model.providerID,
12611262
history: msgs,
1262-
})
1263+
}).catch((error) => log.error("failed to generate title", { error: String(error) }))
12631264

12641265
const model = await Provider.getModel(lastUser.model.providerID, lastUser.model.modelID).catch((e) => {
12651266
if (Provider.ModelNotFoundError.isInstance(e)) {
@@ -4948,7 +4949,17 @@ NOTE: At any point in time through this workflow you should feel free to ask the
49484949
let text = ""
49494950
let failure: unknown
49504951
let model: Provider.Model | undefined
4952+
// The model the request is for, so a failed lookup is reported against the model that was asked
4953+
// for (the title agent's own, or `small_model`), not against the chat model.
4954+
let requested: { providerID: string; modelID: string } | undefined
49514955
try {
4956+
requested = await iife(async () => {
4957+
if (agent.model) return { providerID: String(agent.model.providerID), modelID: String(agent.model.modelID) }
4958+
const small = (await Config.get()).small_model
4959+
if (!small) return undefined
4960+
const parsed = Provider.parseModel(small)
4961+
return { providerID: String(parsed.providerID), modelID: String(parsed.modelID) }
4962+
})
49524963
model = await iife(async () => {
49534964
// agent.model carries core ProviderV2.ID/ModelV2.ID brands; re-brand to fork ProviderID/ModelID (identity at runtime)
49544965
if (agent.model)
@@ -5002,8 +5013,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the
50025013
if (failure !== undefined || !cleaned) {
50035014
const err = failure ?? new Error(text.trim() ? "the model returned no title line" : "the model returned no text")
50045015
const described = describeTitleError(err)
5005-
const providerID = model?.providerID ?? input.providerID
5006-
const modelID = model?.id ?? input.modelID
5016+
const providerID = model?.providerID ?? requested?.providerID ?? input.providerID
5017+
const modelID = model?.id ?? requested?.modelID ?? input.modelID
50075018
// Name, message and status only: a provider error object can carry the request body.
50085019
log.error("failed to generate title", {
50095020
errorType: described.name,

‎packages/opencode/test/session/title-retry.test.ts‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,8 @@ it.live(
275275
failed = events
276276
}
277277
expect(failed).toHaveLength(1)
278-
expect(failed[0]).toMatchObject({ provider_id: "test", model_id: "test-model", attempt: 1 })
278+
// The report names the model that was asked for, not the chat model it fell back from.
279+
expect(failed[0]).toMatchObject({ provider_id: "test", model_id: "does-not-exist", attempt: 1 })
279280
expect(failed[0].error_type).toBe("ProviderModelNotFoundError")
280281
expect(failed[0].http_status).toBeUndefined()
281282
const titleHits = (yield* llm.hits).filter((hit) => JSON.stringify(hit.body).includes(TITLE_MARKER))
@@ -335,3 +336,38 @@ it.live(
335336
),
336337
{ timeout: 60_000 },
337338
)
339+
340+
it.live(
341+
"a name the user gives the session while the title request runs is kept",
342+
() =>
343+
provideTmpdirServerLegacy(
344+
Effect.fnUntraced(function* ({ llm }) {
345+
const { events } = yield* recordTitleFailures
346+
const prompt = yield* SessionPrompt.Service
347+
const sessions = yield* Session.Service
348+
const session = yield* sessions.create({
349+
permission: [{ permission: "*", pattern: "*", action: "allow" }],
350+
})
351+
352+
const gate = Promise.withResolvers<void>()
353+
yield* llm.title(reply().wait(gate.promise).text("Generated title").stop())
354+
yield* llm.text("first answer")
355+
yield* prompt.prompt({
356+
sessionID: session.id,
357+
agent: "build",
358+
parts: [{ type: "text", text: "first message" }],
359+
})
360+
yield* llm.wait(2)
361+
// The user names the session while the title answer is still held.
362+
yield* sessions.setTitle({ sessionID: session.id, title: "My own name" })
363+
gate.resolve()
364+
365+
// The generated answer arrives and is discarded: only a placeholder is ever replaced.
366+
for (let i = 0; i < 25; i++) yield* Effect.sleep("20 millis")
367+
expect((yield* sessions.get(session.id)).title).toBe("My own name")
368+
expect(events).toHaveLength(0)
369+
}),
370+
{ git: true, config: providerCfg },
371+
),
372+
{ timeout: 60_000 },
373+
)

0 commit comments

Comments
 (0)