@kenkaiiii/ggcoder 5.26.0 → 5.26.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/app-sidecar.js +71 -52
  2. package/dist/app-sidecar.js.map +1 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +0 -3
  5. package/dist/cli.js.map +1 -1
  6. package/dist/config.d.ts +0 -2
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +0 -3
  9. package/dist/config.js.map +1 -1
  10. package/dist/config.test.js +0 -12
  11. package/dist/config.test.js.map +1 -1
  12. package/dist/core/agent-session-queue.test.js +65 -0
  13. package/dist/core/agent-session-queue.test.js.map +1 -1
  14. package/dist/core/agent-session.d.ts +11 -13
  15. package/dist/core/agent-session.d.ts.map +1 -1
  16. package/dist/core/agent-session.js +34 -64
  17. package/dist/core/agent-session.js.map +1 -1
  18. package/dist/core/event-bus.d.ts +7 -0
  19. package/dist/core/event-bus.d.ts.map +1 -1
  20. package/dist/core/event-bus.js.map +1 -1
  21. package/dist/core/run-claim.d.ts +37 -0
  22. package/dist/core/run-claim.d.ts.map +1 -0
  23. package/dist/core/run-claim.js +46 -0
  24. package/dist/core/run-claim.js.map +1 -0
  25. package/dist/core/run-claim.test.d.ts +2 -0
  26. package/dist/core/run-claim.test.d.ts.map +1 -0
  27. package/dist/core/run-claim.test.js +107 -0
  28. package/dist/core/run-claim.test.js.map +1 -0
  29. package/dist/core/settings-manager.d.ts +0 -1
  30. package/dist/core/settings-manager.d.ts.map +1 -1
  31. package/dist/core/settings-manager.js +0 -7
  32. package/dist/core/settings-manager.js.map +1 -1
  33. package/dist/ui/App.d.ts +0 -2
  34. package/dist/ui/App.d.ts.map +1 -1
  35. package/dist/ui/App.js +1 -35
  36. package/dist/ui/App.js.map +1 -1
  37. package/dist/ui/render.d.ts +0 -2
  38. package/dist/ui/render.d.ts.map +1 -1
  39. package/dist/ui/render.js +0 -1
  40. package/dist/ui/render.js.map +1 -1
  41. package/package.json +4 -4
  42. package/dist/core/memory/compaction-notes.d.ts +0 -31
  43. package/dist/core/memory/compaction-notes.d.ts.map +0 -1
  44. package/dist/core/memory/compaction-notes.js +0 -162
  45. package/dist/core/memory/compaction-notes.js.map +0 -1
  46. package/dist/core/memory/compaction-notes.test.d.ts +0 -2
  47. package/dist/core/memory/compaction-notes.test.d.ts.map +0 -1
  48. package/dist/core/memory/compaction-notes.test.js +0 -110
  49. package/dist/core/memory/compaction-notes.test.js.map +0 -1
  50. package/dist/core/memory/journal.d.ts +0 -55
  51. package/dist/core/memory/journal.d.ts.map +0 -1
  52. package/dist/core/memory/journal.js +0 -212
  53. package/dist/core/memory/journal.js.map +0 -1
  54. package/dist/core/memory/journal.test.d.ts +0 -2
  55. package/dist/core/memory/journal.test.d.ts.map +0 -1
  56. package/dist/core/memory/journal.test.js +0 -184
  57. package/dist/core/memory/journal.test.js.map +0 -1
