From 813ee3de999c398bb5b4610159b2fbc04f6865eb Mon Sep 17 00:00:00 2001 From: Harish Kumar Date: Tue, 20 Oct 2026 10:42:23 +0530 Subject: [PATCH 1/8] fix(analyst): let analyst read workspace memory and steer it to workspace skills In a linked project the read-only analyst agent could not call any memory tool, yet the workspace identity section told it to save with `altimate_memory_write` and check `altimate_memory_read`. Its prompt named only the built-in data skills and required every reply to finish through `answer_commit`, which suits data values, not a list of conventions. - analyst may call `altimate_memory_read`, `altimate_memory_refresh` and `altimate_memory_audit`; write, delete and extract stay denied, and data-explorer is unchanged. - The team-memory line names only the memory tools the session's agent can call: a read-only agent is pointed at the read tool and told that builder saves; an agent with neither gets no line. - The analyst prompt loads a matching skill by source precedence, answers questions about team conventions in plain text from skills and memory, and keeps `answer_commit` for data questions. Fixes #1569 Co-Authored-By: Claude Opus 5.5 --- packages/opencode/src/agent/agent.ts | 5 ++++ .../opencode/src/altimate/prompts/analyst.txt | 7 +++-- .../src/altimate/workspace/identity.ts | 30 +++++++++++++++---- packages/opencode/src/session/prompt.ts | 9 +++++- .../test/agent/analyst-data-explorer.test.ts | 19 ++++++++++++ .../workspace/identity-section.test.ts | 8 +++++ .../test/altimate/workspace/identity.test.ts | 17 +++++++++++ packages/opencode/test/skill/skill.test.ts | 5 ++++ 8 files changed, 91 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index feea76341b..b5641c9158 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -334,6 +334,11 @@ export const layer = Layer.effect( question: "allow", // Skills return instruction text only (no side effects), so they are safe for a read-only agent. skill: "allow", + // Workspace memory: reading and reloading are read-only. Saving and deleting stay + // denied, so a decision is recorded through builder. + altimate_memory_read: "allow", + altimate_memory_refresh: "allow", + altimate_memory_audit: "allow", // Delegation is limited to the read-only data-explorer subagent: general/explore/build // carry broader permissions than analyst, and children do not inherit parent-agent limits. task: { diff --git a/packages/opencode/src/altimate/prompts/analyst.txt b/packages/opencode/src/altimate/prompts/analyst.txt index f121f71744..4a0674d571 100644 --- a/packages/opencode/src/altimate/prompts/analyst.txt +++ b/packages/opencode/src/altimate/prompts/analyst.txt @@ -10,7 +10,10 @@ Treat inferred text labels as data: fixed rubric, stable IDs, coverage and dupli Use selected columns, partition filters and LIMIT/sampling to inspect cheaply; estimates are not exact final computations. Read JSONL spill paths with `read`; spills contain returned rows, not necessarily all matches. Aggregate in SQL for exact answers. On timeout, revise the query. Report costs only from evidence. ## Skills -Use `skill` when available: /data-question, /cost-report, /query-optimize, /sql-translate, /dbt-analyze, /lineage-diff, /data-viz. File-writing steps require the builder agent. +Use `skill` when available. When the skill list in your system prompt has one matching the request, load it, following the list's source precedence; workspace and project skills are included. Built-in data skills include /data-question, /cost-report, /query-optimize, /sql-translate, /dbt-analyze, /lineage-diff and /data-viz. File-writing steps require the builder agent. + +## Workspace knowledge +A question about the team's conventions, standards or past decisions is answered from workspace skills and memory, not from warehouse data: load the matching skill, check `altimate_memory_read`, and reply in plain text that states what was found or that nothing was found. Such a reply is not a data value, so do not finish it with `answer_commit`. You cannot save memory; when asked to save something, say that the builder agent can. ## Teammate Training You are a trainable AI teammate. Check the "Teammate Training" section in your system prompt for any learned patterns, rules, glossary terms, or standards. Always apply learned training when relevant. @@ -20,4 +23,4 @@ Use `training_list` to review learned knowledge. Skills: /teach, /train, /traini When the request supplies an output JSON schema (the `StructuredOutput` tool is available), finish with `StructuredOutput` instead of `answer_commit` or a plain-text reply; follow that schema. The text finalization rules below apply when no output schema is supplied. -Finish by calling `answer_commit` with the single chosen value(s) with one field per item the request asks for (every named item, such as an entity and its related item or count, must be its own field: when the request asks for only the answer, anything left in the basis or prose is dropped), a one-or-two-sentence basis (population, grain, source object/column), or `unresolved` with the reason, then reply with exactly the text it returns and nothing else. Always supply `basis`, even when unresolved; it is never shown when the request asks for only the answer. Never list excluded candidates or alternative answers; a top-N with a tie at the boundary still returns N concrete rows (note the tie briefly in `rows.note`); when the request asks for only the answer or a specific format, set `answer_only`. If `answer_commit` is unavailable, finish with one or two sentences of basis and a final line `**Answer:** `, or `**Answer:** unable to determine — `; when the request asks for only the answer or a specific format, the final message is exactly the requested value(s) or table in the requested format, with no label and no basis (or `unable to determine — ` if unresolved); never guess. +For a data question, finish by calling `answer_commit` with the single chosen value(s) with one field per item the request asks for (every named item, such as an entity and its related item or count, must be its own field: when the request asks for only the answer, anything left in the basis or prose is dropped), a one-or-two-sentence basis (population, grain, source object/column), or `unresolved` with the reason, then reply with exactly the text it returns and nothing else. Always supply `basis`, even when unresolved; it is never shown when the request asks for only the answer. Never list excluded candidates or alternative answers; a top-N with a tie at the boundary still returns N concrete rows (note the tie briefly in `rows.note`); when the request asks for only the answer or a specific format, set `answer_only`. If `answer_commit` is unavailable, finish with one or two sentences of basis and a final line `**Answer:** `, or `**Answer:** unable to determine — `; when the request asks for only the answer or a specific format, the final message is exactly the requested value(s) or table in the requested format, with no label and no basis (or `unable to determine — ` if unresolved); never guess. diff --git a/packages/opencode/src/altimate/workspace/identity.ts b/packages/opencode/src/altimate/workspace/identity.ts index 2ad6f9f1e2..5410931f2a 100644 --- a/packages/opencode/src/altimate/workspace/identity.ts +++ b/packages/opencode/src/altimate/workspace/identity.ts @@ -85,6 +85,9 @@ export type RenderOptions = { * which store is the team's. `systemSection` derives it from the enablement memo; * the pure formatter takes it as an argument so tests stay deterministic. */ teamMemory?: boolean + /** Which memory tools the current agent may call. Defaults to both: the line names + * `altimate_memory_write`, and an agent that cannot call it must not be told to. */ + memoryTools?: MemoryTools /** For `unknown`: no Altimate account is configured, so nothing could be checked. */ noAccount?: boolean /** For `unknown`: an IDE pin governs this process, so the extension's selection is the thing to check. */ @@ -101,6 +104,20 @@ const TEAM_MEMORY_LINE = "are the engine's separate store and are not what teammates' sessions read. Before saving, check " + "`altimate_memory_read` for an existing block on the same subject and update it rather than add a duplicate." +/** For an agent that can read memory but not save it (analyst): point it at the read tool, + * and say where saving happens instead of naming a tool it does not have. */ +const TEAM_MEMORY_READ_ONLY_LINE = + "Team memory: look up the workspace's saved decisions and conventions with `altimate_memory_read`. " + + "This agent cannot save memory; when asked to save something, say that the builder agent can." + +export type MemoryTools = { read: boolean; write: boolean } + +function teamMemoryLine(tools: MemoryTools = { read: true, write: true }): string | undefined { + if (tools.write) return TEAM_MEMORY_LINE + if (tools.read) return TEAM_MEMORY_READ_ONLY_LINE + return undefined +} + function renderBody(outcome: BindingOutcome, opts: RenderOptions = {}): string { if (outcome.status === "bound") { const id = String(outcome.binding.datamateId) @@ -117,6 +134,7 @@ function renderBody(outcome: BindingOutcome, opts: RenderOptions = {}): string { ? "This session was last known to be pinned by the IDE extension to" : "This project was last known to be linked to" const verifyNoun = pinned ? "pin" : "link" + const memoryLine = opts.teamMemory ? teamMemoryLine(opts.memoryTools) : undefined return [ HEADING, "", @@ -128,7 +146,7 @@ function renderBody(outcome: BindingOutcome, opts: RenderOptions = {}): string { ? " Skills and memory follow this workspace, and so does warehouse tool routing unless " + "integrations are set to local, even where the project's own link names a different one." : ""), - ...(opts.teamMemory ? [TEAM_MEMORY_LINE] : []), + ...(memoryLine ? [memoryLine] : []), `When ${TRIGGER}, the answer is this Altimate Workspace — never substitute ` + "another service's own \"workspace\" (a Databricks workspace, an IDE's " + "workspace folder, etc.) for it, and the reverse: a question about another " + @@ -342,13 +360,13 @@ function governingPin(directory: string): boolean { return pin.kind === "valid" && resolveWithinRoot(directory, pin.root) !== null } -function renderOptions(outcome: BindingOutcome, directory: string): RenderOptions { +function renderOptions(outcome: BindingOutcome, directory: string, memoryTools?: MemoryTools): RenderOptions { if (outcome.status === "unknown") return { pinned: governingPin(directory) } if (outcome.status !== "bound") return {} // A stale outcome is "last known … may since have changed": promising that a // save syncs to that workspace would contradict the line above it. if (outcome.stale) return {} - return { teamMemory: memoryEnabledCached(outcome.binding) === "enabled" } + return { teamMemory: memoryEnabledCached(outcome.binding) === "enabled", memoryTools } } function keyFor(scope: AccountScope, directory: string): string { @@ -415,7 +433,7 @@ async function withContents(outcome: BindingOutcome, directory: string, identity return contents ? `${identity}\n\n${contents}` : identity } -export async function systemSection(): Promise { +export async function systemSection(opts: { memoryTools?: MemoryTools } = {}): Promise { // Behind the same opt-in as everything else about workspaces. A user outside // the pilot has no Altimate Workspace to be linked to, and must not be told // every turn that none is linked and how to link one. @@ -443,7 +461,7 @@ export async function systemSection(): Promise { }) const key = keyFor(scope, directory) const hit = memo.get(key) - if (fresh(hit)) return withContents(hit!.outcome, directory, render(hit!.outcome, MAX_SECTION_CHARS, renderOptions(hit!.outcome, directory))) + if (fresh(hit)) return withContents(hit!.outcome, directory, render(hit!.outcome, MAX_SECTION_CHARS, renderOptions(hit!.outcome, directory, opts.memoryTools))) // The fallback is itself raced against a small budget, so the wait is // bounded by RESOLVE_DEADLINE_MS + FALLBACK_BUDGET_MS, not by the disk. const deadline = after(RESOLVE_DEADLINE_MS, () => @@ -451,7 +469,7 @@ export async function systemSection(): Promise { ) try { const outcome = await Promise.race([resolve(key, directory), deadline]) - return withContents(outcome, directory, render(outcome, MAX_SECTION_CHARS, renderOptions(outcome, directory))) + return withContents(outcome, directory, render(outcome, MAX_SECTION_CHARS, renderOptions(outcome, directory, opts.memoryTools))) } finally { for (const t of timers) clearTimeout(t) } diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 1c137dc78d..37e732cbbe 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1913,7 +1913,14 @@ export namespace SessionPrompt { // pure link identity. `systemSection()` reads `Instance.directory` itself, inside // its own try/catch — NOT passed as an argument here — so a missing instance // context can't throw synchronously at this call site. - const workspaceIdentity = await WorkspaceIdentity.systemSection() + // The team-memory line names memory tools; describe only the ones this agent can call. + const memoryDisabled = PermissionNext.disabled(["altimate_memory_read", "altimate_memory_write"], agent.permission) + const workspaceIdentity = await WorkspaceIdentity.systemSection({ + memoryTools: { + read: !memoryDisabled.has("altimate_memory_read"), + write: !memoryDisabled.has("altimate_memory_write"), + }, + }) // altimate_change end // altimate_change start — workspace tool awareness. // Reads the snapshot `Precedence.refresh` stored for this turn during tool diff --git a/packages/opencode/test/agent/analyst-data-explorer.test.ts b/packages/opencode/test/agent/analyst-data-explorer.test.ts index b91db472c2..72babad45d 100644 --- a/packages/opencode/test/agent/analyst-data-explorer.test.ts +++ b/packages/opencode/test/agent/analyst-data-explorer.test.ts @@ -75,6 +75,25 @@ it.instance("analyst can load skills", () => }), ) +it.instance("analyst can read workspace memory but not save, extract or delete it", () => + Effect.gen(function* () { + const analyst = (yield* Agent.use.get("analyst"))! + const builder = (yield* Agent.use.get("builder"))! + const de = (yield* Agent.use.get("data-explorer"))! + const read = ["altimate_memory_read", "altimate_memory_refresh", "altimate_memory_audit"] + const write = ["altimate_memory_write", "altimate_memory_delete", "altimate_memory_extract"] + for (const tool of read) expect(action(analyst, tool)).toBe("allow") + for (const tool of write) expect(action(analyst, tool)).toBe("deny") + expect(Permission.disabled([...read, ...write], analyst.permission)).toEqual(new Set(write)) + // The data-explorer subagent stays limited to warehouse and file reads. + for (const tool of [...read, ...write]) expect(action(de, tool)).toBe("deny") + for (const tool of ["altimate_memory_read", "altimate_memory_write"]) expect(action(builder, tool)).toBe("allow") + // Reading memory does not loosen the rest of analyst's read-only policy. + expect(action(analyst, "bash")).toBe("deny") + expect(action(analyst, "sql_execute_write")).toBe("deny") + }), +) + it.instance("analyst may delegate only to data-explorer", () => Effect.gen(function* () { const analyst = yield* Agent.use.get("analyst") diff --git a/packages/opencode/test/altimate/workspace/identity-section.test.ts b/packages/opencode/test/altimate/workspace/identity-section.test.ts index c4d9c0a7f0..9f4fa61a62 100644 --- a/packages/opencode/test/altimate/workspace/identity-section.test.ts +++ b/packages/opencode/test/altimate/workspace/identity-section.test.ts @@ -157,6 +157,14 @@ describe("systemSection", () => { memoryEnabledCache.set(77, { checkedAt: Date.now() }) try { expect(await inProject(systemSection)).toContain("Team memory:") + // A read-only agent is told to read, not to save; resolved and memoised outcomes alike. + const readOnly = () => systemSection({ memoryTools: { read: true, write: false } }) + resetOutcomeMemoForTests() + for (let i = 0; i < 2; i++) { + const out = await inProject(readOnly) + expect(out).toContain("Team memory: look up the workspace's saved decisions") + expect(out).not.toContain("altimate_memory_write") + } resetOutcomeMemoForTests() resetEnablementMemoForTests() // A remembered "no" from the workspace switches the line off. diff --git a/packages/opencode/test/altimate/workspace/identity.test.ts b/packages/opencode/test/altimate/workspace/identity.test.ts index 68b66cbab8..e291ea60fc 100644 --- a/packages/opencode/test/altimate/workspace/identity.test.ts +++ b/packages/opencode/test/altimate/workspace/identity.test.ts @@ -39,6 +39,23 @@ describe("bound — a specific Altimate Workspace is linked", () => { expect(render({ status: "unbound" }, undefined, { teamMemory: true })).not.toContain("Team memory") }) + test("the team memory line names only the memory tools the agent can call", () => { + const full = render(boundOutcome, undefined, { teamMemory: true, memoryTools: { read: true, write: true } }) + expect(full).toBe(render(boundOutcome, undefined, { teamMemory: true })) + const readOnly = render(boundOutcome, undefined, { teamMemory: true, memoryTools: { read: true, write: false } }) + expect(readOnly).toContain("Team memory: look up the workspace's saved decisions and conventions with `altimate_memory_read`") + expect(readOnly).toContain("This agent cannot save memory; when asked to save something, say that the builder agent can.") + expect(readOnly).not.toContain("altimate_memory_write") + expect(readOnly.split("\n")).toHaveLength(5) + const none = render(boundOutcome, undefined, { teamMemory: true, memoryTools: { read: false, write: false } }) + expect(none).not.toContain("Team memory") + expect(none).not.toContain("altimate_memory_") + // Workspace memory off still wins over the agent's access. + expect(render(boundOutcome, undefined, { teamMemory: false, memoryTools: { read: true, write: false } })).not.toContain( + "Team memory", + ) + }) + test("names the workspace and forbids substituting another service's 'workspace' for an identity question", () => { expect(boundOut).toContain("## Altimate Workspace") expect(boundOut).toContain('"Foo Corp Data Team"') diff --git a/packages/opencode/test/skill/skill.test.ts b/packages/opencode/test/skill/skill.test.ts index ce8e114abb..5565be3d91 100644 --- a/packages/opencode/test/skill/skill.test.ts +++ b/packages/opencode/test/skill/skill.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test" import { Effect, Layer } from "effect" import { Skill } from "../../src/skill" +import { Agent } from "../../src/agent/agent" import { Discovery } from "../../src/skill/discovery" import { RuntimeFlags } from "../../src/effect/runtime-flags" import { EventV2Bridge } from "../../src/event-v2-bridge" @@ -768,6 +769,10 @@ description: A skill in the .opencode/skills directory. expect(source["project-one"]).toBe("project") expect(Object.values(source)).toContain("built-in") expect(Object.values(source)).not.toContain(undefined) + // The read-only analyst is offered the workspace's skill, labelled as such, like builder. + const analyst = yield* Effect.promise(() => Agent.get("analyst")) + const listing = Skill.fmt(yield* skill.available(analyst), { verbose: true }) + expect(listing).toContain('\n workspace-one') }).pipe(provideInstance(project.path)), ) }), From 60bdf234463a680b1c6f1f39ca3e342a5dac6a28 Mon Sep 17 00:00:00 2001 From: Harish Kumar Date: Tue, 20 Oct 2026 14:42:17 +0530 Subject: [PATCH 2/8] fix(memory): stop the empty memory-read result from naming the write tool analyst can now call `altimate_memory_read` but not `altimate_memory_write`. An empty read told it to "Use altimate_memory_write to save information", steering a read-only agent toward a call it cannot make. The result now only says that no blocks were found. Co-Authored-By: Claude Opus 5.5 --- packages/opencode/src/memory/tools/memory-read.ts | 4 +++- packages/opencode/test/memory/overlay-merge.test.ts | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/memory/tools/memory-read.ts b/packages/opencode/src/memory/tools/memory-read.ts index f87eb5d8a9..f0ca8dd307 100644 --- a/packages/opencode/src/memory/tools/memory-read.ts +++ b/packages/opencode/src/memory/tools/memory-read.ts @@ -107,7 +107,9 @@ export const MemoryReadTool = Tool.define("altimate_memory_read", { return { title: "Memory: empty", metadata: { count: 0 }, - output: "No memory blocks found. Use altimate_memory_write to save information for future sessions.", + // Read-only agents (analyst) call this tool too, so the result must not + // direct them to a save tool they cannot call. + output: "No memory blocks found.", } } diff --git a/packages/opencode/test/memory/overlay-merge.test.ts b/packages/opencode/test/memory/overlay-merge.test.ts index dddceea31f..93ecae5eae 100644 --- a/packages/opencode/test/memory/overlay-merge.test.ts +++ b/packages/opencode/test/memory/overlay-merge.test.ts @@ -252,6 +252,16 @@ describe("the memory read tool", () => { expect(out).toContain("A LOCAL FACT") expect(out).toContain("A WORKSPACE FACT") }) + + test("an empty result does not tell a read-only agent to save", async () => { + // analyst may read memory but not write it; naming the write tool in the + // result steers it toward a call it cannot make. + await hydrate(SES) + const tool = await initTool(MemoryReadTool) + const res: any = await tool.execute({ scope: "all", tags: ["no-such-tag"] }, { sessionID: SES, agent: "analyst" }) + expect(res.metadata.count).toBe(0) + expect(String(res.output)).toEqual("No memory blocks found.") + }) }) describe("on-demand reload", () => { From 9d92bf24a0edb0d2c9d20a272f3801ddf02b5a74 Mon Sep 17 00:00:00 2001 From: Harish Kumar Date: Tue, 20 Oct 2026 16:15:17 +0530 Subject: [PATCH 3/8] test(analyst): pin the data-question scope of the answer_commit finalization rule The analyst prompt now limits answer_commit to data questions and lets workspace-knowledge answers finish in plain text; update the pinned finalization assertion and pin the new exclusion sentence. Co-Authored-By: Claude Opus 5.5 --- packages/opencode/test/altimate/sql-validation-e2e.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/test/altimate/sql-validation-e2e.test.ts b/packages/opencode/test/altimate/sql-validation-e2e.test.ts index ae8c1d3ed2..83e867dbd2 100644 --- a/packages/opencode/test/altimate/sql-validation-e2e.test.ts +++ b/packages/opencode/test/altimate/sql-validation-e2e.test.ts @@ -109,10 +109,11 @@ describe("Tool name consistency in prompts", () => { const paragraph = prompt.trim().split("\n\n").at(-1) // altimate_change start - require one field for each requested answer item expect(paragraph).toContain( - "Finish by calling `answer_commit` with the single chosen value(s) with one field per item the request asks for (every named item, such as an entity and its related item or count, must be its own field: when the request asks for only the answer, anything left in the basis or prose is dropped), a one-or-two-sentence basis (population, grain, source object/column), or `unresolved` with the reason, then reply with exactly the text it returns and nothing else.", + "For a data question, finish by calling `answer_commit` with the single chosen value(s) with one field per item the request asks for (every named item, such as an entity and its related item or count, must be its own field: when the request asks for only the answer, anything left in the basis or prose is dropped), a one-or-two-sentence basis (population, grain, source object/column), or `unresolved` with the reason, then reply with exactly the text it returns and nothing else.", ) // altimate_change end expect(paragraph).toContain("Always supply `basis`, even when unresolved") + expect(prompt).toContain("Such a reply is not a data value, so do not finish it with `answer_commit`.") expect(paragraph).toContain("it is never shown when the request asks for only the answer") expect(paragraph).toContain("Never list excluded candidates or alternative answers") expect(paragraph).toContain("when the request asks for only the answer or a specific format, set `answer_only`") From 2fce07eda2869c35792da877d8ff7c5131663df4 Mon Sep 17 00:00:00 2001 From: Harish Kumar Date: Tue, 20 Oct 2026 16:31:14 +0530 Subject: [PATCH 4/8] fix(analyst): match the memory line to the tools the turn actually sends - Share one removal rule (`LLM.removedTools`) between tool resolution and the workspace identity section, so a memory tool switched off on the request, or not registered, is not named in the prompt. - A write-only agent gets the save line without the read-first step. - The analyst prompt checks `altimate_memory_read` only when it has it. - Update the analyst prompt assertion in sql-validation-e2e to the new finalization wording. - Cover all four read/write combinations and one cached outcome rendered for different agents. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../opencode/src/altimate/prompts/analyst.txt | 2 +- .../src/altimate/workspace/identity.ts | 12 +++++--- packages/opencode/src/session/llm.ts | 15 ++++++---- packages/opencode/src/session/prompt.ts | 12 +++++--- .../workspace/identity-section.test.ts | 5 ++++ .../test/altimate/workspace/identity.test.ts | 29 ++++++++++++++----- packages/opencode/test/session/llm.test.ts | 15 ++++++++++ 7 files changed, 68 insertions(+), 22 deletions(-) diff --git a/packages/opencode/src/altimate/prompts/analyst.txt b/packages/opencode/src/altimate/prompts/analyst.txt index 4a0674d571..d641690cd0 100644 --- a/packages/opencode/src/altimate/prompts/analyst.txt +++ b/packages/opencode/src/altimate/prompts/analyst.txt @@ -13,7 +13,7 @@ Use selected columns, partition filters and LIMIT/sampling to inspect cheaply; e Use `skill` when available. When the skill list in your system prompt has one matching the request, load it, following the list's source precedence; workspace and project skills are included. Built-in data skills include /data-question, /cost-report, /query-optimize, /sql-translate, /dbt-analyze, /lineage-diff and /data-viz. File-writing steps require the builder agent. ## Workspace knowledge -A question about the team's conventions, standards or past decisions is answered from workspace skills and memory, not from warehouse data: load the matching skill, check `altimate_memory_read`, and reply in plain text that states what was found or that nothing was found. Such a reply is not a data value, so do not finish it with `answer_commit`. You cannot save memory; when asked to save something, say that the builder agent can. +A question about the team's conventions, standards or past decisions is answered from workspace skills and memory, not from warehouse data: load the matching skill, check `altimate_memory_read` when it is in your tool list, and reply in plain text that states what was found or that nothing was found. Such a reply is not a data value, so do not finish it with `answer_commit`. You cannot save memory; when asked to save something, say that the builder agent can. ## Teammate Training You are a trainable AI teammate. Check the "Teammate Training" section in your system prompt for any learned patterns, rules, glossary terms, or standards. Always apply learned training when relevant. diff --git a/packages/opencode/src/altimate/workspace/identity.ts b/packages/opencode/src/altimate/workspace/identity.ts index 5410931f2a..4b8ba94199 100644 --- a/packages/opencode/src/altimate/workspace/identity.ts +++ b/packages/opencode/src/altimate/workspace/identity.ts @@ -97,12 +97,16 @@ export type RenderOptions = { /** Two stores answer "remember this". Only one is read by other linked checkouts, * and nothing told the model which — so on a plain "save this for the team" it * reached for the engine's hub and the decision never left the session (#1332). */ -const TEAM_MEMORY_LINE = +const TEAM_MEMORY_SAVE_LINE = "Team memory: save decisions and conventions with `altimate_memory_write` (scope " + '"project" for this project, "global" for everything); they sync to the workspace and to every ' + "linked checkout. The `datamate_*` memory tools (`datamate_add_memories`, `datamate_search_memory`) " + - "are the engine's separate store and are not what teammates' sessions read. Before saving, check " + - "`altimate_memory_read` for an existing block on the same subject and update it rather than add a duplicate." + "are the engine's separate store and are not what teammates' sessions read." + +/** Appended only when the agent can also read, since it names the read tool. */ +const TEAM_MEMORY_DEDUP_HINT = + " Before saving, check `altimate_memory_read` for an existing block on the same subject and update " + + "it rather than add a duplicate." /** For an agent that can read memory but not save it (analyst): point it at the read tool, * and say where saving happens instead of naming a tool it does not have. */ @@ -113,7 +117,7 @@ const TEAM_MEMORY_READ_ONLY_LINE = export type MemoryTools = { read: boolean; write: boolean } function teamMemoryLine(tools: MemoryTools = { read: true, write: true }): string | undefined { - if (tools.write) return TEAM_MEMORY_LINE + if (tools.write) return tools.read ? TEAM_MEMORY_SAVE_LINE + TEAM_MEMORY_DEDUP_HINT : TEAM_MEMORY_SAVE_LINE if (tools.read) return TEAM_MEMORY_READ_ONLY_LINE return undefined } diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 846c2e8ea2..1bf82c7e32 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -473,15 +473,18 @@ export namespace LLM { }) } + // altimate_change start — one removal rule shared with prompt text that names tools + // Tools removed before the model call: denied by the agent, or switched off on the request. + export function removedTools(names: string[], input: Pick): Set { + const disabled = PermissionNext.disabled(names, input.agent.permission) + return new Set(names.filter((tool) => input.user.tools?.[tool] === false || disabled.has(tool))) + } + async function resolveTools(input: Pick) { - const disabled = PermissionNext.disabled(Object.keys(input.tools), input.agent.permission) - for (const tool of Object.keys(input.tools)) { - if (input.user.tools?.[tool] === false || disabled.has(tool)) { - delete input.tools[tool] - } - } + for (const tool of removedTools(Object.keys(input.tools), input)) delete input.tools[tool] return input.tools } + // altimate_change end // altimate_change start — collect tool names from message history to prevent API validation errors // Anthropic API requires every tool_use block in message history to have a matching tool diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 37e732cbbe..717e5134f2 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1913,12 +1913,16 @@ export namespace SessionPrompt { // pure link identity. `systemSection()` reads `Instance.directory` itself, inside // its own try/catch — NOT passed as an argument here — so a missing instance // context can't throw synchronously at this call site. - // The team-memory line names memory tools; describe only the ones this agent can call. - const memoryDisabled = PermissionNext.disabled(["altimate_memory_read", "altimate_memory_write"], agent.permission) + // The team-memory line names memory tools; describe only the ones this turn sends the model. + const memoryRemoved = LLM.removedTools(["altimate_memory_read", "altimate_memory_write"], { + agent, + user: lastUser, + }) + const memoryCallable = (name: string) => name in tools && !memoryRemoved.has(name) const workspaceIdentity = await WorkspaceIdentity.systemSection({ memoryTools: { - read: !memoryDisabled.has("altimate_memory_read"), - write: !memoryDisabled.has("altimate_memory_write"), + read: memoryCallable("altimate_memory_read"), + write: memoryCallable("altimate_memory_write"), }, }) // altimate_change end diff --git a/packages/opencode/test/altimate/workspace/identity-section.test.ts b/packages/opencode/test/altimate/workspace/identity-section.test.ts index 9f4fa61a62..12abb065bb 100644 --- a/packages/opencode/test/altimate/workspace/identity-section.test.ts +++ b/packages/opencode/test/altimate/workspace/identity-section.test.ts @@ -165,6 +165,11 @@ describe("systemSection", () => { expect(out).toContain("Team memory: look up the workspace's saved decisions") expect(out).not.toContain("altimate_memory_write") } + // The memo holds the outcome, not the text: the same cached outcome renders per agent. + expect(await inProject(systemSection)).toContain("Team memory: save decisions") + expect(await inProject(() => systemSection({ memoryTools: { read: false, write: false } }))).not.toContain( + "Team memory:", + ) resetOutcomeMemoForTests() resetEnablementMemoForTests() // A remembered "no" from the workspace switches the line off. diff --git a/packages/opencode/test/altimate/workspace/identity.test.ts b/packages/opencode/test/altimate/workspace/identity.test.ts index e291ea60fc..4ab5f0a3e4 100644 --- a/packages/opencode/test/altimate/workspace/identity.test.ts +++ b/packages/opencode/test/altimate/workspace/identity.test.ts @@ -39,17 +39,32 @@ describe("bound — a specific Altimate Workspace is linked", () => { expect(render({ status: "unbound" }, undefined, { teamMemory: true })).not.toContain("Team memory") }) - test("the team memory line names only the memory tools the agent can call", () => { + const memoryAccessCases: { read: boolean; write: boolean; line?: string; names: string[] }[] = [ + { read: true, write: true, line: "Team memory: save decisions", names: ["altimate_memory_write", "altimate_memory_read"] }, + { read: true, write: false, line: "Team memory: look up the workspace's saved decisions", names: ["altimate_memory_read"] }, + { read: false, write: true, line: "Team memory: save decisions", names: ["altimate_memory_write"] }, + { read: false, write: false, names: [] }, + ] + for (const access of memoryAccessCases) { + test(`the team memory line names only the memory tools the agent can call (read=${access.read}, write=${access.write})`, () => { + const out = render(boundOutcome, undefined, { + teamMemory: true, + memoryTools: { read: access.read, write: access.write }, + }) + const memoryLines = out.split("\n").filter((line) => line.startsWith("Team memory:")) + expect(memoryLines).toHaveLength(access.line ? 1 : 0) + if (access.line) expect(memoryLines[0]).toStartWith(access.line) + for (const tool of ["altimate_memory_read", "altimate_memory_write"]) { + expect(out.includes(tool)).toBe(access.names.includes(tool)) + } + }) + } + + test("the team memory line keeps its text for an agent with full access, and tells a read-only one who saves", () => { const full = render(boundOutcome, undefined, { teamMemory: true, memoryTools: { read: true, write: true } }) expect(full).toBe(render(boundOutcome, undefined, { teamMemory: true })) const readOnly = render(boundOutcome, undefined, { teamMemory: true, memoryTools: { read: true, write: false } }) - expect(readOnly).toContain("Team memory: look up the workspace's saved decisions and conventions with `altimate_memory_read`") expect(readOnly).toContain("This agent cannot save memory; when asked to save something, say that the builder agent can.") - expect(readOnly).not.toContain("altimate_memory_write") - expect(readOnly.split("\n")).toHaveLength(5) - const none = render(boundOutcome, undefined, { teamMemory: true, memoryTools: { read: false, write: false } }) - expect(none).not.toContain("Team memory") - expect(none).not.toContain("altimate_memory_") // Workspace memory off still wins over the agent's access. expect(render(boundOutcome, undefined, { teamMemory: false, memoryTools: { read: true, write: false } })).not.toContain( "Team memory", diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index 76faede77f..8af231fc3c 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -2,6 +2,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:tes import path from "path" import { jsonSchema, tool, type ModelMessage, type Tool } from "ai" import { LLM } from "../../src/session/llm" +import { PermissionNext } from "../../src/permission/next" import { Instance } from "../../src/project/instance" import { Provider } from "../../src/provider/provider" import { ProviderTransform } from "../../src/provider/transform" @@ -140,6 +141,20 @@ describe("session.llm.addHistoricalToolStubs", () => { }) }) +describe("session.llm.removedTools", () => { + test("removes a tool the agent denies or the request switches off, and keeps the rest", () => { + const agent = { + permission: PermissionNext.fromConfig({ "*": "allow", altimate_memory_write: "deny" }), + } as Agent.Info + const user = { tools: { altimate_memory_read: false, altimate_memory_audit: true } } as unknown as MessageV2.User + const names = ["altimate_memory_read", "altimate_memory_write", "altimate_memory_audit", "read"] + expect(LLM.removedTools(names, { agent, user })).toEqual(new Set(["altimate_memory_read", "altimate_memory_write"])) + expect(LLM.removedTools(names, { agent, user: { tools: undefined } as unknown as MessageV2.User })).toEqual( + new Set(["altimate_memory_write"]), + ) + }) +}) + type Capture = { url: URL headers: Headers From 706382d51d214ca7a028d8328a2dac45c651b642 Mon Sep 17 00:00:00 2001 From: Harish Kumar Date: Tue, 20 Oct 2026 17:53:56 +0530 Subject: [PATCH 5/8] fix(analyst): state what a memory reload changes, and test the prompt wiring - The analyst permission comment now says a reload syncs like the load every session runs and can drop a local copy of an archived block, rather than calling it read-only. - The prompt comment scopes the memory line to the tools the agent and request keep; tool-free steps withhold every tool the prompt names. - A prompt-loop test checks that the identity section is given the same memory access the tools handed to the model call imply. - Document analyst's memory and workspace-skill access. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/docs/data-engineering/agent-modes.md | 1 + packages/opencode/src/agent/agent.ts | 6 ++- packages/opencode/src/session/prompt.ts | 4 +- packages/opencode/test/session/prompt.test.ts | 50 +++++++++++++++++++ 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/docs/docs/data-engineering/agent-modes.md b/docs/docs/data-engineering/agent-modes.md index 6e8e8333b6..57a35a2763 100644 --- a/docs/docs/data-engineering/agent-modes.md +++ b/docs/docs/data-engineering/agent-modes.md @@ -134,6 +134,7 @@ Estimated savings: ~60 credits/week (71% reduction on this query alone) - Search warehouse metadata - Detect PII columns - Save training entries +- Read and reload workspace memory, and answer questions about team conventions from workspace skills (saving memory is done with builder) --- diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index b5641c9158..91e413e8ac 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -334,8 +334,10 @@ export const layer = Layer.effect( question: "allow", // Skills return instruction text only (no side effects), so they are safe for a read-only agent. skill: "allow", - // Workspace memory: reading and reloading are read-only. Saving and deleting stay - // denied, so a decision is recorded through builder. + // Workspace memory: read, reload and view the audit log. Reloading syncs like the + // load every session runs (it can drop a local copy of a block the workspace has + // archived), but cannot create or edit memory. Saving and deleting stay denied, so a + // decision is recorded through builder. altimate_memory_read: "allow", altimate_memory_refresh: "allow", altimate_memory_audit: "allow", diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 717e5134f2..eca2d3c749 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1913,7 +1913,9 @@ export namespace SessionPrompt { // pure link identity. `systemSection()` reads `Instance.directory` itself, inside // its own try/catch — NOT passed as an argument here — so a missing instance // context can't throw synchronously at this call site. - // The team-memory line names memory tools; describe only the ones this turn sends the model. + // The team-memory line names memory tools; describe only the ones this agent and request + // keep. Steps that send no tools at all (answer echo, budget finalization) withhold every + // tool this prompt names, not just these, and are handled on their own paths. const memoryRemoved = LLM.removedTools(["altimate_memory_read", "altimate_memory_write"], { agent, user: lastUser, diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 4efc4d80ee..cc7320206f 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -35,6 +35,7 @@ import { Session as LegacySession } from "../../src/session" import { Session } from "@/session/session" import { SessionMessageTable } from "@opencode-ai/core/session/sql" import { LLM } from "../../src/session/llm" +import * as WorkspaceIdentity from "../../src/altimate/workspace/identity" import { MessageV2 } from "../../src/session/message-v2" import { FSUtil } from "@opencode-ai/core/fs-util" import { SessionCompaction } from "../../src/session/compaction" @@ -624,6 +625,55 @@ noLLMServer.instance( ) // altimate_change end +// altimate_change start - the workspace memory line follows the tools the turn keeps +for (const turn of [ + { agent: "analyst", tools: undefined, expected: { read: true, write: false } }, + { agent: "builder", tools: undefined, expected: { read: true, write: true } }, + { agent: "builder", tools: { altimate_memory_read: false }, expected: { read: false, write: true } }, +] as const) { + noLLMServer.instance( + `prompt loop describes memory tools as ${turn.agent}${turn.tools ? " with read switched off" : ""} keeps them`, + () => Effect.gen(function* () { + const section = spyOn(WorkspaceIdentity, "systemSection").mockResolvedValue("") + const sent: Pick[] = [] + const stream = spyOn(LLM, "stream").mockImplementation(async (input) => { + sent.push({ tools: input.tools, agent: input.agent, user: input.user }) + const usage = { inputTokens: 10, outputTokens: 5, totalTokens: 15 } + async function* fullStream() { + yield { type: "start" } + yield { type: "start-step" } + yield { type: "text-start", id: "reply" } + yield { type: "text-delta", id: "reply", text: "Done." } + yield { type: "text-end", id: "reply" } + yield { type: "finish-step", finishReason: "stop", usage } + yield { type: "finish", finishReason: "stop", totalUsage: usage } + } + return { fullStream: fullStream() } as unknown as Awaited> + }) + try { + const { prompt, chat } = yield* boot() + yield* prompt.prompt({ + sessionID: chat.id, + agent: turn.agent, + model: promptRef, + tools: turn.tools, + parts: [{ type: "text", text: "What are our naming conventions?" }], + }) + expect(section.mock.calls[0]?.[0]).toEqual({ memoryTools: turn.expected }) + // Same answer as the filter applied to the tools handed to the model call. + const kept = (name: string) => name in sent[0].tools && !LLM.removedTools([name], sent[0]).has(name) + expect({ read: kept("altimate_memory_read"), write: kept("altimate_memory_write") }).toEqual(turn.expected) + } finally { + section.mockRestore() + stream.mockRestore() + } + }), + { config: { ...cfg, snapshot: false, learn: { enabled: false } } }, + 30_000, + ) +} +// altimate_change end + // altimate_change start - approved lessons are harness-delivered without changing cached prefixes for (const change of ["lower budget", "replacement", "disabled config", "disabled env"] as const) { noLLMServer.instance( From fb215f847ab8a1ca689e0a9d933487ff3b115a89 Mon Sep 17 00:00:00 2001 From: Harish Kumar Date: Tue, 20 Oct 2026 18:40:35 +0530 Subject: [PATCH 6/8] docs(analyst): list workspace memory and skills in the agents reference Match the agent-modes page, and name the prompt-loop memory tests by what they check. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/docs/configure/agents.md | 1 + packages/opencode/test/session/prompt.test.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/docs/configure/agents.md b/docs/docs/configure/agents.md index e55b63bb16..65c0cda730 100644 --- a/docs/docs/configure/agents.md +++ b/docs/docs/configure/agents.md @@ -28,6 +28,7 @@ The agent for asking questions about your data. Truly read-only mode for safe da - **Bash**: Safe commands auto-allowed (`ls`, `grep`, `cat`, `head`, `tail`, `find`, `wc`). dbt read commands allowed (`dbt list`, `dbt ls`, `dbt debug`). Everything else denied. - **Web**: Fetch and search allowed without prompts - **Schema/warehouse/finops**: All inspection tools available +- **Workspace knowledge**: Loads workspace and project skills; reads and reloads workspace memory. Saving memory is done with `builder`. !!! tip Use `analyst` when exploring data to ensure no accidental writes. Switch to `builder` when you're ready to create or modify models. diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index cc7320206f..79751989be 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -632,7 +632,7 @@ for (const turn of [ { agent: "builder", tools: { altimate_memory_read: false }, expected: { read: false, write: true } }, ] as const) { noLLMServer.instance( - `prompt loop describes memory tools as ${turn.agent}${turn.tools ? " with read switched off" : ""} keeps them`, + `prompt loop describes only the memory tools ${turn.agent} keeps${turn.tools ? " with read switched off on the request" : ""}`, () => Effect.gen(function* () { const section = spyOn(WorkspaceIdentity, "systemSection").mockResolvedValue("") const sent: Pick[] = [] From 212ff7b61b679f312ce5cbdefeec80ab7d502178 Mon Sep 17 00:00:00 2001 From: Harish Kumar Date: Tue, 20 Oct 2026 19:52:29 +0530 Subject: [PATCH 7/8] docs(analyst): describe the memory audit log and what a reload changes Add an analyst turn with no memory tools to the prompt-loop test and name the test for what it asserts. Co-Authored-By: Claude Opus 5.5 --- docs/docs/configure/agents.md | 2 +- docs/docs/data-engineering/agent-modes.md | 2 +- packages/opencode/test/session/prompt.test.ts | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/docs/configure/agents.md b/docs/docs/configure/agents.md index 65c0cda730..a45858470c 100644 --- a/docs/docs/configure/agents.md +++ b/docs/docs/configure/agents.md @@ -28,7 +28,7 @@ The agent for asking questions about your data. Truly read-only mode for safe da - **Bash**: Safe commands auto-allowed (`ls`, `grep`, `cat`, `head`, `tail`, `find`, `wc`). dbt read commands allowed (`dbt list`, `dbt ls`, `dbt debug`). Everything else denied. - **Web**: Fetch and search allowed without prompts - **Schema/warehouse/finops**: All inspection tools available -- **Workspace knowledge**: Loads workspace and project skills; reads and reloads workspace memory. Saving memory is done with `builder`. +- **Workspace knowledge**: Loads workspace and project skills; reads workspace memory and views this checkout's memory audit log. Reloading memory syncs the local copy with the workspace, which can remove a local copy of a block the workspace archived. Saving memory is done with `builder`. !!! tip Use `analyst` when exploring data to ensure no accidental writes. Switch to `builder` when you're ready to create or modify models. diff --git a/docs/docs/data-engineering/agent-modes.md b/docs/docs/data-engineering/agent-modes.md index 57a35a2763..2c8b0c3fc8 100644 --- a/docs/docs/data-engineering/agent-modes.md +++ b/docs/docs/data-engineering/agent-modes.md @@ -134,7 +134,7 @@ Estimated savings: ~60 credits/week (71% reduction on this query alone) - Search warehouse metadata - Detect PII columns - Save training entries -- Read and reload workspace memory, and answer questions about team conventions from workspace skills (saving memory is done with builder) +- Read and reload workspace memory, view this checkout's memory audit log, and answer questions about team conventions from workspace skills (saving memory is done with builder) --- diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 79751989be..0469bd3772 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -630,9 +630,10 @@ for (const turn of [ { agent: "analyst", tools: undefined, expected: { read: true, write: false } }, { agent: "builder", tools: undefined, expected: { read: true, write: true } }, { agent: "builder", tools: { altimate_memory_read: false }, expected: { read: false, write: true } }, + { agent: "analyst", tools: { altimate_memory_read: false }, expected: { read: false, write: false } }, ] as const) { noLLMServer.instance( - `prompt loop describes only the memory tools ${turn.agent} keeps${turn.tools ? " with read switched off on the request" : ""}`, + `prompt loop passes the identity section only the memory tools ${turn.agent} keeps${turn.tools ? " with read switched off on the request" : ""}`, () => Effect.gen(function* () { const section = spyOn(WorkspaceIdentity, "systemSection").mockResolvedValue("") const sent: Pick[] = [] From ecfba20b7b438166fd3569e28975520da5fe6dec Mon Sep 17 00:00:00 2001 From: Harish Kumar Date: Thu, 22 Oct 2026 09:47:37 +0530 Subject: [PATCH 8/8] fix(analyst): scope the no-save line to memory blocks and note what a reload removes Analyst keeps training_save for corrections to its own behavior, so the read-only memory line now refers to workspace memory blocks only and no longer names one specific agent. The refresh tool description says it can remove local copies of archived blocks. Co-Authored-By: Claude Opus 5.5 --- packages/opencode/src/altimate/prompts/analyst.txt | 2 +- packages/opencode/src/altimate/workspace/identity.ts | 3 ++- packages/opencode/src/memory/tools/memory-refresh.ts | 3 ++- packages/opencode/test/altimate/workspace/identity.test.ts | 5 ++++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/altimate/prompts/analyst.txt b/packages/opencode/src/altimate/prompts/analyst.txt index d641690cd0..f0460ec7e9 100644 --- a/packages/opencode/src/altimate/prompts/analyst.txt +++ b/packages/opencode/src/altimate/prompts/analyst.txt @@ -13,7 +13,7 @@ Use selected columns, partition filters and LIMIT/sampling to inspect cheaply; e Use `skill` when available. When the skill list in your system prompt has one matching the request, load it, following the list's source precedence; workspace and project skills are included. Built-in data skills include /data-question, /cost-report, /query-optimize, /sql-translate, /dbt-analyze, /lineage-diff and /data-viz. File-writing steps require the builder agent. ## Workspace knowledge -A question about the team's conventions, standards or past decisions is answered from workspace skills and memory, not from warehouse data: load the matching skill, check `altimate_memory_read` when it is in your tool list, and reply in plain text that states what was found or that nothing was found. Such a reply is not a data value, so do not finish it with `answer_commit`. You cannot save memory; when asked to save something, say that the builder agent can. +A question about the team's conventions, standards or past decisions is answered from workspace skills and memory, not from warehouse data: load the matching skill, check `altimate_memory_read` when it is in your tool list, and reply in plain text that states what was found or that nothing was found. Such a reply is not a data value, so do not finish it with `answer_commit`. You cannot save workspace memory blocks; when asked to save a team decision or convention, say that the builder agent can. A correction to your own behavior is still saved with `training_save`, as below. ## Teammate Training You are a trainable AI teammate. Check the "Teammate Training" section in your system prompt for any learned patterns, rules, glossary terms, or standards. Always apply learned training when relevant. diff --git a/packages/opencode/src/altimate/workspace/identity.ts b/packages/opencode/src/altimate/workspace/identity.ts index 4b8ba94199..0490cbc40c 100644 --- a/packages/opencode/src/altimate/workspace/identity.ts +++ b/packages/opencode/src/altimate/workspace/identity.ts @@ -112,7 +112,8 @@ const TEAM_MEMORY_DEDUP_HINT = * and say where saving happens instead of naming a tool it does not have. */ const TEAM_MEMORY_READ_ONLY_LINE = "Team memory: look up the workspace's saved decisions and conventions with `altimate_memory_read`. " + - "This agent cannot save memory; when asked to save something, say that the builder agent can." + "This agent cannot save workspace memory blocks; when asked to save a decision or " + + "convention for the team, say that an agent with memory write access, such as builder, can." export type MemoryTools = { read: boolean; write: boolean } diff --git a/packages/opencode/src/memory/tools/memory-refresh.ts b/packages/opencode/src/memory/tools/memory-refresh.ts index 7c6bdf52bd..c1f974e5dc 100644 --- a/packages/opencode/src/memory/tools/memory-refresh.ts +++ b/packages/opencode/src/memory/tools/memory-refresh.ts @@ -20,7 +20,8 @@ export const MemoryRefreshTool = Tool.define("altimate_memory_refresh", { "workspace memory once at start; anything written after that is invisible", "until this runs.", "", - "Does nothing when the project is not linked to a workspace.", + "Does nothing when the project is not linked to a workspace. May remove local", + "copies of blocks the workspace has archived; never creates or edits memory.", ].join("\n"), parameters: z.object({}), async execute(_args, ctx) { diff --git a/packages/opencode/test/altimate/workspace/identity.test.ts b/packages/opencode/test/altimate/workspace/identity.test.ts index 4ab5f0a3e4..37a31dccb1 100644 --- a/packages/opencode/test/altimate/workspace/identity.test.ts +++ b/packages/opencode/test/altimate/workspace/identity.test.ts @@ -64,7 +64,10 @@ describe("bound — a specific Altimate Workspace is linked", () => { const full = render(boundOutcome, undefined, { teamMemory: true, memoryTools: { read: true, write: true } }) expect(full).toBe(render(boundOutcome, undefined, { teamMemory: true })) const readOnly = render(boundOutcome, undefined, { teamMemory: true, memoryTools: { read: true, write: false } }) - expect(readOnly).toContain("This agent cannot save memory; when asked to save something, say that the builder agent can.") + expect(readOnly).toContain( + "This agent cannot save workspace memory blocks; when asked to save a decision or " + + "convention for the team, say that an agent with memory write access, such as builder, can.", + ) // Workspace memory off still wins over the agent's access. expect(render(boundOutcome, undefined, { teamMemory: false, memoryTools: { read: true, write: false } })).not.toContain( "Team memory",