@herbertgao/pi-extensions 2026.9.6 → 2026.9.7

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 (240) hide show
  1. package/README.md +3 -8
  2. package/THIRD_PARTY_NOTICES.md +0 -4
  3. package/node_modules/@narumitw/pi-btw/README.md +2 -0
  4. package/node_modules/@narumitw/pi-btw/dist/index.ts +284 -365
  5. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  6. package/node_modules/@narumitw/pi-btw/package.json +52 -53
  7. package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +494 -547
  8. package/node_modules/@narumitw/pi-btw/src/btw.ts +814 -856
  9. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +673 -674
  10. package/node_modules/@narumitw/pi-btw/src/keybindings.ts +228 -270
  11. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +309 -318
  12. package/node_modules/@narumitw/pi-btw/src/menu.ts +416 -454
  13. package/node_modules/@narumitw/pi-btw/src/settings.ts +203 -219
  14. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +173 -201
  15. package/node_modules/@narumitw/pi-btw/src/text.ts +21 -23
  16. package/node_modules/@narumitw/pi-btw/src/transcript-markdown.ts +65 -0
  17. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +611 -662
  18. package/node_modules/pi-lens/CHANGELOG.md +239 -0
  19. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -0
  20. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +30 -4
  21. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +2 -0
  22. package/node_modules/pi-lens/dist/clients/bash-file-access.js +26 -0
  23. package/node_modules/pi-lens/dist/clients/biome-client.js +1 -1
  24. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +403 -35
  25. package/node_modules/pi-lens/dist/clients/bootstrap.js +6 -1
  26. package/node_modules/pi-lens/dist/clients/cascade-format.js +200 -4
  27. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +10 -2
  28. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +16 -1
  29. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +12 -1
  30. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +65 -0
  31. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +28 -0
  32. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +9 -2
  33. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +222 -0
  34. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +389 -13
  35. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +175 -25
  36. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +62 -0
  37. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
  38. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +6 -1
  39. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +7 -7
  40. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -12
  41. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +2 -21
  42. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +1 -1
  43. package/node_modules/pi-lens/dist/clients/dispatch/types.js +4 -1
  44. package/node_modules/pi-lens/dist/clients/disposition-publish.js +1 -0
  45. package/node_modules/pi-lens/dist/clients/effective-config.js +1 -1
  46. package/node_modules/pi-lens/dist/clients/file-kinds.js +26 -1
  47. package/node_modules/pi-lens/dist/clients/file-role.js +21 -21
  48. package/node_modules/pi-lens/dist/clients/file-utils.js +156 -10
  49. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +56 -9
  50. package/node_modules/pi-lens/dist/clients/format-service.js +39 -21
  51. package/node_modules/pi-lens/dist/clients/formatters.js +60 -13
  52. package/node_modules/pi-lens/dist/clients/generation-guard.js +2 -2
  53. package/node_modules/pi-lens/dist/clients/git-guard.js +11 -3
  54. package/node_modules/pi-lens/dist/clients/indent-retarget.js +78 -8
  55. package/node_modules/pi-lens/dist/clients/installer/index.js +333 -114
  56. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +7 -0
  57. package/node_modules/pi-lens/dist/clients/instance-reaper.js +20 -10
  58. package/node_modules/pi-lens/dist/clients/language-profile.js +23 -1
  59. package/node_modules/pi-lens/dist/clients/language-registry.js +5 -1
  60. package/node_modules/pi-lens/dist/clients/latency-logger.js +4 -1
  61. package/node_modules/pi-lens/dist/clients/lens-engine.js +12 -3
  62. package/node_modules/pi-lens/dist/clients/lens-events.js +2 -1
  63. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +79 -0
  64. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +2 -1
  65. package/node_modules/pi-lens/dist/clients/lsp/client.js +25 -21
  66. package/node_modules/pi-lens/dist/clients/lsp/config.js +2 -2
  67. package/node_modules/pi-lens/dist/clients/lsp/index.js +7 -0
  68. package/node_modules/pi-lens/dist/clients/lsp/launch.js +16 -4
  69. package/node_modules/pi-lens/dist/clients/lsp/server.js +19 -2
  70. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +2 -1
  71. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +5 -3
  72. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +12 -8
  73. package/node_modules/pi-lens/dist/clients/mcp/session.js +36 -4
  74. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +24 -7
  75. package/node_modules/pi-lens/dist/clients/observed-mutation.js +40 -2
  76. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +23 -3
  77. package/node_modules/pi-lens/dist/clients/opengrep-client.js +110 -15
  78. package/node_modules/pi-lens/dist/clients/path-utils.js +123 -3
  79. package/node_modules/pi-lens/dist/clients/performance-report.js +5 -1
  80. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +1 -1
  81. package/node_modules/pi-lens/dist/clients/pipeline.js +74 -5
  82. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +37 -5
  83. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +145 -18
  84. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
  85. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +59 -3
  86. package/node_modules/pi-lens/dist/clients/project-lens-config.js +45 -2
  87. package/node_modules/pi-lens/dist/clients/read-guard.js +70 -11
  88. package/node_modules/pi-lens/dist/clients/resource-sampler.js +147 -9
  89. package/node_modules/pi-lens/dist/clients/ruff-client.js +1 -1
  90. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +33 -5
  91. package/node_modules/pi-lens/dist/clients/runtime-context.js +6 -2
  92. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +77 -7
  93. package/node_modules/pi-lens/dist/clients/runtime-session.js +21 -2
  94. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +43 -12
  95. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +330 -85
  96. package/node_modules/pi-lens/dist/clients/runtime-turn.js +147 -17
  97. package/node_modules/pi-lens/dist/clients/safe-spawn.js +258 -5
  98. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +6 -0
  99. package/node_modules/pi-lens/dist/clients/session-event-guard.js +50 -5
  100. package/node_modules/pi-lens/dist/clients/sg-runner.js +23 -11
  101. package/node_modules/pi-lens/dist/clients/sgconfig.js +79 -2
  102. package/node_modules/pi-lens/dist/clients/test-runner-client.js +6 -41
  103. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +43 -0
  104. package/node_modules/pi-lens/dist/clients/tool-agreement.js +423 -0
  105. package/node_modules/pi-lens/dist/clients/tool-config.js +50 -0
  106. package/node_modules/pi-lens/dist/clients/tool-cwd.js +47 -76
  107. package/node_modules/pi-lens/dist/clients/tool-policy.js +163 -7
  108. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +120 -0
  109. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +127 -189
  110. package/node_modules/pi-lens/dist/clients/widget-state.js +98 -0
  111. package/node_modules/pi-lens/dist/clients/word-index.js +5 -2
  112. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +7 -0
  113. package/node_modules/pi-lens/dist/index.js +27416 -24153
  114. package/node_modules/pi-lens/dist/mcp/server.js +5 -2
  115. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +7 -1
  116. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +168 -53
  117. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +244 -72
  118. package/node_modules/pi-lens/docs/audit1.md +5 -5
  119. package/node_modules/pi-lens/docs/dependencies.md +11 -0
  120. package/node_modules/pi-lens/docs/dispositions.md +52 -0
  121. package/node_modules/pi-lens/docs/environment-variables.md +2 -2
  122. package/node_modules/pi-lens/docs/features.md +4 -4
  123. package/node_modules/pi-lens/docs/globalconfig.md +3 -1
  124. package/node_modules/pi-lens/docs/language-coverage.md +1 -0
  125. package/node_modules/pi-lens/docs/pi-lens-fixer.md +53 -94
  126. package/node_modules/pi-lens/docs/pi-lens-investigator.md +43 -38
  127. package/node_modules/pi-lens/docs/pi-lens-retro.md +97 -0
  128. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +53 -66
  129. package/node_modules/pi-lens/docs/release-qa-baseline.md +22 -0
  130. package/node_modules/pi-lens/docs/settings.md +7 -4
  131. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
  132. package/node_modules/pi-lens/docs/usage.md +2 -0
  133. package/node_modules/pi-lens/package.json +1 -1
  134. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +24 -12
  135. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +40 -0
  136. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +15 -0
  137. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +7 -0
  138. package/package.json +5 -11
  139. package/node_modules/@luxusai/pi-hindsight/CHANGELOG.md +0 -376
  140. package/node_modules/@luxusai/pi-hindsight/README.md +0 -92
  141. package/node_modules/@luxusai/pi-hindsight/docs/adr/001-memory-lifecycle-and-scope.md +0 -56
  142. package/node_modules/@luxusai/pi-hindsight/docs/adr/002-explicit-routing-strategy-seam.md +0 -169
  143. package/node_modules/@luxusai/pi-hindsight/docs/adr/003-tui-memory-mode-vocabulary.md +0 -107
  144. package/node_modules/@luxusai/pi-hindsight/docs/adr/004-lifeos-dual-bank-design.md +0 -89
  145. package/node_modules/@luxusai/pi-hindsight/docs/adr/005-domain-banks-and-agent-first-surface.md +0 -189
  146. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark.webp +0 -0
  147. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark@2x.webp +0 -0
  148. package/node_modules/@luxusai/pi-hindsight/docs/coding-memory-evaluation.md +0 -54
  149. package/node_modules/@luxusai/pi-hindsight/docs/compatibility.md +0 -75
  150. package/node_modules/@luxusai/pi-hindsight/docs/hindsight-core-functions.md +0 -300
  151. package/node_modules/@luxusai/pi-hindsight/docs/mission-and-mental-model-quality.md +0 -158
  152. package/node_modules/@luxusai/pi-hindsight/docs/next-opt-out-design.md +0 -164
  153. package/node_modules/@luxusai/pi-hindsight/docs/risky-memory-modes.md +0 -139
  154. package/node_modules/@luxusai/pi-hindsight/docs/starter-mental-model-suggestions.md +0 -74
  155. package/node_modules/@luxusai/pi-hindsight/docs/surface-reference.md +0 -261
  156. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-operations.ts +0 -151
  157. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-selection.ts +0 -18
  158. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-settings-presenter.ts +0 -46
  159. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-templates.ts +0 -383
  160. package/node_modules/@luxusai/pi-hindsight/extensions/banks/banking.ts +0 -240
  161. package/node_modules/@luxusai/pi-hindsight/extensions/banks/knowledge-page-seed.ts +0 -176
  162. package/node_modules/@luxusai/pi-hindsight/extensions/banks/retain-strategies.ts +0 -178
  163. package/node_modules/@luxusai/pi-hindsight/extensions/client/client-retry.ts +0 -41
  164. package/node_modules/@luxusai/pi-hindsight/extensions/client/client.ts +0 -386
  165. package/node_modules/@luxusai/pi-hindsight/extensions/client/fetch-compat.ts +0 -39
  166. package/node_modules/@luxusai/pi-hindsight/extensions/client/timeout.ts +0 -34
  167. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-defaults.ts +0 -140
  168. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-model.ts +0 -61
  169. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-registry.ts +0 -925
  170. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-field-paths.ts +0 -247
  171. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-normalize.ts +0 -547
  172. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-writer.ts +0 -480
  173. package/node_modules/@luxusai/pi-hindsight/extensions/config/config.ts +0 -182
  174. package/node_modules/@luxusai/pi-hindsight/extensions/config/setup-gate.ts +0 -86
  175. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-execute.ts +0 -1010
  176. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-parse.ts +0 -175
  177. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-plan.ts +0 -425
  178. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-presentation.ts +0 -210
  179. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-sessions.ts +0 -817
  180. package/node_modules/@luxusai/pi-hindsight/extensions/index.ts +0 -25
  181. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/git-seed.ts +0 -319
  182. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-recall.ts +0 -196
  183. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-retain.ts +0 -189
  184. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-runtime.ts +0 -65
  185. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle.ts +0 -260
  186. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/mental-models.ts +0 -244
  187. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/observation-scopes.ts +0 -59
  188. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-cleanup.ts +0 -82
  189. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-visibility.ts +0 -45
  190. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall.ts +0 -331
  191. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-cursor.ts +0 -354
  192. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-job-builder.ts +0 -66
  193. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-receipts.ts +0 -132
  194. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain.ts +0 -228
  195. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-bank-template-operations.ts +0 -74
  196. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-config-operations.ts +0 -16
  197. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-control-operations.ts +0 -648
  198. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-diagnostics-operations.ts +0 -105
  199. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-identity.ts +0 -68
  200. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-service.ts +0 -71
  201. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-types.ts +0 -13
  202. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-recall-operations.ts +0 -162
  203. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-retain-operations.ts +0 -131
  204. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-scope.ts +0 -104
  205. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-session-operations.ts +0 -37
  206. package/node_modules/@luxusai/pi-hindsight/extensions/operations/operation-catalog.ts +0 -893
  207. package/node_modules/@luxusai/pi-hindsight/extensions/operations/reflect-presenter.ts +0 -35
  208. package/node_modules/@luxusai/pi-hindsight/extensions/operations/scope-migrate.ts +0 -177
  209. package/node_modules/@luxusai/pi-hindsight/extensions/operations/tools.ts +0 -7
  210. package/node_modules/@luxusai/pi-hindsight/extensions/queue/flush-presenter.ts +0 -21
  211. package/node_modules/@luxusai/pi-hindsight/extensions/queue/jsonl-queue-store.ts +0 -112
  212. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-delivery.ts +0 -111
  213. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-lock.ts +0 -214
  214. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-operations.ts +0 -70
  215. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue.ts +0 -392
  216. package/node_modules/@luxusai/pi-hindsight/extensions/tui/bank-template-presentation.ts +0 -45
  217. package/node_modules/@luxusai/pi-hindsight/extensions/tui/commands.ts +0 -9
  218. package/node_modules/@luxusai/pi-hindsight/extensions/tui/guided-setup.ts +0 -860
  219. package/node_modules/@luxusai/pi-hindsight/extensions/tui/prefill-input.ts +0 -105
  220. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-flow.ts +0 -189
  221. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-server-probe.ts +0 -299
  222. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-actions.ts +0 -201
  223. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-facts.ts +0 -47
  224. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-render.ts +0 -239
  225. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-types.ts +0 -50
  226. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui.ts +0 -259
  227. package/node_modules/@luxusai/pi-hindsight/extensions/tui/tool-presenters.ts +0 -77
  228. package/node_modules/@luxusai/pi-hindsight/extensions/types.ts +0 -534
  229. package/node_modules/@luxusai/pi-hindsight/extensions/utils/diagnostics.ts +0 -319
  230. package/node_modules/@luxusai/pi-hindsight/extensions/utils/messages.ts +0 -325
  231. package/node_modules/@luxusai/pi-hindsight/extensions/utils/sanitize.ts +0 -42
  232. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-memory-meta.ts +0 -244
  233. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-operations.ts +0 -56
  234. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session.ts +0 -50
  235. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-fields.ts +0 -167
  236. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-health.ts +0 -207
  237. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status.ts +0 -120
  238. package/node_modules/@luxusai/pi-hindsight/extensions/version.ts +0 -8
  239. package/node_modules/@luxusai/pi-hindsight/package.json +0 -118
  240. package/node_modules/@luxusai/pi-hindsight/skills/hindsight-memory-doctor/SKILL.md +0 -75
