pi-ui-extend 1.0.41 → 1.0.45

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 (236) hide show
  1. package/README.md +50 -13
  2. package/dist/app/app.d.ts +13 -0
  3. package/dist/app/app.js +158 -8
  4. package/dist/app/cli/install.js +2 -2
  5. package/dist/app/commands/command-controller.d.ts +4 -0
  6. package/dist/app/commands/command-controller.js +11 -0
  7. package/dist/app/commands/command-git-actions.d.ts +25 -0
  8. package/dist/app/commands/command-git-actions.js +381 -0
  9. package/dist/app/commands/command-host.d.ts +4 -0
  10. package/dist/app/commands/command-host.js +5 -2
  11. package/dist/app/commands/command-model-actions.d.ts +1 -0
  12. package/dist/app/commands/command-model-actions.js +57 -35
  13. package/dist/app/commands/command-navigation-actions.d.ts +1 -0
  14. package/dist/app/commands/command-navigation-actions.js +16 -3
  15. package/dist/app/commands/command-registry.d.ts +2 -0
  16. package/dist/app/commands/command-registry.js +15 -1
  17. package/dist/app/commands/command-session-actions.js +9 -3
  18. package/dist/app/commands/reload-context-inventory.d.ts +11 -0
  19. package/dist/app/commands/reload-context-inventory.js +70 -0
  20. package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
  21. package/dist/app/extensions/extension-actions-controller.js +6 -0
  22. package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
  23. package/dist/app/extensions/subagent-catalog-state.js +23 -0
  24. package/dist/app/input/autocomplete-controller.js +61 -34
  25. package/dist/app/input/input-action-controller.d.ts +2 -0
  26. package/dist/app/input/input-action-controller.js +8 -1
  27. package/dist/app/input/input-controller.d.ts +2 -1
  28. package/dist/app/input/input-controller.js +7 -2
  29. package/dist/app/input/prompt-enhancer-controller.js +53 -35
  30. package/dist/app/input/voice-controller.d.ts +41 -45
  31. package/dist/app/input/voice-controller.js +351 -419
  32. package/dist/app/model/model-usage-controller.js +17 -7
  33. package/dist/app/model/model-usage-status.d.ts +4 -1
  34. package/dist/app/model/model-usage-status.js +270 -46
  35. package/dist/app/popup/menu-items-controller.d.ts +13 -3
  36. package/dist/app/popup/menu-items-controller.js +37 -21
  37. package/dist/app/popup/popup-action-controller.d.ts +12 -2
  38. package/dist/app/popup/popup-action-controller.js +77 -24
  39. package/dist/app/popup/popup-menu-controller.d.ts +36 -14
  40. package/dist/app/popup/popup-menu-controller.js +239 -69
  41. package/dist/app/rendering/dcp-stats.d.ts +9 -4
  42. package/dist/app/rendering/dcp-stats.js +40 -425
  43. package/dist/app/rendering/editor-panels.js +10 -4
  44. package/dist/app/rendering/popup-menu-renderer.d.ts +3 -5
  45. package/dist/app/rendering/popup-menu-renderer.js +42 -37
  46. package/dist/app/rendering/render-controller.js +23 -2
  47. package/dist/app/rendering/status-line-renderer.d.ts +5 -0
  48. package/dist/app/rendering/status-line-renderer.js +41 -5
  49. package/dist/app/rendering/tab-line-renderer.js +26 -20
  50. package/dist/app/runtime.d.ts +12 -1
  51. package/dist/app/runtime.js +120 -12
  52. package/dist/app/screen/mouse-controller.d.ts +2 -0
  53. package/dist/app/screen/mouse-controller.js +17 -7
  54. package/dist/app/screen/status-controller.d.ts +4 -0
  55. package/dist/app/screen/status-controller.js +5 -0
  56. package/dist/app/session/lazy-session-manager.js +34 -0
  57. package/dist/app/session/session-event-controller.d.ts +1 -0
  58. package/dist/app/session/session-event-controller.js +10 -1
  59. package/dist/app/session/session-history.d.ts +1 -0
  60. package/dist/app/session/session-history.js +12 -1
  61. package/dist/app/session/session-lifecycle-controller.d.ts +7 -1
  62. package/dist/app/session/session-lifecycle-controller.js +16 -1
  63. package/dist/app/session/tabs-controller.d.ts +26 -1
  64. package/dist/app/session/tabs-controller.js +454 -147
  65. package/dist/app/subagents/subagents-files.js +60 -1
  66. package/dist/app/subagents/subagents-model.d.ts +1 -0
  67. package/dist/app/subagents/subagents-model.js +18 -2
  68. package/dist/app/subagents/subagents-widget-controller.d.ts +1 -0
  69. package/dist/app/subagents/subagents-widget-controller.js +6 -0
  70. package/dist/app/types.d.ts +16 -1
  71. package/dist/app/workspace/workspace-actions-controller.js +10 -2
  72. package/dist/app/workspace/workspace-undo.d.ts +1 -0
  73. package/dist/app/workspace/workspace-undo.js +1 -0
  74. package/dist/bundled-extensions/question/index.js +9 -1
  75. package/dist/bundled-extensions/question/remote.d.ts +4 -0
  76. package/dist/bundled-extensions/question/remote.js +33 -0
  77. package/dist/bundled-extensions/telegram-connector/bot.d.ts +43 -0
  78. package/dist/bundled-extensions/telegram-connector/bot.js +166 -0
  79. package/dist/bundled-extensions/telegram-connector/config.d.ts +8 -0
  80. package/dist/bundled-extensions/telegram-connector/config.js +87 -0
  81. package/dist/bundled-extensions/telegram-connector/coordinator.d.ts +66 -0
  82. package/dist/bundled-extensions/telegram-connector/coordinator.js +413 -0
  83. package/dist/bundled-extensions/telegram-connector/index.d.ts +3 -0
  84. package/dist/bundled-extensions/telegram-connector/index.js +195 -0
  85. package/dist/bundled-extensions/terminal-bell/index.d.ts +0 -8
  86. package/dist/bundled-extensions/terminal-bell/index.js +0 -76
  87. package/dist/bundled-extensions/workspace-undo/index.d.ts +26 -0
  88. package/dist/bundled-extensions/workspace-undo/index.js +191 -0
  89. package/dist/config.d.ts +21 -2
  90. package/dist/config.js +159 -32
  91. package/dist/default-pix-config.js +25 -5
  92. package/dist/schemas/index.d.ts +1 -0
  93. package/dist/schemas/index.js +1 -0
  94. package/dist/schemas/pi-tools-suite-schema.d.ts +88 -62
  95. package/dist/schemas/pi-tools-suite-schema.js +54 -83
  96. package/dist/schemas/pix-schema.d.ts +19 -2
  97. package/dist/schemas/pix-schema.js +47 -5
  98. package/dist/schemas/tasks-schema.d.ts +18 -0
  99. package/dist/schemas/tasks-schema.js +41 -0
  100. package/docs/concurrency.md +9 -1
  101. package/docs/desktop-mvp.md +22 -6
  102. package/docs/desktop-task-manager.md +148 -84
  103. package/docs/release.md +30 -6
  104. package/external/pi-tools-suite/README.md +197 -84
  105. package/external/pi-tools-suite/docs/evals.md +1 -1
  106. package/external/pi-tools-suite/docs/session-recovery.md +47 -14
  107. package/external/pi-tools-suite/docs/subagent-model-pools.md +48 -30
  108. package/external/pi-tools-suite/docs/ui-qa-subagent.md +440 -0
  109. package/external/pi-tools-suite/package.json +6 -1
  110. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +2 -1
  111. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +9 -3
  112. package/external/pi-tools-suite/src/antigravity-auth/headers.ts +41 -3
  113. package/external/pi-tools-suite/src/antigravity-auth/models.ts +94 -21
  114. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +5 -17
  115. package/external/pi-tools-suite/src/antigravity-auth/payload.ts +72 -7
  116. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +13 -1
  117. package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
  118. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +1 -1
  119. package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
  120. package/external/pi-tools-suite/src/async-subagents/agents/research.md +5 -3
  121. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/browser.mjs +346 -0
  122. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/desktop.mjs +1038 -0
  123. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/tui.mjs +759 -0
  124. package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/scripts/browser-qa-runner.mjs +32 -23
  125. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/chrome-devtools/chrome-devtools-provider.mjs +895 -0
  126. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/linux/linux-atspi.py +494 -0
  127. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/macos/macos-accessibility.swift +1085 -0
  128. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/bridge-client.mjs +50 -0
  129. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/native-terminal-host.mjs +501 -0
  130. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/windows/windows-uia.ps1 +454 -0
  131. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-auth.md +114 -0
  132. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-chrome-devtools.md +91 -0
  133. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-playwright.md +145 -0
  134. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser.md +82 -0
  135. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-linux.md +26 -0
  136. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-macos.md +29 -0
  137. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-windows.md +23 -0
  138. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop.md +54 -0
  139. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-native-terminal.md +59 -0
  140. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-pty.md +37 -0
  141. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui.md +60 -0
  142. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/scripts/ui-qa-runner.mjs +535 -0
  143. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa.md +75 -0
  144. package/external/pi-tools-suite/src/async-subagents/commands.ts +15 -71
  145. package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
  146. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +5 -4
  147. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
  148. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +8 -5
  149. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +16 -2
  150. package/external/pi-tools-suite/src/async-subagents/core/config.ts +160 -327
  151. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +2 -1
  152. package/external/pi-tools-suite/src/async-subagents/core/registry.ts +158 -33
  153. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +21 -5
  154. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +40 -26
  155. package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
  156. package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
  157. package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +51 -45
  158. package/external/pi-tools-suite/src/async-subagents/index.ts +69 -5
  159. package/external/pi-tools-suite/src/async-subagents/lib.ts +16 -9
  160. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +8 -5
  161. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +5 -4
  162. package/external/pi-tools-suite/src/async-subagents/types.ts +1 -0
  163. package/external/pi-tools-suite/src/coding-discipline/index.ts +90 -59
  164. package/external/pi-tools-suite/src/config.ts +55 -1
  165. package/external/pi-tools-suite/src/context-gateway/accounting-log.ts +282 -0
  166. package/external/pi-tools-suite/src/context-gateway/config.ts +77 -3
  167. package/external/pi-tools-suite/src/context-gateway/efficiency.ts +414 -0
  168. package/external/pi-tools-suite/src/context-gateway/enforcement.ts +222 -0
  169. package/external/pi-tools-suite/src/context-gateway/index.ts +206 -19
  170. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +7 -7
  171. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +101 -28
  172. package/external/pi-tools-suite/src/context-gateway/types.ts +16 -3
  173. package/external/pi-tools-suite/src/context-inventory.ts +99 -0
  174. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +39 -4
  175. package/external/pi-tools-suite/src/dcp/auto-compress.ts +128 -49
  176. package/external/pi-tools-suite/src/dcp/commands.ts +32 -156
  177. package/external/pi-tools-suite/src/dcp/compress-tool.ts +177 -46
  178. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +6 -58
  179. package/external/pi-tools-suite/src/dcp/compression-preview.ts +9 -0
  180. package/external/pi-tools-suite/src/dcp/compression-progress.ts +22 -0
  181. package/external/pi-tools-suite/src/dcp/config.ts +114 -17
  182. package/external/pi-tools-suite/src/dcp/conversation-index.ts +36 -9
  183. package/external/pi-tools-suite/src/dcp/diagnostics.ts +41 -0
  184. package/external/pi-tools-suite/src/dcp/fresh-tool-results.ts +38 -0
  185. package/external/pi-tools-suite/src/dcp/index.ts +338 -66
  186. package/external/pi-tools-suite/src/dcp/journal.ts +126 -4
  187. package/external/pi-tools-suite/src/dcp/progress-controller.ts +2 -1
  188. package/external/pi-tools-suite/src/dcp/prompts.ts +83 -192
  189. package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
  190. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +93 -3
  191. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +6 -10
  192. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +36 -33
  193. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -3
  194. package/external/pi-tools-suite/src/dcp/pruner.ts +1 -0
  195. package/external/pi-tools-suite/src/dcp/routine-pressure.ts +86 -0
  196. package/external/pi-tools-suite/src/dcp/state.ts +9 -0
  197. package/external/pi-tools-suite/src/dcp/statistics.d.ts +8 -0
  198. package/external/pi-tools-suite/src/dcp/statistics.js +156 -0
  199. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +78 -22
  200. package/external/pi-tools-suite/src/index.ts +7 -1
  201. package/external/pi-tools-suite/src/lib/project.ts +36 -1
  202. package/external/pi-tools-suite/src/model-tools/index.ts +10 -7
  203. package/external/pi-tools-suite/src/repo-discovery/index.ts +304 -4
  204. package/external/pi-tools-suite/src/resource-registry/index.ts +2551 -0
  205. package/external/pi-tools-suite/src/session-recovery/index.ts +17 -0
  206. package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
  207. package/external/pi-tools-suite/src/todo/index.ts +21 -0
  208. package/external/pi-tools-suite/src/todo/todo.ts +19 -1
  209. package/external/pi-tools-suite/src/tool-descriptions.ts +47 -19
  210. package/package.json +10 -9
  211. package/schemas/pi-tools-suite.json +466 -287
  212. package/schemas/pix.json +129 -11
  213. package/schemas/tasks.json +131 -0
  214. package/skills/simplify/SKILL.md +33 -5
  215. package/docs/desktop-markdown-media.md +0 -77
  216. package/external/pi-tools-suite/docs/browser-qa-subagent.md +0 -177
  217. package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
  218. package/external/pi-tools-suite/src/async-subagents/agents/browser-qa.md +0 -598
  219. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -54
  220. package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
  221. package/skills/playwright-cli/SKILL.md +0 -420
  222. package/skills/playwright-cli/references/element-attributes.md +0 -23
  223. package/skills/playwright-cli/references/playwright-tests.md +0 -50
  224. package/skills/playwright-cli/references/request-mocking.md +0 -87
  225. package/skills/playwright-cli/references/running-code.md +0 -241
  226. package/skills/playwright-cli/references/session-management.md +0 -273
  227. package/skills/playwright-cli/references/spec-driven-testing.md +0 -311
  228. package/skills/playwright-cli/references/storage-state.md +0 -290
  229. package/skills/playwright-cli/references/test-generation.md +0 -142
  230. package/skills/playwright-cli/references/tracing.md +0 -154
  231. package/skills/playwright-cli/references/video-recording.md +0 -147
  232. package/skills/spec-lite/SKILL.md +0 -140
  233. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-auth.example.jsonc +0 -0
  234. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-flow.example.jsonc +0 -0
  235. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.LICENSE +0 -0
  236. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.mjs +0 -0
