@tekmidian/pai 0.43.0 → 0.45.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.
Files changed (40) hide show
  1. package/dist/{chain-CEC1a5mX.mjs → chain-DAEC9AU7.mjs} +2 -2
  2. package/dist/{chain-CEC1a5mX.mjs.map → chain-DAEC9AU7.mjs.map} +1 -1
  3. package/dist/cli/index.mjs +5 -5
  4. package/dist/cli/program.mjs +5 -5
  5. package/dist/daemon/index.mjs +4 -4
  6. package/dist/{daemon-BhldpFvO.mjs → daemon-DNBEFHaj.mjs} +6 -6
  7. package/dist/{daemon-BhldpFvO.mjs.map → daemon-DNBEFHaj.mjs.map} +1 -1
  8. package/dist/{daemon-QIvCXB5f.mjs → daemon-Dq2ZSdex.mjs} +4 -4
  9. package/dist/daemon-mcp/index.mjs +3 -3
  10. package/dist/{fallback-BQJ8PbCK.mjs → fallback-CxGDTXWt.mjs} +2 -2
  11. package/dist/{fallback-BQJ8PbCK.mjs.map → fallback-CxGDTXWt.mjs.map} +1 -1
  12. package/dist/hooks/context-compression-hook.mjs.map +2 -2
  13. package/dist/hooks/load-project-context.mjs +28 -12
  14. package/dist/hooks/load-project-context.mjs.map +2 -2
  15. package/dist/hooks/mcp-deferred-reminder.mjs.map +1 -1
  16. package/dist/hooks/post-compact-inject.mjs.map +2 -2
  17. package/dist/{planner-BlKGgpWI.mjs → planner-B4DvNpXd.mjs} +3 -3
  18. package/dist/{planner-BlKGgpWI.mjs.map → planner-B4DvNpXd.mjs.map} +1 -1
  19. package/dist/{program-C5jRQ1kg.mjs → program-DvuMelhj.mjs} +645 -55
  20. package/dist/program-DvuMelhj.mjs.map +1 -0
  21. package/dist/{run-ByBKowN5.mjs → run-BgExF8Xg.mjs} +16 -4
  22. package/dist/{run-ByBKowN5.mjs.map → run-BgExF8Xg.mjs.map} +1 -1
  23. package/dist/{tools-DiRC5glA.mjs → tools-B77WHeVZ.mjs} +2 -2
  24. package/dist/{tools-DiRC5glA.mjs.map → tools-B77WHeVZ.mjs.map} +1 -1
  25. package/dist/{tools-CAG2sUjf.mjs → tools-NnR_n-Ja.mjs} +2 -2
  26. package/dist/{wakeup-BfElduEU.mjs → wakeup-UrhiW-KU.mjs} +27 -14
  27. package/dist/wakeup-UrhiW-KU.mjs.map +1 -0
  28. package/docs/commands/README.md +3 -1
  29. package/docs/commands/audit.md +25 -2
  30. package/package.json +1 -1
  31. package/src/hooks/ts/lib/context-fill.test.ts +40 -0
  32. package/src/hooks/ts/lib/context-fill.ts +12 -6
  33. package/src/hooks/ts/session-start/load-project-context.ts +8 -4
  34. package/src/hooks/ts/session-start/mcp-deferred-reminder.ts +2 -2
  35. package/src/hooks/ts/session-start/session-start-worker-guard.test.ts +0 -1
  36. package/dist/hooks/session-commands.mjs +0 -26
  37. package/dist/hooks/session-commands.mjs.map +0 -7
  38. package/dist/program-C5jRQ1kg.mjs.map +0 -1
  39. package/dist/wakeup-BfElduEU.mjs.map +0 -1
  40. package/src/hooks/ts/session-start/session-commands.ts +0 -30
@@ -1,6 +1,6 @@
1
1
  import "./embeddings-DvRKcQyt.mjs";
2
2
  import "./search-B69cQwDB.mjs";
3
- import "./wakeup-BfElduEU.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-DiRC5glA.mjs";
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
- if (charCount + trimmed.length + 1 > maxChars) break;
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
- if (!extracted) continue;
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-BfElduEU.mjs.map
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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"}
@@ -45,16 +45,18 @@ Generated man pages for every `pai` command area. Read any page in the terminal
45
45
  | Command | Description |
46
46
  |---------|-------------|
47
47
  | `pai audit` | Diagnostic audits: token-waste report and friends |
48
- | `pai audit tokens` | Token-waste audit: memory files, hooks, session usage, spawn overhead, daemon, env, schedule, skill catalogue |
48
+ | `pai audit tokens` | Token-waste audit: memory files, hooks, session usage, spawn overhead, daemon, env, schedule, skill catalogue, subagents, MCP |
49
49
  | `pai audit tokens daemon` | LLM spawns, KG-extraction parse failures, work-queue counts from the daemon log |
