Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/opencode/src/altimate/workspace/contents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
19 changes: 12 additions & 7 deletions packages/opencode/src/altimate/workspace/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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 " +
Comment on lines 99 to +100

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🔵 LOW] Since the text has been updated from "Team memory" to "Workspace memory" to clarify that the records are private to the user and not shared with teammates, consider renaming the constant TEAM_MEMORY_LINE to WORKSPACE_MEMORY_LINE to better reflect its new semantic and avoid confusion. You might also consider renaming the teamMemory property in RenderOptions to workspaceMemory for consistency.

Suggested change:

Suggested change
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 " +
const WORKSPACE_MEMORY_LINE =
"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 <name>`). 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") {
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/altimate/workspace/starter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}

Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/memory/tools/memory-write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>\`). 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(
Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/test/altimate/workspace/contents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
}
Expand Down
15 changes: 9 additions & 6 deletions packages/opencode/test/altimate/workspace/identity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>`")
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", () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/test/altimate/workspace/starter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?",
Expand Down
12 changes: 12 additions & 0 deletions packages/opencode/test/memory/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>`")
expect(description).not.toContain("TEAM's memory")
})
})
Loading