@@ -0,0 +1,282 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+
5
+ import type { ContextGatewayResolvedConfig } from "./types.js";
6
+
7
+ const TRUE_ENV_RE = /^(1|true|yes|on)$/i;
8
+ const FALSE_ENV_RE = /^(0|false|no|off)$/i;
9
+ const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
10
+ const DEFAULT_MAX_BACKUPS = 3;
11
+ const MIN_MAX_BACKUPS = 1;
12
+ const MAX_MAX_BYTES = 64 * 1024 * 1024;
13
+ const MAX_MAX_BACKUPS = 100;
14
+ const ROTATION_CHECK_INTERVAL = 32;
15
+ const MAX_TRACKED_LOG_PATHS = 64;
16
+ const MAX_REPORTED_FAILURES = 32;
17
+ const ROTATION_LOCK_STALE_MS = 30_000;
18
+
19
+ interface RotationLockOwner {
20
+ pid: number;
21
+ token: string;
22
+ }
23
+
24
+ function truthyEnv(value: string | undefined): boolean | undefined {
25
+ if (value === undefined) return undefined;
26
+ const trimmed = value.trim();
27
+ if (TRUE_ENV_RE.test(trimmed)) return true;
28
+ if (FALSE_ENV_RE.test(trimmed)) return false;
29
+ return undefined;
30
+ }
31
+
32
+ function positiveIntEnv(value: string | undefined): number | undefined {
33
+ if (value === undefined) return undefined;
34
+ const normalized = value.trim();
35
+ if (!/^\d+$/.test(normalized)) return undefined;
36
+ const parsed = Number(normalized);
37
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
38
+ }
39
+
40
+ export function contextGatewayAccountingLogEnabled(config: ContextGatewayResolvedConfig): boolean {
41
+ return truthyEnv(process.env.PI_CONTEXT_GATEWAY_ACCOUNTING_LOG_ENABLED)
42
+ ?? config.accountingLog?.enabled
43
+ ?? true;
44
+ }
45
+
46
+ export function contextGatewayAccountingLogPath(): string {
47
+ const explicit = process.env.PI_CONTEXT_GATEWAY_ACCOUNTING_LOG?.trim();
48
+ if (explicit) return explicit;
49
+ const agentDir = process.env.PI_AGENT_DIR || path.join(os.homedir(), ".pi", "agent");
50
+ return path.join(agentDir, "context-gateway-accounting.jsonl");
51
+ }
52
+
53
+ export function contextGatewayAccountingLogMaxBytes(config: ContextGatewayResolvedConfig): number {
54
+ const envValue = positiveIntEnv(process.env.PI_CONTEXT_GATEWAY_ACCOUNTING_MAX_BYTES);
55
+ return (envValue !== undefined && envValue <= MAX_MAX_BYTES ? envValue : undefined)
56
+ ?? config.accountingLog?.maxBytes
57
+ ?? DEFAULT_MAX_BYTES;
58
+ }
59
+
60
+ export function contextGatewayAccountingLogMaxBackups(config: ContextGatewayResolvedConfig): number {
61
+ const envValue = positiveIntEnv(process.env.PI_CONTEXT_GATEWAY_ACCOUNTING_MAX_BACKUPS);
62
+ const value = (envValue !== undefined && envValue <= MAX_MAX_BACKUPS ? envValue : undefined)
63
+ ?? config.accountingLog?.maxBackups
64
+ ?? DEFAULT_MAX_BACKUPS;
65
+ return Math.max(MIN_MAX_BACKUPS, Math.floor(value));
66
+ }
67
+
68
+ let writeChain: Promise<void> = Promise.resolve();
69
+ const ensuredDirs = new Set<string>();
70
+ const appendsSinceRotationCheck = new Map<string, number>();
71
+ const reportedFailures = new Set<string>();
72
+
73
+ function errorCode(error: unknown): string {
74
+ if (!error || typeof error !== "object" || !("code" in error)) return "unknown";
75
+ const code = (error as { code?: unknown }).code;
76
+ return typeof code === "string" ? code : "unknown";
77
+ }
78
+
79
+ function reportFailure(operation: string, error: unknown): void {
80
+ const key = `${operation}:${errorCode(error)}`;
81
+ if (reportedFailures.has(key)) return;
82
+ if (reportedFailures.size >= MAX_REPORTED_FAILURES) reportedFailures.clear();
83
+ reportedFailures.add(key);
84
+ process.emitWarning(
85
+ `Context Gateway accounting ${operation} failed (${errorCode(error)}); accounting remains best-effort.`,
86
+ { code: "PI_CONTEXT_GATEWAY_ACCOUNTING_FAILURE" },
87
+ );
88
+ }
89
+
90
+ function setAppendCount(logPath: string, count: number): void {
91
+ if (!appendsSinceRotationCheck.has(logPath) && appendsSinceRotationCheck.size >= MAX_TRACKED_LOG_PATHS) {
92
+ const oldest = appendsSinceRotationCheck.keys().next().value;
93
+ if (oldest !== undefined) appendsSinceRotationCheck.delete(oldest);
94
+ }
95
+ appendsSinceRotationCheck.set(logPath, count);
96
+ }
97
+
98
+ async function ensureLogDir(logPath: string): Promise<void> {
99
+ const dir = path.dirname(logPath);
100
+ if (ensuredDirs.delete(dir)) {
101
+ ensuredDirs.add(dir);
102
+ return;
103
+ }
104
+ await fs.mkdir(dir, { recursive: true });
105
+ if (ensuredDirs.size >= MAX_TRACKED_LOG_PATHS) {
106
+ const oldest = ensuredDirs.values().next().value;
107
+ if (oldest !== undefined) ensuredDirs.delete(oldest);
108
+ }
109
+ ensuredDirs.add(dir);
110
+ }
111
+
112
+ function rotationLockOwner(): RotationLockOwner {
113
+ return {
114
+ pid: process.pid,
115
+ token: `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`,
116
+ };
117
+ }
118
+
119
+ function isRotationLockOwner(value: unknown): value is RotationLockOwner {
120
+ if (!value || typeof value !== "object") return false;
121
+ const candidate = value as Partial<RotationLockOwner>;
122
+ return Number.isInteger(candidate.pid) && (candidate.pid ?? 0) > 0 && typeof candidate.token === "string";
123
+ }
124
+
125
+ async function readRotationLockOwner(lockPath: string): Promise<RotationLockOwner | undefined> {
126
+ try {
127
+ const parsed: unknown = JSON.parse(await fs.readFile(lockPath, "utf8"));
128
+ return isRotationLockOwner(parsed) ? parsed : undefined;
129
+ } catch {
130
+ return undefined;
131
+ }
132
+ }
133
+
134
+ function isProcessAlive(pid: number): boolean {
135
+ try {
136
+ process.kill(pid, 0);
137
+ return true;
138
+ } catch (error) {
139
+ return errorCode(error) !== "ESRCH";
140
+ }
141
+ }
142
+
143
+ async function createRotationLock(lockPath: string, owner: RotationLockOwner): Promise<boolean> {
144
+ let handle: Awaited<ReturnType<typeof fs.open>>;
145
+ try {
146
+ handle = await fs.open(lockPath, "wx", 0o600);
147
+ } catch (error) {
148
+ if (errorCode(error) === "EEXIST") return false;
149
+ throw error;
150
+ }
151
+ try {
152
+ await handle.writeFile(JSON.stringify(owner), "utf8");
153
+ } catch (error) {
154
+ await handle.close();
155
+ await fs.unlink(lockPath).catch(() => {});
156
+ throw error;
157
+ }
158
+ await handle.close();
159
+ return true;
160
+ }
161
+
162
+ async function acquireRotationLock(lockPath: string, owner: RotationLockOwner): Promise<boolean> {
163
+ if (await createRotationLock(lockPath, owner)) return true;
164
+
165
+ let lockStat: Awaited<ReturnType<typeof fs.stat>>;
166
+ try {
167
+ lockStat = await fs.stat(lockPath);
168
+ } catch (error) {
169
+ if (errorCode(error) === "ENOENT") return createRotationLock(lockPath, owner);
170
+ throw error;
171
+ }
172
+ const existingOwner = await readRotationLockOwner(lockPath);
173
+ if (existingOwner && isProcessAlive(existingOwner.pid)) return false;
174
+ if (!existingOwner && Date.now() - lockStat.mtimeMs <= ROTATION_LOCK_STALE_MS) return false;
175
+
176
+ const quarantinePath = `${lockPath}.stale-${owner.token}`;
177
+ try {
178
+ await fs.rename(lockPath, quarantinePath);
179
+ } catch (error) {
180
+ if (errorCode(error) === "ENOENT") return createRotationLock(lockPath, owner);
181
+ throw error;
182
+ }
183
+ const movedStat = await fs.stat(quarantinePath);
184
+ if (movedStat.dev !== lockStat.dev || movedStat.ino !== lockStat.ino) {
185
+ await fs.rename(quarantinePath, lockPath).catch(() => {});
186
+ return false;
187
+ }
188
+ await fs.rm(quarantinePath, { recursive: true, force: true });
189
+ return createRotationLock(lockPath, owner);
190
+ }
191
+
192
+ async function releaseRotationLock(lockPath: string, owner: RotationLockOwner): Promise<void> {
193
+ const currentOwner = await readRotationLockOwner(lockPath);
194
+ if (currentOwner?.token !== owner.token) {
195
+ reportFailure("rotation lock ownership", { code: "EBUSY" });
196
+ return;
197
+ }
198
+ await fs.unlink(lockPath);
199
+ }
200
+
201
+ async function rotateIfNeeded(logPath: string, maxBytes: number, maxBackups: number): Promise<void> {
202
+ const lockPath = `${logPath}.rotation.lock`;
203
+ const lockOwner = rotationLockOwner();
204
+ let locked = false;
205
+ try {
206
+ locked = await acquireRotationLock(lockPath, lockOwner);
207
+ if (!locked) {
208
+ reportFailure("rotation lock", { code: "EBUSY" });
209
+ return;
210
+ }
211
+
212
+ let stat: Awaited<ReturnType<typeof fs.stat>>;
213
+ try {
214
+ stat = await fs.stat(logPath);
215
+ } catch (error) {
216
+ if (errorCode(error) === "ENOENT") return;
217
+ throw error;
218
+ }
219
+ if (stat.size < maxBytes) return;
220
+
221
+ const dir = path.dirname(logPath);
222
+ const base = path.basename(logPath);
223
+ await fs.rm(path.join(dir, `${base}.${maxBackups}`), { force: true });
224
+ for (let index = maxBackups - 1; index >= 1; index--) {
225
+ try {
226
+ await fs.rename(path.join(dir, `${base}.${index}`), path.join(dir, `${base}.${index + 1}`));
227
+ } catch (error) {
228
+ if (errorCode(error) !== "ENOENT") throw error;
229
+ }
230
+ }
231
+ await fs.rename(logPath, path.join(dir, `${base}.1`));
232
+ } catch (error) {
233
+ reportFailure("rotation", error);
234
+ } finally {
235
+ if (locked) {
236
+ await releaseRotationLock(lockPath, lockOwner).catch((error) => reportFailure("rotation unlock", error));
237
+ }
238
+ }
239
+ }
240
+
241
+ async function appendRecord(
242
+ logPath: string,
243
+ record: Record<string, unknown>,
244
+ maxBytes: number,
245
+ maxBackups: number,
246
+ ): Promise<void> {
247
+ await ensureLogDir(logPath);
248
+ const appendCount = appendsSinceRotationCheck.get(logPath) ?? ROTATION_CHECK_INTERVAL;
249
+ if (appendCount >= ROTATION_CHECK_INTERVAL) {
250
+ await rotateIfNeeded(logPath, maxBytes, maxBackups);
251
+ setAppendCount(logPath, 0);
252
+ }
253
+ await fs.appendFile(logPath, `${JSON.stringify(record)}\n`, "utf8");
254
+ setAppendCount(logPath, (appendsSinceRotationCheck.get(logPath) ?? 0) + 1);
255
+ }
256
+
257
+ export function writeContextGatewayAccountingLog(
258
+ config: ContextGatewayResolvedConfig,
259
+ event: string,
260
+ details: Record<string, unknown> = {},
261
+ ): void {
262
+ if (!contextGatewayAccountingLogEnabled(config)) return;
263
+ const record = {
264
+ version: 1,
265
+ ts: new Date().toISOString(),
266
+ event,
267
+ ...details,
268
+ };
269
+ const logPath = contextGatewayAccountingLogPath();
270
+ const maxBytes = contextGatewayAccountingLogMaxBytes(config);
271
+ const maxBackups = contextGatewayAccountingLogMaxBackups(config);
272
+ writeChain = writeChain
273
+ .then(() => appendRecord(logPath, record, maxBytes, maxBackups))
274
+ .catch((error) => {
275
+ // Accounting must never affect the session, provider call, or tool outcome.
276
+ reportFailure("write", error);
277
+ });
278
+ }
279
+
280
+ export function contextGatewayAccountingLogDrain(): Promise<void> {
281
+ return writeChain;
282
+ }
@@ -4,9 +4,11 @@ import { parse as parseJsonc } from "jsonc-parser";
4
4
 
