@tekmidian/pai 0.44.0 → 0.46.0
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/cli/index.mjs +2 -2
- package/dist/cli/program.mjs +2 -2
- package/dist/daemon/index.mjs +3 -3
- package/dist/{daemon-ebLDoKM3.mjs → daemon-DNBEFHaj.mjs} +5 -5
- package/dist/{daemon-ebLDoKM3.mjs.map → daemon-DNBEFHaj.mjs.map} +1 -1
- package/dist/{daemon-BVbvL8IA.mjs → daemon-Dq2ZSdex.mjs} +3 -3
- package/dist/hooks/context-compression-hook.mjs.map +2 -2
- package/dist/hooks/load-project-context.mjs +28 -12
- package/dist/hooks/load-project-context.mjs.map +2 -2
- package/dist/hooks/post-compact-inject.mjs.map +2 -2
- package/dist/{program-cr5U1-DK.mjs → program-Dk_EIMqC.mjs} +4145 -4006
- package/dist/program-Dk_EIMqC.mjs.map +1 -0
- package/dist/{tools-DiRC5glA.mjs → tools-B77WHeVZ.mjs} +2 -2
- package/dist/{tools-DiRC5glA.mjs.map → tools-B77WHeVZ.mjs.map} +1 -1
- package/dist/{tools-CAG2sUjf.mjs → tools-NnR_n-Ja.mjs} +2 -2
- package/dist/{wakeup-BfElduEU.mjs → wakeup-UrhiW-KU.mjs} +27 -14
- package/dist/wakeup-UrhiW-KU.mjs.map +1 -0
- package/docs/commands/config.md +1 -1
- package/package.json +1 -1
- package/src/hooks/ts/lib/context-fill.ts +12 -6
- package/src/hooks/ts/session-start/load-project-context.ts +5 -1
- package/dist/program-cr5U1-DK.mjs.map +0 -1
- package/dist/wakeup-BfElduEU.mjs.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "./embeddings-DvRKcQyt.mjs";
|
|
2
2
|
import "./search-B69cQwDB.mjs";
|
|
3
|
-
import "./wakeup-
|
|
4
|
-
import { A as formatProject, C as toolProjectInfo, D as toolMemoryGet, E as combineHybridResults, O as toolMemorySearch, S as toolProjectHealth, T as toolProjectTodo, _ as toolZettelThemes, a as toolMemoryWakeup, b as toolSessionRoute, c as toolKgInvalidate, d as toolZettelConverse, f as toolZettelExplore, g as toolZettelSurprise, h as toolZettelSuggest, i as toolMemoryTaxonomy, j as lookupProjectId, k as detectProjectFromPath, l as toolKgQuery, m as toolZettelHealth, n as toolMemoryFeedback, o as toolKgAdd, p as toolZettelGodNotes, r as toolMemoryTunnels, s as toolKgContradictions, t as toolMemoryKgSearch, u as toolZettelCommunities, v as toolRegistrySearch, w as toolProjectList, x as toolProjectDetect, y as toolSessionList } from "./tools-
|
|
3
|
+
import "./wakeup-UrhiW-KU.mjs";
|
|
4
|
+
import { A as formatProject, C as toolProjectInfo, D as toolMemoryGet, E as combineHybridResults, O as toolMemorySearch, S as toolProjectHealth, T as toolProjectTodo, _ as toolZettelThemes, a as toolMemoryWakeup, b as toolSessionRoute, c as toolKgInvalidate, d as toolZettelConverse, f as toolZettelExplore, g as toolZettelSurprise, h as toolZettelSuggest, i as toolMemoryTaxonomy, j as lookupProjectId, k as detectProjectFromPath, l as toolKgQuery, m as toolZettelHealth, n as toolMemoryFeedback, o as toolKgAdd, p as toolZettelGodNotes, r as toolMemoryTunnels, s as toolKgContradictions, t as toolMemoryKgSearch, u as toolZettelCommunities, v as toolRegistrySearch, w as toolProjectList, x as toolProjectDetect, y as toolSessionList } from "./tools-B77WHeVZ.mjs";
|
|
5
5
|
|
|
6
6
|
export { toolKgAdd, toolKgContradictions, toolKgInvalidate, toolKgQuery, toolMemoryTunnels, toolZettelCommunities, toolZettelConverse, toolZettelExplore, toolZettelGodNotes, toolZettelHealth, toolZettelSuggest, toolZettelSurprise, toolZettelThemes };
|
|
@@ -128,28 +128,24 @@ function findSessionNotes(notesDir) {
|
|
|
128
128
|
function normalizeLine(line) {
|
|
129
129
|
return line.replace(/^(- \[[ x]\] |- |\* |\d+\.\s)/, "").trim().replace(/\s+/g, " ");
|
|
130
130
|
}
|
|
131
|
-
function extractKeyLines(content, maxChars, seen = /* @__PURE__ */ new Set()) {
|
|
131
|
+
function extractKeyLines(content, maxChars, seen = /* @__PURE__ */ new Set(), seenLabels = /* @__PURE__ */ new Set()) {
|
|
132
132
|
const lines = content.split("\n");
|
|
133
133
|
const selected = [];
|
|
134
134
|
let inTargetSection = false;
|
|
135
135
|
let currentSection = "";
|
|
136
136
|
let charCount = 0;
|
|
137
|
+
let pendingLabel = null;
|
|
137
138
|
for (const line of lines) {
|
|
138
139
|
const h2Match = line.match(/^## (.+)$/);
|
|
139
140
|
const h3Match = line.match(/^### (.+)$/);
|
|
140
141
|
if (h2Match) {
|
|
141
142
|
currentSection = h2Match[1];
|
|
142
143
|
inTargetSection = EXTRACT_SECTIONS.some((s) => currentSection.toLowerCase().includes(s.toLowerCase()));
|
|
144
|
+
pendingLabel = null;
|
|
143
145
|
continue;
|
|
144
146
|
}
|
|
145
147
|
if (h3Match) {
|
|
146
|
-
if (inTargetSection)
|
|
147
|
-
const label = `[${h3Match[1]}]`;
|
|
148
|
-
if (charCount + label.length < maxChars) {
|
|
149
|
-
selected.push(label);
|
|
150
|
-
charCount += label.length + 1;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
148
|
+
if (inTargetSection) pendingLabel = h3Match[1];
|
|
153
149
|
continue;
|
|
154
150
|
}
|
|
155
151
|
if (!inTargetSection) continue;
|
|
@@ -158,7 +154,19 @@ function extractKeyLines(content, maxChars, seen = /* @__PURE__ */ new Set()) {
|
|
|
158
154
|
if (trimmed.startsWith("- ") || trimmed.startsWith("* ") || trimmed.match(/^\d+\./) || trimmed.startsWith("**")) {
|
|
159
155
|
const normalized = normalizeLine(trimmed);
|
|
160
156
|
if (seen.has(normalized)) continue;
|
|
161
|
-
|
|
157
|
+
let labelText = "";
|
|
158
|
+
let labelKey = "";
|
|
159
|
+
if (pendingLabel !== null) {
|
|
160
|
+
labelKey = pendingLabel.toLowerCase().trim();
|
|
161
|
+
if (!seenLabels.has(labelKey)) labelText = `[${pendingLabel}]`;
|
|
162
|
+
}
|
|
163
|
+
if (charCount + labelText.length + trimmed.length + 2 > maxChars) break;
|
|
164
|
+
if (labelText) {
|
|
165
|
+
selected.push(labelText);
|
|
166
|
+
seenLabels.add(labelKey);
|
|
167
|
+
charCount += labelText.length + 1;
|
|
168
|
+
}
|
|
169
|
+
pendingLabel = null;
|
|
162
170
|
seen.add(normalized);
|
|
163
171
|
selected.push(trimmed);
|
|
164
172
|
charCount += trimmed.length + 1;
|
|
@@ -184,6 +192,7 @@ function buildL1EssentialStory(rootPath, tokenBudget = L1_TOKEN_BUDGET) {
|
|
|
184
192
|
if (noteFiles.length === 0) return "";
|
|
185
193
|
const sections = [];
|
|
186
194
|
const seen = /* @__PURE__ */ new Set();
|
|
195
|
+
const seenLabels = /* @__PURE__ */ new Set();
|
|
187
196
|
let remaining = charBudget;
|
|
188
197
|
for (const noteFile of noteFiles) {
|
|
189
198
|
if (remaining <= 50) break;
|
|
@@ -198,8 +207,9 @@ function buildL1EssentialStory(rootPath, tokenBudget = L1_TOKEN_BUDGET) {
|
|
|
198
207
|
const dateLabel = titleMatch ? titleMatch[1] : "";
|
|
199
208
|
const titleLabel = titleMatch ? titleMatch[2] : name.replace(/^\d+ - /, "").replace(/\.md$/, "");
|
|
200
209
|
const perNoteChars = Math.min(remaining, Math.floor(charBudget / noteFiles.length) + 200);
|
|
201
|
-
const extracted = extractKeyLines(content, perNoteChars, seen);
|
|
202
|
-
|
|
210
|
+
const extracted = extractKeyLines(content, perNoteChars, seen, seenLabels);
|
|
211
|
+
const hasContentLine = extracted.split("\n").some((l) => !/^\[.*\]$/.test(l));
|
|
212
|
+
if (!extracted || !hasContentLine) continue;
|
|
203
213
|
const noteBlock = `[${dateLabel} - ${titleLabel}]\n${extracted}`;
|
|
204
214
|
sections.push(noteBlock);
|
|
205
215
|
remaining -= noteBlock.length + 1;
|
|
@@ -215,10 +225,13 @@ function buildL1EssentialStory(rootPath, tokenBudget = L1_TOKEN_BUDGET) {
|
|
|
215
225
|
*
|
|
216
226
|
* @param rootPath Project root path for L1 note lookup. Optional.
|
|
217
227
|
* @param tokenBudget L1 token budget. Default 800.
|
|
228
|
+
* @param opts.skipStory Skip the L1 essential story. Set when a handover
|
|
229
|
+
* checkpoint was already injected — the story re-summarises the same note
|
|
230
|
+
* the handover came from, at lower quality.
|
|
218
231
|
*/
|
|
219
|
-
function buildWakeupContext(rootPath, tokenBudget = L1_TOKEN_BUDGET) {
|
|
232
|
+
function buildWakeupContext(rootPath, tokenBudget = L1_TOKEN_BUDGET, opts = {}) {
|
|
220
233
|
const identity = loadL0Identity();
|
|
221
|
-
const essentialStory = rootPath ? buildL1EssentialStory(rootPath, tokenBudget) : "";
|
|
234
|
+
const essentialStory = rootPath && !opts.skipStory ? buildL1EssentialStory(rootPath, tokenBudget) : "";
|
|
222
235
|
if (!identity && !essentialStory) return "";
|
|
223
236
|
const parts = [];
|
|
224
237
|
if (identity) parts.push(`## L0 Identity\n\n${identity}`);
|
|
@@ -228,4 +241,4 @@ function buildWakeupContext(rootPath, tokenBudget = L1_TOKEN_BUDGET) {
|
|
|
228
241
|
|
|
229
242
|
//#endregion
|
|
230
243
|
export { migrateIdentityFile as n, buildWakeupContext as t };
|
|
231
|
-
//# sourceMappingURL=wakeup-
|
|
244
|
+
//# sourceMappingURL=wakeup-UrhiW-KU.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wakeup-UrhiW-KU.mjs","names":[],"sources":["../src/memory/wakeup.ts"],"sourcesContent":["/**\n * Wake-up context system — progressive context loading inspired by mempalace.\n *\n * Layers:\n * L0 Identity (~100 tokens) — user identity from ~/.pai/identity.txt. Always loaded.\n * L1 Essential Story (~500-800t) — top session notes for the project, key lines extracted.\n * L2 On-Demand — triggered by topic queries (handled by memory_search).\n * L3 Deep Search — unlimited federated memory search (memory_search tool).\n */\n\nimport { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { paiHomePath, migratePaiFile, type MigrateFileResult } from \"../config/pai-home.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Maximum tokens for the L1 essential story block. Approx 4 chars/token.\n * Measured 2026-09-20: 800 tokens was mostly stale bullets. Reduced to 300.\n */\nconst L1_TOKEN_BUDGET = 300;\nconst L1_CHAR_BUDGET = L1_TOKEN_BUDGET * 4; // ~1600 chars\n\n/** Maximum session notes to scan when building L1. Reduced from 10 to 3 (2026-09-20). */\nconst L1_MAX_NOTES = 3;\n\n/** Sections to extract from session notes (in priority order). */\nconst EXTRACT_SECTIONS = [\n \"Work Done\",\n \"Key Decisions\",\n \"Next Steps\",\n \"Checkpoint\",\n];\n\n/** Identity file location. */\nconst IDENTITY_FILE = join(homedir(), \".pai\", \"identity.txt\");\n\n// ---------------------------------------------------------------------------\n// L0: Identity\n// ---------------------------------------------------------------------------\n\n/**\n * Load L0 identity. Prefers the PAI_HOME location (identity.txt migrated by\n * `pai config migrate`), falling back to the legacy ~/.pai/identity.txt so a\n * not-yet-migrated file keeps working. Returns \"\" if neither exists. Never\n * throws.\n */\nexport function loadL0Identity(\n opts: { newPath?: string; legacyPath?: string } = {}\n): string {\n const newPath = opts.newPath ?? paiHomePath(\"identity.txt\");\n const legacyPath = opts.legacyPath ?? IDENTITY_FILE;\n const path = existsSync(newPath) ? newPath : legacyPath;\n if (!existsSync(path)) return \"\";\n try {\n return readFileSync(path, \"utf-8\").trim();\n } catch {\n return \"\";\n }\n}\n\n/**\n * Move ~/.pai/identity.txt into PAI_HOME, following the same copy /\n * verify-byte-identical / rename-aside semantics as every other per-user\n * file (see migratePaiFile). `from`/`to` are overridable for tests.\n */\nexport function migrateIdentityFile(\n opts: { dryRun?: boolean; from?: string; to?: string } = {}\n): MigrateFileResult {\n const toPath = opts.to ?? paiHomePath(\"identity.txt\");\n const fromPath = opts.from ?? IDENTITY_FILE;\n return migratePaiFile(toPath, [fromPath], { dryRun: opts.dryRun });\n}\n\n// ---------------------------------------------------------------------------\n// L1: Essential Story\n// ---------------------------------------------------------------------------\n\n/**\n * Find the Notes directory for a project given its root_path from the registry.\n * Checks local Notes/ first, then central ~/.claude/projects/... path.\n */\nfunction findNotesDirForProject(rootPath: string): string | null {\n // Check local Notes directories first\n const localCandidates = [\n join(rootPath, \"Notes\"),\n join(rootPath, \"notes\"),\n join(rootPath, \".claude\", \"Notes\"),\n ];\n for (const p of localCandidates) {\n if (existsSync(p)) return p;\n }\n\n // Fall back to central ~/.claude/projects/{encoded}/Notes\n const encoded = rootPath\n .replace(/\\//g, \"-\")\n .replace(/\\./g, \"-\")\n .replace(/ /g, \"-\");\n const centralNotes = join(\n homedir(),\n \".claude\",\n \"projects\",\n encoded,\n \"Notes\"\n );\n if (existsSync(centralNotes)) return centralNotes;\n\n return null;\n}\n\n/**\n * Recursively find all .md session note files in a Notes directory.\n * Handles both flat layout (Notes/*.md) and month-subdirectory layout\n * (Notes/YYYY/MM/*.md). Returns files sorted newest-first by filename\n * (note numbers are monotonically increasing, so lexicographic = newest-last,\n * so we reverse).\n */\nfunction findSessionNotes(notesDir: string): string[] {\n const result: string[] = [];\n\n const scanDir = (dir: string) => {\n if (!existsSync(dir)) return;\n let entries: string[];\n try {\n entries = readdirSync(dir, { withFileTypes: true } as Parameters<typeof readdirSync>[1] as any)\n .map((e: any) => ({ name: e.name, isDir: e.isDirectory() }));\n } catch {\n return;\n }\n\n for (const entry of entries as Array<{ name: string; isDir: boolean }>) {\n const fullPath = join(dir, entry.name);\n if (entry.isDir) {\n // Recurse into YYYY/MM subdirectories\n scanDir(fullPath);\n } else if (entry.name.match(/^\\d{3,4}[\\s_-].*\\.md$/)) {\n result.push(fullPath);\n }\n }\n };\n\n scanDir(notesDir);\n\n // Sort newest-first by the DATE in the filename, not the note number.\n //\n // The note number is only monotonic within one uninterrupted numbering run.\n // It restarts — per month directory, and after a registry merge renumbers a\n // project — so \"highest number\" and \"most recent\" diverge. Observed live:\n // `0184 - 2026-02-22` outranked `0008 - 2026-08-01`, and the wake-up context\n // handed a resumed session five-month-old material as its recent history.\n //\n // `NNNN - YYYY-MM-DD - Title.md` is the enforced layout, so the date is the\n // reliable key. Number breaks ties within a day; mtime is the last resort\n // for notes that predate the naming convention.\n const dateOf = (p: string): string =>\n basename(p).match(/(\\d{4}-\\d{2}-\\d{2})/)?.[1] ?? \"\";\n const numberOf = (p: string): number =>\n parseInt(basename(p).match(/^(\\d+)/)?.[1] ?? \"0\", 10);\n const mtimeOf = (p: string): number => {\n try {\n return statSync(p).mtimeMs;\n } catch {\n return 0;\n }\n };\n\n result.sort((a, b) => {\n const dateA = dateOf(a);\n const dateB = dateOf(b);\n // Undated notes sort last rather than winning on an empty string.\n if (dateA !== dateB) {\n if (!dateA) return 1;\n if (!dateB) return -1;\n return dateB.localeCompare(dateA);\n }\n const byNumber = numberOf(b) - numberOf(a);\n if (byNumber !== 0) return byNumber;\n return mtimeOf(b) - mtimeOf(a);\n });\n\n return result;\n}\n\n/**\n * Extract the most important lines from a session note.\n * Prioritises: Work Done items, Key Decisions, Next Steps, Checkpoint headings.\n * Returns a condensed string under maxChars.\n */\nfunction normalizeLine(line: string): string {\n return line\n .replace(/^(- \\[[ x]\\] |- |\\* |\\d+\\.\\s)/, \"\")\n .trim()\n .replace(/\\s+/g, \" \");\n}\n\nfunction extractKeyLines(\n content: string,\n maxChars: number,\n seen: Set<string> = new Set(),\n seenLabels: Set<string> = new Set()\n): string {\n const lines = content.split(\"\\n\");\n const selected: string[] = [];\n let inTargetSection = false;\n let currentSection = \"\";\n let charCount = 0;\n // Heading text seen since the last line was emitted. Buffered rather than\n // pushed immediately: a heading with no surviving lines under it (every\n // line deduped against an earlier note) must not appear at all.\n let pendingLabel: string | null = null;\n\n // First pass: collect lines from priority sections\n for (const line of lines) {\n // Detect section headers\n const h2Match = line.match(/^## (.+)$/);\n const h3Match = line.match(/^### (.+)$/);\n if (h2Match) {\n currentSection = h2Match[1];\n inTargetSection = EXTRACT_SECTIONS.some((s) =>\n currentSection.toLowerCase().includes(s.toLowerCase())\n );\n pendingLabel = null;\n continue;\n }\n if (h3Match) {\n // Checkpoints / sub-sections — buffer as a candidate label.\n if (inTargetSection) {\n pendingLabel = h3Match[1];\n }\n continue;\n }\n\n if (!inTargetSection) continue;\n\n // Skip blank lines and HTML comments\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"<!--\") || trimmed === \"---\") continue;\n\n // Include checkbox items, bold text, and plain text lines\n if (\n trimmed.startsWith(\"- \") ||\n trimmed.startsWith(\"* \") ||\n trimmed.match(/^\\d+\\./) ||\n trimmed.startsWith(\"**\")\n ) {\n const normalized = normalizeLine(trimmed);\n if (seen.has(normalized)) continue;\n\n let labelText = \"\";\n let labelKey = \"\";\n if (pendingLabel !== null) {\n labelKey = pendingLabel.toLowerCase().trim();\n if (!seenLabels.has(labelKey)) {\n labelText = `[${pendingLabel}]`;\n }\n }\n\n if (charCount + labelText.length + trimmed.length + 2 > maxChars) break;\n\n if (labelText) {\n selected.push(labelText);\n seenLabels.add(labelKey);\n charCount += labelText.length + 1;\n }\n pendingLabel = null;\n\n seen.add(normalized);\n selected.push(trimmed);\n charCount += trimmed.length + 1;\n }\n }\n\n return selected.join(\"\\n\");\n}\n\n/**\n * Build the L1 essential story block.\n *\n * Reads the most recent session notes for the project and extracts the key\n * lines (Work Done, Key Decisions, Next Steps) within the token budget.\n *\n * @param rootPath The project root path (from the registry).\n * @param tokenBudget Max tokens to consume. Default 800 (~3200 chars).\n * @returns Formatted L1 block, or empty string if no notes found.\n */\nexport function buildL1EssentialStory(\n rootPath: string,\n tokenBudget = L1_TOKEN_BUDGET\n): string {\n const charBudget = tokenBudget * 4;\n const notesDir = findNotesDirForProject(rootPath);\n if (!notesDir) return \"\";\n\n const noteFiles = findSessionNotes(notesDir).slice(0, L1_MAX_NOTES);\n if (noteFiles.length === 0) return \"\";\n\n const sections: string[] = [];\n const seen = new Set<string>();\n const seenLabels = new Set<string>();\n let remaining = charBudget;\n\n for (const noteFile of noteFiles) {\n if (remaining <= 50) break;\n\n let content: string;\n try {\n content = readFileSync(noteFile, \"utf-8\");\n } catch {\n continue;\n }\n\n // Extract the note date and title from the filename\n const name = basename(noteFile);\n const titleMatch = name.match(/^\\d+ - (\\d{4}-\\d{2}-\\d{2}) - (.+)\\.md$/);\n const dateLabel = titleMatch ? titleMatch[1] : \"\";\n const titleLabel = titleMatch\n ? titleMatch[2]\n : name.replace(/^\\d+ - /, \"\").replace(/\\.md$/, \"\");\n\n // Skip if nothing useful extracted from this note\n const perNoteChars = Math.min(remaining, Math.floor(charBudget / noteFiles.length) + 200);\n const extracted = extractKeyLines(content, perNoteChars, seen, seenLabels);\n // A note whose every extracted line survived only as a label (all its\n // real lines deduped against an earlier, newer note) contributes nothing\n // — including its title.\n const hasContentLine = extracted\n .split(\"\\n\")\n .some((l) => !/^\\[.*\\]$/.test(l));\n if (!extracted || !hasContentLine) continue;\n\n const noteBlock = `[${dateLabel} - ${titleLabel}]\\n${extracted}`;\n sections.push(noteBlock);\n remaining -= noteBlock.length + 1;\n }\n\n if (sections.length === 0) return \"\";\n\n return sections.join(\"\\n\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Combined: buildWakeupContext\n// ---------------------------------------------------------------------------\n\n/**\n * Build the combined wake-up context block (L0 + L1).\n *\n * Returns a formatted string suitable for injection as a system-reminder,\n * or an empty string if both layers are empty.\n *\n * @param rootPath Project root path for L1 note lookup. Optional.\n * @param tokenBudget L1 token budget. Default 800.\n * @param opts.skipStory Skip the L1 essential story. Set when a handover\n * checkpoint was already injected — the story re-summarises the same note\n * the handover came from, at lower quality.\n */\nexport function buildWakeupContext(\n rootPath?: string,\n tokenBudget = L1_TOKEN_BUDGET,\n opts: { skipStory?: boolean } = {}\n): string {\n const identity = loadL0Identity();\n const essentialStory = rootPath && !opts.skipStory\n ? buildL1EssentialStory(rootPath, tokenBudget)\n : \"\";\n\n if (!identity && !essentialStory) return \"\";\n\n const parts: string[] = [];\n\n if (identity) {\n parts.push(`## L0 Identity\\n\\n${identity}`);\n }\n\n if (essentialStory) {\n parts.push(`## L1 Essential Story\\n\\n${essentialStory}`);\n }\n\n return parts.join(\"\\n\\n\");\n}\n"],"mappings":"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"}
|
package/docs/commands/config.md
CHANGED
|
@@ -47,7 +47,7 @@ the new location, or with nothing old to migrate, are skipped.
|
|
|
47
47
|
| Option | Description | Default |
|
|
48
48
|
|--------|-------------|---------|
|
|
49
49
|
| `--dry-run` | Print the plan without writing anything | |
|
|
50
|
-
| `--logs` | Also move ~/.claude/logs/workers into PAI_HOME
|
|
50
|
+
| `--logs` | Also move ~/.claude/logs/workers into PAI_HOME: an atomic rename plus a symlink left at the old path, so any worker still writing there keeps landing in the new directory (falls back to copy+verify across volumes, refusing while a worker is RUNNING — see `pai worker ps` — only in that fallback case) | |
|
|
51
51
|
| `--history` | Also move ~/.claude/History/, agent-sessions.json, session-routing.json and history/security/security-events.jsonl into PAI_HOME. These are written by hooks on EVERY session's every turn, not just spawned workers — refuses while any worker other than the interactive session is RUNNING (see `pai worker ps`), but that guard cannot see OTHER interactive `claude` sessions on this machine. Confirm none are active (`ps aux \| grep claude`, `aibroker sessions list`) before passing this flag. | |
|
|
52
52
|
|
|
53
53
|
|
package/package.json
CHANGED
|
@@ -263,16 +263,22 @@ export function formatContextFill(reading: ContextFillReading): FillDisplay {
|
|
|
263
263
|
// summary ahead of a compaction.
|
|
264
264
|
// ---------------------------------------------------------------------------
|
|
265
265
|
//
|
|
266
|
-
// THE CONFIGURED VALUE
|
|
267
|
-
//
|
|
268
|
-
// following regimes — the same configured number, two
|
|
266
|
+
// THE CONFIGURED VALUE PREDICTS THE TRIGGER ONLY WHILE THE PLATFORM'S FORMULA
|
|
267
|
+
// HOLDS. CLAUDE_AUTOCOMPACT_PCT_OVERRIDE was 80 throughout, on this machine,
|
|
268
|
+
// across BOTH of the following regimes — the same configured number, two
|
|
269
|
+
// different truths:
|
|
269
270
|
//
|
|
270
|
-
// 2026-08-16 .. 2026-09-10 preTokens ~ 993,096 – 1,002,500 (~100% of a 1M window)
|
|
271
|
-
// 2026-09-12 .. 2026-09-
|
|
271
|
+
// 2026-08-16 .. 2026-09-10 preTokens ~ 993,096 – 1,002,500 (~100% of a 1M window; override ignored)
|
|
272
|
+
// 2026-09-12 .. 2026-09-20 preTokens ~ 782,981 – 791,995 (1M window) and
|
|
273
|
+
// preTokens ~ 143,280 – 154,832 (200k window)
|
|
274
|
+
// = pct/100 × (window − 20,000), measured 2026-09-20
|
|
272
275
|
//
|
|
273
276
|
// A threshold derived only from the configured override was wrong for the
|
|
274
|
-
//
|
|
277
|
+
// first regime and will be wrong again the next time the formula moves — the
|
|
275
278
|
// config is not ground truth, it is what Claude Code claims it will honor.
|
|
279
|
+
// Two more facts (2026-09-20): the settings.json `env` value beats the
|
|
280
|
+
// process env, so there is no per-launch override; and headless `-p`
|
|
281
|
+
// sessions do not compact at the derived trigger at all.
|
|
276
282
|
//
|
|
277
283
|
// GROUND TRUTH IS ON DISK ALREADY: every real compaction writes a
|
|
278
284
|
// `compact_boundary` system event to the transcript with
|
|
@@ -503,10 +503,12 @@ Session Commands:
|
|
|
503
503
|
// knew the magic word. Writing a handover nobody delivers is not a
|
|
504
504
|
// handover. Inject it, and say plainly that it is the previous session's
|
|
505
505
|
// state rather than an instruction to act on.
|
|
506
|
+
let handoverInjected = false;
|
|
506
507
|
if (existsSync(todoPath)) {
|
|
507
508
|
try {
|
|
508
509
|
const checkpoint = readContinueCheckpoint(readFileSync(todoPath, 'utf-8'));
|
|
509
510
|
if (checkpoint) {
|
|
511
|
+
handoverInjected = true;
|
|
510
512
|
const from = checkpoint.meta?.session
|
|
511
513
|
? `\nFrom session: ${checkpoint.meta.session}`
|
|
512
514
|
: '';
|
|
@@ -579,7 +581,9 @@ THE ABOVE INSTRUCTIONS ARE MANDATORY. Follow them exactly.
|
|
|
579
581
|
wakeupRootPath = cwd;
|
|
580
582
|
}
|
|
581
583
|
|
|
582
|
-
|
|
584
|
+
// Handover supersedes the L1 story: both summarise the same recent note,
|
|
585
|
+
// and the handover is a curated message while the story is auto-extracted.
|
|
586
|
+
const wakeupBlock = buildWakeupContext(wakeupRootPath, undefined, { skipStory: handoverInjected });
|
|
583
587
|
if (wakeupBlock) {
|
|
584
588
|
const wakeupReminder = `\n<system-reminder>\nWAKEUP CONTEXT\n\n${wakeupBlock}\n</system-reminder>\n`;
|
|
585
589
|
console.log(wakeupReminder);
|