50
50
  | `pai audit tokens env` | ANTHROPIC_BASE_URL / model-override env on every live claude process |
51
51
  | `pai audit tokens files [paths...]` | Token count per memory file (CLAUDE.md chain, CORE skill, whisper rules, auto-memory) |
52
52
  | `pai audit tokens hooks` | Token cost of every SessionStart / UserPromptSubmit hook |
53
53
  | `pai audit tokens ladder` | LIVE: first-turn context at increasing headless-worker configuration (spawns real claude -p calls) |
54
+ | `pai audit tokens mcp` | MCP servers: configured vs. pinned vs. loaded-live vs. used in the last 30 days |
54
55
  | `pai audit tokens schedule` | Launchd agents / crontab entries that outpace the measured prompt-cache TTL |
55
56
  | `pai audit tokens session [path]` | Cache/input/output token split for a session transcript (default: newest) |
56
57
  | `pai audit tokens skills` | Token cost of the SKILL.md / command / plugin catalogue, top entries, duplicate names |
57
58
  | `pai audit tokens spawn` | Spawn-overhead comparison: Agent-tool subagents vs. pai workers |
59
+ | `pai audit tokens subagents` | Token cost of Claude Code subagent definitions (~/.claude/agents, <cwd>/.claude/agents) and their model pinning |
58
60
  | `pai backup` | Backup registry, config, and Postgres database to PAI_HOME/backups/ |
59
61
  | `pai clear-names` | Recovery: wipe corrupted iTerm2 session name state. |
60
62
  | `pai config` | PAI_HOME namespace dir: path, migrate |
@@ -14,17 +14,18 @@ pai audit <subcommand> [options]
14
14
 
15
15
  | Command | Description |
16
16
  |---------|-------------|
17
- | [`pai audit tokens`](#pai-audit-tokens) | Token-waste audit: memory files, hooks, session usage, spawn overhead, daemon, env, schedule, skill catalogue |
17
+ | [`pai audit tokens`](#pai-audit-tokens) | Token-waste audit: memory files, hooks, session usage, spawn overhead, daemon, env, schedule, skill catalogue, subagents, MCP |
18
18
 
19
19
  ### pai audit tokens
20
20
 
21
- Token-waste audit: memory files, hooks, session usage, spawn overhead, daemon, env, schedule, skill catalogue
21
+ Token-waste audit: memory files, hooks, session usage, spawn overhead, daemon, env, schedule, skill catalogue, subagents, MCP
22
22
 
23
23
  **Options**
24
24
 
25
25
  | Option | Description | Default |
26
26
  |--------|-------------|---------|
27
27
  | `--json` | Print JSON instead of a table | |
28
+ | `--record <dir>` | Write a dated, numbered markdown+JSON snapshot of the combined report to <dir> | |
28
29
 
29
30
 
30
31
  ### pai audit tokens files [paths...]
@@ -53,6 +54,12 @@ Cache/input/output token split for a session transcript (default: newest)
53
54
  |----------|------|-------------|
54
55
  | `[path]` | optional | Session JSONL path (default: newest under ~/.claude/projects) |
55
56
 
57
+ **Options**
58
+
59
+ | Option | Description | Default |
60
+ |--------|-------------|---------|
61
+ | `--ctx-threshold <n>` | Per-turn context size above which a turn counts as 'above threshold' (default 200000) | |
62
+
56
63
 
57
64
  ### pai audit tokens spawn
58
65
 
@@ -92,6 +99,22 @@ Token cost of the SKILL.md / command / plugin catalogue, top entries, duplicate
92
99
  | `--top <n>` | How many top-by-tokens entries to show (default 15) | |
93
100
 
94
101
 
102
+ ### pai audit tokens subagents
103
+
104
+ Token cost of Claude Code subagent definitions (~/.claude/agents, <cwd>/.claude/agents) and their model pinning
105
+
106
+
107
+ ### pai audit tokens mcp
108
+
109
+ MCP servers: configured vs. pinned vs. loaded-live vs. used in the last 30 days
110
+
111
+ **Options**
112
+
113
+ | Option | Description | Default |
114
+ |--------|-------------|---------|
115
+ | `--connect` | Actually connect to every stdio MCP server to count exposed tools (side-effecting; never on by default) | |
116
+
117
+
95
118
  ### pai audit tokens ladder
96
119
 
97
120
  LIVE: first-turn context at increasing headless-worker configuration (spawns real claude -p calls)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekmidian/pai",
3
- "version": "0.43.0",
3
+ "version": "0.45.0",
4
4
  "description": "PAI Knowledge OS — Personal AI Infrastructure with federated memory and project management",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -632,6 +632,46 @@ describe("contextFillThresholds — trigger source", () => {
632
632
  });
633
633
  });
634
634
 