5
5
  import { getPiToolsSuiteUserConfigPath } from "../config.js";
6
6
  import type {
7
+ ContextGatewayAccountingLogConfig,
7
8
  ContextGatewayBudgets,
8
9
  ContextGatewayMode,
9
10
  ContextGatewayResolvedConfig,
11
+ ContextGatewayToolClass,
10
12
  } from "./types.js";
11
13
 
12
14
  export const DEFAULT_CONTEXT_GATEWAY_BUDGETS: Readonly<ContextGatewayBudgets> = Object.freeze({
@@ -17,8 +19,22 @@ export const DEFAULT_CONTEXT_GATEWAY_BUDGETS: Readonly<ContextGatewayBudgets> =
17
19
  maxSearchMatches: 12,
18
20
  });
19
21
 
20
- export const CONTEXT_GATEWAY_ENFORCE_UNAVAILABLE =
21
- "Context Gateway enforce is unavailable in P01; mode unchanged until capture/store/result-stage integration is implemented.";
22
+ export const DEFAULT_CONTEXT_GATEWAY_ACCOUNTING_LOG: Readonly<ContextGatewayAccountingLogConfig> = Object.freeze({
23
+ enabled: true,
24
+ maxBytes: 5 * 1024 * 1024,
25
+ maxBackups: 3,
26
+ });
27
+
28
+ export function contextGatewayBudgetForClass(
29
+ toolClass: ContextGatewayToolClass,
30
+ budgets: ContextGatewayBudgets,
31
+ ): number {
32
+ if (toolClass === "code-read") return budgets.maxExactReadBytes;
33
+ if (["repo-search", "repo-ast", "repo-structure", "ast-grep"].includes(toolClass)) {
34
+ return budgets.maxSearchBytes;
35
+ }
36
+ return budgets.maxResultBytes;
37
+ }
22
38
 
