@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/README.md +21 -0
- package/dist/kb/cli.d.ts.map +1 -1
- package/dist/kb/cli.js +9 -1
- package/dist/kb/cli.js.map +1 -1
- package/dist/kb/search.d.ts +15 -0
- package/dist/kb/search.d.ts.map +1 -1
- package/dist/kb/search.js +195 -18
- package/dist/kb/search.js.map +1 -1
- package/dist/kb/view-template.d.ts +4 -3
- package/dist/kb/view-template.d.ts.map +1 -1
- package/dist/kb/view-template.js +4 -3
- package/dist/kb/view-template.js.map +1 -1
- package/dist/kb/view.d.ts +4 -1
- package/dist/kb/view.d.ts.map +1 -1
- package/dist/kb/view.js +15 -2
- package/dist/kb/view.js.map +1 -1
- package/hooks/capture-payload.mjs +11 -4
- package/hooks/codex-kb-recall.mjs +19 -3
- package/hooks/cursor-kb-recall.mjs +15 -2
- package/hooks/elicit-core.mjs +4 -1
- package/hooks/kb-elicit.mjs +25 -7
- package/hooks/kb-recall.mjs +18 -2
- package/hooks/recall-query.mjs +83 -0
- package/hooks/recall-seen.mjs +76 -0
- package/hooks/trigger.mjs +14 -4
- package/package.json +1 -1
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
|
|
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;
|
package/dist/kb/view.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
/**
|
|
80
|
+
/** Escape the three chars that matter inside an HTML text node (here, <title>). */
|
|
81
|
+
function escapeHtml(s) {
|
|
82
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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
|
-
|
|
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) {
|
package/dist/kb/view.js.map
CHANGED
|
@@ -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
|
-
(
|
|
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
|
|
106
|
-
(.coldstartignore) or already captured.
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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", [
|
|
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 ` +
|
package/hooks/elicit-core.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/hooks/kb-elicit.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
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}`); }
|
package/hooks/kb-recall.mjs
CHANGED
|
@@ -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 =
|
|
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", [
|
|
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
|
-
|
|
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
|
-
.
|
|
159
|
-
|
|
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;
|