@cstart/coldstart 2.2.8 → 2.2.10

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.
package/dist/kb/view.d.ts CHANGED
@@ -30,7 +30,10 @@ export declare function buildViewData(root: string, generated: string): {
30
30
  backlinks: string[];
31
31
  }[];
32
32
  };
33
- /** Bake data into the template. `</` is neutralized so note text can't break the script tag. */
33
+ /** Bake data into the template. `</` is neutralized so note text can't break the
34
+ * script tag; the tab title is scoped to the repo already carried in the data
35
+ * (`summary.repo`, = basename(root)) so every notebook's view is named for its
36
+ * own repo rather than a hardcoded placeholder. */
34
37
  export declare function renderViewHtml(template: string, data: unknown): string;
35
38
  export interface ViewOptions {
36
39
  open?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../src/kb/view.ts"],"names":[],"mappings":"AAkBA,KAAK,UAAU,GAAG,OAAO,GAAG,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;AA4CjE,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;mBA3BvC,UAAU;;;;;;mBAsBX,MAAM,EAAE;;EA2B5B;AAED,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAGtE;AAyBD,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;0EAQ0E;AAC1E,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAYhF"}
1
+ {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../src/kb/view.ts"],"names":[],"mappings":"AAkBA,KAAK,UAAU,GAAG,OAAO,GAAG,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;AA4CjE,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;mBA3BvC,UAAU;;;;;;mBAsBX,MAAM,EAAE;;EA2B5B;AAOD;;;oDAGoD;AACpD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAStE;AAyBD,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;0EAQ0E;AAC1E,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAYhF"}
package/dist/kb/view.js CHANGED
@@ -77,10 +77,23 @@ export function buildViewData(root, generated) {
77
77
  }
78
78
  return { summary: { total: notes.length, byType, byFreshness, repo: basename(root), generated }, notes };
79
79
  }
80
- /** Bake data into the template. `</` is neutralized so note text can't break the script tag. */
80
+ /** Escape the three chars that matter inside an HTML text node (here, <title>). */
81
+ function escapeHtml(s) {
82
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
83
+ }
84
+ /** Bake data into the template. `</` is neutralized so note text can't break the
85
+ * script tag; the tab title is scoped to the repo already carried in the data
86
+ * (`summary.repo`, = basename(root)) so every notebook's view is named for its
87
+ * own repo rather than a hardcoded placeholder. */
81
88
  export function renderViewHtml(template, data) {
82
89
  const json = JSON.stringify(data).replace(/<\//g, '<\\/');
83
- return template.replace('__DATA_JSON__', () => json);
90
+ const repo = data?.summary?.repo;
91
+ const title = typeof repo === 'string' && repo.trim()
92
+ ? `Coldstart Notebook — ${escapeHtml(repo)}`
93
+ : 'Coldstart Notebook';
94
+ return template
95
+ .replace('__TITLE__', () => title)
96
+ .replace('__DATA_JSON__', () => json);
84
97
  }
85
98
  /** Best-effort open in the OS default browser. Never throws. */
86
99
  function openInBrowser(file) {
@@ -1 +1 @@
1
- {"version":3,"file":"view.js","sourceRoot":"","sources":["../../src/kb/view.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"view.js","sourceRoot":"","sources":["../../src/kb/view.ts"],"names":[],"mappings":"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"}
@@ -87,7 +87,7 @@ for it — your findings, not the notebook decision.`
87
87
  body = body.includes("{{WORKLIST}}")
88
88
  ? body.replaceAll("{{WORKLIST}}", worklist)
89
89
  : `${body.trimEnd()}\n\nWORKLIST — files you actually read this session, most-worked first \
90
- (the complete scope; never write a note for an unlisted file):\n\n${worklist}`;
90
+ (your scope; if you edited or deep-read a file that isn't listed, you can note it too):\n\n${worklist}`;
91
91
  return `**Notebook capture point.** ${opening}\n\n${body.trimEnd()}${tail}`;
92
92
  }
93
93
 
@@ -102,8 +102,9 @@ branch, reviewing a PR, or reading vendored/generated code, that knowledge is ab
102
102
  or the future — write nothing and say so.
103
103
  2. If nothing about the current code is worth recording, no note is the right answer. If a \
104
104
  future agent would not act differently for knowing it, don't store it.
105
- 3. The worklist below is the complete scope. Unlisted files are out — ignored \
106
- (.coldstartignore) or already captured. Never write a note for an unlisted file.
105
+ 3. The worklist below is your scope, most-worked first. Unlisted files are out — ignored \
106
+ (.coldstartignore) or already captured. If you edited or deep-read a file this session that \
107
+ isn't listed, you can write a note for it too.
107
108
 
108
109
  WORKLIST — files you actually read this session, most-worked first:
109
110
 
@@ -119,7 +120,13 @@ naming convention) is exactly what to record — ONLY if you actually observed i
119
120
  session's work. Never guess at usage or go searching for it now. Files flagged "no consumers \
120
121
  in import graph" are where an observed usage fact matters most.
121
122
  7. Fixed a bug? The cause goes in the culpable file's note; the SYMPTOM words go in \
122
- "aliases" — symptoms are what a future agent will search.
123
+ "aliases" — symptoms are what a future agent will search. Aliases are SEARCH KEYS, not \
124
+ sentences: each one 2-5 words, no filler ("after", "already", "another", "still"), because \
125
+ every word in an alias is an independent match chance and a long alias makes the note fire \
126
+ on prompts it has nothing to do with. Cover the vocabulary THIS FILE owns — the concept \
127
+ nouns someone would type when they want this file, including the ones already in its name \
128
+ and symbols. A note about alias handling that never says "aliases" cannot be found by \
129
+ someone asking about aliases. 6-10 aliases is plenty; more surface area is not more recall.
123
130
  8. Read a note this session that proved wrong? Correct or retract it now — you are the warm \
124
131
  agent; there is no "next".
125
132
  9. Firsthand only: nothing from a subagent's report you didn't verify yourself.
@@ -27,6 +27,11 @@ import { existsSync, appendFileSync, readFileSync, writeFileSync } from "node:fs
27
27
  import { join } from "node:path";
28
28
  import { tmpdir } from "node:os";
29
29
  import { fileURLToPath } from "node:url";
30
+ // Strip host telemetry wrappers before the query is built — see recall-query.mjs.
31
+ import { recallQuery } from "./recall-query.mjs";
32
+ // Per-session dedup shared with the other recall hooks — see recall-seen.mjs.
33
+ // Hosts with no transcript path never reset; dedup still holds for the session.
34
+ import { readSeen, writeSeen, idsInPage, seenArgs } from "./recall-seen.mjs";
30
35
 
31
36
  // hooks/ sits beside dist/ in both the repo and the published package.
32
37
  const CLI = fileURLToPath(new URL("../dist/index.js", import.meta.url));
@@ -71,15 +76,23 @@ process.on("unhandledRejection", (e) => { log(`unhandled ${e?.stack || e}`); pro
71
76
  if (!root) process.exit(0);
72
77
  setLogRoot(root);
73
78
 
79
+ const sid = String(input.session_id || "").replace(/[^\w-]/g, "");
80
+
74
81
  // No notebook → no tax, not even a child process.
75
82
  if (!existsSync(join(root, ".coldstart", "notebook", ".raw"))) process.exit(0);
76
83
 
77
- const prompt = String(input.prompt || "").slice(0, MAX_QUERY_CHARS).trim();
84
+ const prompt = recallQuery(input.prompt, MAX_QUERY_CHARS);
78
85
  if (!prompt) process.exit(0);
79
86
 
87
+ // Ephemeral Codex runs expose no rollout path; readSeen then never resets,
88
+ // which is the safe direction — dedup simply holds for the whole session.
89
+ const tPath = String(input.transcript_path || "");
90
+ const seen = readSeen(sid, tPath, log);
91
+
80
92
  let page = "";
81
93
  try {
82
- page = execFileSync("node", [CLI, "kb", "search", "--hook", "--max", "3", "--root", root, prompt], {
94
+ const args = [CLI, "kb", "search", "--hook", "--max", "3", "--root", root, ...seenArgs(seen.ids)];
95
+ page = execFileSync("node", [...args, prompt], {
83
96
  encoding: "utf8",
84
97
  timeout: SEARCH_TIMEOUT_MS,
85
98
  stdio: ["ignore", "pipe", "ignore"],
@@ -94,6 +107,10 @@ process.on("unhandledRejection", (e) => { log(`unhandled ${e?.stack || e}`); pro
94
107
  process.exit(0);
95
108
  }
96
109
 
110
+ // Record what this page hands over before the size guards can trim it — a
111
+ // note the agent was shown is seen, whichever way it ends up rendered.
112
+ writeSeen(sid, [...seen.ids, ...idsInPage(page)], tPath);
113
+
97
114
  // Pointer page (rulings 2026-07-06/08): titles + gists + an OPENABLE note
98
115
  // path, never a full body — a wrong pointer costs a glance, a wrong
99
116
  // implant poisons the whole session. Depth is one Read of the → path away
@@ -130,7 +147,6 @@ process.on("unhandledRejection", (e) => { log(`unhandled ${e?.stack || e}`); pro
130
147
  // grep-spiral unguarded otherwise. Path/shape must match the handler's state
131
148
  // file: literal /tmp + main-agent key = session_id.
132
149
  try {
133
- const sid = String(input.session_id || "");
134
150
  if (sid && /^[\w-]+$/.test(sid)) {
135
151
  const sf = `/tmp/find_nudge_${sid}.json`;
136
152
  let st = {};
@@ -29,6 +29,11 @@ import { cursorRoot } from "./cursor-input.mjs";
29
29
  // stop wrote its worklist payload to a pending file instead of blocking; it
30
30
  // rides this same next-prompt channel — capture first, then the user's request.
31
31
  import { takePendingCapture } from "./elicit-core.mjs";
32
+ // Strip host telemetry wrappers before the query is built — see recall-query.mjs.
33
+ import { recallQuery } from "./recall-query.mjs";
34
+ // Per-session dedup shared with the other recall hooks — see recall-seen.mjs.
35
+ // Hosts with no transcript path never reset; dedup still holds for the session.
36
+ import { readSeen, writeSeen, idsInPage, seenArgs } from "./recall-seen.mjs";
32
37
 
33
38
  // hooks/ sits beside dist/ in both the repo and the published package.
34
39
  const CLI = fileURLToPath(new URL("../dist/index.js", import.meta.url));
@@ -80,13 +85,17 @@ process.on("unhandledRejection", (e) => { log(`unhandled ${e?.stack || e}`); pro
80
85
  // (the first capture is what creates it).
81
86
  const pending = takePendingCapture(sid);
82
87
 
83
- const prompt = String(input.prompt || "").slice(0, MAX_QUERY_CHARS).trim();
88
+ const prompt = recallQuery(input.prompt, MAX_QUERY_CHARS);
89
+
90
+ const tPath = String(input.transcript_path || "");
91
+ const seen = readSeen(sid, tPath, log);
84
92
 
85
93
  let page = "";
86
94
  // No notebook / no prompt → no recall search, not even a child process.
87
95
  if (prompt && existsSync(join(root, ".coldstart", "notebook", ".raw"))) {
96
+ const args = [CLI, "kb", "search", "--hook", "--max", "3", "--root", root, ...seenArgs(seen.ids)];
88
97
  try {
89
- page = execFileSync("node", [CLI, "kb", "search", "--hook", "--max", "3", "--root", root, prompt], {
98
+ page = execFileSync("node", [...args, prompt], {
90
99
  encoding: "utf8",
91
100
  timeout: SEARCH_TIMEOUT_MS,
92
101
  stdio: ["ignore", "pipe", "ignore"],
@@ -108,6 +117,10 @@ process.on("unhandledRejection", (e) => { log(`unhandled ${e?.stack || e}`); pro
108
117
  // Pointer page — titles + gists + an OPENABLE note path, never a full body.
109
118
  // (Same framing as codex-kb-recall.mjs; notes are REFERENCE DATA, not
110
119
  // instructions.)
120
+ // Record what this page hands over before the size guards can trim it — a
121
+ // note the agent was shown is seen, whichever way it ends up rendered.
122
+ if (page) writeSeen(sid, [...seen.ids, ...idsInPage(page)], tPath);
123
+
111
124
  let block = "";
112
125
  if (page) block =
113
126
  `The repo's notebook (notes written by past agents after real tasks here) has entries ` +
@@ -16,7 +16,10 @@ import { tmpdir } from "node:os";
16
16
  import { execFileSync } from "node:child_process";
17
17
  import { existsSync, readFileSync, writeFileSync, unlinkSync, appendFileSync, mkdirSync } from "node:fs";
18
18
 
19
- export const MAX_WORKLIST = 30;
19
+ import { MAX_CAPTURE_FILES } from "./trigger.mjs";
20
+ // One source of truth: the display cap equals what a fire claims, so the prompt
21
+ // never shows fewer (or more) files than the fire marks captured.
22
+ export const MAX_WORKLIST = MAX_CAPTURE_FILES;
20
23
 
21
24
  const noop = () => {};
22
25
 
@@ -38,7 +38,7 @@ import { initialState, step } from "./trigger.mjs";
38
38
  import { loadIgnore } from "./ignore.mjs";
39
39
  import { buildCapturePayload } from "./capture-payload.mjs";
40
40
  import {
41
- worklistEntries, freshNotedSet, gitHead, logCaptureEvent, writePendingCapture,
41
+ worklistEntries, freshNotedSet, gitHead, logCaptureEvent, writePendingCapture, MAX_WORKLIST,
42
42
  } from "./elicit-core.mjs";
43
43
 
44
44
  // hooks/ sits beside dist/ in both the repo and the published package.
@@ -180,7 +180,23 @@ if (process.argv.includes("--manual")) {
180
180
  process.exit(0);
181
181
  }
182
182
  const { sid, state, path: markerPath } = found;
183
- const files = Object.keys(state.files).filter((rel) => !state.files[rel].captured);
183
+ // A manual /capture-notes is an EXPLICIT request to write up THIS session's
184
+ // work, so its scope is everything worked on — not merely what an automatic
185
+ // fire has yet to offer. Files EDITED this session are the highest-value
186
+ // notes (the fix, a brand-new helper), and an earlier automatic fire may
187
+ // have already marked them `captured` — a flag that means "offered once",
188
+ // never "a good note exists". Excluding them left the single most valuable
189
+ // note unwritten and a now-stale note unfixed (the Bug-1 report). So:
190
+ // include every edited file regardless of the flag, and drop only READ-ONLY
191
+ // files that were already offered (re-listing those on demand is just
192
+ // noise). Rank most-worked first (edits → retouches → reads) so new/edited
193
+ // files lead; the per-file annotations flag when a fresh note needs nothing.
194
+ const files = Object.entries(state.files)
195
+ .filter(([, f]) => (f.reads + f.edits + f.gs) > 0 && (f.edits > 0 || !f.captured))
196
+ .sort((a, b) => (b[1].edits - a[1].edits)
197
+ || ((b[1].retouches || 0) - (a[1].retouches || 0))
198
+ || (b[1].reads - a[1].reads))
199
+ .map(([rel]) => rel);
184
200
  if (!files.length) {
185
201
  process.stdout.write("Everything worked on so far is already captured — nothing new to write right now.\n");
186
202
  log(`MANUAL nothing-new session=${sid}`);
@@ -188,14 +204,16 @@ if (process.argv.includes("--manual")) {
188
204
  }
189
205
  const entries = worklistEntries(CLI, root, files, state.files, log);
190
206
  const payload = buildCapturePayload({ root, cli: CLI, sid, entries, envelope: "manual" });
191
- // Mark ONLY the listed files captured. Re-read first: a Stop hook may have
192
- // written the marker since we loaded it, and we must not clobber its
193
- // counters. Best-effort a failure here costs a duplicate ask, never the
194
- // payload the user asked for.
207
+ // Mark ONLY the files actually LISTED (the worklist caps its display to
208
+ // MAX_WORKLIST) captured any overflow stays uncaptured so it rolls into
209
+ // the next capture rather than being marked done but never shown. Re-read
210
+ // first: a Stop hook may have written the marker since we loaded it, and we
211
+ // must not clobber its counters. Best-effort — a failure here costs a
212
+ // duplicate ask, never the payload the user asked for.
195
213
  try {
196
214
  const live = JSON.parse(readFileSync(markerPath, "utf8"));
197
215
  if (live && live.files) {
198
- for (const rel of files) if (live.files[rel]) live.files[rel].captured = true;
216
+ for (const rel of files.slice(0, MAX_WORKLIST)) if (live.files[rel]) live.files[rel].captured = true;
199
217
  writeFileSync(markerPath, JSON.stringify(live));
200
218
  }
201
219
  } catch (e) { log(`MANUAL mark-captured skipped ${e}`); }
@@ -34,6 +34,13 @@ import { fileURLToPath } from "node:url";
34
34
  // the user's request. Consumed (deleted) on delivery; stale pendings (>24h,
35
35
  // e.g. a session resumed days later) are dropped.
36
36
  import { takePendingCapture } from "./elicit-core.mjs";
37
+ // The host wraps the user's words in editor/subagent telemetry; that text is
38
+ // file-rich and hijacks the path-name override in kb search. Strip it BEFORE
39
+ // truncating, or a long <ide_selection> eats the whole query budget.
40
+ import { recallQuery } from "./recall-query.mjs";
41
+ // Per-session dedup: don't re-show a note this session already got, and forget
42
+ // what was shown once the transcript shrinks (a compact). See recall-seen.mjs.
43
+ import { readSeen, writeSeen, idsInPage, seenArgs } from "./recall-seen.mjs";
37
44
 
38
45
  // hooks/ sits beside dist/ in both the repo and the published package.
39
46
  const CLI = fileURLToPath(new URL("../dist/index.js", import.meta.url));
@@ -41,6 +48,7 @@ const CLI = fileURLToPath(new URL("../dist/index.js", import.meta.url));
41
48
  const MAX_QUERY_CHARS = 2000; // pasted-code prompts: the head carries the ask
42
49
  const SEARCH_TIMEOUT_MS = 4000;
43
50
 
51
+
44
52
  let LOG_FILE = join(tmpdir(), "coldstart-kb-hook.log");
45
53
  function setLogRoot(root) { if (root) LOG_FILE = join(root, ".coldstart", "kb-hook.log"); }
46
54
  function log(msg) {
@@ -85,13 +93,17 @@ process.on("unhandledRejection", (e) => { log(`unhandled ${e?.stack || e}`); pro
85
93
  // (the first capture is what creates it).
86
94
  const pending = takePendingCapture(sid);
87
95
 
88
- const prompt = String(input.prompt || "").slice(0, MAX_QUERY_CHARS).trim();
96
+ const prompt = recallQuery(input.prompt, MAX_QUERY_CHARS);
97
+
98
+ const tPath = String(input.transcript_path || "");
99
+ const seen = readSeen(sid, tPath, log);
89
100
 
90
101
  let page = "";
91
102
  // No notebook / no prompt → no recall search, not even a child process.
92
103
  if (prompt && existsSync(join(root, ".coldstart", "notebook", ".raw"))) {
104
+ const args = [CLI, "kb", "search", "--hook", "--max", "3", "--root", root, ...seenArgs(seen.ids)];
93
105
  try {
94
- page = execFileSync("node", [CLI, "kb", "search", "--hook", "--max", "3", "--root", root, prompt], {
106
+ page = execFileSync("node", [...args, prompt], {
95
107
  encoding: "utf8",
96
108
  timeout: SEARCH_TIMEOUT_MS,
97
109
  stdio: ["ignore", "pipe", "ignore"],
@@ -117,6 +129,10 @@ process.on("unhandledRejection", (e) => { log(`unhandled ${e?.stack || e}`); pro
117
129
  // agents never used it). Trust framing: [fresh] content is reliable
118
130
  // as-is; the caution that remains is about COMPLETENESS (a note names a
119
131
  // finding, not necessarily your whole file set), not about content.
132
+ // Record what this page hands over BEFORE the size guards below can trim
133
+ // it — a note the agent was shown is seen, whichever way it was rendered.
134
+ if (page) writeSeen(sid, [...seen.ids, ...idsInPage(page)], tPath);
135
+
120
136
  let block = "";
121
137
  if (page) block =
122
138
  `The repo's notebook (notes written by past agents after real tasks here) has entries ` +
@@ -0,0 +1,83 @@
1
+ /**
2
+ * recall-query.mjs — turn a raw UserPromptSubmit payload into a recall QUERY.
3
+ *
4
+ * The hosts do not hand us the user's words. They hand us the user's words
5
+ * WRAPPED IN HARNESS TELEMETRY: the file that happens to be open in the editor,
6
+ * the current selection, a finished subagent's summary, the expansion of a
7
+ * slash command. That text is file- and symbol-rich, so `kb search --hook`
8
+ * treats it as if the user had named those files — most damagingly through the
9
+ * path-name override in src/kb/search.ts, which boosts a note past every
10
+ * eligibility gate when the query string contains its anchor path. A stale-open
11
+ * editor tab then hijacks recall for turns at a time.
12
+ *
13
+ * So: strip the wrappers (tag AND content) before the query is built, and
14
+ * before truncation — a 1.5KB <ide_selection> ahead of a short question would
15
+ * otherwise eat the whole MAX_QUERY_CHARS budget and leave the real ask cut off.
16
+ *
17
+ * Only a NAMED set of tags is stripped. Anything else — including XML or HTML
18
+ * the user pasted deliberately — is left alone; over-stripping would silently
19
+ * drop real questions, which is the failure this is meant to prevent.
20
+ *
21
+ * Fail-open: on any error the raw text is returned unchanged.
22
+ */
23
+
24
+ /** Harness-emitted wrappers, union across Claude Code / Cursor / Codex. A tag
25
+ * a given host never emits simply never matches. */
26
+ export const HARNESS_TAGS = [
27
+ // Claude Code
28
+ "system-reminder",
29
+ "ide_opened_file",
30
+ "ide_selection",
31
+ "ide_diagnostics",
32
+ "task-notification",
33
+ "local-command-caveat",
34
+ "local-command-stdout",
35
+ "local-command-stderr",
36
+ "command-name",
37
+ "command-message",
38
+ "command-args",
39
+ "user-prompt-submit-hook",
40
+ // Cursor
41
+ "browser_instruction",
42
+ // Codex
43
+ "environment_context",
44
+ "user_instructions",
45
+ ];
46
+
47
+ const NAMES = HARNESS_TAGS.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
48
+ // Well-formed pair, content included. Non-greedy, /s so it spans newlines.
49
+ const PAIRED = new RegExp(`<(${NAMES})(?:\\s[^>]*)?>[\\s\\S]*?<\\/\\1\\s*>`, "gi");
50
+ // Leftovers: self-closing, or a lone open/close tag from a truncated payload.
51
+ const LONE = new RegExp(`<\\/?(?:${NAMES})(?:\\s[^>]*)?\\/?>`, "gi");
52
+
53
+ /**
54
+ * Strip harness wrappers from a raw prompt.
55
+ * @param {string} raw
56
+ * @returns {string} the user's own text; "" when nothing but telemetry remains.
57
+ */
58
+ export function stripHarnessWrappers(raw) {
59
+ try {
60
+ let s = String(raw ?? "");
61
+ if (!s) return "";
62
+ // Nested wrappers (a system-reminder inside a task-notification) need more
63
+ // than one pass; bounded so a pathological payload cannot spin.
64
+ for (let i = 0; i < 4; i++) {
65
+ const next = s.replace(PAIRED, " ");
66
+ if (next === s) break;
67
+ s = next;
68
+ }
69
+ return s.replace(LONE, " ").replace(/[ \t]+/g, " ").trim();
70
+ } catch {
71
+ return String(raw ?? "");
72
+ }
73
+ }
74
+
75
+ /**
76
+ * The full prompt → query pipeline: strip, THEN truncate.
77
+ * @param {string} raw raw `input.prompt`
78
+ * @param {number} maxChars
79
+ * @returns {string} "" when there is nothing left to search for.
80
+ */
81
+ export function recallQuery(raw, maxChars) {
82
+ return stripHarnessWrappers(raw).slice(0, maxChars).trim();
83
+ }
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * recall-seen.mjs — per-session dedup state for the recall hooks.
4
+ *
5
+ * 61% of notebook injections re-showed a note the SAME session had already
6
+ * been given (worst case 12x). That is pure tax: the title and gist are
7
+ * already in the agent's context, so the repeat buys nothing and pushes the
8
+ * page toward the host's size cap. Each recall hook remembers the ids it has
9
+ * shown and passes them to `kb search --seen`, which drops them from the page
10
+ * it would otherwise have rendered (no backfill; all-seen => inject nothing).
11
+ *
12
+ * Reset on COMPACTION, not on a clock. After a compact the session id is
13
+ * unchanged but the agent's context is gone, so a previously-shown note is
14
+ * genuinely absent again and SHOULD be re-injectable. A shrinking transcript
15
+ * file is the observable signal — the same one kb-elicit uses for its own
16
+ * resume handling. Hosts that expose no transcript path (ephemeral Codex runs)
17
+ * simply never reset: dedup still works, it just holds for the whole session.
18
+ *
19
+ * Shared by kb-recall.mjs (Claude), cursor-kb-recall.mjs and
20
+ * codex-kb-recall.mjs so the three cannot drift. Every function is fail-open:
21
+ * any error leaves dedup OFF rather than blocking recall.
22
+ */
23
+
24
+ import { readFileSync, writeFileSync, statSync } from "node:fs";
25
+ import { join } from "node:path";
26
+ import { tmpdir } from "node:os";
27
+
28
+ const SEEN_TTL_MS = 24 * 60 * 60 * 1000;
29
+ /** os.tmpdir(), NOT literal /tmp — that path is reserved for the find_nudge
30
+ * state file, which the nudge handler reads back by an agreed literal path. */
31
+ const seenPath = (sid) => join(tmpdir(), `coldstart-recall-seen-${sid}.json`);
32
+
33
+ function transcriptSize(p) {
34
+ try { return p ? statSync(p).size : 0; } catch { return 0; }
35
+ }
36
+
37
+ /**
38
+ * Ids already shown this session. `log` is optional (each host has its own).
39
+ * Returns { ids, size } — pass both back to writeSeen.
40
+ */
41
+ export function readSeen(sid, transcriptPath, log) {
42
+ if (!sid) return { ids: [], size: 0 };
43
+ try {
44
+ const st = JSON.parse(readFileSync(seenPath(sid), "utf8"));
45
+ if (!st || !Array.isArray(st.ids)) return { ids: [], size: 0 };
46
+ if (Date.now() - (st.ts || 0) > SEEN_TTL_MS) return { ids: [], size: 0 };
47
+ const now = transcriptSize(transcriptPath);
48
+ if (now && st.size && now < st.size) {
49
+ log?.(`compaction detected (${st.size} -> ${now} bytes): clearing ${st.ids.length} seen ids`);
50
+ return { ids: [], size: now };
51
+ }
52
+ return { ids: st.ids, size: st.size || 0 };
53
+ } catch { return { ids: [], size: 0 }; }
54
+ }
55
+
56
+ export function writeSeen(sid, ids, transcriptPath) {
57
+ if (!sid) return;
58
+ try {
59
+ writeFileSync(seenPath(sid), JSON.stringify({
60
+ ids: [...new Set(ids)].slice(-200), // bounded; a session never shows this many
61
+ size: transcriptSize(transcriptPath),
62
+ ts: Date.now(),
63
+ }));
64
+ } catch { /* fail-open: dedup is an optimisation, never a gate */ }
65
+ }
66
+
67
+ /** Note ids on a rendered pointer page — each hit carries `→ open: …/<id>.md`.
68
+ * Read back off the rendered text so the hooks need no structured output. */
69
+ export function idsInPage(page) {
70
+ return [...page.matchAll(/→ open:\s*\S*?notes\/([\w.-]+)\.md/g)].map((m) => m[1]);
71
+ }
72
+
73
+ /** The `--seen a,b,c` argv fragment, or [] when there is nothing to exclude. */
74
+ export function seenArgs(ids) {
75
+ return ids.length ? ["--seen", ids.join(",")] : [];
76
+ }
package/hooks/trigger.mjs CHANGED
@@ -43,6 +43,12 @@
43
43
 
44
44
  export const T_ARM = 10;
45
45
  export const T_CAP = 20;
46
+ /** Files a single fire CLAIMS (marks captured) and lists — the same cap the
47
+ * worklist prompt renders (elicit-core slices its display to it). Files beyond
48
+ * it stay UNCAPTURED so they roll into the NEXT fire's worklist, rather than
49
+ * being marked done but never shown — which silently dropped notes on sessions
50
+ * that touched more than this many files (a long autonomous grind). */
51
+ export const MAX_CAPTURE_FILES = 30;
46
52
  export const SETTLE_ACTIVE_STOPS = 3;
47
53
  export const DESCENT_QUIET = 1;
48
54
  /** Uncaptured-file floor for the BLOCKING/backlog paths (head-drift, cap).
@@ -152,11 +158,15 @@ export function step(state, obs) {
152
158
 
153
159
  let decision = null;
154
160
  if (fire) {
155
- // worklist = the uncaptured set, most-worked first (edits, then retouches)
156
- const files = uncaptured
161
+ // worklist = the uncaptured set, most-worked first (edits, then retouches),
162
+ // capped to what one fire claims. Mark ONLY the listed files captured; the
163
+ // overflow stays uncaptured and rolls into the next fire (which re-fires
164
+ // soon, since the backlog keeps the score high).
165
+ const ranked = uncaptured
157
166
  .sort((a, b) => (b[1].edits - a[1].edits) || (b[1].retouches - a[1].retouches) || (b[1].reads - a[1].reads))
158
- .map(([rel]) => rel);
159
- for (const [, f] of uncaptured) f.captured = true;
167
+ .slice(0, MAX_CAPTURE_FILES);
168
+ const files = ranked.map(([rel]) => rel);
169
+ for (const [, f] of ranked) f.captured = true;
160
170
  s.armed = false;
161
171
  s.activeStops = 0;
162
172
  s.stopsSinceFire = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cstart/coldstart",
3
- "version": "2.2.8",
3
+ "version": "2.2.10",
4
4
  "mcpName": "io.github.AkashGoenka/coldstart",
5
5
  "publishConfig": {
6
6
  "access": "public"