diff --git a/packages/opencode/src/altimate/workspace/contents.ts b/packages/opencode/src/altimate/workspace/contents.ts index 7bdfc4f8fe..c4f776c33f 100644 --- a/packages/opencode/src/altimate/workspace/contents.ts +++ b/packages/opencode/src/altimate/workspace/contents.ts @@ -320,7 +320,9 @@ function renderWith(contents: WorkspaceContents, detail: SkillDetail): string { : `Integrations: ${contents.integrations.join(", ")}.`, ) if (contents.memoryEnabled !== null) - lines.push(`Workspace memory: ${contents.memoryEnabled ? "on — saved memories are shared with the team" : "off"}.`) + lines.push( + `Workspace memory: ${contents.memoryEnabled ? "on — memories saved with `altimate_memory_write` sync to each user's own checkouts, not to teammates" : "off"}.`, + ) const k = contents.knowledge if (k?.kind === "off") lines.push("Knowledge: none — the knowledge engine is off for this workspace.") else if (k?.kind === "all") diff --git a/packages/opencode/src/altimate/workspace/identity.ts b/packages/opencode/src/altimate/workspace/identity.ts index 2ad6f9f1e2..51d54ad430 100644 --- a/packages/opencode/src/altimate/workspace/identity.ts +++ b/packages/opencode/src/altimate/workspace/identity.ts @@ -82,7 +82,7 @@ export function render(outcome: BindingOutcome, cap = MAX_SECTION_CHARS, opts: R export type RenderOptions = { /** The bound workspace has memory on (or has not said otherwise): tell the model - * which store is the team's. `systemSection` derives it from the enablement memo; + * which store syncs, and to whom. `systemSection` derives it from the enablement memo; * the pure formatter takes it as an argument so tests stay deterministic. */ teamMemory?: boolean /** For `unknown`: no Altimate account is configured, so nothing could be checked. */ @@ -93,13 +93,18 @@ 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). */ + * reached for the engine's hub and the decision never left the session (#1332). + * Those checkouts are the same user's: mirrored records are private, so team-wide + * conventions belong in a published workspace skill (#1404). */ const TEAM_MEMORY_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." + "Workspace memory: save decisions and conventions with `altimate_memory_write` (scope " + + '"project" for this project, "global" for everything); they sync to the workspace for this user only ' + + "and follow them to their other linked checkouts. Teammates' sessions do not read them, so for a " + + "convention the whole team should follow, suggest publishing a workspace skill " + + "(`altimate-code skill publish `). The `datamate_*` memory tools (`datamate_add_memories`, " + + "`datamate_search_memory`) are the engine's separate store and are not what linked checkouts read. " + + "Before saving, check `altimate_memory_read` for an existing block on the same subject and update it " + + "rather than add a duplicate." function renderBody(outcome: BindingOutcome, opts: RenderOptions = {}): string { if (outcome.status === "bound") { diff --git a/packages/opencode/src/altimate/workspace/starter.ts b/packages/opencode/src/altimate/workspace/starter.ts index 7532073926..245c96e52d 100644 --- a/packages/opencode/src/altimate/workspace/starter.ts +++ b/packages/opencode/src/altimate/workspace/starter.ts @@ -56,7 +56,7 @@ function integrationsLine(ids: string[] | null): string | null { function memoryLine(on: boolean | null): string | null { if (on === null) return null return on - ? "Memory: on. What you teach Altimate Code is saved automatically and shared with your team in the CLI, the IDE and Studio." + ? "Memory: on. What you teach Altimate Code is saved automatically and follows you in the CLI, the IDE and Studio. It's private to you: to share a convention with your team, publish it as a workspace skill." : "Memory: off for this workspace, so saved memory stays on this machine." } diff --git a/packages/opencode/src/memory/tools/memory-write.ts b/packages/opencode/src/memory/tools/memory-write.ts index 4613d9756d..09c381e7a7 100644 --- a/packages/opencode/src/memory/tools/memory-write.ts +++ b/packages/opencode/src/memory/tools/memory-write.ts @@ -6,7 +6,7 @@ import { MEMORY_MAX_BLOCK_SIZE, MEMORY_MAX_BLOCKS_PER_SCOPE, CitationSchema, Mem const idSchema = MemoryBlockSchema.shape.id export const MemoryWriteTool = Tool.define("altimate_memory_write", { - description: `Save an Altimate Memory block for cross-session persistence. Use this to store information worth remembering across sessions — warehouse configurations, naming conventions, team preferences, data model notes, or past analysis decisions. When the project is linked to an Altimate Workspace with workspace sync on and workspace memory enabled this is the TEAM's memory: blocks sync to the workspace and to every linked checkout, so a decision saved here is what teammates' sessions read. Before saving, call \`altimate_memory_read\` for an existing block on the same subject and update it rather than add a duplicate. The datamate_* memory tools (datamate_add_memories, datamate_search_memory) are the engine's separate store and are not what linked checkouts read. Each block is a Markdown file persisted to disk. Max ${MEMORY_MAX_BLOCK_SIZE} chars per block, ${MEMORY_MAX_BLOCKS_PER_SCOPE} blocks per scope. Supports hierarchical IDs with slashes (e.g., 'warehouse/snowflake-config'), optional TTL expiration, and citation-backed memories.`, + description: `Save an Altimate Memory block for cross-session persistence. Use this to store information worth remembering across sessions — warehouse configurations, naming conventions, team preferences, data model notes, or past analysis decisions. When the project is linked to an Altimate Workspace with workspace sync on and workspace memory enabled, saved blocks sync to the workspace for this user only: they follow the user to their other linked checkouts, and teammates' sessions do not read them. For a convention the whole team should follow, suggest publishing it as a workspace skill (\`altimate-code skill publish \`). Before saving, call \`altimate_memory_read\` for an existing block on the same subject and update it rather than add a duplicate. The datamate_* memory tools (datamate_add_memories, datamate_search_memory) are the engine's separate store and are not what linked checkouts read. Each block is a Markdown file persisted to disk. Max ${MEMORY_MAX_BLOCK_SIZE} chars per block, ${MEMORY_MAX_BLOCKS_PER_SCOPE} blocks per scope. Supports hierarchical IDs with slashes (e.g., 'warehouse/snowflake-config'), optional TTL expiration, and citation-backed memories.`, parameters: z.object({ id: idSchema .describe( diff --git a/packages/opencode/test/altimate/workspace/contents.test.ts b/packages/opencode/test/altimate/workspace/contents.test.ts index 1bcb3ecd57..14f2302648 100644 --- a/packages/opencode/test/altimate/workspace/contents.test.ts +++ b/packages/opencode/test/altimate/workspace/contents.test.ts @@ -42,6 +42,9 @@ describe("workspace contents section", () => { expect(text).toContain("- dbt-style") expect(text).toContain("Integrations: github, snowflake.") expect(text).toContain("Workspace memory: on") + // Workspace memory is per-user, not shared with teammates (#1404). + expect(text).toContain("sync to each user's own checkouts, not to teammates") + expect(text).not.toContain("shared with the team") expect(text).toContain("answer from this section only") expect(text).toContain("built-in") expect(text).toContain("not instructions") diff --git a/packages/opencode/test/altimate/workspace/identity-section.test.ts b/packages/opencode/test/altimate/workspace/identity-section.test.ts index c4d9c0a7f0..31b0660657 100644 --- a/packages/opencode/test/altimate/workspace/identity-section.test.ts +++ b/packages/opencode/test/altimate/workspace/identity-section.test.ts @@ -142,7 +142,7 @@ describe("systemSection", () => { expect(out).not.toContain("What this Altimate Workspace provides") }) - test("a bound project gets the team-memory line only once the workspace's memory is confirmed on", async () => { + test("a bound project gets the workspace-memory line only once the workspace's memory is confirmed on", async () => { const { memoryEnabledCache, resetEnablementMemoForTests, noteMemoryDisabledForTests } = await import( "../../../src/altimate/workspace/memory-sync" ) @@ -156,18 +156,18 @@ describe("systemSection", () => { resetEnablementMemoForTests() memoryEnabledCache.set(77, { checkedAt: Date.now() }) try { - expect(await inProject(systemSection)).toContain("Team memory:") + expect(await inProject(systemSection)).toContain("Workspace memory: save decisions") resetOutcomeMemoForTests() resetEnablementMemoForTests() // A remembered "no" from the workspace switches the line off. noteMemoryDisabledForTests(77) - expect(await inProject(systemSection)).not.toContain("Team memory:") + expect(await inProject(systemSection)).not.toContain("Workspace memory: save decisions") expect(await inProject(systemSection)).toContain('is "Team"') // Not yet checked is not "on": the write path uploads only on a confirmed // yes, so the prompt must not promise a sync that would stay local. (bot review) resetOutcomeMemoForTests() resetEnablementMemoForTests() - expect(await inProject(systemSection)).not.toContain("Team memory:") + expect(await inProject(systemSection)).not.toContain("Workspace memory: save decisions") expect(await inProject(systemSection)).toContain('is "Team"') } finally { resetEnablementMemoForTests() @@ -196,10 +196,10 @@ describe("systemSection", () => { resetEnablementMemoForTests() resetPollMemoForTests() try { - expect(await inProject(systemSection)).not.toContain("Team memory:") + expect(await inProject(systemSection)).not.toContain("Workspace memory: save decisions") expect(await memoryEnabledForPoller(binding as never)).toBe("enabled") resetOutcomeMemoForTests() - expect(await inProject(systemSection)).toContain("Team memory:") + expect(await inProject(systemSection)).toContain("Workspace memory: save decisions") } finally { resetEnablementMemoForTests() resetPollMemoForTests() @@ -376,7 +376,7 @@ describe("systemSection", () => { expect(out).not.toContain("This project is linked to Altimate Workspace id 9") // A link that may have changed must not be followed by that workspace's contents as if current. expect(out).not.toContain("What this Altimate Workspace provides") - expect(out).not.toContain("Team memory:") + expect(out).not.toContain("Workspace memory: save decisions") } finally { resetEnablementMemoForTests() } diff --git a/packages/opencode/test/altimate/workspace/identity.test.ts b/packages/opencode/test/altimate/workspace/identity.test.ts index 68b66cbab8..11a3feb983 100644 --- a/packages/opencode/test/altimate/workspace/identity.test.ts +++ b/packages/opencode/test/altimate/workspace/identity.test.ts @@ -25,18 +25,21 @@ describe("bound — a specific Altimate Workspace is linked", () => { } const boundOut = render(boundOutcome) - test("with team memory on, the section names the team's store and the engine's hub as separate (#1332)", () => { + test("with workspace memory on, the section names the synced store as per-user and the engine's hub as separate (#1332, #1404)", () => { const out = render(boundOutcome, undefined, { teamMemory: true }) - expect(out).toContain("Team memory: save decisions and conventions with `altimate_memory_write`") - expect(out).toContain("sync to the workspace and to every linked checkout") + expect(out).toContain("Workspace memory: save decisions and conventions with `altimate_memory_write`") + expect(out).toContain("sync to the workspace for this user only") + expect(out).toContain("Teammates' sessions do not read them") + expect(out).toContain("`altimate-code skill publish `") + expect(out).not.toContain("Team memory") expect(out).toContain("`datamate_add_memories`") expect(out).toContain("are the engine's separate store") expect(out).toContain("check `altimate_memory_read` for an existing block") expect(out.split("\n")).toHaveLength(5) // Off (workspace memory disabled) and by default (pure formatter): no line. - expect(render(boundOutcome, undefined, { teamMemory: false })).not.toContain("Team memory") - expect(boundOut).not.toContain("Team memory") - expect(render({ status: "unbound" }, undefined, { teamMemory: true })).not.toContain("Team memory") + expect(render(boundOutcome, undefined, { teamMemory: false })).not.toContain("Workspace memory: save decisions") + expect(boundOut).not.toContain("Workspace memory: save decisions") + expect(render({ status: "unbound" }, undefined, { teamMemory: true })).not.toContain("Workspace memory: save decisions") }) test("names the workspace and forbids substituting another service's 'workspace' for an identity question", () => { diff --git a/packages/opencode/test/altimate/workspace/starter.test.ts b/packages/opencode/test/altimate/workspace/starter.test.ts index 52a2104d1b..4fd0dd6301 100644 --- a/packages/opencode/test/altimate/workspace/starter.test.ts +++ b/packages/opencode/test/altimate/workspace/starter.test.ts @@ -22,7 +22,7 @@ describe("renderStarter", () => { "- Skills (2): acme-incident-runbook, acme-staging-conventions", "- Knowledge (2): Data handbook, KPI glossary", "- Integrations: github, snowflake", - "- Memory: on. What you teach Altimate Code is saved automatically and shared with your team in the CLI, the IDE and Studio.", + "- Memory: on. What you teach Altimate Code is saved automatically and follows you in the CLI, the IDE and Studio. It's private to you: to share a convention with your team, publish it as a workspace skill.", "", "What do you want to do? For example:", " 1. Which of our workspace skills fit this project?", diff --git a/packages/opencode/test/memory/tools.test.ts b/packages/opencode/test/memory/tools.test.ts index ecf43e1495..c8bcd50d9b 100644 --- a/packages/opencode/test/memory/tools.test.ts +++ b/packages/opencode/test/memory/tools.test.ts @@ -703,3 +703,15 @@ describe("Global opt-out (ALTIMATE_DISABLE_MEMORY)", () => { expect(altTruthy("ALTIMATE_DISABLE_MEMORY", "OPENCODE_DISABLE_MEMORY")).toBe(true) }) }) + +describe("MemoryWriteTool description", () => { + test("says synced workspace memory is per-user, not shared with teammates (#1404)", async () => { + const { initTool } = await import("../altimate/tool-fixture") + const { MemoryWriteTool } = await import("../../src/memory/tools/memory-write") + const { description } = await initTool(MemoryWriteTool) + expect(description).toContain("saved blocks sync to the workspace for this user only") + expect(description).toContain("teammates' sessions do not read them") + expect(description).toContain("`altimate-code skill publish `") + expect(description).not.toContain("TEAM's memory") + }) +})