635
+ // ---------------------------------------------------------------------------
636
+ // contextFillThresholds — the 2026-09-20 regime change (override 80 -> 20 on
637
+ // a 1M window). Same min(measured, configured) formula, no code change
638
+ // needed, but the specific numbers of that day are worth pinning down: a
639
+ // project's measured history (784k, from the prior 78-79% regime) is now
640
+ // HIGHER than the newly configured 20%-of-1M trigger (200k), so the clamp
641
+ // must take the configured value, not the stale measured one.
642
+ // ---------------------------------------------------------------------------
643
+
644
+ describe("contextFillThresholds — 2026-09-20 regime change (override 80 -> 20)", () => {
645
+ it("measured 784k + configured 200k (override=20, 1M window) -> clamps to 200k", () => {
646
+ const t = contextFillThresholds(readingAt(1_000_000), { CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: "20" }, { measuredTrigger: 784_000 });
647
+ expect(t.configuredTriggerTokens).toBe(200_000);
648
+ expect(t.triggerSource).toBe("measured-clamped");
649
+ expect(t.effectiveTriggerTokens).toBe(200_000);
650
+ expect(t.warmupTokens).toBe(100_000); // 200,000 - 100,000
651
+ });
652
+
653
+ it("measured 784k + configured 800k (override=80, 1M window) -> measured wins at 784k", () => {
654
+ const t = contextFillThresholds(readingAt(1_000_000), { CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: "80" }, { measuredTrigger: 784_000 });
655
+ expect(t.configuredTriggerTokens).toBe(800_000);
656
+ expect(t.triggerSource).toBe("measured");
657
+ expect(t.effectiveTriggerTokens).toBe(784_000);
658
+ });
659
+
660
+ it("measured null + configured 200k (override=20, no compaction history yet) -> 200k", () => {
661
+ const t = contextFillThresholds(readingAt(1_000_000), { CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: "20" }, { measuredTrigger: null });
662
+ expect(t.triggerSource).toBe("configured");
663
+ expect(t.effectiveTriggerTokens).toBe(200_000);
664
+ });
665
+
666
+ it("measured 784k + override unset (defaults to 80, configured 800k) -> measured wins at 784k", () => {
667
+ const t = contextFillThresholds(readingAt(1_000_000), {}, { measuredTrigger: 784_000 });
668
+ expect(t.autocompactPct).toBe(80);
669
+ expect(t.configuredTriggerTokens).toBe(800_000);
670
+ expect(t.triggerSource).toBe("measured");
671
+ expect(t.effectiveTriggerTokens).toBe(784_000);
672
+ });
673
+ });
674
+
635
675
  // ---------------------------------------------------------------------------
636
676
  // Foreign-model transcripts — a headless worker on another provider (a
637
677
  // different context window) shares the project folder; its compactions must
@@ -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 DOES NOT PREDICT THE TRIGGER. CLAUDE_AUTOCOMPACT_
267
- // PCT_OVERRIDE was 80 throughout, on this machine, across BOTH of the
268
- // following regimes — the same configured number, two different truths:
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-15 preTokens ~ 782,981 – 791,995 (~78-79% of a 1M window)
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
- // second regime and would be wrong again the next time it moves — the
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
@@ -498,15 +498,17 @@ Session Commands:
498
498
  // 8.5. INJECT THE PAUSE CHECKPOINT
499
499
  //
500
500
  // `pai pause` writes the handover to TODO.md under `## Continue`. Until now
501
- // nothing read it back: the session-commands hook only told the model to look
502
- // there *if the user typed "go"*, so a resumed session started blind unless
503
- // the user knew the magic word. Writing a handover nobody delivers is not a
501
+ // nothing read it back: CORE SKILL.md only told the model to look there *if
502
+ // the user typed "go"*, so a resumed session started blind unless the user
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
- const wakeupBlock = buildWakeupContext(wakeupRootPath);
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);
@@ -11,8 +11,8 @@
11
11
  * call: see ../lib/mcp-deferred-reminder.ts for the rationale and text.
12
12
  *
13
13
  * stdout is captured by Claude Code and injected into the session context,
14
- * same channel as session-commands/inject-observations. Never blocks session
15
- * start: always exits 0.
14
+ * same channel as inject-observations. Never blocks session start: always
15
+ * exits 0.
16
16
  */
17
17
 
18
18
  import { decideMcpDeferredReminder, type ReminderHookInput } from "../lib/mcp-deferred-reminder.js";
