@pi-unipi/compactor 2.6.0 → 2.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +5 -4
  2. package/package.json +3 -3
  3. package/skills/compactor/SKILL.md +1 -1
  4. package/skills/compactor-detail/SKILL.md +3 -5
  5. package/skills/compactor-doctor/SKILL.md +1 -1
  6. package/skills/compactor-stats/SKILL.md +1 -1
  7. package/src/commands/index.ts +47 -78
  8. package/src/compaction/brief.ts +161 -90
  9. package/src/compaction/build-sections.ts +3 -4
  10. package/src/compaction/compact-args.ts +86 -0
  11. package/src/compaction/cut.ts +270 -28
  12. package/src/compaction/drill-down.ts +261 -0
  13. package/src/compaction/format-recall.ts +96 -0
  14. package/src/compaction/format.ts +8 -3
  15. package/src/compaction/hooks.ts +248 -72
  16. package/src/compaction/merge.ts +34 -4
  17. package/src/compaction/rank.ts +270 -0
  18. package/src/compaction/recall-scope.ts +28 -0
  19. package/src/compaction/search-entries.ts +333 -96
  20. package/src/compaction/skill-collapse.ts +35 -0
  21. package/src/compaction/summarize.ts +37 -6
  22. package/src/compaction/token-estimate.ts +104 -0
  23. package/src/compaction/touched-files.ts +35 -0
  24. package/src/config/manager.ts +2 -27
  25. package/src/config/presets.ts +0 -2
  26. package/src/config/schema.ts +2 -16
  27. package/src/executor/executor.ts +6 -15
  28. package/src/executor/runtime.ts +2 -12
  29. package/src/index.ts +12 -122
  30. package/src/info-screen.ts +3 -10
  31. package/src/security/evaluator.ts +0 -53
  32. package/src/security/policy.ts +7 -8
  33. package/src/session/db.ts +0 -6
  34. package/src/tools/ctx-execute-file.ts +0 -5
  35. package/src/tools/register.ts +27 -50
  36. package/src/tools/vcc-recall.ts +86 -48
  37. package/src/tui/settings-overlay.ts +20 -40
  38. package/src/types.ts +43 -100
  39. package/src/display/diff-renderer.ts +0 -281
  40. package/src/display/line-width-safety.ts +0 -28
  41. package/src/display/render-utils.ts +0 -52
  42. package/src/display/thinking-label.ts +0 -18
  43. package/src/display/tool-overrides.ts +0 -136
  44. package/src/tools/compact.ts +0 -20
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Touched-file aggregation (pi-vcc parity, mode:"touched") — files worked on
3
+ * with their entry indices, aggregated across the searched window.
4
+ */
5
+
6
+ import type { NormalizedBlock } from "../types.js";
7
+ import { extractPath } from "./extract/files.js";
8
+
9
+ /** A file touched in one block — used by mode:touched aggregation. */
10
+ export interface FileTouch {
11
+ index: number;
12
+ toolName: string;
13
+ }
14
+
15
+ /** Aggregated view of a file touched across multiple blocks. */
16
+ export interface TouchedFile {
17
+ path: string;
18
+ entries: FileTouch[];
19
+ }
20
+
21
+ /** Aggregate file operations across tool_call blocks. */
22
+ export function getTouchedFiles(blocks: NormalizedBlock[]): TouchedFile[] {
23
+ const map = new Map<string, TouchedFile>();
24
+ for (const b of blocks) {
25
+ if (b.kind !== "tool_call") continue;
26
+ const path = extractPath(b.args);
27
+ if (!path) continue;
28
+ const index = b.sourceIndex ?? 0;
29
+ if (!map.has(path)) {
30
+ map.set(path, { path, entries: [] });
31
+ }
32
+ map.get(path)!.entries.push({ index, toolName: b.name });
33
+ }
34
+ return Array.from(map.values());
35
+ }
@@ -91,36 +91,11 @@ export function saveConfig(config: CompactorConfig, opts?: { perProject?: boolea
91
91
 
92
92
  /**
93
93
  * Migrate partial config to full schema, filling missing keys from defaults.
94
+ * Uses deepMerge so nested strategy objects merge recursively.
94
95
  */
95
96
  export function migrateConfig(partial: Partial<CompactorConfig>): CompactorConfig {
96
97
  const defaults = structuredClone(DEFAULT_COMPACTOR_CONFIG);
97
-
98
- function mergeStrategy<K extends keyof CompactorConfig>(
99
- key: K,
100
- defaultVal: CompactorConfig[K],
101
- partialVal: CompactorConfig[K] | undefined,
102
- ): CompactorConfig[K] {
103
- if (!partialVal || typeof partialVal !== "object") return defaultVal;
104
- return { ...(defaultVal as any), ...(partialVal as any) };
105
- }
106
-
107
- return {
108
- sessionGoals: mergeStrategy("sessionGoals", defaults.sessionGoals, partial.sessionGoals),
109
- filesAndChanges: mergeStrategy("filesAndChanges", defaults.filesAndChanges, partial.filesAndChanges),
110
- commits: mergeStrategy("commits", defaults.commits, partial.commits),
111
- outstandingContext: mergeStrategy("outstandingContext", defaults.outstandingContext, partial.outstandingContext),
112
- userPreferences: mergeStrategy("userPreferences", defaults.userPreferences, partial.userPreferences),
113
- briefTranscript: mergeStrategy("briefTranscript", defaults.briefTranscript, partial.briefTranscript),
114
- sessionContinuity: mergeStrategy("sessionContinuity", defaults.sessionContinuity, partial.sessionContinuity),
115
- fts5Index: mergeStrategy("fts5Index", defaults.fts5Index, partial.fts5Index),
116
- sandboxExecution: mergeStrategy("sandboxExecution", defaults.sandboxExecution, partial.sandboxExecution),
117
- toolDisplay: mergeStrategy("toolDisplay", defaults.toolDisplay, partial.toolDisplay),
118
- pipeline: mergeStrategy("pipeline", defaults.pipeline, (partial as any).pipeline) as any,
119
- autoCompaction: mergeStrategy("autoCompaction", defaults.autoCompaction, partial.autoCompaction),
120
- overrideDefaultCompaction: partial.overrideDefaultCompaction ?? defaults.overrideDefaultCompaction,
121
- debug: partial.debug ?? defaults.debug,
122
- showTruncationHints: partial.showTruncationHints ?? defaults.showTruncationHints,
123
- };
98
+ return deepMerge(defaults, partial);
124
99
  }
125
100
 
126
101
  /**
@@ -34,8 +34,6 @@ const preset = (
34
34
  // Legacy compatibility data only; presets never advertise or alter it.
35
35
  fts5Index: { ...DEFAULT_COMPACTOR_CONFIG.fts5Index },
36
36
  sandboxExecution: { ...DEFAULT_COMPACTOR_CONFIG.sandboxExecution, ...(overrides.sandboxExecution ?? {}) },
37
- // Legacy compatibility data only; presets never advertise or alter it.
38
- toolDisplay: { ...DEFAULT_COMPACTOR_CONFIG.toolDisplay },
39
37
  pipeline: pipeline(overrides.pipeline),
40
38
  autoCompaction: { ...DEFAULT_COMPACTOR_CONFIG.autoCompaction, ...(overrides.autoCompaction ?? {}) },
41
39
  });
@@ -39,23 +39,8 @@ export const DEFAULT_COMPACTOR_CONFIG: CompactorConfig = {
39
39
  allowedLanguages: ["javascript", "typescript", "python", "shell"],
40
40
  outputLimit: 100 * 1024 * 1024,
41
41
  },
42
- toolDisplay: {
43
- ...strategy(true, "opencode"),
44
- mode: "opencode",
45
- diffLayout: "auto",
46
- diffIndicator: "bars",
47
- showThinkingLabels: true,
48
- showUserMessageBox: true,
49
- showBashSpinner: true,
50
- showPendingPreviews: true,
51
- },
52
42
  pipeline: {
53
- ttlCache: false,
54
43
  autoInjection: false,
55
- proximityReranking: false,
56
- timelineSort: false,
57
- progressiveThrottling: false,
58
- mmapPragma: false,
59
44
  customNoisePatterns: [],
60
45
  },
61
46
  autoCompaction: {
@@ -66,6 +51,7 @@ export const DEFAULT_COMPACTOR_CONFIG: CompactorConfig = {
66
51
  notify: true,
67
52
  },
68
53
  overrideDefaultCompaction: true,
54
+ smartKeepTail: true,
55
+ continueAfterThresholdCompact: true,
69
56
  debug: false,
70
- showTruncationHints: true,
71
57
  };
@@ -2,7 +2,7 @@
2
2
  * PolyglotExecutor — sandboxed code execution for 11 languages
3
3
  */
4
4
 
5
- import { spawn, execSync, execFileSync } from "node:child_process";
5
+ import { spawn, execSync } from "node:child_process";
6
6
  import { mkdtempSync, writeFileSync, rmSync, existsSync } from "node:fs";
7
7
  import { join, resolve } from "node:path";
8
8
  import { tmpdir } from "node:os";
@@ -11,19 +11,7 @@ import type { ExecResult } from "../types.js";
11
11
 
12
12
  const isWin = process.platform === "win32";
13
13
 
14
- const OS_TMPDIR = (() => {
15
- if (isWin) return process.env.TEMP ?? process.env.TMP ?? tmpdir();
16
- try {
17
- const result = execFileSync(
18
- process.platform === "darwin" ? "getconf" : "mktemp",
19
- process.platform === "darwin" ? ["DARWIN_USER_TEMP_DIR"] : ["-u", "-d"],
20
- { env: { ...process.env, TMPDIR: undefined as unknown as string }, encoding: "utf-8" },
21
- ).trim();
22
- const dir = process.platform === "darwin" ? result : resolve(result, "..");
23
- if (dir && dir !== process.cwd()) return dir;
24
- } catch { /* fall through */ }
25
- return "/tmp";
26
- })();
14
+ const OS_TMPDIR = isWin ? (process.env.TEMP ?? process.env.TMP ?? tmpdir()) : tmpdir();
27
15
 
28
16
  function killTree(proc: ReturnType<typeof spawn>): void {
29
17
  if (isWin && proc.pid) {
@@ -73,8 +61,11 @@ interface ExecuteOptions {
73
61
  background?: boolean;
74
62
  }
75
63
 
76
- interface ExecuteFileOptions extends ExecuteOptions {
64
+ interface ExecuteFileOptions {
65
+ language: Language;
77
66
  path: string;
67
+ timeout?: number;
68
+ background?: boolean;
78
69
  }
79
70
 
80
71
  export class PolyglotExecutor {
@@ -2,18 +2,8 @@
2
2
  * Runtime detection for sandbox executor
3
3
  */
4
4
 
5
- export type Language =
6
- | "javascript"
7
- | "typescript"
8
- | "python"
9
- | "shell"
10
- | "ruby"
11
- | "go"
12
- | "rust"
13
- | "php"
14
- | "perl"
15
- | "r"
16
- | "elixir";
5
+ import type { Language } from "../types.js";
6
+ export type { Language };
17
7
 
18
8
  export interface RuntimeMap {
19
9
  javascript: string;
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
- import { MODULES, UNIPI_EVENTS, COMPACTOR_COMMANDS, COMPACTOR_TOOLS, COMPACTOR_INSTRUCTION, emitEvent } from "@pi-unipi/core";
6
+ import { MODULES, UNIPI_EVENTS, COMPACTOR_COMMANDS, COMPACTOR_TOOLS, COMPACTOR_INSTRUCTION, emitEvent, formatTokens } from "@pi-unipi/core";
7
7
  import { scaffoldConfig, loadConfig } from "./config/manager.js";
8
8
  import { registerCompactionHooks } from "./compaction/hooks.js";
9
9
  import {
@@ -22,45 +22,13 @@ import { registerCompactorTools } from "./tools/register.js";
22
22
  import { normalizeMessages } from "./compaction/normalize.js";
23
23
  import { filterNoise } from "./compaction/filter-noise.js";
24
24
  import { recallBlocksFromContext } from "./session/recall-blocks.js";
25
- import type { NormalizedBlock, CompactorStrategyConfig, RuntimeCounters, RuntimeStats } from "./types.js";
25
+ import type { NormalizedBlock, CompactorStrategyConfig, RuntimeCounters } from "./types.js";
26
26
 
27
- /** Debug logger — only logs when config.debug === true */
28
- function createDebugLogger(getConfig: () => { debug: boolean }) {
29
- return (_event: string, _data?: Record<string, unknown>) => {
30
- // Debug logging disabled — was writing to stdout causing TUI rendering issues.
31
- return;
32
- };
33
- }
34
27
 
35
- const formatTokenCount = (n: number): string => {
36
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
37
- if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
38
- return String(n);
39
- };
40
28
 
41
29
  /** Measure byte size of a tool_result event's response content. */
42
- function measureResponseBytes(event: any): number {
43
- try {
44
- const content = event.content;
45
- if (typeof content === "string") return Buffer.byteLength(content, "utf-8");
46
- if (Array.isArray(content)) {
47
- return content.reduce((sum: number, block: any) => {
48
- if (typeof block?.text === "string") return sum + Buffer.byteLength(block.text, "utf-8");
49
- if (typeof block === "string") return sum + Buffer.byteLength(block, "utf-8");
50
- return sum;
51
- }, 0);
52
- }
53
- if (event.output && typeof event.output === "string") return Buffer.byteLength(event.output, "utf-8");
54
- } catch {
55
- // Non-blocking: byte measurement errors silently skipped
56
- }
57
- return 0;
58
- }
59
30
 
60
31
  /** Check if a tool is a sandbox tool (output stays in sandbox, not context). */
61
- function isSandboxTool(name: string): boolean {
62
- return name === "bash" || name === "Bash";
63
- }
64
32
 
65
33
  export default function compactorExtension(pi: ExtensionAPI): void {
66
34
  let sessionDB: SessionDB | null = null;
@@ -78,17 +46,6 @@ export default function compactorExtension(pi: ExtensionAPI): void {
78
46
  };
79
47
  const getCounters = () => counters;
80
48
 
81
- const runtimeStats: RuntimeStats = {
82
- bytesReturned: {},
83
- bytesSandboxed: 0,
84
- calls: {},
85
- sessionStart: Date.now(),
86
- cacheHits: 0,
87
- cacheBytesSaved: 0,
88
- };
89
-
90
- const debug = createDebugLogger(() => config);
91
-
92
49
  const init = async () => {
93
50
  scaffoldConfig();
94
51
  config = loadConfig();
@@ -134,8 +91,6 @@ export default function compactorExtension(pi: ExtensionAPI): void {
134
91
  const fullSessionId = `${sessionId}${suffix}`;
135
92
  currentSessionId = fullSessionId;
136
93
 
137
- debug("session_start", { sessionId: fullSessionId, projectDir });
138
-
139
94
  // Seed runtime counters from DB so they reflect prior usage
140
95
  if (sessionDB) {
141
96
  try {
@@ -148,12 +103,6 @@ export default function compactorExtension(pi: ExtensionAPI): void {
148
103
  }
149
104
 
150
105
  // Reset runtime stats for new session
151
- runtimeStats.bytesReturned = {};
152
- runtimeStats.bytesSandboxed = 0;
153
- runtimeStats.calls = {};
154
- runtimeStats.sessionStart = Date.now();
155
- runtimeStats.cacheHits = 0;
156
- runtimeStats.cacheBytesSaved = 0;
157
106
  autoCompactionState = createAutoCompactionState();
158
107
 
159
108
  sessionDB?.ensureSession(fullSessionId, projectDir);
@@ -226,8 +175,6 @@ export default function compactorExtension(pi: ExtensionAPI): void {
226
175
  tools: Object.values(COMPACTOR_TOOLS),
227
176
  });
228
177
 
229
- debug("MODULE_READY", { commands: Object.values(COMPACTOR_COMMANDS), tools: Object.values(COMPACTOR_TOOLS) });
230
-
231
178
  ctx.ui.notify("🗜️ Compactor ready", "info");
232
179
  });
233
180
 
@@ -235,7 +182,6 @@ export default function compactorExtension(pi: ExtensionAPI): void {
235
182
  const cwd = (ctx as any).cwd ?? process.cwd();
236
183
  config = loadConfig(cwd);
237
184
  currentSessionId = `${(ctx as any).sessionId ?? "default"}${getWorktreeSuffix()}`;
238
- debug("before_agent_start", { sessionId: currentSessionId, configDebug: config.debug });
239
185
 
240
186
  // Evaluate autoDetect conditions for strategies
241
187
  try {
@@ -248,7 +194,6 @@ export default function compactorExtension(pi: ExtensionAPI): void {
248
194
  if ((strat as any).autoDetect === "git") {
249
195
  const gitDir = join(cwd, ".git");
250
196
  if (!existsSync(gitDir)) {
251
- debug("autoDetect_disable", { strategy: key, reason: "no .git dir" });
252
197
  // Non-destructive: temporarily disable at runtime, don't modify config file
253
198
  strat.enabled = false;
254
199
  }
@@ -280,12 +225,18 @@ export default function compactorExtension(pi: ExtensionAPI): void {
280
225
  const continuityEnabled = isSessionContinuityEnabled(config);
281
226
  if (sessionDB && continuityEnabled) {
282
227
  const resumeContext = await buildResumeContextMessage(sessionDB, currentSessionId);
283
- debug("resume_snapshot", { injected: !!resumeContext });
284
228
  return resumeContext;
285
229
  }
286
230
  });
287
231
 
288
- pi.on("turn_end", async (_event, ctx) => {
232
+ // Trigger percentage auto-compaction at agent_end, NOT turn_end.
233
+ // Pi's ctx.compact() aborts the active agent operation first
234
+ // (AgentSession.compact() -> await this.abort()). turn_end fires between
235
+ // turns of a still-running agent loop, so compacting there kills the next
236
+ // in-flight provider request ("This operation was aborted" stopReason=error
237
+ // turns). agent_end fires only after the run has fully settled, matching
238
+ // Pi core's own native auto-compaction check point ("checked on agent_end").
239
+ pi.on("agent_end", async (_event, ctx) => {
289
240
  const cwd = (ctx as any).cwd ?? process.cwd();
290
241
  config = loadConfig(cwd);
291
242
 
@@ -297,23 +248,12 @@ export default function compactorExtension(pi: ExtensionAPI): void {
297
248
  });
298
249
  autoCompactionState = decision.state;
299
250
 
300
- debug("auto_compaction_decision", {
301
- enabled: config.autoCompaction.enabled,
302
- reason: decision.reason,
303
- shouldTrigger: decision.shouldTrigger,
304
- percent: decision.usage?.percent,
305
- tokens: decision.usage?.tokens,
306
- thresholdPercent: decision.thresholdPercent,
307
- cooldownRemainingMs: decision.cooldownRemainingMs,
308
- tokenGrowth: decision.tokenGrowth,
309
- });
310
-
311
251
  if (!decision.shouldTrigger) return;
312
252
 
313
253
  const notify = config.autoCompaction.notify;
314
254
  if (notify && decision.usage) {
315
255
  ctx.ui.notify(
316
- `Auto-compacting at ${decision.usage.percent.toFixed(1)}% context (~${formatTokenCount(decision.usage.tokens)} tokens; threshold ${decision.thresholdPercent}%).`,
256
+ `Auto-compacting at ${decision.usage.percent.toFixed(1)}% context (~${formatTokens(decision.usage.tokens)} tokens; threshold ${decision.thresholdPercent}%).`,
317
257
  "info",
318
258
  );
319
259
  }
@@ -351,7 +291,6 @@ export default function compactorExtension(pi: ExtensionAPI): void {
351
291
  const sessionId = currentSessionId;
352
292
  const events = sessionDB.getEvents(sessionId, { limit: 1000 });
353
293
  const stats = sessionDB.getSessionStats(sessionId);
354
- debug("session_before_compact", { sessionId, eventCount: events.length, compactCount: stats?.compact_count ?? 0 });
355
294
  const { buildResumeSnapshot } = await import("./session/snapshot.js");
356
295
  const snapshot = buildResumeSnapshot(events, {
357
296
  compactCount: stats?.compact_count ?? 1,
@@ -419,12 +358,10 @@ export default function compactorExtension(pi: ExtensionAPI): void {
419
358
  tokensSaved,
420
359
  compressionRatio: kept > 0 ? `${Math.round(totalEstimated / kept)}:1` : "0:1",
421
360
  });
422
- debug("session_compact", { sessionId, tokensBefore, hasCompactionEntry: !!compactionEntry });
423
361
  }
424
362
  });
425
363
 
426
364
  pi.on("session_shutdown", async (_event, _ctx) => {
427
- debug("session_shutdown");
428
365
  if (sessionDB) {
429
366
  sessionDB.cleanupOldSessions(7);
430
367
  }
@@ -435,7 +372,6 @@ export default function compactorExtension(pi: ExtensionAPI): void {
435
372
  pi.on("input", async (event, _ctx) => {
436
373
  const toolName = (event as any).toolName ?? "";
437
374
  const args = (event as any).args ?? {};
438
- debug("input", { toolName, args: JSON.stringify(args).slice(0, 200) });
439
375
 
440
376
  // Existing network tool guard
441
377
  if (toolName === "bash" || toolName === "Bash") {
@@ -447,7 +383,7 @@ export default function compactorExtension(pi: ExtensionAPI): void {
447
383
 
448
384
  // Security scanner/evaluator wiring (fail-open pattern)
449
385
  try {
450
- const { evaluateCommand, evaluateFilePath, loadProjectPermissions } = await import("./security/evaluator.js");
386
+ const { evaluateCommand, evaluateFilePath } = await import("./security/evaluator.js");
451
387
  const { hasShellEscapes, scanForShellEscapes } = await import("./security/scanner.js");
452
388
  const { readsOrCreatesPolicy } = await import("./security/policy.js");
453
389
 
@@ -461,7 +397,6 @@ export default function compactorExtension(pi: ExtensionAPI): void {
461
397
  if (cmd) {
462
398
  const decision = evaluateCommand(cmd, denyPolicy);
463
399
  if (decision === "deny") {
464
- debug("security_deny", { toolName, cmd: cmd.slice(0, 100) });
465
400
  return {
466
401
  content: [{ type: "text", text: `Command blocked by security policy: ${cmd.slice(0, 80)}` }],
467
402
  isError: true,
@@ -478,7 +413,6 @@ export default function compactorExtension(pi: ExtensionAPI): void {
478
413
  if (language && language !== "shell" && code) {
479
414
  if (hasShellEscapes(code, language)) {
480
415
  const findings = scanForShellEscapes(code, language);
481
- debug("security_shell_escapes", { toolName, language, findings });
482
416
  // Fail-open: log but don't block (the hooks system is enforcement)
483
417
  }
484
418
  }
@@ -491,14 +425,12 @@ export default function compactorExtension(pi: ExtensionAPI): void {
491
425
  if (filePath) {
492
426
  const decision = evaluateFilePath(filePath, denyPolicy, cwd);
493
427
  if (decision === "deny") {
494
- debug("security_deny_file", { toolName, filePath });
495
428
  // Non-fatal: log warning but allow through (fail-open)
496
429
  }
497
430
  }
498
431
  }
499
432
  } catch (err) {
500
433
  // Fail-open: security checks are advisory, never block on errors
501
- debug("security_check_error", { error: String(err) });
502
434
  }
503
435
 
504
436
  return undefined;
@@ -511,8 +443,6 @@ export default function compactorExtension(pi: ExtensionAPI): void {
511
443
  const toolNameRaw = (event as any).toolName ?? "";
512
444
  const isError = (event as any).isError ?? false;
513
445
 
514
- debug("tool_result", { toolName: toolNameRaw, isError, sessionId });
515
-
516
446
  // Extract and store session events
517
447
  const toolEvents = extractEventsFromToolResult({
518
448
  toolName: (event as any).toolName ?? "",
@@ -523,22 +453,6 @@ export default function compactorExtension(pi: ExtensionAPI): void {
523
453
 
524
454
  for (const ev of toolEvents) {
525
455
  sessionDB.insertEvent(sessionId, ev, "PostToolUse");
526
- debug("event_stored", { category: ev.category, type: ev.type });
527
- }
528
-
529
- // Track byte consumption per tool for analytics
530
- try {
531
- const responseBytes = measureResponseBytes(event);
532
- if (responseBytes > 0) {
533
- const tName = (event as any).toolName ?? "unknown";
534
- runtimeStats.calls[tName] = (runtimeStats.calls[tName] || 0) + 1;
535
- runtimeStats.bytesReturned[tName] = (runtimeStats.bytesReturned[tName] || 0) + responseBytes;
536
- if (isSandboxTool(tName)) {
537
- runtimeStats.bytesSandboxed += responseBytes;
538
- }
539
- }
540
- } catch {
541
- // Non-blocking: byte tracking errors silently skipped
542
456
  }
543
457
 
544
458
  const toolName = (event as any).toolName ?? "";
@@ -558,36 +472,12 @@ export default function compactorExtension(pi: ExtensionAPI): void {
558
472
  );
559
473
  const clamped = clampDiffToWidth(details.diff);
560
474
  if (clamped !== details.diff) {
561
- debug("diff_width_clamped", { toolName });
562
475
  return { details: { ...details, diff: clamped } } as any;
563
476
  }
564
477
  }
565
478
  } catch (err) {
566
- debug("diff_width_clamp_error", { error: String(err) });
567
479
  }
568
480
  }
569
481
  });
570
482
 
571
- pi.on("message_update", async (event, _ctx) => {
572
- const msg = (event as any).message;
573
- if (msg?.thinking) {
574
- debug("message_update", { thinking: true, length: String(msg.thinking).length });
575
- }
576
- });
577
-
578
- pi.on("message_end", async (_event, _ctx) => {
579
- debug("message_end");
580
- });
581
-
582
- pi.on("context", async (event, _ctx) => {
583
- const { sanitizeThinkingArtifacts } = await import("./display/thinking-label.js");
584
- const ctxStr = (event as any).context;
585
- if (typeof ctxStr === "string") {
586
- const sanitized = sanitizeThinkingArtifacts(ctxStr);
587
- if (sanitized !== ctxStr) {
588
- debug("context", { sanitized: true, beforeLen: ctxStr.length, afterLen: sanitized.length });
589
- }
590
- (event as any).context = sanitized;
591
- }
592
- });
593
483
  }
@@ -1,3 +1,4 @@
1
+ import { formatTokens } from "@pi-unipi/core";
1
2
  /**
2
3
  * Info-screen integration for @pi-unipi/compactor
3
4
  *
@@ -11,7 +12,7 @@
11
12
  */
12
13
 
13
14
  import type { SessionDB } from "./session/db.js";
14
- import { getLastCompactionStats } from "./compaction/hooks.js";
15
+ import { getLastCompactionStats, formatCompactionStats } from "./compaction/hooks.js";
15
16
  import { parseUsageStatsAsync } from "@pi-unipi/info-screen/usage-parser.js";
16
17
  import type { RuntimeCounters } from "./types.js";
17
18
 
@@ -25,14 +26,6 @@ export interface CompactorInfoData {
25
26
  }
26
27
 
27
28
  /** Format token count for display (e.g., "12.4k", "1.2M"). */
28
- function formatTokens(n: number): string {
29
- if (n < 1000) return String(n);
30
- if (n < 10_000) return `${(n / 1000).toFixed(1)}k`;
31
- if (n < 1_000_000) return `${Math.round(n / 1000)}k`;
32
- if (n < 10_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
33
- return `${Math.round(n / 1_000_000)}M`;
34
- }
35
-
36
29
  /** Format cost for display (e.g., "$0.34", "<$0.01"). */
37
30
  function formatCost(n: number): string {
38
31
  if (n === 0) return "$0.00";
@@ -184,7 +177,7 @@ export async function getInfoScreenData(
184
177
  compactions: {
185
178
  value: String(compactionCount),
186
179
  detail: compactStats
187
- ? `Last: ${compactStats.totalMessages} messages (~${formatTokens(compactStats.tokensBefore)} tokens) → ${compactStats.kept} messages (~${formatTokens(compactStats.tokensAfterEst)} tokens)`
180
+ ? `Last: ${formatCompactionStats(compactStats)}`
188
181
  : compactionCount > 0
189
182
  ? `${compactionCount} compaction(s) across all sessions`
190
183
  : "No compactions yet",
@@ -14,28 +14,6 @@ import { parseBashPattern, parseToolPattern, globToRegex, fileGlobToRegex } from
14
14
  * Load permission patterns from .pi/settings.json in the given directory.
15
15
  * Returns a SecurityPolicy merged with the provided policy.
16
16
  */
17
- export function loadProjectPermissions(
18
- cwd: string,
19
- basePolicy: SecurityPolicy,
20
- ): SecurityPolicy {
21
- const settingsPath = join(cwd, ".pi", "settings.json");
22
- if (!existsSync(settingsPath)) return basePolicy;
23
-
24
- try {
25
- const raw = readFileSync(settingsPath, "utf-8");
26
- const settings = JSON.parse(raw);
27
- const permissions = settings.permissions ?? settings.security ?? {};
28
-
29
- return {
30
- deny: [...basePolicy.deny, ...(permissions.deny ?? [])],
31
- ask: [...basePolicy.ask, ...(permissions.ask ?? [])],
32
- allow: [...basePolicy.allow, ...(permissions.allow ?? [])],
33
- };
34
- } catch {
35
- return basePolicy;
36
- }
37
- }
38
-
39
17
  export function evaluateCommand(
40
18
  command: string,
41
19
  policy: SecurityPolicy,
@@ -73,37 +51,6 @@ export function evaluateCommand(
73
51
  return "allow";
74
52
  }
75
53
 
76
- export function splitChainedCommands(command: string): string[] {
77
- const commands: string[] = [];
78
- let current = "";
79
- let inQuotes = false;
80
- let quoteChar = "";
81
-
82
- for (let i = 0; i < command.length; i++) {
83
- const char = command[i];
84
-
85
- if (!inQuotes && (char === '"' || char === "'" || char === "`")) {
86
- inQuotes = true;
87
- quoteChar = char;
88
- current += char;
89
- } else if (inQuotes && char === quoteChar) {
90
- inQuotes = false;
91
- quoteChar = "";
92
- current += char;
93
- } else if (!inQuotes && (char === "&" || char === "|" || char === ";")) {
94
- if (current.trim()) commands.push(current.trim());
95
- current = "";
96
- // Skip the next char if it's part of && or ||
97
- if ((char === "&" || char === "|") && command[i + 1] === char) i++;
98
- } else {
99
- current += char;
100
- }
101
- }
102
-
103
- if (current.trim()) commands.push(current.trim());
104
- return commands;
105
- }
106
-
107
54
  export function evaluateFilePath(
108
55
  filePath: string,
109
56
  policy: SecurityPolicy,
@@ -23,14 +23,13 @@ export function parseToolPattern(pattern: string): { tool: string; glob: string
23
23
  return match ? { tool: match[1], glob: match[2] } : null;
24
24
  }
25
25
 
26
- function escapeRegex(str: string): string {
27
- return str.replace(/[.*+?^${}()|[\]\\/\-]/g, "\\$&");
28
- }
29
-
30
26
  function convertGlobPart(glob: string): string {
27
+ // Replace wildcards FIRST, then escape the literal remainder — escaping first
28
+ // would turn `*` into `\*` and the wildcard replace would never fire.
31
29
  return glob
32
- .replace(/[.+?^${}()|[\]\\/\-]/g, "\\$&")
33
- .replace(/\*/g, ".*");
30
+ .split("*")
31
+ .map((part) => RegExp.escape(part))
32
+ .join(".*");
34
33
  }
35
34
 
36
35
  export function globToRegex(glob: string, caseInsensitive: boolean = false): RegExp {
@@ -39,7 +38,7 @@ export function globToRegex(glob: string, caseInsensitive: boolean = false): Reg
39
38
  if (colonIdx !== -1) {
40
39
  const command = glob.slice(0, colonIdx);
41
40
  const argsGlob = glob.slice(colonIdx + 1);
42
- const escapedCmd = escapeRegex(command);
41
+ const escapedCmd = RegExp.escape(command);
43
42
  const argsRegex = convertGlobPart(argsGlob);
44
43
  regexStr = `^${escapedCmd}(\\s${argsRegex})?$`;
45
44
  } else {
@@ -68,7 +67,7 @@ export function fileGlobToRegex(glob: string, caseInsensitive: boolean = false):
68
67
  regexStr += "[^/]";
69
68
  i++;
70
69
  } else {
71
- regexStr += escapeRegex(glob[i]);
70
+ regexStr += RegExp.escape(glob[i]);
72
71
  i++;
73
72
  }
74
73
  }
package/src/session/db.ts CHANGED
@@ -191,7 +191,6 @@ export class SessionDB {
191
191
  p("addCompactionStats", `UPDATE session_meta SET total_chars_before = total_chars_before + ?, total_chars_kept = total_chars_kept + ?, total_messages_summarized = total_messages_summarized + ? WHERE session_id = ?`);
192
192
  p("getAllTimeStats", `SELECT COALESCE(SUM(total_chars_before), 0) AS all_chars_before, COALESCE(SUM(total_chars_kept), 0) AS all_chars_kept, COALESCE(SUM(total_messages_summarized), 0) AS all_messages_summarized, COALESCE(SUM(compact_count), 0) AS all_compactions, COALESCE(SUM(sandbox_runs), 0) AS all_sandbox_runs, COALESCE(SUM(search_queries), 0) AS all_search_queries FROM session_meta`);
193
193
  p("incrementSandboxRuns", `UPDATE session_meta SET sandbox_runs = sandbox_runs + 1 WHERE session_id = ?`);
194
- p("incrementSearchQueries", `UPDATE session_meta SET search_queries = search_queries + 1 WHERE session_id = ?`);
195
194
  p("upsertResume", `INSERT INTO session_resume (session_id, snapshot, event_count) VALUES (?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET snapshot = excluded.snapshot, event_count = excluded.event_count, created_at = datetime('now'), consumed = 0`);
196
195
  p("getResume", `SELECT snapshot, event_count, consumed FROM session_resume WHERE session_id = ?`);
197
196
  p("markResumeConsumed", `UPDATE session_resume SET consumed = 1 WHERE session_id = ?`);
@@ -283,11 +282,6 @@ export class SessionDB {
283
282
  this.stmt("incrementSandboxRuns").run(sessionId);
284
283
  }
285
284
 
286
- incrementSearchQueries(sessionId: string): void {
287
- if (!this.stmts) return;
288
- this.stmt("incrementSearchQueries").run(sessionId);
289
- }
290
-
291
285
  upsertResume(sessionId: string, snapshot: string, eventCount?: number): void {
292
286
  if (!this.stmts) return;
293
287
  this.stmt("upsertResume").run(sessionId, snapshot, eventCount ?? 0);
@@ -4,7 +4,6 @@
4
4
 
5
5
  import { PolyglotExecutor } from "../executor/executor.js";
6
6
  import type { Language, ExecResult } from "../types.js";
7
- import { readFileSync } from "node:fs";
8
7
 
9
8
  export interface CtxExecuteFileInput {
10
9
  language: Language;
@@ -16,13 +15,9 @@ export async function ctxExecuteFile(
16
15
  input: CtxExecuteFileInput,
17
16
  executor = new PolyglotExecutor(),
18
17
  ): Promise<ExecResult> {
19
- const content = readFileSync(input.path, "utf-8");
20
- const code = `const FILE_CONTENT = ${JSON.stringify(content)};\n// User script follows:\n`;
21
-
22
18
  return executor.executeFile({
23
19
  language: input.language,
24
20
  path: input.path,
25
- code,
26
21
  timeout: input.timeout ?? 30000,
27
22
  });
28
23
  }