23
39
  const MODES = new Set<ContextGatewayMode>(["off", "observe", "enforce"]);
24
40
  const MAX_BYTE_BUDGET = 64 * 1024 * 1024;
@@ -57,6 +73,24 @@ function parseMode(value: unknown): ContextGatewayMode | undefined {
57
73
  return MODES.has(normalized) ? normalized : undefined;
58
74
  }
59
75
 
76
+ function parseBoolean(value: unknown): boolean | undefined {
77
+ return typeof value === "boolean" ? value : undefined;
78
+ }
79
+
80
+ function parseBooleanEnv(value: string): boolean | undefined {
81
+ const normalized = value.trim().toLowerCase();
82
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
83
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
84
+ return undefined;
85
+ }
86
+
87
+ function parsePositiveIntegerEnv(value: string): number | undefined {
88
+ const normalized = value.trim();
89
+ if (!/^\d+$/.test(normalized)) return undefined;
90
+ const parsed = Number(normalized);
91
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
92
+ }
93
+
60
94
  function mergeBudget(
61
95
  budgets: ContextGatewayBudgets,
62
96
  issues: string[],
@@ -79,6 +113,7 @@ export function loadContextGatewayConfig(
79
113
  ): ContextGatewayResolvedConfig {
80
114
  let mode: ContextGatewayMode = "off";
81
115
  const budgets: ContextGatewayBudgets = { ...DEFAULT_CONTEXT_GATEWAY_BUDGETS };
116
+ const accountingLog: ContextGatewayAccountingLogConfig = { ...DEFAULT_CONTEXT_GATEWAY_ACCOUNTING_LOG };
82
117
  const issues: string[] = [];
83
118
 
84
119
  const layers = [getPiToolsSuiteUserConfigPath(homeDir)];
@@ -99,6 +134,29 @@ export function loadContextGatewayConfig(
99
134
  mergeBudget(budgets, issues, key, section.budgets[key]);
100
135
  }
101
136
  }
137
+ if (isRecord(section.accountingLog)) {
138
+ if (Object.prototype.hasOwnProperty.call(section.accountingLog, "enabled")) {
139
+ const enabled = parseBoolean(section.accountingLog.enabled);
140
+ if (enabled !== undefined) accountingLog.enabled = enabled;
141
+ else issues.push(`contextGateway.accountingLog.enabled must be boolean; keeping ${accountingLog.enabled}.`);
142
+ }
143
+ const maxBytes = section.accountingLog.maxBytes;
144
+ if (maxBytes !== undefined) {
145
+ if (typeof maxBytes === "number" && Number.isSafeInteger(maxBytes) && maxBytes > 0 && maxBytes <= MAX_BYTE_BUDGET) {
146
+ accountingLog.maxBytes = maxBytes;
147
+ } else {
148
+ issues.push(`contextGateway.accountingLog.maxBytes must be an integer in 1..${MAX_BYTE_BUDGET}; keeping ${accountingLog.maxBytes}.`);
149
+ }
150
+ }
151
+ const maxBackups = section.accountingLog.maxBackups;
152
+ if (maxBackups !== undefined) {
153
+ if (typeof maxBackups === "number" && Number.isSafeInteger(maxBackups) && maxBackups >= 1 && maxBackups <= 100) {
154
+ accountingLog.maxBackups = maxBackups;
155
+ } else {
156
+ issues.push(`contextGateway.accountingLog.maxBackups must be an integer in 1..100; keeping ${accountingLog.maxBackups}.`);
157
+ }
158
+ }
159
+ }
102
160
  }
