From 0bc08ceb1188afacad16c4b51e839279ee2a3db4 Mon Sep 17 00:00:00 2001 From: Ryota Murakami Date: Mon, 26 Oct 2026 23:42:50 +0900 Subject: [PATCH 01/18] fix: Cycle 0056 pt1: two conditional edges between stacked nodes keep readable labels (task 1) --- extension/README.md | 2 +- extension/scripts/tests/publishWiki.test.mjs | 54 +++++++++++++++++++ extension/skills/codebook/SKILL.md | 2 +- .../skills/codebook/render-wiki-diagrams.mjs | 17 +++--- .../src/tests/bundledSkillTestRuns.test.ts | 4 +- 5 files changed, 67 insertions(+), 12 deletions(-) diff --git a/extension/README.md b/extension/README.md index 8861f399e..67bd50a2b 100644 --- a/extension/README.md +++ b/extension/README.md @@ -177,7 +177,7 @@ To write the Wiki elsewhere, set the folder in your settings (a path inside the - **Stopping it.** **Codebook: Stop LLM Wiki Run** stops this window's generation or update. In VS Code it stops Codebook's watch, not the chat (the status bar's **Stop watching** does the same): the chat may still publish, Codebook waits for it 55 minutes after the watch's end before it starts another generation or update of that Wiki, and a publish of that chat ends the wait. In Cursor it stops the agent. While the run is still starting (its checks, the confirmation, the claim of the Wiki), it says `Codebook is starting an LLM Wiki run in this window. Once it runs, its progress shows and Stop LLM Wiki Run stops it.`; **Open LLM Wiki in Browser** says the same then instead of offering a second generation. - **Output record.** Every generation and update end writes one line to Output › Codebook (**Codebook: Show LLM Wiki Output**): the run's kind, editor, start and end time, how it ended in Codebook's words, the agent's exit code and the output folder with your home folder shown as `~`. It never holds the agent's reply, its errors or a chat's error text. - **Buttons of an older notice.** **Generate anyway**, **Update anyway** and the offer's **Update LLM Wiki** wait only for the start whose notice showed them; pressed while another run of the window works, they are told that a run is under way at once and start nothing when it ends. -- **Names.** **Codebook: Open LLM Wiki in Browser** was titled **Open DeepWiki in Browser** before 0.2.45; its command id `codebook.openDeepWiki` is unchanged, so keybindings and scripts keep working. The bundled skill 1.0.49 names it in the reader's offline note. +- **Names.** **Codebook: Open LLM Wiki in Browser** was titled **Open DeepWiki in Browser** before 0.2.45; its command id `codebook.openDeepWiki` is unchanged, so keybindings and scripts keep working. The bundled skill 1.0.50 names it in the reader's offline note. - **Several windows.** Update every window of a project to this version before you generate: an earlier Codebook reads a generation's marker with an update's 15-minute bounds and may start an update beside a generation that still runs (the publisher still refuses a second publish while one runs). Stop a running generation before you go back to an earlier Codebook. ## LLM Wiki: open and update diff --git a/extension/scripts/tests/publishWiki.test.mjs b/extension/scripts/tests/publishWiki.test.mjs index 0ff28d425..0f59f7ea0 100644 --- a/extension/scripts/tests/publishWiki.test.mjs +++ b/extension/scripts/tests/publishWiki.test.mjs @@ -5261,6 +5261,60 @@ test('pending and success branches retain their own conditions and operations', } }) +// Value: protects=two conditional edges between the same two stacked nodes keep their labels readable in a published diagram; fails_when=the labels of a Show result edge and a Show error edge share one label band, so their backing boxes overlap; no_claim=how a browser paints the SVG or the room left of the first column +test('two branch edges from one node to the node below it draw label boxes that do not overlap', async () => { + // Arrange + const root = await realpath( + await mkdtemp(join(tmpdir(), 'codebook-diagram-label-boxes-')), + ) + try { + const manifest = await diagramInput(root), + diagram = manifest.pages[0].diagrams[0] + diagram.nodes[1].row = 1 + diagram.nodes[1].column = 0 + diagram.edges[0] = { + ...diagram.edges[0], + kind: 'branch', + label: 'Show the saved result', + condition: 'ready', + } + diagram.edges.push({ + ...diagram.edges[0], + id: 'failure', + label: 'Show the failure notice', + condition: 'denied', + }) + // Act + const html = ( + await renderWikiPages(manifest, { + projectRoot: root, + outputDirectory: join(root, '.codebook/wiki'), + }) + ).get('index.html') + const boxes = ['read', 'failure'].map((id) => { + const [, x, y, width, height] = edgeGroup(html, id).match( + / { // Arrange const root = await realpath( diff --git a/extension/skills/codebook/SKILL.md b/extension/skills/codebook/SKILL.md index ae704fe86..3cbb373f3 100644 --- a/extension/skills/codebook/SKILL.md +++ b/extension/skills/codebook/SKILL.md @@ -2,7 +2,7 @@ name: codebook description: Explain live code paths, save or replay learning, or generate and update a DeepWiki for the current editor workspace. metadata: - version: "1.0.49" + version: "1.0.50" --- # Codebook diff --git a/extension/skills/codebook/render-wiki-diagrams.mjs b/extension/skills/codebook/render-wiki-diagrams.mjs index 354d55611..02386a9ca 100644 --- a/extension/skills/codebook/render-wiki-diagrams.mjs +++ b/extension/skills/codebook/render-wiki-diagrams.mjs @@ -111,16 +111,17 @@ function edgeSvg(edge, prefix, path, labelX, labelY) { /** Renders grid positions and explicit connectors without inferring temporal order. @example flowSvg(diagram, 'diagram-auth') */ function flowSvg(diagram, prefix) { const nodeHeight = Math.max(...diagram.nodes.map(node => wrap(node.label, 20).length)) * LAYOUT.lineHeight + LAYOUT.nodePadding * 2; - const horizontalGroups = new Map(); + const labelGroups = new Map(); const definitions = new Map(diagram.nodes.map(node => [node.id, node])); for (const edge of diagram.edges) { const from = definitions.get(edge.from), to = definitions.get(edge.to); - if (from.id === to.id || from.row !== to.row) continue; + // Edges between one pair of nodes share a label band; stacking them keeps their label boxes apart, in a row and in a column. + if (from.id === to.id) continue; const key = [from.id, to.id].sort().join(':'); - const group = horizontalGroups.get(key) ?? []; - group.push(edge); horizontalGroups.set(key, group); + const group = labelGroups.get(key) ?? []; + group.push(edge); labelGroups.set(key, group); } - const groupedHeight = [...horizontalGroups.values()].map(group => group.reduce((height, edge) => height + edgeLines(edge).length * LAYOUT.lineHeight, (group.length - 1) * LAYOUT.labelGap)); + const groupedHeight = [...labelGroups.values()].map(group => group.reduce((height, edge) => height + edgeLines(edge).length * LAYOUT.lineHeight, (group.length - 1) * LAYOUT.labelGap)); const labelHeight = Math.max(LAYOUT.lineHeight, ...diagram.edges.map(edge => edgeLines(edge).length * LAYOUT.lineHeight), ...groupedHeight); const rowStride = nodeHeight + labelHeight + LAYOUT.rowGap, columnStride = LAYOUT.nodeWidth + LAYOUT.columnGap; const positions = new Map(diagram.nodes.map(node => [node.id, { x: LAYOUT.margin + node.column * columnStride, y: LAYOUT.margin + labelHeight + node.row * rowStride, height: nodeHeight }])); @@ -147,10 +148,10 @@ function flowSvg(diagram, prefix) { const midpointX = (startX + endX) / 2, midpointY = (startY + endY) / 2; const offsetX = horizontal ? 0 : lane, offsetY = horizontal ? lane : 0; const path = `M${startX} ${startY} C${midpointX + offsetX} ${startY + offsetY} ${midpointX + offsetX} ${endY + offsetY} ${endX} ${endY}`; - // Horizontal labels use the row's reserved band, clear of both nodes and their connector. - const group = horizontalGroups.get([edge.from, edge.to].sort().join(':')) ?? [edge]; + // Horizontal labels use the row's reserved band, clear of both nodes and their connector; vertical ones stack from the top of the gap between the two nodes. + const group = labelGroups.get([edge.from, edge.to].sort().join(':')) ?? [edge]; const priorHeight = group.slice(0, group.indexOf(edge)).reduce((height, previous) => height + edgeLines(previous).length * LAYOUT.lineHeight + LAYOUT.labelGap, 0); - const labelY = horizontal ? from.y - labelHeight + LAYOUT.fontSize + priorHeight : midpointY + offsetY - labelHeight / 2; + const labelY = horizontal ? from.y - labelHeight + LAYOUT.fontSize + priorHeight : midpointY + offsetY - labelHeight / 2 + priorHeight; return edgeSvg(edge, prefix, path, midpointX + offsetX, labelY); }).join(''); return { width, height, content: connectors + diagram.nodes.map(node => nodeSvg(node, positions.get(node.id))).join('') }; diff --git a/extension/src/tests/bundledSkillTestRuns.test.ts b/extension/src/tests/bundledSkillTestRuns.test.ts index 8de396171..f00326bd3 100644 --- a/extension/src/tests/bundledSkillTestRuns.test.ts +++ b/extension/src/tests/bundledSkillTestRuns.test.ts @@ -24,12 +24,12 @@ test('the bundled skill routes a question about a test, or about library behavio }) // Value: protects=the skill's version being the one the release names, in one place; fails_when=the skill text changes without its version moving, or the version moves without this pin; why_new=F86 (the version was pinned inside the routing test); seam=none; no_claim=the wording of the skill -test('the bundled skill declares version 1.0.49 in its metadata', async () => { +test('the bundled skill declares version 1.0.50 in its metadata', async () => { // Arrange / Act const skill = await readBundledSkill() // Assert - assert.ok(skill.includes('metadata:\n version: "1.0.49"\n')) + assert.ok(skill.includes('metadata:\n version: "1.0.50"\n')) }) // Value: protects=a test stop explained in chat, never through codebook_explain, which belongs to the browser profile; fails_when=the skill tells the chat to save every committed pause with codebook_explain, which refuses a test stop; why_new=1.0.45 said codebook_explain at every pause of any run (C49-7, AC9); seam=none; no_claim=the refusal codebook_explain gives a test stop (service tests) From 545de338d1545dc28311b0e76e82bc582a8b6e5a Mon Sep 17 00:00:00 2001 From: Ryota Murakami Date: Mon, 26 Oct 2026 23:44:52 +0900 Subject: [PATCH 02/18] fix: Cycle 0056 pt1: each Wiki run helper and sentence lives in its own utility file (task 3) --- extension/src/constants.ts | 7 +++++ extension/src/extension.ts | 17 ++++------- extension/src/tests/startWikiRun.test.ts | 2 +- extension/src/tests/wikiRunBoundMs.test.ts | 8 ++--- extension/src/tests/wikiRunRecord.test.ts | 3 +- extension/src/utils/agentExitOf.ts | 26 +++++++++++++++++ extension/src/utils/describeExistingWiki.ts | 9 ++++++ extension/src/utils/describeMissingWiki.ts | 9 ++++++ extension/src/utils/longestWikiRunMs.ts | 23 +++++++++++++++ .../src/utils/readLiveWikiUpdateMarker.ts | 8 ++--- extension/src/utils/readLlmWikiViewFacts.ts | 2 +- extension/src/utils/startWikiRun.ts | 10 ------- extension/src/utils/wikiRunBoundMs.ts | 29 ------------------- extension/src/utils/wikiRunRecord.ts | 28 +----------------- 14 files changed, 91 insertions(+), 90 deletions(-) create mode 100644 extension/src/utils/agentExitOf.ts create mode 100644 extension/src/utils/describeExistingWiki.ts create mode 100644 extension/src/utils/describeMissingWiki.ts create mode 100644 extension/src/utils/longestWikiRunMs.ts diff --git a/extension/src/constants.ts b/extension/src/constants.ts index c88f1277c..4997574c6 100644 --- a/extension/src/constants.ts +++ b/extension/src/constants.ts @@ -931,6 +931,13 @@ export const WIKI_RUN_STARTING_SENTENCE = export const WIKI_UPDATE_WATCH_TIMEOUT_MS = 900_000 /** Longest a Wiki generation runs in either host, the VS Code watch and the Cursor agent alike, before it ends as timed out: 55 minutes, set by Cycle 0055's E1 (the longest measured generation, 34 min 33 s in VS Code, × 1.5, rounded up to whole 5 minutes; Owner decision O9). A stopped VS Code generation's reservation lasts as long after its watch's end. */ export const WIKI_GENERATE_TIMEOUT_MS = 3_300_000 +/** The longest any run of any kind and host can last ({@link longestWikiRunMs}): a VS Code generation's chat opening plus its bound. A marker file whose kind cannot be read counts this long, never the update's shorter bound. */ +export const LONGEST_WIKI_RUN_MS = Math.max( + WIKI_GENERATE_TIMEOUT_MS, + WIKI_CHAT_OPEN_TIMEOUT_MS + WIKI_GENERATE_TIMEOUT_MS, + WIKI_CURSOR_UPDATE_TIMEOUT_MS, + WIKI_CHAT_OPEN_TIMEOUT_MS + WIKI_UPDATE_WATCH_TIMEOUT_MS, +) /** The chat request a learner can paste to generate the LLM Wiki without Codebook's entry; every generate sentence that sends the learner to the chat names it. */ export const WIKI_GENERATE_CHAT_REQUEST = '/codebook Generate the LLM Wiki for this project' diff --git a/extension/src/extension.ts b/extension/src/extension.ts index dc3d8f604..f3c6f108b 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -72,6 +72,7 @@ import { repositoryActionSchema } from './repositoryView' import { startServer } from './server' import { StudyService } from './service' import { StudyWorkspace, type UnsavedStudyArchive } from './studyWorkspace' +import { agentExitOf, type WikiAgentExit } from './utils/agentExitOf' import { aiChatReadyMessage } from './utils/aiChatReadyMessage' import { answerDeepWikiQuestion, @@ -96,6 +97,8 @@ import { import { cursorRemovalNotice } from './utils/cursorRemovalNotice' import { deepWikiOpenFailureNotice } from './utils/deepWikiOpenFailureNotice' import { describeBrokenWikiWatch } from './utils/describeBrokenWikiWatch' +import { describeExistingWiki } from './utils/describeExistingWiki' +import { describeMissingWiki } from './utils/describeMissingWiki' import { describeOtherWikiWindow } from './utils/describeOtherWikiWindow' import { describeSecondWikiUpdateStart, @@ -158,11 +161,7 @@ import { startWikiChatUpdate, type WikiChatStart, } from './utils/startWikiChatUpdate' -import { - describeMissingWiki, - startWikiGenerate, - type WikiRunInTheWay, -} from './utils/startWikiRun' +import { startWikiGenerate, type WikiRunInTheWay } from './utils/startWikiRun' import { thrownMessage } from './utils/thrownMessage' import { unreadWikiUpdateEnd } from './utils/unreadWikiUpdateEnd' import { waitForWikiPublish } from './utils/waitForWikiPublish' @@ -171,11 +170,7 @@ import { wikiGenerateNotice } from './utils/wikiGenerateNotice' import { wikiManifestIdentity } from './utils/wikiManifestIdentity' import { wikiOutputLabel } from './utils/wikiOutputLabel' import { wikiRunBoundMs } from './utils/wikiRunBoundMs' -import { - agentExitOf, - wikiRunRecord, - type WikiAgentExit, -} from './utils/wikiRunRecord' +import { wikiRunRecord } from './utils/wikiRunRecord' import { OPEN_WIKI_ACTION, wikiUpdateNotice } from './utils/wikiUpdateNotice' import { WikiUpdateRun, @@ -2436,7 +2431,7 @@ export function activate( const offeringAttempt = wikiUpdate void showNotice( 'information', - `An LLM Wiki already exists in ${wikiOutput.outputLabel}. Update it instead?`, + describeExistingWiki(wikiOutput.outputLabel), UPDATE_WIKI_ACTION, ).then( (choice) => { diff --git a/extension/src/tests/startWikiRun.test.ts b/extension/src/tests/startWikiRun.test.ts index 9dba061f4..a2f7cfac6 100644 --- a/extension/src/tests/startWikiRun.test.ts +++ b/extension/src/tests/startWikiRun.test.ts @@ -1,8 +1,8 @@ import assert from 'node:assert/strict' import { test } from 'node:test' +import { describeMissingWiki } from '../utils/describeMissingWiki' import { - describeMissingWiki, startWikiGenerate, type WikiGenerateStartDependencies, } from '../utils/startWikiRun' diff --git a/extension/src/tests/wikiRunBoundMs.test.ts b/extension/src/tests/wikiRunBoundMs.test.ts index 9853f7f2e..fbc71a627 100644 --- a/extension/src/tests/wikiRunBoundMs.test.ts +++ b/extension/src/tests/wikiRunBoundMs.test.ts @@ -1,11 +1,9 @@ import assert from 'node:assert/strict' import { test } from 'node:test' -import { - LONGEST_WIKI_RUN_MS, - longestWikiRunMs, - wikiRunBoundMs, -} from '../utils/wikiRunBoundMs' +import { LONGEST_WIKI_RUN_MS } from '../constants' +import { longestWikiRunMs } from '../utils/longestWikiRunMs' +import { wikiRunBoundMs } from '../utils/wikiRunBoundMs' // Value: protects=Eng decision 74, every place that reads a run's bound; fails_when=a generation gets an update's 15 minutes in either host, an update's bounds change, or a marker of an earlier version (no kind) is read with a generation's bound; no_claim=where each place reads it (marker, leftover and host tests); seam=none test('a generation is bounded at 55 minutes in both hosts, while an update keeps 15 minutes in each and a run without a kind is an update', () => { diff --git a/extension/src/tests/wikiRunRecord.test.ts b/extension/src/tests/wikiRunRecord.test.ts index df87a223f..9ad8840f4 100644 --- a/extension/src/tests/wikiRunRecord.test.ts +++ b/extension/src/tests/wikiRunRecord.test.ts @@ -1,7 +1,8 @@ import assert from 'node:assert/strict' import { test } from 'node:test' -import { agentExitOf, wikiRunRecord } from '../utils/wikiRunRecord' +import { agentExitOf } from '../utils/agentExitOf' +import { wikiRunRecord } from '../utils/wikiRunRecord' // Value: protects=AC 12 and Eng decision 81, the Output record a learner copies into an issue; fails_when=the record holds the failure's own text (an agent's reply, a chat's error reply, a thrown error), names no exit code, or shows the home folder in full; no_claim=the notice, which shows the sentence; seam=a seeded fake secret in the reason test('the record of a failed Cursor generation names its kind, host, times, end, a Codebook summary, the exit code and the folder under ~, and none of the failure’s text', () => { diff --git a/extension/src/utils/agentExitOf.ts b/extension/src/utils/agentExitOf.ts new file mode 100644 index 000000000..cf858f41a --- /dev/null +++ b/extension/src/utils/agentExitOf.ts @@ -0,0 +1,26 @@ +/** What an agent process's exit said, as data: the Cursor agent's exit code or signal ({@link CursorAgentExitError}); absent for a VS Code chat and for a run that ended otherwise. */ +export type WikiAgentExit = { + exitCode: number | null + exitSignal: string | null +} + +/** + * Reads the exit code or signal off a failure of the Cursor agent's process ({@link CursorAgentExitError}), by its fields, so a stand-in of the runner in a test is read as the runner is. + * @param error - What the Cursor agent's run threw. + * @returns The exit code and signal where the failure carries them; `undefined` for any other failure. + * @example + * agentExitOf(Object.assign(new Error('…'), { exitCode: 1, exitSignal: null })) // => { exitCode: 1, exitSignal: null } + * agentExitOf(new Error('The Wiki update was cancelled.')) // => undefined + */ +export function agentExitOf(error: unknown): WikiAgentExit | undefined { + if (typeof error !== 'object' || error === null) return undefined + const exitCode: unknown = Reflect.get(error, 'exitCode') + const exitSignal: unknown = Reflect.get(error, 'exitSignal') + // Only a failure that carries the process's own answer has one; a cancel, a bound or a start failure carries none. + if (typeof exitCode !== 'number' && typeof exitSignal !== 'string') + return undefined + return { + exitCode: typeof exitCode === 'number' ? exitCode : null, + exitSignal: typeof exitSignal === 'string' ? exitSignal : null, + } +} diff --git a/extension/src/utils/describeExistingWiki.ts b/extension/src/utils/describeExistingWiki.ts new file mode 100644 index 000000000..f321495c8 --- /dev/null +++ b/extension/src/utils/describeExistingWiki.ts @@ -0,0 +1,9 @@ +/** + * Words that a folder already holds an LLM Wiki and offers its update, for the notice a generation shows instead of overwriting it (Cycle 0055, D2). + * @param outputLabel - The Wiki folder as the learner reads it. + * @returns The sentence that names the folder and asks for the update. + * @example describeExistingWiki('.codebook/wiki') // => 'An LLM Wiki already exists in .codebook/wiki. Update it instead?' + */ +export function describeExistingWiki(outputLabel: string): string { + return `An LLM Wiki already exists in ${outputLabel}. Update it instead?` +} diff --git a/extension/src/utils/describeMissingWiki.ts b/extension/src/utils/describeMissingWiki.ts new file mode 100644 index 000000000..41c8474f8 --- /dev/null +++ b/extension/src/utils/describeMissingWiki.ts @@ -0,0 +1,9 @@ +/** + * Words that a folder holds no LLM Wiki, for the notices of Open LLM Wiki in Browser and Update LLM Wiki, which offer Generate LLM Wiki beside it (Cycle 0055, proposed change 3). + * @param outputLabel - The Wiki folder as the learner reads it. + * @returns The sentence that names the folder. + * @example describeMissingWiki('.codebook/wiki') // => 'Codebook found no LLM Wiki in .codebook/wiki.' + */ +export function describeMissingWiki(outputLabel: string): string { + return `Codebook found no LLM Wiki in ${outputLabel}.` +} diff --git a/extension/src/utils/longestWikiRunMs.ts b/extension/src/utils/longestWikiRunMs.ts new file mode 100644 index 000000000..0229296b3 --- /dev/null +++ b/extension/src/utils/longestWikiRunMs.ts @@ -0,0 +1,23 @@ +import { WIKI_CHAT_OPEN_TIMEOUT_MS } from '../constants' + +import { wikiRunBoundMs } from './wikiRunBoundMs' +import type { WikiRunKind, WikiUpdateHost } from './wikiUpdateRun' + +/** + * Names the longest a run of one kind and host can last from its marker's start to its end; called by the marker reader for a run's marker that may have expired. + * @param runKind - The run's kind; `undefined` reads as an update. + * @param host - The editor host that runs it. + * @returns The Cursor agent's bound, or in VS Code the chat's opening ({@link WIKI_CHAT_OPEN_TIMEOUT_MS}) and the watch's bound. + * @example + * longestWikiRunMs('generate', 'vscode') // => 3_330_000 + * longestWikiRunMs('update', 'cursor') // => 900_000 + */ +export function longestWikiRunMs( + runKind: WikiRunKind | undefined, + host: WikiUpdateHost, +): number { + // A VS Code run first waits for the chat to open, then watches for the publish. + return host === 'cursor' + ? wikiRunBoundMs(runKind, host) + : WIKI_CHAT_OPEN_TIMEOUT_MS + wikiRunBoundMs(runKind, host) +} diff --git a/extension/src/utils/readLiveWikiUpdateMarker.ts b/extension/src/utils/readLiveWikiUpdateMarker.ts index 96f014c7a..03c8a4aef 100644 --- a/extension/src/utils/readLiveWikiUpdateMarker.ts +++ b/extension/src/utils/readLiveWikiUpdateMarker.ts @@ -3,6 +3,7 @@ import { lstat } from 'node:fs/promises' import { z } from 'zod' import { + LONGEST_WIKI_RUN_MS, WIKI_METADATA_FILE_LIMIT_BYTES, WIKI_UPDATE_MARKER_CLOCK_TOLERANCE_MS, WIKI_UPDATE_MARKER_EXPIRY_GRACE_MS, @@ -14,14 +15,11 @@ import { canJudgeOwnerHere } from './canJudgeOwnerHere' import { currentStorageOwner } from './currentStorageOwner' import { isProcessGone } from './isProcessGone' import { isRecordedOwnerGone } from './isRecordedOwnerGone' +import { longestWikiRunMs } from './longestWikiRunMs' import { readProcessStart, type ProcessStartReader } from './readProcessStart' import type { StorageOwnerIdentity } from './readRecordedOwner' import { readWikiRegularFile } from './readWikiRegularFile' -import { - LONGEST_WIKI_RUN_MS, - longestWikiRunMs, - wikiRunBoundMs, -} from './wikiRunBoundMs' +import { wikiRunBoundMs } from './wikiRunBoundMs' /** What a running Wiki update writes beside the output folder: who runs it and since when. */ const wikiUpdateMarkerSchema = z.object({ diff --git a/extension/src/utils/readLlmWikiViewFacts.ts b/extension/src/utils/readLlmWikiViewFacts.ts index 37e66004e..63a722000 100644 --- a/extension/src/utils/readLlmWikiViewFacts.ts +++ b/extension/src/utils/readLlmWikiViewFacts.ts @@ -2,6 +2,7 @@ import { basename } from 'node:path' import { WIKI_UPDATE_RECOVERY_REQUEST } from '../constants' +import type { WikiAgentExit } from './agentExitOf' import { describeWikiGenerateBlock } from './describeWikiGenerateBlock' import type { WikiUpdateStartBlock } from './findWikiUpdateStartBlock' import type { AwaitedWikiChat } from './hasWikiPublishEndedWait' @@ -16,7 +17,6 @@ import type { import { wikiGenerateNotice } from './wikiGenerateNotice' import { wikiOutputLabel } from './wikiOutputLabel' import { wikiRunBoundMs } from './wikiRunBoundMs' -import type { WikiAgentExit } from './wikiRunRecord' import { wikiUpdateNotice } from './wikiUpdateNotice' import type { WikiUpdateHost, WikiUpdateStatus } from './wikiUpdateRun' diff --git a/extension/src/utils/startWikiRun.ts b/extension/src/utils/startWikiRun.ts index d5cde4b51..6ffdac2d4 100644 --- a/extension/src/utils/startWikiRun.ts +++ b/extension/src/utils/startWikiRun.ts @@ -3,16 +3,6 @@ import { describeWikiGenerateBlock } from './describeWikiGenerateBlock' import type { WikiUpdateStartBlock } from './findWikiUpdateStartBlock' import type { WikiUpdateOutput } from './wikiUpdateRun' -/** - * Words that a folder holds no LLM Wiki, for the notices of Open LLM Wiki in Browser and Update LLM Wiki, which offer Generate LLM Wiki beside it (Cycle 0055, proposed change 3). - * @param outputLabel - The Wiki folder as the learner reads it. - * @returns The sentence that names the folder. - * @example describeMissingWiki('.codebook/wiki') // => 'Codebook found no LLM Wiki in .codebook/wiki.' - */ -export function describeMissingWiki(outputLabel: string): string { - return `Codebook found no LLM Wiki in ${outputLabel}.` -} - /** Another window's running generation or update, a stopped VS Code watch's reservation or an earlier chat's leftover in the way of a new run: the sentence of the refusal and, where the learner may go on anyway, what the override starts. */ export type WikiRunInTheWay = { reason: string; override?: () => void } diff --git a/extension/src/utils/wikiRunBoundMs.ts b/extension/src/utils/wikiRunBoundMs.ts index 707597edd..2848b8ba6 100644 --- a/extension/src/utils/wikiRunBoundMs.ts +++ b/extension/src/utils/wikiRunBoundMs.ts @@ -1,5 +1,4 @@ import { - WIKI_CHAT_OPEN_TIMEOUT_MS, WIKI_CURSOR_UPDATE_TIMEOUT_MS, WIKI_GENERATE_TIMEOUT_MS, WIKI_UPDATE_WATCH_TIMEOUT_MS, @@ -37,31 +36,3 @@ export function wikiRunBoundMs( if (runKind === 'generate') return bounds.generateMs return host === 'cursor' ? bounds.cursorUpdateMs : bounds.watchedUpdateMs } - -/** - * Names the longest a run of one kind and host can last from its marker's start to its end; called by the marker reader for a run's marker that may have expired. - * @param runKind - The run's kind; `undefined` reads as an update. - * @param host - The editor host that runs it. - * @returns The Cursor agent's bound, or in VS Code the chat's opening ({@link WIKI_CHAT_OPEN_TIMEOUT_MS}) and the watch's bound. - * @example - * longestWikiRunMs('generate', 'vscode') // => 3_330_000 - * longestWikiRunMs('update', 'cursor') // => 900_000 - */ -export function longestWikiRunMs( - runKind: WikiRunKind | undefined, - host: WikiUpdateHost, -): number { - // A VS Code run first waits for the chat to open, then watches for the publish. - return host === 'cursor' - ? wikiRunBoundMs(runKind, host) - : WIKI_CHAT_OPEN_TIMEOUT_MS + wikiRunBoundMs(runKind, host) -} - -/** The longest any run of any kind and host can last ({@link longestWikiRunMs}); a marker file whose kind cannot be read counts this long, never the update's shorter bound. */ -export const LONGEST_WIKI_RUN_MS = Math.max( - ...(['generate', 'update'] as const).flatMap((runKind) => - (['vscode', 'cursor'] as const).map((host) => - longestWikiRunMs(runKind, host), - ), - ), -) diff --git a/extension/src/utils/wikiRunRecord.ts b/extension/src/utils/wikiRunRecord.ts index 175f89992..6754a95d2 100644 --- a/extension/src/utils/wikiRunRecord.ts +++ b/extension/src/utils/wikiRunRecord.ts @@ -6,35 +6,9 @@ import { DELETE_CHARACTER_CODE, } from '../constants' +import type { WikiAgentExit } from './agentExitOf' import type { WikiRunKind, WikiUpdateStatus } from './wikiUpdateRun' -/** What an agent process's exit said, as data: the Cursor agent's exit code or signal ({@link CursorAgentExitError}); absent for a VS Code chat and for a run that ended otherwise. */ -export type WikiAgentExit = { - exitCode: number | null - exitSignal: string | null -} - -/** - * Reads the exit code or signal off a failure of the Cursor agent's process ({@link CursorAgentExitError}), by its fields, so a stand-in of the runner in a test is read as the runner is. - * @param error - What the Cursor agent's run threw. - * @returns The exit code and signal where the failure carries them; `undefined` for any other failure. - * @example - * agentExitOf(Object.assign(new Error('…'), { exitCode: 1, exitSignal: null })) // => { exitCode: 1, exitSignal: null } - * agentExitOf(new Error('The Wiki update was cancelled.')) // => undefined - */ -export function agentExitOf(error: unknown): WikiAgentExit | undefined { - if (typeof error !== 'object' || error === null) return undefined - const exitCode: unknown = Reflect.get(error, 'exitCode') - const exitSignal: unknown = Reflect.get(error, 'exitSignal') - // Only a failure that carries the process's own answer has one; a cancel, a bound or a start failure carries none. - if (typeof exitCode !== 'number' && typeof exitSignal !== 'string') - return undefined - return { - exitCode: typeof exitCode === 'number' ? exitCode : null, - exitSignal: typeof exitSignal === 'string' ? exitSignal : null, - } -} - /** One ended run as its record takes it: the state the run ended in; its start and the Cursor agent's exit come beside it in {@link wikiRunRecord}'s `details`. */ type EndedWikiRun = Exclude From d0d9117ce5576097fb170899cf1bb5c717fe58d8 Mon Sep 17 00:00:00 2001 From: Ryota Murakami Date: Mon, 26 Oct 2026 23:49:38 +0900 Subject: [PATCH 03/18] fix: Cycle 0056 pt1: a stop uses one verb per host, a block refusal shows no raw cause id, and the record keeps the id and its missing rows (tasks 7-10) --- extension/README.md | 4 +- extension/src/constants.ts | 2 +- extension/src/extension.ts | 8 ++-- .../tests/describeWikiGenerateBlock.test.ts | 4 +- extension/src/tests/startWikiRun.test.ts | 27 +++++++++++ .../src/tests/wikiGenerateNotice.test.ts | 26 +++++++++- extension/src/tests/wikiRunRecord.test.ts | 48 +++++++++++++++++-- .../src/utils/describeWikiGenerateBlock.ts | 3 +- extension/src/utils/describeWikiUpdate.ts | 6 +-- extension/src/utils/startWikiRun.ts | 7 ++- extension/src/utils/wikiGenerateNotice.ts | 6 ++- extension/src/utils/wikiRunRecord.ts | 10 +++- 12 files changed, 130 insertions(+), 21 deletions(-) diff --git a/extension/README.md b/extension/README.md index 67bd50a2b..daf8b3225 100644 --- a/extension/README.md +++ b/extension/README.md @@ -175,7 +175,7 @@ To write the Wiki elsewhere, set the folder in your settings (a path inside the - **Existing Wiki.** Generate never overwrites a Wiki: in a folder that holds a manifest it says `An LLM Wiki already exists in . Update it instead?` with **Update LLM Wiki**, also when a Wiki was published between its first look and its start. To generate again from scratch, ask the chat `/codebook Generate the LLM Wiki for this project`; the publisher keeps the earlier Wiki beside the new one. - **Its bound.** A generation runs up to 55 minutes in both editors, the longest measured first generation times 1.5; the time counts while the computer sleeps. A generation that publishes counts as generated even when sources changed meanwhile: the notice then says `LLM Wiki generated in . source files changed while it was written.` and offers **Update LLM Wiki** first. - **Stopping it.** **Codebook: Stop LLM Wiki Run** stops this window's generation or update. In VS Code it stops Codebook's watch, not the chat (the status bar's **Stop watching** does the same): the chat may still publish, Codebook waits for it 55 minutes after the watch's end before it starts another generation or update of that Wiki, and a publish of that chat ends the wait. In Cursor it stops the agent. While the run is still starting (its checks, the confirmation, the claim of the Wiki), it says `Codebook is starting an LLM Wiki run in this window. Once it runs, its progress shows and Stop LLM Wiki Run stops it.`; **Open LLM Wiki in Browser** says the same then instead of offering a second generation. -- **Output record.** Every generation and update end writes one line to Output › Codebook (**Codebook: Show LLM Wiki Output**): the run's kind, editor, start and end time, how it ended in Codebook's words, the agent's exit code and the output folder with your home folder shown as `~`. It never holds the agent's reply, its errors or a chat's error text. +- **Output record.** Every generation and update end writes one line to Output › Codebook (**Codebook: Show LLM Wiki Output**): the run's kind, editor, start and end time, how it ended in Codebook's words, the agent's exit code, the cause id of a block that refused a generation's start (the notice names the file in the way and leaves the id out) and the output folder with your home folder shown as `~`. It never holds the agent's reply, its errors or a chat's error text. - **Buttons of an older notice.** **Generate anyway**, **Update anyway** and the offer's **Update LLM Wiki** wait only for the start whose notice showed them; pressed while another run of the window works, they are told that a run is under way at once and start nothing when it ends. - **Names.** **Codebook: Open LLM Wiki in Browser** was titled **Open DeepWiki in Browser** before 0.2.45; its command id `codebook.openDeepWiki` is unchanged, so keybindings and scripts keep working. The bundled skill 1.0.50 names it in the reader's offline note. - **Several windows.** Update every window of a project to this version before you generate: an earlier Codebook reads a generation's marker with an update's 15-minute bounds and may start an update beside a generation that still runs (the publisher still refuses a second publish while one runs). Stop a running generation before you go back to an earlier Codebook. @@ -269,7 +269,7 @@ Each entry is a sentence Codebook shows when a generation ends or does not start #### Generation stopped -`Codebook stopped watching the LLM Wiki generation at