Skip to content
Merged
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: 4 additions & 0 deletions packages/opencode/src/altimate/workspace/memory-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export interface MirrorMetadata {
visibility: "private"
block_created: string
block_updated: string
/** Heading shown for the record in the workspace UI. The create's extractor
* writes its own, but the repair ``update`` replaces the metadata dict
* wholesale, so a synced block has no heading unless we supply one. */
title?: string

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit (Code Quality): title?: string is optional here, but buildMetadata always populates it. Worth a one-line addition to the doc comment explaining the optionality is for backward compatibility with records written before this field existed (and for archiveNow, which spreads possibly-pre-PR metadata).

/** Absent for global blocks — that is what makes them span workspaces. */
datamate_id?: string
datamate_name?: string
Expand Down
36 changes: 36 additions & 0 deletions packages/opencode/src/altimate/workspace/memory-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,41 @@ export function decodeTags(raw: unknown): string[] {
.filter(Boolean)
}

/** Longest heading we will mirror. A block may be up to MEMORY_MAX_BLOCK_SIZE,
* and its first line can be most of that. */
const TITLE_MAX = 120

/** Heading for a synced block.
*
* A create runs the extractor, which writes its own `title`, but the repair
* `update` replaces the metadata dict wholesale — so without this the record
* reaches the workspace with no heading at all. Blocks conventionally
* open with a markdown heading; the block id is a readable fallback for those
* that do not.
*/
export function blockTitle(block: MemoryBlock): string {
// A training block opens with its metadata comment; the heading is after it.
for (const line of stripTrainingMeta(block.content).split("\n")) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[WARNING]: Preserve indentation when stripping training metadata

stripTrainingMeta() ends with .trim(), so using it here removes all leading spaces from ordinary blocks before the CommonMark check. As a result, content such as # example (an indented code block, not an ATX heading) becomes # example and is published as the workspace title instead of falling back to the block ID. Strip only TRAINING_META_COMMENT for this scan, or otherwise avoid trimming indentation before matching, and add a four-space regression case.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor (Design): This only recognizes ATX (#) headings, not Setext (===/----underlined) headings. A block whose first line is Deploy Steps followed by a line of === is valid CommonMark but falls straight to the block.id fallback here. Low-impact (worst case is the id fallback, not a wrong title), but worth a one-line comment scoping this to ATX intentionally so a future reader doesn't assume all heading forms are handled.

// CommonMark: up to three leading spaces, and a closing run of hashes that
// is decoration rather than title text. The closing run must be preceded by
// whitespace, so a heading like `# C#` keeps its hash. (stripTrainingMeta
// trims, so deeper indentation on the first line is gone before we look —
// a usable title beats falling back to the id over leading whitespace.)
const heading = line.match(/^ {0,3}#{1,6}\s+(\S.*?)(?:\s+#+)?\s*$/)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor (Logic Error): For a line like # ### or ## ##, CommonMark treats the trailing hash run as closing decoration on an empty heading (canonical example: # ## renders as <h1></h1>). The mandatory (\S.*?) capture here instead consumes those hashes as literal heading text, producing a title of "###" or "##" rather than falling back to block.id. Rare content shape, but a real deviation from the CommonMark handling the rest of this function is careful about.

Suggestion: after matching, check whether the captured text is itself only hashes/whitespace and treat that as no heading.

if (heading) {
const text = heading[1]
// Count code points, not UTF-16 units: slicing mid-surrogate leaves a
// lone half that renders as a replacement character in the workspace.
const points = Array.from(text)
if (points.length <= TITLE_MAX) return text
return `${points.slice(0, TITLE_MAX - 1).join("")}\u2026`
}
// Content that opens with body text has no heading to borrow.
if (line.trim()) break
}
return block.id
}

export function buildMetadata(block: MemoryBlock, binding: CachedBinding | null): MirrorMetadata {
const meta: MirrorMetadata = {
source: MIRROR_SOURCE,
Expand All @@ -311,6 +346,7 @@ export function buildMetadata(block: MemoryBlock, binding: CachedBinding | null)
visibility: "private",
block_created: block.created,
block_updated: block.updated,
title: blockTitle(block),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor (Design): contentHash (line ~449, unchanged by this PR) hashes content + tags + expires, not title. So an index entry written before this PR still matches on re-save, and push() returns "unchanged" before this line — and therefore blockTitle() — ever runs, for any block whose content/tags/expiry haven't changed since it was last synced. This matches the PR's Scope note ("Existing records keep their missing heading").

Worth confirming with the author: the "separate backfill call" mentioned in Scope would hit the same wall if it means today's backfill() — it calls the same partitionPending()/contentHash gate, so a plain re-run would also skip these records rather than fixing them.

}
// JSON, not a comma join: a tag containing a comma split into two on read.
if (block.tags.length > 0) meta.block_tags = JSON.stringify(block.tags)
Expand Down
69 changes: 69 additions & 0 deletions packages/opencode/test/altimate/workspace/memory-sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const {
backfill,
belongsHere,
buildMetadata,
blockTitle,
hydrate,
isEnabled,
memoryEnabledCached,
Expand Down Expand Up @@ -332,6 +333,14 @@ describe("buildMetadata", () => {
expect(buildMetadata(block(), null).visibility).toBe("private")
})

test("a heading is always supplied, so the record is not title-less", () => {
// The create's extractor writes a title, but the repair update() replaces
// the metadata dict wholesale — without one here the record reaches the
// workspace with no heading.
const meta = buildMetadata(block({ content: "# Staging Model Convention\n\n- prefix stg_" }), null)
expect(meta.title).toBe("Staging Model Convention")
})

test("created and updated timestamps are carried", () => {
const meta = buildMetadata(block({ created: NOW, updated: NOW }), null)
expect(meta.block_created).toBe(NOW)
Expand Down Expand Up @@ -361,6 +370,66 @@ function gatedFetch() {
}
}

describe("blockTitle", () => {
test("uses the block's leading markdown heading", () => {
expect(blockTitle(block({ content: "# Warehouse access\n\nbody" }))).toBe("Warehouse access")
})

test("accepts any heading level and trims surrounding space", () => {
expect(blockTitle(block({ content: "### Deploy steps \nbody" }))).toBe("Deploy steps")
})

test("skips blank lines before the heading", () => {
expect(blockTitle(block({ content: "\n\n## Naming rules\nbody" }))).toBe("Naming rules")
})

test("falls back to the block id when the content opens with body text", () => {
// Borrowing a body line would produce a heading the user never wrote.
const b = block({ id: "warehouse/snowflake", content: "Snowflake account is acme-prod.\n\n# Later heading" })
expect(blockTitle(b)).toBe("warehouse/snowflake")
})

test("falls back to the block id for a hash with no heading text", () => {
expect(blockTitle(block({ id: "a/b", content: "#hashtag not a heading" }))).toBe("a/b")
})

test("reads past a training block's metadata comment to its heading", () => {
// Training blocks always open with this comment, so scanning raw content
// sees it as body text and falls back to the id.
const content = "<!-- training\nkind: rule\napplied: 3\n-->\n# Naming rules\n\nbody"
expect(blockTitle(block({ id: "t/1", content }))).toBe("Naming rules")
})

test("allows the three leading spaces CommonMark permits", () => {
expect(blockTitle(block({ content: " # Indented heading\nbody" }))).toBe("Indented heading")
})

test("drops a closing run of hashes", () => {
expect(blockTitle(block({ content: "## Release notes ##\nbody" }))).toBe("Release notes")
})

test("keeps a hash that is part of the heading text", () => {
// The closing run must be preceded by whitespace, so this is not one.
expect(blockTitle(block({ content: "# Style guide for C#\nbody" }))).toBe("Style guide for C#")
})

test("truncating never splits an emoji into a lone surrogate", () => {
// slice() counts UTF-16 units; cutting mid-pair renders as U+FFFD.
const content = `# ${"x".repeat(118)}\u{1F600}${"y".repeat(10)}`
const title = blockTitle(block({ content }))
expect(Array.from(title).length).toBe(120)
expect(title).toContain("\u{1F600}")
expect(title.isWellFormed()).toBe(true)
})

test("truncates a heading longer than the cap", () => {
const long = "x".repeat(200)
const title = blockTitle(block({ content: `# ${long}` }))
expect(title.length).toBe(120)
expect(title.endsWith("\u2026")).toBe(true)
})
})

// ── write path ──────────────────────────────────────────────────────────────
describe("mirrorBlock", () => {
test("flushPendingMirrors waits for a mirror a short-lived process would abandon (#1332)", async () => {
Expand Down
Loading