103
161
 
104
162
  if (env.PI_CONTEXT_GATEWAY_MODE !== undefined) {
@@ -107,5 +165,21 @@ export function loadContextGatewayConfig(
107
165
  else issues.push("PI_CONTEXT_GATEWAY_MODE must be off, observe, or enforce; ignoring it.");
108
166
  }
109
167
 
110
- return { mode, budgets, issues };
168
+ if (env.PI_CONTEXT_GATEWAY_ACCOUNTING_LOG_ENABLED !== undefined) {
169
+ const enabled = parseBooleanEnv(env.PI_CONTEXT_GATEWAY_ACCOUNTING_LOG_ENABLED);
170
+ if (enabled !== undefined) accountingLog.enabled = enabled;
171
+ else issues.push("PI_CONTEXT_GATEWAY_ACCOUNTING_LOG_ENABLED must be true or false; ignoring it.");
172
+ }
173
+ if (env.PI_CONTEXT_GATEWAY_ACCOUNTING_MAX_BYTES !== undefined) {
174
+ const maxBytes = parsePositiveIntegerEnv(env.PI_CONTEXT_GATEWAY_ACCOUNTING_MAX_BYTES);
175
+ if (maxBytes !== undefined && maxBytes <= MAX_BYTE_BUDGET) accountingLog.maxBytes = maxBytes;
176
+ else issues.push(`PI_CONTEXT_GATEWAY_ACCOUNTING_MAX_BYTES must be an integer in 1..${MAX_BYTE_BUDGET}; ignoring it.`);
177
+ }
178
+ if (env.PI_CONTEXT_GATEWAY_ACCOUNTING_MAX_BACKUPS !== undefined) {
179
+ const maxBackups = parsePositiveIntegerEnv(env.PI_CONTEXT_GATEWAY_ACCOUNTING_MAX_BACKUPS);
180
+ if (maxBackups !== undefined && maxBackups <= 100) accountingLog.maxBackups = maxBackups;
181
+ else issues.push("PI_CONTEXT_GATEWAY_ACCOUNTING_MAX_BACKUPS must be an integer in 1..100; ignoring it.");
182
+ }
183
+
184
+ return { mode, budgets, accountingLog, issues };
111
185
  }