@@ -15,7 +15,6 @@ describe("session-start hooks: worker sessions get no injected context", () => {
15
15
  "src/hooks/ts/session-start/load-project-context.ts",
16
16
  "src/hooks/ts/session-start/mcp-deferred-reminder.ts",
17
17
  "src/hooks/ts/session-start/post-compact-inject.ts",
18
- "src/hooks/ts/session-start/session-commands.ts",
19
18
  ];
20
19
 
21
20
  function runHook(entrypoint: string, env: NodeJS.ProcessEnv): string {
@@ -1,26 +0,0 @@
1
- #!/usr/bin/env node
2
- import { createRequire as __paiCreateRequire } from 'node:module';
3
- const require = __paiCreateRequire(import.meta.url);
4
- if (!process.env.PAI_QUIET_NOTICES) process.env.PAI_QUIET_NOTICES = '1';
5
-
6
- // src/hooks/ts/lib/worker-session.ts
7
- function isWorkerSession(env = process.env) {
8
- return env.PAI_WORKER === "1";
9
- }
10
-
11
- // src/hooks/ts/session-start/session-commands.ts
12
- if (isWorkerSession()) process.exit(0);
13
- var commands = `<system-reminder>
14
- SESSION COMMANDS (always available):
15
-
16
- - "go" / "continue" / "weiter" \u2192 Read Notes/TODO.md, find ## Continue section, resume work
17
- - "pause session" \u2192 Save state to TODO.md, update session note, stop (no compact)
18
- - "end session" \u2192 Finalize note, commit if needed, start fresh next time
19
- - "cpp" \u2192 Release workflow: version bump \u2192 build \u2192 npm publish \u2192 git add ALL \u2192 ONE commit \u2192 git push
20
-
21
- When user says "go" with no other context, ALWAYS check TODO.md first.
22
-
23
- IMPORTANT: Full PAI context may be truncated due to size. For complete instructions (response format, workflows, principles), READ: ~/.claude/Skills/CORE/SKILL.md
24
- </system-reminder>`;
25
- console.log(commands);
26
- //# sourceMappingURL=session-commands.mjs.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/hooks/ts/lib/worker-session.ts", "../../../src/hooks/ts/session-start/session-commands.ts"],
4
- "sourcesContent": ["/**\n * Worker-session detection.\n *\n * A disposable headless worker (a `claude -p` run started by an orchestrating\n * session, possibly against a different model provider with a different\n * context window) shares the project directory and the hook configuration\n * with the real session that spawned it. Left alone, the hooks treat it as a\n * session in its own right: they inject project context into it, create and\n * rename a numbered session note for it, autosave it, and enqueue a\n * model-written handover for it. Its compactions also land in the project's\n * transcript folder, where they are indistinguishable from a real session's\n * and drag the measured compaction trigger down (observed 2026-09-17: two\n * workers compacting at ~151k tokens pulled a project's trigger from ~784k\n * to ~151k, and the real session's handover fired at ~50k tokens).\n *\n * The launcher marks such sessions with `PAI_WORKER=1`. Every hook that does\n * per-session bookkeeping returns immediately when this predicate is true.\n * Deliberately NOT guarded: the security validator (a worker's shell\n * commands must still be checked) and observability capture.\n */\nexport function isWorkerSession(env: NodeJS.ProcessEnv = process.env): boolean {\n return env.PAI_WORKER === \"1\";\n}\n", "#!/usr/bin/env node\n\n/**\n * session-commands.ts\n *\n * Tiny SessionStart hook that injects critical session commands inline.\n * Stays under 2KB to avoid persisted-output truncation.\n *\n * This runs BEFORE load-core-context so these commands are always available\n * even when the full CORE skill gets truncated.\n */\n\nimport { isWorkerSession } from \"../lib/worker-session.js\";\n\nif (isWorkerSession()) process.exit(0); // disposable worker: no per-session bookkeeping\n\nconst commands = `<system-reminder>\nSESSION COMMANDS (always available):\n\n- \"go\" / \"continue\" / \"weiter\" \u2192 Read Notes/TODO.md, find ## Continue section, resume work\n- \"pause session\" \u2192 Save state to TODO.md, update session note, stop (no compact)\n- \"end session\" \u2192 Finalize note, commit if needed, start fresh next time\n- \"cpp\" \u2192 Release workflow: version bump \u2192 build \u2192 npm publish \u2192 git add ALL \u2192 ONE commit \u2192 git push\n\nWhen user says \"go\" with no other context, ALWAYS check TODO.md first.\n\nIMPORTANT: Full PAI context may be truncated due to size. For complete instructions (response format, workflows, principles), READ: ~/.claude/Skills/CORE/SKILL.md\n</system-reminder>`;\n\nconsole.log(commands);\n"],
5
- "mappings": ";;;;;;AAoBO,SAAS,gBAAgB,MAAyB,QAAQ,KAAc;AAC7E,SAAO,IAAI,eAAe;AAC5B;;;ACRA,IAAI,gBAAgB,EAAG,SAAQ,KAAK,CAAC;AAErC,IAAM,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAajB,QAAQ,IAAI,QAAQ;",
6
- "names": []
7
- }