@@ -1,35 +0,0 @@
1
- function summarizeReflectTrace(trace: unknown): string {
2
- if (!trace || typeof trace !== "object") return "";
3
- const record = trace as Record<string, unknown>;
4
- const toolCalls = Array.isArray(record.tool_calls) ? record.tool_calls : [];
5
- const llmCalls = Array.isArray(record.llm_calls) ? record.llm_calls : [];
6
- if (toolCalls.length === 0 && llmCalls.length === 0) return "";
7
- const toolNames = toolCalls
8
- .map((call) =>
9
- call && typeof call === "object" ? (call as Record<string, unknown>).tool : undefined,
10
- )
11
- .filter((name): name is string => typeof name === "string");
12
- const toolPart = toolCalls.length
13
- ? `${toolCalls.length} tool call${toolCalls.length === 1 ? "" : "s"}${
14
- toolNames.length ? ` (${[...new Set(toolNames)].join(", ")})` : ""
15
- }`
16
- : "";
17
- const llmPart = llmCalls.length
18
- ? `${llmCalls.length} LLM call${llmCalls.length === 1 ? "" : "s"}`
19
- : "";
20
- return `Trace: ${[toolPart, llmPart].filter(Boolean).join("; ")}`;
21
- }
22
-
23
- export function formatReflectResult(result: unknown): string {
24
- if (typeof result === "string") return result;
25
- if (!result || typeof result !== "object") return JSON.stringify(result, null, 2);
26
- const record = result as Record<string, unknown>;
27
- const parts: string[] = [];
28
- if (typeof record.text === "string" && record.text.trim()) parts.push(record.text.trim());
29
- if (record.structured_output && typeof record.structured_output === "object") {
30
- parts.push(`Structured output:\n${JSON.stringify(record.structured_output, null, 2)}`);
31
- }
32
- const traceSummary = summarizeReflectTrace(record.trace);
33
- if (traceSummary) parts.push(traceSummary);
34
- return parts.length ? parts.join("\n\n") : JSON.stringify(result, null, 2);
35
- }
@@ -1,177 +0,0 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
3
- import {
4
- deriveProjectBankId,
5
- legacyRepoScopeTag,
6
- projectScopeTag,
7
- resolveProjectIdentity,
8
- } from "../banks/banking.js";
9
- import type { ResolvedConfig } from "../types.js";
10
-
11
- export interface ScopeMigratePlan {
12
- dryRun: true;
13
- /** Never rewrites banks/tags from this plan alone. */
14
- rewrite: "none";
15
- bankId: string;
16
- pathDerivedBank: boolean;
17
- scopeMode: ResolvedConfig["scope"]["mode"];
18
- projectTag: string;
19
- legacyRepoTag: string;
20
- projectId: string;
21
- projectIdBasis: "pin" | "remote" | "basename";
22
- projectIdSource: string;
23
- dualTagWindow: true;
24
- /**
25
- * True while retain still stamps legacy repo:<slug>-<path-hash>.
26
- * That legacy tag always depends on absolute path; dual-tag project: is the stable recovery path.
27
- */
28
- legacyPathHashTag: true;
29
- /**
30
- * True when project id itself is weak (basename only). Remote/pin identities are stable;
31
- * do not conflate them with path-hash legacy tags.
32
- */
33
- identityBasisWeak: boolean;
34
- findings: string[];
35
- guidance: string[];
36
- /** Optional remote tag sample counts when listTags results were provided. */
37
- remoteTagCounts?: {
38
- projectTagHits: number;
39
- legacyRepoTagHits: number;
40
- otherRepoTags: string[];
41
- sampleSize: number;
42
- };
43
- createdAt: string;
44
- }
45
-
46
- export interface ScopeMigrateReceipt extends ScopeMigratePlan {
47
- receiptPath: string;
48
- }
49
-
50
- /**
51
- * Build a dry-run scope migration plan. Never mutates Hindsight banks or tags.
52
- * Dual-tag retain/recall already covers the live window; this plan documents risk and next steps.
53
- */
54
- export function buildScopeMigratePlan(args: {
55
- cwd: string;
56
- config: ResolvedConfig;
57
- projectBankId?: string;
58
- bankTags?: string[];
59
- now?: Date;
60
- }): ScopeMigratePlan {
61
- const identity = resolveProjectIdentity(args.cwd, args.config);
62
- const bankId = args.projectBankId ?? deriveProjectBankId(args.cwd, args.config);
63
- const pathDerivedBank = !args.config.banks.project.bankId;
64
- const projectTag = projectScopeTag(identity.projectId);
65
- const legacyRepoTag = legacyRepoScopeTag(identity.legacyRepoKey);
66
- const identityBasisWeak = identity.basis === "basename";
67
-
68
- const findings: string[] = [];
69
- const guidance: string[] = [];
70
-
71
- findings.push(
72
- `Active dual-tag window: retain writes ${projectTag} and ${legacyRepoTag}; recall any_strict matches either.`,
73
- );
74
- findings.push(`Project id basis=${identity.basis} source=${identity.source} → ${projectTag}.`);
75
- findings.push(
76
- `Legacy tag ${legacyRepoTag} is path-hash based (machine/path moves change the hash). Stable project: tag is the recovery path for new writes.`,
77
- );
78
-
79
- if (pathDerivedBank) {
80
- findings.push(
81
- `Coding bank is path-derived (${bankId}); domain-tagged mode requires banks.project.bankId for a shared coding bank.`,
82
- );
83
- guidance.push(
84
- "Set banks.project.bankId (and setupComplete) so repos share one coding bank; do not rely on path-hash bank ids.",
85
- );
86
- }
87
-
88
- if (identityBasisWeak) {
89
- findings.push(
90
- "Project id is basename-derived; remotes or pins are more stable across clones and renames.",
91
- );
92
- guidance.push(
93
- 'Prefer scope.projectId pin or projectIdStrategy "remote" when a git origin exists.',
94
- );
95
- } else {
96
- findings.push(
97
- `Project id basis is ${identity.basis} (stable across absolute path moves when dual-tag project: is present).`,
98
- );
99
- }
100
-
101
- let remoteTagCounts: ScopeMigratePlan["remoteTagCounts"];
102
- if (args.bankTags?.length) {
103
- const otherRepoTags = [
104
- ...new Set(args.bankTags.filter((t) => t.startsWith("repo:") && t !== legacyRepoTag)),
105
- ].sort();
106
- const projectTagHits = args.bankTags.filter((t) => t === projectTag).length;
107
- const legacyRepoTagHits = args.bankTags.filter((t) => t === legacyRepoTag).length;
108
- remoteTagCounts = {
109
- projectTagHits,
110
- legacyRepoTagHits,
111
- otherRepoTags,
112
- sampleSize: args.bankTags.length,
113
- };
114
- findings.push(
115
- `Remote tag sample (${args.bankTags.length}): project hits=${projectTagHits}, legacy hits=${legacyRepoTagHits}, other repo:* tags=${otherRepoTags.length}.`,
116
- );
117
- if (legacyRepoTagHits > 0 && projectTagHits === 0) {
118
- findings.push(
119
- "Sample has legacy repo tags without the stable project tag — reimport or dual-tag new retains before dropping dual-tag.",
120
- );
121
- }
122
- if (otherRepoTags.length > 0) {
123
- findings.push(
124
- `Other path-hash repo tags present (likely other machines/paths): ${otherRepoTags.slice(0, 5).join(", ")}${otherRepoTags.length > 5 ? "…" : ""}.`,
125
- );
126
- guidance.push(
127
- "Other repo:* path-hashes are expected after path moves; dual-tag project: is the recovery path for new retains.",
128
- );
129
- }
130
- } else {
131
- guidance.push(
132
- "Optional: pass bank tag inventory (listTags) into dry-run for remote counts; this local plan never rewrites.",
133
- );
134
- }
135
-
136
- guidance.push(
137
- "No silent rewrite: this dry-run only writes a local receipt under .pi/hindsight/.",
138
- );
139
- guidance.push(
140
- "When dual-tag can end: every active project memory carries project:<id> (or you completed export/import rebuild).",
141
- );
142
-
143
- return {
144
- dryRun: true,
145
- rewrite: "none",
146
- bankId,
147
- pathDerivedBank,
148
- scopeMode: args.config.scope.mode,
149
- projectTag,
150
- legacyRepoTag,
151
- projectId: identity.projectId,
152
- projectIdBasis: identity.basis,
153
- projectIdSource: identity.source,
154
- dualTagWindow: true,
155
- legacyPathHashTag: true,
156
- identityBasisWeak,
157
- findings,
158
- guidance,
159
- ...(remoteTagCounts ? { remoteTagCounts } : {}),
160
- createdAt: (args.now ?? new Date()).toISOString(),
161
- };
162
- }
163
-
164
- export function resolveScopeMigrateReceiptPath(cwd: string): string {
165
- return join(cwd, ".pi", "hindsight", "scope-migrate-receipt.json");
166
- }
167
-
168
- export async function writeScopeMigrateReceipt(
169
- cwd: string,
170
- plan: ScopeMigratePlan,
171
- ): Promise<ScopeMigrateReceipt> {
172
- const receiptPath = resolveScopeMigrateReceiptPath(cwd);
173
- await mkdir(dirname(receiptPath), { recursive: true });
174
- const receipt: ScopeMigrateReceipt = { ...plan, receiptPath };
175
- await writeFile(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`, "utf8");
176
- return receipt;
177
- }
@@ -1,7 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import type { MemoryOperationsDeps } from "./memory-operation-service.js";
3
- import { createOperationCatalog } from "./operation-catalog.js";
4
-
5
- export function registerTools(pi: ExtensionAPI, deps: MemoryOperationsDeps) {
6
- for (const tool of createOperationCatalog(deps).tools) pi.registerTool(tool);
7
- }
@@ -1,21 +0,0 @@
1
- import type { RetainOutcome } from "../types.js";
2
- import type { FlushRetainQueueResult } from "./queue.js";
3
-
4
- export function formatRetainOutcome(outcome: RetainOutcome | undefined): string {
5
- if (!outcome) return "";
6
- const parts: string[] = [];
7
- if (outcome.itemsCount !== undefined) parts.push(`${outcome.itemsCount} items`);
8
- if (outcome.operations !== undefined) parts.push(`${outcome.operations} operations`);
9
- if (outcome.tokens !== undefined) parts.push(`${outcome.tokens} tokens`);
10
- return parts.join(", ");
11
- }
12
-
13
- export function formatFlushRetainQueueResult(result: FlushRetainQueueResult): string {
14
- const base = `Hindsight flushed ${result.sent}; dead-lettered ${result.deadLettered}; remaining ${result.remaining}`;
15
- const outcome = formatRetainOutcome(result.outcome);
16
- return outcome ? `${base}; retained ${outcome}` : base;
17
- }
18
-
19
- export function flushRetainQueueNotifyLevel(result: FlushRetainQueueResult): "info" | "warning" {
20
- return result.remaining || result.deadLettered || result.malformed ? "warning" : "info";
21
- }
@@ -1,112 +0,0 @@
1
- import { appendFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
- import { dirname } from "node:path";
3
-
4
- export interface JsonlQueueFileSummary {
5
- path: string;
6
- valid: number;
7
- malformed: number;
8
- error: string | null;
9
- }
10
-
11
- export interface ParsedJsonlQueue<T> {
12
- jobs: T[];
13
- malformedLines: string[];
14
- }
15
-
16
- export class JsonlQueueStore<T> {
17
- constructor(
18
- readonly path: string,
19
- private readonly isItem: (value: unknown) => value is T,
20
- ) {}
21
-
22
- async count(): Promise<number> {
23
- try {
24
- return (await readFile(this.path, "utf8")).split("\n").filter(Boolean).length;
25
- } catch (error) {
26
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return 0;
27
- throw error;
28
- }
29
- }
30
-
31
- async append(items: T[]): Promise<void> {
32
- if (items.length === 0) return;
33
- await mkdir(dirname(this.path), { recursive: true });
34
- await appendFile(
35
- this.path,
36
- items.map((item) => JSON.stringify(item)).join("\n") + "\n",
37
- "utf8",
38
- );
39
- }
40
-
41
- async readStrict(): Promise<T[]> {
42
- const parsed = await this.readTolerant();
43
- if (parsed.malformedLines.length > 0) {
44
- throw new Error(
45
- `Malformed queue file ${this.path}: ${parsed.malformedLines.length} invalid line(s)`,
46
- );
47
- }
48
- return parsed.jobs;
49
- }
50
-
51
- async readTolerant(): Promise<ParsedJsonlQueue<T>> {
52
- try {
53
- const text = await readFile(this.path, "utf8");
54
- const jobs: T[] = [];
55
- const malformedLines: string[] = [];
56
- for (const line of text.split("\n").filter(Boolean)) {
57
- try {
58
- const parsed = JSON.parse(line) as unknown;
59
- if (this.isItem(parsed)) jobs.push(parsed);
60
- else malformedLines.push(line);
61
- } catch {
62
- malformedLines.push(line);
63
- }
64
- }
65
- return { jobs, malformedLines };
66
- } catch (error) {
67
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
68
- return { jobs: [], malformedLines: [] };
69
- }
70
- throw error;
71
- }
72
- }
73
-
74
- async summarize(): Promise<JsonlQueueFileSummary> {
75
- try {
76
- const text = await readFile(this.path, "utf8");
77
- let valid = 0;
78
- let malformed = 0;
79
- for (const line of text.split("\n").filter(Boolean)) {
80
- try {
81
- const parsed = JSON.parse(line) as unknown;
82
- if (this.isItem(parsed)) valid += 1;
83
- else malformed += 1;
84
- } catch {
85
- malformed += 1;
86
- }
87
- }
88
- return { path: this.path, valid, malformed, error: null };
89
- } catch (error) {
90
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
91
- return { path: this.path, valid: 0, malformed: 0, error: null };
92
- }
93
- return {
94
- path: this.path,
95
- valid: 0,
96
- malformed: 0,
97
- error: error instanceof Error ? error.message : String(error),
98
- };
99
- }
100
- }
101
-
102
- async replace(items: T[]): Promise<void> {
103
- await mkdir(dirname(this.path), { recursive: true });
104
- const tmp = `${this.path}.tmp`;
105
- await writeFile(
106
- tmp,
107
- items.map((item) => JSON.stringify(item)).join("\n") + (items.length ? "\n" : ""),
108
- "utf8",
109
- );
110
- await rename(tmp, this.path);
111
- }
112
- }
@@ -1,111 +0,0 @@
1
- import type { HindsightLikeClient, RetainJob } from "../types.js";
2
-
3
- export interface RetainDeliveryOutcome {
4
- itemsCount?: number;
5
- operationIds: string[];
6
- tokens?: number;
7
- }
8
-
9
- export function redactQueueError(error: unknown): string {
10
- const message = error instanceof Error ? error.message : String(error);
11
- const text = message
12
- .replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]{12,}/gi, "$1[REDACTED]")
13
- .replace(/\b(sk-[A-Za-z0-9_-]{12,})\b/g, "[REDACTED_API_KEY]")
14
- .replace(/\b(ghp_[A-Za-z0-9_]{12,})\b/g, "[REDACTED_GITHUB_TOKEN]")
15
- .replace(
16
- /\b([A-Z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD)[A-Z0-9_]*\s*=\s*)[^\s\n"',}]+/gi,
17
- "$1[REDACTED]",
18
- )
19
- .replace(
20
- /\b((?:api[_-]?key|token|secret|password|authorization)\s*:\s*)["']?[^\s\n"',}]+["']?/gi,
21
- "$1[REDACTED]",
22
- )
23
- .replace(
24
- /(["'](?:api[_-]?key|token|secret|password|authorization)["']\s*:\s*)["'][^"']+["']/gi,
25
- '$1"[REDACTED]"',
26
- )
27
- .replace(/\b((?:Cookie|Set-Cookie)\s*:\s*)[^\r\n]+/gi, "$1[REDACTED]")
28
- .replace(/(https?:\/\/)([^\s/@]+):([^\s/@]+)@/gi, "$1[REDACTED]@");
29
- return text.replace(/https?:\/\/[^\s"'<>]+/gi, (url) =>
30
- url.replace(/([?&])([^=&#\s]+)=([^&#\s]+)/g, (match, separator, key) => {
31
- if (!/(?:token|key|secret|password|api[_-]?key|apikey)/i.test(String(key))) {
32
- return String(match);
33
- }
34
- return `${String(separator)}${String(key)}=[REDACTED]`;
35
- }),
36
- );
37
- }
38
-
39
- export function retainOptionsForJob(job: RetainJob) {
40
- const async = job.item.async;
41
- return {
42
- context: job.item.context,
43
- ...(job.item.timestamp ? { timestamp: job.item.timestamp } : {}),
44
- ...(job.item.metadata ? { metadata: job.item.metadata } : {}),
45
- ...(async !== undefined ? { async } : {}),
46
- // Stable per queue job so async retain retries are idempotent (Hindsight operation_id).
47
- ...(async === true ? { operationId: job.id } : {}),
48
- ...(job.item.entities?.length ? { entities: job.item.entities } : {}),
49
- ...(job.item.tags ? { tags: job.item.tags } : {}),
50
- ...(job.item.observationScopes ? { observationScopes: job.item.observationScopes } : {}),
51
- ...(job.item.documentTags ? { documentTags: job.item.documentTags } : {}),
52
- ...(job.item.strategy ? { strategy: job.item.strategy } : {}),
53
- documentId: job.documentId,
54
- updateMode: job.updateMode,
55
- };
56
- }
57
-
58
- export function operationIdsFromResponse(response: unknown): string[] {
59
- if (!response || typeof response !== "object") return [];
60
- const record = response as Record<string, unknown>;
61
- const candidates = [record.operation_id, record.operationId, record.id];
62
- const operationIds = candidates.filter(
63
- (candidate): candidate is string => typeof candidate === "string" && candidate.length > 0,
64
- );
65
- const nested = record.operations;
66
- if (Array.isArray(nested)) {
67
- for (const item of nested) {
68
- if (typeof item === "string") operationIds.push(item);
69
- else if (item && typeof item === "object") {
70
- const id =
71
- (item as Record<string, unknown>).id ?? (item as Record<string, unknown>).operation_id;
72
- if (typeof id === "string") operationIds.push(id);
73
- }
74
- }
75
- }
76
- const operationIdsArray = record.operation_ids ?? record.operationIds;
77
- if (Array.isArray(operationIdsArray)) {
78
- for (const item of operationIdsArray) {
79
- if (typeof item === "string" && item.length > 0) operationIds.push(item);
80
- }
81
- }
82
- return [...new Set(operationIds)];
83
- }
84
-
85
- function positiveNumber(value: unknown): number | undefined {
86
- return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
87
- }
88
-
89
- export function parseRetainOutcome(response: unknown): RetainDeliveryOutcome {
90
- const operationIds = operationIdsFromResponse(response);
91
- if (!response || typeof response !== "object") return { operationIds };
92
- const record = response as Record<string, unknown>;
93
- const usage =
94
- record.usage && typeof record.usage === "object"
95
- ? (record.usage as Record<string, unknown>)
96
- : undefined;
97
- const itemsCount = positiveNumber(record.items_count ?? record.itemsCount);
98
- const tokens = usage ? positiveNumber(usage.total_tokens) : undefined;
99
- return {
100
- operationIds,
101
- ...(itemsCount !== undefined ? { itemsCount } : {}),
102
- ...(tokens !== undefined ? { tokens } : {}),
103
- };
104
- }
105
-
106
- export async function deliverRetainJob(
107
- client: HindsightLikeClient,
108
- job: RetainJob,
109
- ): Promise<unknown> {
110
- return client.retain(job.bankId, job.item.content, retainOptionsForJob(job));
111
- }
@@ -1,214 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
3
- import { dirname } from "node:path";
4
-
5
- const queueLocks = new Map<string, Promise<void>>();
6
-
7
- const LOCK_RETRY_MS = 25;
8
- const LOCK_TIMEOUT_MS = 5_000;
9
- const LOCK_STALE_MS = 30_000;
10
-
11
- export const RETAIN_QUEUE_LOCK = {
12
- retryMs: LOCK_RETRY_MS,
13
- timeoutMs: LOCK_TIMEOUT_MS,
14
- staleMs: LOCK_STALE_MS,
15
- };
16
-
17
- export interface QueueLockOwner {
18
- pid?: number;
19
- acquiredAt?: string;
20
- token?: string;
21
- }
22
-
23
- function sleep(ms: number): Promise<void> {
24
- return new Promise((resolve) => setTimeout(resolve, ms));
25
- }
26
-
27
- async function removeDirectory(path: string): Promise<void> {
28
- await rm(path, { recursive: true, force: true, maxRetries: 3, retryDelay: 10 });
29
- }
30
-
31
- async function writeQueueHeartbeat(lockPath: string, token: string): Promise<void> {
32
- const heartbeatPath = `${lockPath}/heartbeat-${token}`;
33
- const tmpPath = `${heartbeatPath}.${process.pid}.tmp`;
34
- await writeFile(tmpPath, new Date().toISOString(), "utf8");
35
- await rename(tmpPath, heartbeatPath);
36
- }
37
-
38
- async function writeQueueLockOwner(lockPath: string, token: string): Promise<void> {
39
- const ownerPath = `${lockPath}/owner`;
40
- const tmpPath = `${ownerPath}.${process.pid}.${token}.tmp`;
41
- await writeFile(
42
- tmpPath,
43
- JSON.stringify({ pid: process.pid, acquiredAt: new Date().toISOString(), token }),
44
- "utf8",
45
- );
46
- await rename(tmpPath, ownerPath);
47
- await writeQueueHeartbeat(lockPath, token);
48
- }
49
-
50
- function startQueueLockHeartbeat(lockPath: string, token: string): NodeJS.Timeout {
51
- const heartbeatMs = Math.max(1, Math.min(5_000, Math.floor(RETAIN_QUEUE_LOCK.staleMs / 2)));
52
- const heartbeat = setInterval(() => {
53
- void writeQueueHeartbeat(lockPath, token).catch(() => undefined);
54
- }, heartbeatMs);
55
- heartbeat.unref?.();
56
- return heartbeat;
57
- }
58
-
59
- export function isQueueLockOwnerStale(
60
- owner: QueueLockOwner | undefined,
61
- now = Date.now(),
62
- staleMs = RETAIN_QUEUE_LOCK.staleMs,
63
- ): boolean {
64
- if (!owner) return true;
65
- const acquiredAt = owner.acquiredAt ? Date.parse(owner.acquiredAt) : Number.NaN;
66
- return !Number.isFinite(acquiredAt) || now - acquiredAt > staleMs;
67
- }
68
-
69
- async function readQueueLockOwner(lockPath: string): Promise<QueueLockOwner | undefined> {
70
- try {
71
- return JSON.parse(await readFile(`${lockPath}/owner`, "utf8")) as QueueLockOwner;
72
- } catch {
73
- return undefined;
74
- }
75
- }
76
-
77
- async function isOwnerlessLockDirectoryStale(lockPath: string): Promise<boolean> {
78
- try {
79
- const info = await stat(lockPath);
80
- return Date.now() - info.mtimeMs > RETAIN_QUEUE_LOCK.staleMs;
81
- } catch (error) {
82
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
83
- return true;
84
- }
85
- }
86
-
87
- async function isQueueLockStale(lockPath: string, owner: QueueLockOwner): Promise<boolean> {
88
- if (!owner.token) return isQueueLockOwnerStale(owner);
89
- try {
90
- const heartbeat = await stat(`${lockPath}/heartbeat-${owner.token}`);
91
- return Date.now() - heartbeat.mtimeMs > RETAIN_QUEUE_LOCK.staleMs;
92
- } catch {
93
- return isQueueLockOwnerStale(owner);
94
- }
95
- }
96
-
97
- async function removeLockIfOwned(lockPath: string, token: string): Promise<void> {
98
- const owner = await readQueueLockOwner(lockPath);
99
- if (owner?.token === token) await removeDirectory(lockPath);
100
- }
101
-
102
- export function isQueueLockRaceError(error: unknown): boolean {
103
- const code = (error as NodeJS.ErrnoException).code;
104
- return code === "EEXIST" || code === "EPERM" || code === "ENOTEMPTY" || code === "EBUSY";
105
- }
106
-
107
- function staleClaimPath(lockPath: string): string {
108
- return `${lockPath}.stale-claim`;
109
- }
110
-
111
- async function isStaleCleanupClaimActive(lockPath: string): Promise<boolean> {
112
- const claimPath = staleClaimPath(lockPath);
113
- try {
114
- const info = await stat(claimPath);
115
- if (Date.now() - info.mtimeMs <= RETAIN_QUEUE_LOCK.staleMs) return true;
116
- await removeDirectory(claimPath);
117
- return false;
118
- } catch (error) {
119
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
120
- throw error;
121
- }
122
- }
123
-
124
- async function withStaleCleanupClaim<T>(
125
- lockPath: string,
126
- fn: () => Promise<T>,
127
- ): Promise<T | undefined> {
128
- const claimPath = staleClaimPath(lockPath);
129
- try {
130
- await mkdir(claimPath, { recursive: false });
131
- } catch (error) {
132
- if ((error as NodeJS.ErrnoException).code === "EEXIST") return undefined;
133
- throw error;
134
- }
135
- try {
136
- return await fn();
137
- } finally {
138
- await removeDirectory(claimPath);
139
- }
140
- }
141
-
142
- async function removeLockIfStillStale(lockPath: string): Promise<boolean> {
143
- return (
144
- (await withStaleCleanupClaim(lockPath, async () => {
145
- const owner = await readQueueLockOwner(lockPath);
146
- const stale = owner
147
- ? await isQueueLockStale(lockPath, owner)
148
- : await isOwnerlessLockDirectoryStale(lockPath);
149
- if (!stale) return false;
150
- await removeDirectory(lockPath);
151
- return true;
152
- })) ?? false
153
- );
154
- }
155
-
156
- async function acquireFileLock(path: string): Promise<() => Promise<void>> {
157
- const lockPath = `${path}.lock`;
158
- const started = Date.now();
159
- await mkdir(dirname(path), { recursive: true });
160
- while (true) {
161
- if (await isStaleCleanupClaimActive(lockPath)) {
162
- if (Date.now() - started > RETAIN_QUEUE_LOCK.timeoutMs)
163
- throw new Error(`Timed out waiting for retain queue lock ${lockPath}`);
164
- await sleep(RETAIN_QUEUE_LOCK.retryMs);
165
- continue;
166
- }
167
- const token = randomUUID();
168
- try {
169
- await mkdir(lockPath, { recursive: false });
170
- try {
171
- await writeQueueLockOwner(lockPath, token);
172
- } catch (error) {
173
- await removeDirectory(lockPath);
174
- if (["ENOENT", "EINVAL"].includes((error as NodeJS.ErrnoException).code ?? "")) continue;
175
- throw error;
176
- }
177
- const heartbeat = startQueueLockHeartbeat(lockPath, token);
178
- return async () => {
179
- clearInterval(heartbeat);
180
- await removeLockIfOwned(lockPath, token);
181
- };
182
- } catch (error) {
183
- const code = (error as NodeJS.ErrnoException).code;
184
- if (!isQueueLockRaceError(error)) throw error;
185
- if (code === "EEXIST" && (await removeLockIfStillStale(lockPath))) continue;
186
- if (Date.now() - started > RETAIN_QUEUE_LOCK.timeoutMs)
187
- throw new Error(`Timed out waiting for retain queue lock ${lockPath}`);
188
- await sleep(RETAIN_QUEUE_LOCK.retryMs);
189
- }
190
- }
191
- }
192
-
193
- export async function withQueueLock<T>(path: string, fn: () => Promise<T>): Promise<T> {
194
- const previous = queueLocks.get(path) ?? Promise.resolve();
195
- let release!: () => void;
196
- const next = new Promise<void>((resolve) => {
197
- release = resolve;
198
- });
199
- const lock = previous.catch(() => undefined).then(() => next);
200
- queueLocks.set(path, lock);
201
- await previous.catch(() => undefined);
202
- let releaseFileLock: (() => Promise<void>) | undefined;
203
- try {
204
- releaseFileLock = await acquireFileLock(path);
205
- return await fn();
206
- } finally {
207
- try {
208
- if (releaseFileLock) await releaseFileLock();
209
- } finally {
210
- release();
211
- if (queueLocks.get(path) === lock) queueLocks.delete(path);
212
- }
213
- }
214
- }