@@ -1,31 +0,0 @@
1
- /**
2
- * Turn the detail compaction is about to throw away into durable journal
3
- * entries.
4
- *
5
- * Compaction is one-way: the summary replaces the messages and the specifics
6
- * are gone for good. That is the actual hole the journal exists to fill — an
7
- * entry written here survives the summary, the session and the next restart.
8
- *
9
- * **Every entry must be a past-tense record of something that already
10
- * happened.** Bench G showed that an injected note suppresses verification
11
- * entirely (5/5 checks → 0/5), so an entry that can later become false will be
12
- * asserted with full confidence and never re-checked. History cannot become
13
- * false, which is what makes this writer safe while a free-form "save a fact"
14
- * tool was not. Do not add a producer here that records current state.
15
- *
16
- * Deliberately conservative: at most a handful of bounded entries per
17
- * compaction. A journal that floods itself is one nobody can afford to read.
18
- */
19
- import type { Message } from "@kenkaiiii/gg-ai";
20
- /** Max notes written per compaction. A flooded log is an unreadable log. */
21
- export declare const MAX_NOTES_PER_COMPACTION = 3;
22
- export interface CompactionNote {
23
- text: string;
24
- tag: string;
25
- }
26
- /**
27
- * Build the notes to persist for one compaction. `dropped` are the messages
28
- * being replaced; `summary` is the text replacing them.
29
- */
30
- export declare function buildCompactionNotes(dropped: readonly Message[], summary: string): CompactionNote[];
31
- //# sourceMappingURL=compaction-notes.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"compaction-notes.d.ts","sourceRoot":"","sources":["../../../src/core/memory/compaction-notes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAe,MAAM,kBAAkB,CAAC;AAK7D,4EAA4E;AAC5E,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAM1C,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAoID;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,SAAS,OAAO,EAAE,EAC3B,OAAO,EAAE,MAAM,GACd,cAAc,EAAE,CA0BlB"}
@@ -1,162 +0,0 @@
1
- // Single source of truth for entry truncation — both writers must cut at the
2
- // same boundaries, or an entry's rendering would depend on which path made it.
3
- import { clampEntryText } from "./journal.js";
4
- /** Max notes written per compaction. A flooded log is an unreadable log. */
5
- export const MAX_NOTES_PER_COMPACTION = 3;
6
- /** Max file paths named in the files note. */
7
- const MAX_FILES_LISTED = 8;
8
- /** Max chars of the summary carried into a note. */
9
- const SUMMARY_NOTE_CHARS = 360;
10
- /**
11
- * Sections of the compactor's summary worth keeping in a durable journal.
12
- *
13
- * The compactor emits seven labelled sections. Most must NOT be journalled:
14
- *
15
- * - `Primary Request and Intent` / `User Messages` — already captured verbatim,
16
- * and far more cheaply, by the `request` entry. Live measurement (bench H)
17
- * showed 64% of the request entry's words repeated inside the summary.
18
- * - `Files Touched` — already captured exactly by the `files` entry.
19
- * - `Current Work` / `Next Step` — in-flight state. True at the moment of
20
- * compaction, false minutes later, and bench G proved a stale entry is
21
- * asserted with full confidence and never re-checked. Journalling these
22
- * would reintroduce exactly the failure this design exists to avoid.
23
- *
24
- * That leaves the two sections that are pure, permanent history — what was
25
- * actually done, and what went wrong and how it was fixed. Those are also the
26
- * only parts a future session cannot cheaply re-derive by reading the code.
27
- */
28
- const DURABLE_SUMMARY_SECTIONS = ["What Was Done", "Errors and Fixes"];
29
- const SECTION_HEADING = /^#{1,6}\s*(.+?)\s*$/;
30
- /** Split a compactor summary into `heading -> body` pairs. */
31
- function splitSections(summary) {
32
- const sections = new Map();
33
- let heading = null;
34
- let body = [];
35
- const flush = () => {
36
- if (heading)
37
- sections.set(heading.toLowerCase(), body.join(" ").trim());
38
- };
39
- for (const line of summary.split("\n")) {
40
- const match = SECTION_HEADING.exec(line);
41
- if (match && line.trimStart().startsWith("#")) {
42
- flush();
43
- heading = match[1];
44
- body = [];
45
- continue;
46
- }
47
- body.push(line);
48
- }
49
- flush();
50
- return sections;
51
- }
52
- /** Strip markdown so an entry stays one clean, diffable line. */
53
- function toProse(text) {
54
- return text
55
- .replace(/<(read|modified)-files>[\s\S]*?<\/\1-files>/g, " ")
56
- .replace(/^\s*[-*]\s+/gm, " ")
57
- .replace(/^\s*\d+\.\s+/gm, " ")
58
- .replace(/[`*_>#]/g, "")
59
- .replace(/\s+/g, " ")
60
- .trim();
61
- }
62
- /**
63
- * The durable, non-duplicated part of a compaction summary. Falls back to the
64
- * whole summary when the expected headings are absent (the compactor's
65
- * extractive fallback path emits unstructured prose).
66
- */
67
- function summaryToProse(summary) {
68
- const stripped = summary.replace(/^\[Previous conversation summary\]/, "");
69
- const sections = splitSections(stripped);
70
- if (sections.size > 0) {
71
- const kept = DURABLE_SUMMARY_SECTIONS.map((name) => sections.get(name.toLowerCase()))
72
- .filter((body) => Boolean(body && body.trim()))
73
- .map(toProse)
74
- .filter(Boolean);
75
- if (kept.length > 0)
76
- return kept.join(" ");
77
- }
78
- return toProse(stripped);
79
- }
80
- const MUTATING_TOOLS = new Set(["write", "edit"]);
81
- /**
82
- * Safety net for the structural de-duplication above: drop any entry whose text
83
- * already appears verbatim inside another entry. Every journal entry is paid
84
- * for on every subsequent turn, so a duplicate is not a cosmetic problem.
85
- */
86
- function dropContainedNotes(notes) {
87
- return notes.filter((note, index) => notes.every((other, otherIndex) => otherIndex === index ||
88
- !other.text.includes(note.text) ||
89
- // Identical texts: keep the first occurrence only.
90
- (other.text === note.text && otherIndex > index)));
91
- }
92
- function textOf(content) {
93
- if (typeof content === "string")
94
- return content;
95
- if (!Array.isArray(content))
96
- return "";
97
- return content
98
- .filter((part) => part.type === "text")
99
- .map((part) => part.text)
100
- .join(" ");
101
- }
102
- /** Files the discarded span actually changed, in first-touch order. */
103
- function changedFiles(dropped) {
104
- const files = [];
105
- for (const message of dropped) {
106
- if (message.role !== "assistant" || !Array.isArray(message.content))
107
- continue;
108
- for (const part of message.content) {
109
- if (part.type !== "tool_call" || !MUTATING_TOOLS.has(part.name))
110
- continue;
111
- const target = part.args?.file_path;
112
- if (typeof target !== "string" || files.includes(target))
113
- continue;
114
- files.push(target);
115
- if (files.length >= MAX_FILES_LISTED)
116
- return files;
117
- }
118
- }
119
- return files;
120
- }
121
- /** The first real user request in the discarded span, if there was one. */
122
- function firstUserRequest(dropped) {
123
- for (const message of dropped) {
124
- if (message.role !== "user")
125
- continue;
126
- const text = textOf(message.content).trim();
127
- // Harness-injected framing (steering, loop-break, compaction summary) is
128
- // not something worth remembering across sessions.
129
- if (!text || text.startsWith("["))
130
- continue;
131
- return text;
132
- }
133
- return undefined;
134
- }
135
- /**
136
- * Build the notes to persist for one compaction. `dropped` are the messages
137
- * being replaced; `summary` is the text replacing them.
138
- */
139
- export function buildCompactionNotes(dropped, summary) {
140
- const notes = [];
141
- // Past tense throughout: these describe a session that is already over.
142
- const request = firstUserRequest(dropped);
143
- if (request) {
144
- notes.push({
145
- tag: "request",
146
- text: clampEntryText(`Was asked to: ${request}`, SUMMARY_NOTE_CHARS),
147
- });
148
- }
149
- const files = changedFiles(dropped);
150
- if (files.length > 0) {
151
- notes.push({
152
- tag: "files",
153
- text: clampEntryText(`Edited: ${files.join(", ")}`, SUMMARY_NOTE_CHARS),
154
- });
155
- }
156
- const summaryText = clampEntryText(summaryToProse(summary), SUMMARY_NOTE_CHARS);
157
- if (summaryText) {
158
- notes.push({ tag: "summary", text: summaryText });
159
- }
160
- return dropContainedNotes(notes).slice(0, MAX_NOTES_PER_COMPACTION);
161
- }
162
- //# sourceMappingURL=compaction-notes.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"compaction-notes.js","sourceRoot":"","sources":["../../../src/core/memory/compaction-notes.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=compaction-notes.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"compaction-notes.test.d.ts","sourceRoot":"","sources":["../../../src/core/memory/compaction-notes.test.ts"],"names":[],"mappings":""}
@@ -1,110 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { buildCompactionNotes, MAX_NOTES_PER_COMPACTION } from "./compaction-notes.js";
3
- import { ENTRY_MAX_CHARS } from "./journal.js";
4
- function edit(file) {
5
- return {
6
- role: "assistant",
7
- content: [{ type: "tool_call", id: file, name: "edit", args: { file_path: file } }],
8
- };
9
- }
10
- describe("buildCompactionNotes", () => {
11
- it("captures the request, the changed files and the summary", () => {
12
- const dropped = [
13
- { role: "user", content: "add retry logic to the uploader" },
14
- edit("/repo/src/upload.ts"),
15
- edit("/repo/src/retry.ts"),
16
- { role: "assistant", content: "done" },
17
- ];
18
- const notes = buildCompactionNotes(dropped, "[Previous conversation summary]\n\nAdded retries.");
19
- expect(notes.map((n) => n.tag)).toEqual(["request", "files", "summary"]);
20
- // Past tense throughout — an entry that reads as current state is the bug.
21
- expect(notes[0].text).toBe("Was asked to: add retry logic to the uploader");
22
- expect(notes[1].text).toContain("/repo/src/upload.ts");
23
- expect(notes[1].text).toContain("/repo/src/retry.ts");
24
- expect(notes[2].text).toBe("Added retries.");
25
- });
26
- it("ignores harness-injected user messages when picking the request", () => {
27
- const dropped = [
28
- { role: "user", content: "[Previous conversation summary]\n\nolder stuff" },
29
- { role: "user", content: "[The user added this while you were working — ...]" },
30
- { role: "user", content: "the actual ask" },
31
- ];
32
- expect(buildCompactionNotes(dropped, "")[0].text).toContain("the actual ask");
33
- });
34
- it("never writes more notes than the cap, each within one record", () => {
35
- const dropped = [
36
- { role: "user", content: "x".repeat(5000) },
37
- ...Array.from({ length: 40 }, (_, i) => edit(`/repo/file-${i}.ts`)),
38
- ];
39
- const notes = buildCompactionNotes(dropped, "y".repeat(5000));
40
- expect(notes.length).toBeLessThanOrEqual(MAX_NOTES_PER_COMPACTION);
41
- for (const note of notes)
42
- expect(note.text.length).toBeLessThanOrEqual(ENTRY_MAX_CHARS);
43
- });
44
- it("deduplicates repeatedly edited files", () => {
45
- const dropped = [edit("/repo/a.ts"), edit("/repo/a.ts"), edit("/repo/b.ts")];
46
- const files = buildCompactionNotes(dropped, "")[0];
47
- expect(files.tag).toBe("files");
48
- expect(files.text).toBe("Edited: /repo/a.ts, /repo/b.ts");
49
- });
50
- /** The compactor's real section structure (see compaction/compactor.ts). */
51
- const FULL_SUMMARY = "[Previous conversation summary]\n\n" +
52
- "### Primary Request and Intent\n" +
53
- "Edit `src/slugify.ts` so the function replaces spaces with hyphens.\n\n" +
54
- "### User Messages\n" +
55
- '"Read src/slugify.ts, then edit it so the function also replaces spaces with hyphens."\n\n' +
56
- "### What Was Done\n" +
57
- "Replaced the naive lowercase call with a regex substitution.\n\n" +
58
- "### Files Touched\n" +
59
- "- `src/slugify.ts` — rewrote the transform\n\n" +
60
- "### Errors and Fixes\n" +
61
- "The first regex dropped unicode; fixed by using a character class.\n\n" +
62
- "### Current Work\n" +
63
- "About to add trimming of leading hyphens.\n\n" +
64
- "### Next Step\n" +
65
- "Run the test suite.";
66
- it("keeps only the durable summary sections and drops the duplicated ones", () => {
67
- const dropped = [
68
- { role: "user", content: "edit slugify to replace spaces with hyphens" },
69
- edit("/repo/src/slugify.ts"),
70
- ];
71
- const summaryNote = buildCompactionNotes(dropped, FULL_SUMMARY).find((n) => n.tag === "summary");
72
- // Kept: pure history a future session cannot cheaply re-derive.
73
- expect(summaryNote.text).toContain("Replaced the naive lowercase call");
74
- expect(summaryNote.text).toContain("first regex dropped unicode");
75
- // Dropped: already covered by the `request` and `files` entries.
76
- expect(summaryNote.text).not.toContain("Primary Request and Intent");
77
- expect(summaryNote.text).not.toContain("Read src/slugify.ts, then edit");
78
- expect(summaryNote.text).not.toContain("rewrote the transform");
79
- // Dropped: in-flight state that would be stale on the next read.
80
- expect(summaryNote.text).not.toContain("About to add trimming");
81
- expect(summaryNote.text).not.toContain("Run the test suite");
82
- });
83
- it("renders each entry as one clean markdown-free line", () => {
84
- const note = buildCompactionNotes([{ role: "assistant", content: "ok" }], FULL_SUMMARY)[0];
85
- expect(note.text).not.toContain("#");
86
- expect(note.text).not.toContain("`");
87
- expect(note.text).not.toContain("\n");
88
- });
89
- it("falls back to the whole summary when it has no section headings", () => {
90
- // The compactor's extractive fallback path emits unstructured prose.
91
- const note = buildCompactionNotes([{ role: "assistant", content: "ok" }], "[Previous conversation summary]\n\nModified two files and fixed a failing test.")[0];
92
- expect(note.text).toBe("Modified two files and fixed a failing test.");
93
- });
94
- it("never emits an entry whose text is contained in another entry", () => {
95
- const dropped = [{ role: "user", content: "do the thing" }];
96
- const notes = buildCompactionNotes(dropped, "### What Was Done\nWas asked to: do the thing");
97
- const texts = notes.map((n) => n.text);
98
- // Every journal entry is re-read on every later turn, so a contained
99
- // duplicate is recurring waste, not a cosmetic issue.
100
- for (const [i, text] of texts.entries()) {
101
- const others = texts.filter((_, j) => j !== i);
102
- expect(others.some((other) => other.includes(text))).toBe(false);
103
- }
104
- });
105
- it("writes nothing when there is nothing worth remembering", () => {
106
- expect(buildCompactionNotes([], "")).toEqual([]);
107
- expect(buildCompactionNotes([{ role: "assistant", content: "ok" }], "")).toEqual([]);
108
- });
109
- });
110
- //# sourceMappingURL=compaction-notes.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"compaction-notes.test.js","sourceRoot":"","sources":["../../../src/core/memory/compaction-notes.test.ts"],"names":[],"mappings":"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"}
@@ -1,55 +0,0 @@
1
- /** Project-relative location. In the repo, so it shows up in review. */
2
- export declare const JOURNAL_RELATIVE_PATH: string;
3
- /** Max characters of a single entry body. */
4
- export declare const ENTRY_MAX_CHARS = 400;
5
- /** Entries rendered into the prompt tail. Older ones stay in the file only. */
6
- export declare const PROMPT_ENTRY_LIMIT = 8;
7
- /** Hard cap on prompt-tail characters. */
8
- export declare const PROMPT_CHAR_BUDGET = 1200;
9
- export interface JournalEntry {
10
- /** ISO date (day precision) the entry was recorded. */
11
- date: string;
12
- /** Short grouping label, e.g. "request", "files", "summary". */
13
- tag: string;
14
- /** Bounded, past-tense body. */
15
- text: string;
16
- }
17
- /**
18
- * Collapse whitespace and fit `text` into `maxChars`, cutting at the cleanest
19
- * boundary available: sentence, then word, then a hard slice.
20
- *
21
- * A mid-word cut (`…read src/router.ts in fu…`) is what the live run produced,
22
- * and it is worse than it looks. The tail of an entry is usually its most
23
- * specific part — a file path, an identifier — so a fragment there invites a
24
- * later reader to guess at what was severed, which is exactly the kind of
25
- * confident guessing bench G showed the model will not go back and check.
26
- *
27
- * The result never exceeds `maxChars`, ellipsis included.
28
- */
29
- export declare function clampEntryText(text: string, maxChars: number): string;
30
- export declare class ProjectJournal {
31
- private readonly now;
32
- readonly filePath: string;
33
- constructor(cwd: string, now?: () => Date);
34
- /** Entries in file order, oldest first. Hand-edits are read back as written. */
35
- read(): JournalEntry[];
36
- /**
37
- * Append entries under a file lock, re-reading inside it. Two daemons sharing
38
- * a project must not clobber each other, and an append is never a rewrite of
39
- * anything a human edited above it.
40
- */
41
- append(entries: readonly Omit<JournalEntry, "date">[]): Promise<number>;
42
- }
43
- /**
44
- * Recent journal entries for the uncached system-prompt tail, newest last.
45
- * Returns "" when the journal is empty, so an unused journal costs zero tokens.
46
- *
47
- * Older entries are deliberately NOT summarized into the prompt — they stay in
48
- * the file, and the pointer below tells the model how to read them. A summary
49
- * would be one more unverifiable assertion in context.
50
- */
51
- export declare function buildJournalPromptTail(journal: ProjectJournal, options?: {
52
- entryLimit?: number;
53
- charBudget?: number;
54
- }): string;
55
- //# sourceMappingURL=journal.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"journal.d.ts","sourceRoot":"","sources":["../../../src/core/memory/journal.ts"],"names":[],"mappings":"AAoCA,wEAAwE;AACxE,eAAO,MAAM,qBAAqB,QAAgC,CAAC;AAEnE,6CAA6C;AAC7C,eAAO,MAAM,eAAe,MAAM,CAAC;AACnC,+EAA+E;AAC/E,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC,0CAA0C;AAC1C,eAAO,MAAM,kBAAkB,OAAO,CAAC;AAWvC,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,GAAG,EAAE,MAAM,CAAC;IACZ,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;CACd;AAuBD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAiBrE;AAmBD,qBAAa,cAAc;IAKvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAJtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAGxB,GAAG,EAAE,MAAM,EACM,GAAG,GAAE,MAAM,IAAuB;IAKrD,gFAAgF;IAChF,IAAI,IAAI,YAAY,EAAE;IAQtB;;;;OAIG;IACG,MAAM,CAAC,OAAO,EAAE,SAAS,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;CAoC9E;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,cAAc,EACvB,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GACzD,MAAM,CA6BR"}
@@ -1,212 +0,0 @@
1
- /**
2
- * Project journal — durable, human-reviewable notes that survive compaction and
3
- * session restarts.
4
- *
5
- * The design is dictated by a measured result, not by taste. Bench G
6
- * (`bench/f-memory-staleness.mjs`) found that injected memory does not merely
7
- * inform a model, it REPLACES the act of checking: with no memory the model
8
- * verified against the repo 5/5 times; with any note present, 0/5 — and a false
9
- * note was then asserted as fact 5/5. Showing note ages and an explicit
10
- * "unverified, verify first" header fixed none of it. This is consistent with
11
- * published work: prompt-level freshness instructions are not a reliable fix.
12
- *
13
- * Three consequences, each of which is a rule here:
14
- *
15
- * 1. **Only past-tense history is stored.** An entry about something that
16
- * already happened ("chose X over Y because Z") cannot become false, so
17
- * suppressing re-verification of it costs nothing. Claims about current
18
- * state ("the test runner is jest") decay silently and are then asserted
19
- * with full confidence. Nothing in this module can write the latter: the
20
- * only writer is the compactor, recording what a past session did.
21
- *
22
- * 2. **The file is markdown, in the project, and git-diffable.** Since the
23
- * model provably will not catch a bad entry, the defence has to be outside
24
- * the model: a human reads `.gg/memory.md` in a diff. A binary log in
25
- * `~/.gg` would be invisible to exactly the reviewer who can catch it.
26
- *
27
- * 3. **No bespoke retrieval layer.** Recent entries ride in the prompt; the
28
- * whole file is readable with the ordinary `read` tool. A custom
29
- * store/tree/zoom stack would be machinery whose only job is to re-derive
30
- * what `read` already does.
31
- */
32
- import fs from "node:fs/promises";
33
- import fsSync from "node:fs";
34
- import path from "node:path";
35
- import { withFileLock } from "@kenkaiiii/gg-core";
36
- /** Project-relative location. In the repo, so it shows up in review. */
37
- export const JOURNAL_RELATIVE_PATH = path.join(".gg", "memory.md");
38
- /** Max characters of a single entry body. */
39
- export const ENTRY_MAX_CHARS = 400;
40
- /** Entries rendered into the prompt tail. Older ones stay in the file only. */
41
- export const PROMPT_ENTRY_LIMIT = 8;
42
- /** Hard cap on prompt-tail characters. */
43
- export const PROMPT_CHAR_BUDGET = 1200;
44
- const HEADER = [
45
- "# Project memory",
46
- "",
47
- "Past-tense notes written automatically when a session's history is compacted.",
48
- "Each entry records something that already happened, so it stays true.",
49
- "Edit or delete entries freely — this file is meant to be reviewed by humans.",
50
- "",
51
- ].join("\n");
52
- /**
53
- * Fraction of the budget a boundary must retain to be worth cutting back to.
54
- * Below this we would be discarding more content than the tidier ending is
55
- * worth, so a plain word cut wins.
56
- */
57
- const MIN_BOUNDARY_RATIO = 0.6;
58
- /** End of a sentence: terminal punctuation, optional closer, then a space. */
59
- const SENTENCE_END = /[.!?]["')\]]?\s/g;
60
- /** Index just past the last sentence ending inside `window`, or -1. */
61
- function lastSentenceEnd(window) {
62
- let index = -1;
63
- SENTENCE_END.lastIndex = 0;
64
- let match;
65
- while ((match = SENTENCE_END.exec(window)) !== null) {
66
- index = match.index + match[0].trimEnd().length;
67
- }
68
- return index;
69
- }
70
- /**
71
- * Collapse whitespace and fit `text` into `maxChars`, cutting at the cleanest
72
- * boundary available: sentence, then word, then a hard slice.
73
- *
74
- * A mid-word cut (`…read src/router.ts in fu…`) is what the live run produced,
75
- * and it is worse than it looks. The tail of an entry is usually its most
76
- * specific part — a file path, an identifier — so a fragment there invites a
77
- * later reader to guess at what was severed, which is exactly the kind of
78
- * confident guessing bench G showed the model will not go back and check.
79
- *
80
- * The result never exceeds `maxChars`, ellipsis included.
81
- */
82
- export function clampEntryText(text, maxChars) {
83
- const collapsed = text.replace(/\s+/g, " ").trim();
84
- if (collapsed.length <= maxChars)
85
- return collapsed;
86
- // Reserve room for the widest ellipsis form (" \u2026").
87
- const budget = Math.max(1, maxChars - 2);
88
- const window = collapsed.slice(0, budget);
89
- const floor = Math.floor(budget * MIN_BOUNDARY_RATIO);
90
- const sentence = lastSentenceEnd(window);
91
- // Terminal punctuation is kept, so separate the marker rather than doubling up.
92
- if (sentence >= floor)
93
- return `${collapsed.slice(0, sentence)} \u2026`;
94
- const word = window.lastIndexOf(" ");
95
- if (word >= floor)
96
- return `${collapsed.slice(0, word)}\u2026`;
97
- return `${window}\u2026`;
98
- }
99
- /** `- 2026-07-27 [tag] body` — one entry, one line, trivially diffable. */
100
- function formatEntry(entry) {
101
- return `- ${entry.date} [${entry.tag}] ${entry.text}`;
102
- }
103
- const ENTRY_PATTERN = /^-\s+(\d{4}-\d{2}-\d{2})\s+\[([^\]]+)\]\s+(.*)$/;
104
- function parseEntries(markdown) {
105
- const entries = [];
106
- for (const line of markdown.split("\n")) {
107
- const match = ENTRY_PATTERN.exec(line.trim());
108
- if (!match)
109
- continue;
110
- entries.push({ date: match[1], tag: match[2], text: match[3].trim() });
111
- }
112
- return entries;
113
- }
114
- export class ProjectJournal {
115
- now;
116
- filePath;
117
- constructor(cwd, now = () => new Date()) {
118
- this.now = now;
119
- this.filePath = path.join(cwd, JOURNAL_RELATIVE_PATH);
120
- }
121
- /** Entries in file order, oldest first. Hand-edits are read back as written. */
122
- read() {
123
- try {
124
- return parseEntries(fsSync.readFileSync(this.filePath, "utf-8"));
125
- }
126
- catch {
127
- return [];
128
- }
129
- }
130
- /**
131
- * Append entries under a file lock, re-reading inside it. Two daemons sharing
132
- * a project must not clobber each other, and an append is never a rewrite of
133
- * anything a human edited above it.
134
- */
135
- async append(entries) {
136
- const dated = entries
137
- .map((entry) => ({
138
- date: this.now().toISOString().slice(0, 10),
139
- tag: entry.tag,
140
- text: clampEntryText(entry.text, ENTRY_MAX_CHARS),
141
- }))
142
- .filter((entry) => entry.text.length > 0);
143
- if (dated.length === 0)
144
- return 0;
145
- await fs.mkdir(path.dirname(this.filePath), { recursive: true });
146
- return withFileLock(this.filePath, async () => {
147
- let existing;
148
- try {
149
- existing = await fs.readFile(this.filePath, "utf-8");
150
- }
151
- catch {
152
- existing = HEADER;
153
- }
154
- // Skip entries whose exact text is already recorded: repeated compactions
155
- // of an unchanged span would otherwise stack identical lines.
156
- const known = new Set(parseEntries(existing).map((entry) => entry.text));
157
- const fresh = dated.filter((entry) => !known.has(entry.text));
158
- if (fresh.length === 0)
159
- return 0;
160
- // Blank line between the prose header and the entry list, so the file is
161
- // valid, readable markdown rather than a list glued to a paragraph.
162
- const trimmed = existing.trimEnd();
163
- const separator = /\n-\s/.test(trimmed) ? "\n" : "\n\n";
164
- const body = `${trimmed}${separator}${fresh.map(formatEntry).join("\n")}\n`;
165
- // Atomic replace: a crash mid-write leaves the previous file intact.
166
- const tempPath = `${this.filePath}.tmp`;
167
- await fs.writeFile(tempPath, body, "utf-8");
168
- await fs.rename(tempPath, this.filePath);
169
- return fresh.length;
170
- });
171
- }
172
- }
173
- /**
174
- * Recent journal entries for the uncached system-prompt tail, newest last.
175
- * Returns "" when the journal is empty, so an unused journal costs zero tokens.
176
- *
177
- * Older entries are deliberately NOT summarized into the prompt — they stay in
178
- * the file, and the pointer below tells the model how to read them. A summary
179
- * would be one more unverifiable assertion in context.
180
- */
181
- export function buildJournalPromptTail(journal, options = {}) {
182
- const entryLimit = options.entryLimit ?? PROMPT_ENTRY_LIMIT;
183
- const charBudget = options.charBudget ?? PROMPT_CHAR_BUDGET;
184
- const all = journal.read();
185
- if (all.length === 0)
186
- return "";
187
- const lines = [];
188
- let chars = 0;
189
- for (let i = all.length - 1; i >= 0 && lines.length < entryLimit; i--) {
190
- const line = formatEntry(all[i]);
191
- if (chars + line.length > charBudget)
192
- break;
193
- chars += line.length;
194
- lines.push(line);
195
- }
196
- if (lines.length === 0)
197
- return "";
198
- lines.reverse();
199
- const older = all.length - lines.length;
200
- return [
201
- "## Project memory",
202
- `What earlier sessions did, oldest first. These are records of past events, not a ` +
203
- `description of the project's current state — check the code before relying on any of it.`,
204
- ...lines,
205
- older > 0
206
- ? `(${older} older ${older === 1 ? "entry" : "entries"} in ${JOURNAL_RELATIVE_PATH} — read that file if you need them.)`
207
- : "",
208
- ]
209
- .filter(Boolean)
210
- .join("\n");
211
- }
212
- //# sourceMappingURL=journal.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"journal.js","sourceRoot":"","sources":["../../../src/core/memory/journal.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=journal.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"journal.test.d.ts","sourceRoot":"","sources":["../../../src/core/memory/journal.test.ts"],"names":[],"mappings":""}