@tekmidian/pai 0.66.7 → 0.66.8

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 (93) hide show
  1. package/dist/{aibroker-client-BevXY1Zz.mjs → aibroker-client-BfnVwGZl.mjs} +3 -3
  2. package/dist/{aibroker-client-BevXY1Zz.mjs.map → aibroker-client-BfnVwGZl.mjs.map} +1 -1
  3. package/dist/{aibroker-client-CxTpL_u7.mjs → aibroker-client-C-O3Fflh.mjs} +1 -1
  4. package/dist/{auto-route-BVeAp0fl.mjs → auto-route-D8TBDNqb.mjs} +3 -3
  5. package/dist/{auto-route-BVeAp0fl.mjs.map → auto-route-D8TBDNqb.mjs.map} +1 -1
  6. package/dist/auto-route-DCxTKz19.mjs +7 -0
  7. package/dist/{chain-8hFKr23C.mjs → chain-BL4khGgy.mjs} +4 -3
  8. package/dist/{chain-8hFKr23C.mjs.map → chain-BL4khGgy.mjs.map} +1 -1
  9. package/dist/cli/index.mjs +20 -17
  10. package/dist/cli/index.mjs.map +1 -1
  11. package/dist/cli/program.mjs +19 -16
  12. package/dist/{config-sfaArCsl.mjs → config-DjZELvoA.mjs} +2 -2
  13. package/dist/{config-sfaArCsl.mjs.map → config-DjZELvoA.mjs.map} +1 -1
  14. package/dist/{run-env-CkAmx3lb.mjs → config-Dpe2JlbY.mjs} +5 -114
  15. package/dist/config-Dpe2JlbY.mjs.map +1 -0
  16. package/dist/{config-UWtzLpXi.mjs → config-yomayAwe.mjs} +1 -1
  17. package/dist/{context-handover-cache-Bb-H_mTa.mjs → context-handover-cache-DFqxNh-H.mjs} +27 -12
  18. package/dist/context-handover-cache-DFqxNh-H.mjs.map +1 -0
  19. package/dist/daemon/index.mjs +20 -16
  20. package/dist/daemon/index.mjs.map +1 -1
  21. package/dist/{daemon-CkbX-hV7.mjs → daemon-CsR1S_Qr.mjs} +20 -19
  22. package/dist/{daemon-CkbX-hV7.mjs.map → daemon-CsR1S_Qr.mjs.map} +1 -1
  23. package/dist/daemon-WrncnGua.mjs +23 -0
  24. package/dist/daemon-mcp/index.mjs +15 -13
  25. package/dist/daemon-mcp/index.mjs.map +1 -1
  26. package/dist/{embedding-gate-BVrr0BgS.mjs → embedding-gate-DqbKvn56.mjs} +4 -4
  27. package/dist/{embedding-gate-BVrr0BgS.mjs.map → embedding-gate-DqbKvn56.mjs.map} +1 -1
  28. package/dist/{embedding-gate-Crj8awk7.mjs → embedding-gate-SJMCPwxG.mjs} +3 -3
  29. package/dist/{embeddings-DFl88_y-.mjs → embeddings-BUjZMD9F.mjs} +1 -1
  30. package/dist/{embeddings-DFl88_y-.mjs.map → embeddings-BUjZMD9F.mjs.map} +1 -1
  31. package/dist/{embeddings-BuA7v4lk.mjs → embeddings-DrJ-ewfi.mjs} +1 -1
  32. package/dist/{env-DqTjeKoA.mjs → env-CPCNHz3i.mjs} +1 -1
  33. package/dist/{env-DqTjeKoA.mjs.map → env-CPCNHz3i.mjs.map} +1 -1
  34. package/dist/factory-DjtXElOI.mjs +9 -0
  35. package/dist/{factory-mNBitkL_.mjs → factory-yElzJc6f.mjs} +10 -10
  36. package/dist/{factory-mNBitkL_.mjs.map → factory-yElzJc6f.mjs.map} +1 -1
  37. package/dist/{fallback-DdC1_s2d.mjs → fallback-2yWDDYiT.mjs} +5 -4
  38. package/dist/{fallback-DdC1_s2d.mjs.map → fallback-2yWDDYiT.mjs.map} +1 -1
  39. package/dist/{helpers-CZsi_49C.mjs → helpers-B9LHRJ8R.mjs} +1 -1
  40. package/dist/{helpers-CZsi_49C.mjs.map → helpers-B9LHRJ8R.mjs.map} +1 -1
  41. package/dist/index.d.mts.map +1 -1
  42. package/dist/index.mjs +7 -4
  43. package/dist/{ipc-client-BgSzY9QJ.mjs → ipc-client-DVZ_EGnu.mjs} +2 -2
  44. package/dist/{ipc-client-BgSzY9QJ.mjs.map → ipc-client-DVZ_EGnu.mjs.map} +1 -1
  45. package/dist/main-resolver-DjQsexge.mjs +8 -0
  46. package/dist/{main-resolver-DEXky_Km.mjs → main-resolver-Dxvn4Mv8.mjs} +7 -6
  47. package/dist/{main-resolver-DEXky_Km.mjs.map → main-resolver-Dxvn4Mv8.mjs.map} +1 -1
  48. package/dist/{pai-marker-DXVpFsYz.mjs → pai-marker-1B7iC7oq.mjs} +121 -3
  49. package/dist/pai-marker-1B7iC7oq.mjs.map +1 -0
  50. package/dist/{planner-DBoB5GtC.mjs → planner-CspP59ya.mjs} +13 -11
  51. package/dist/{planner-DBoB5GtC.mjs.map → planner-CspP59ya.mjs.map} +1 -1
  52. package/dist/{postgres-D-96lgE1.mjs → postgres-CCAGq1qp.mjs} +5 -5
  53. package/dist/{postgres-D-96lgE1.mjs.map → postgres-CCAGq1qp.mjs.map} +1 -1
  54. package/dist/postgres-CzkEPfAm.mjs +7 -0
  55. package/dist/{program-BXns_5kO.mjs → program-cWqZI0_L.mjs} +77 -119
  56. package/dist/program-cWqZI0_L.mjs.map +1 -0
  57. package/dist/{query-feedback-VI7Cl-Lr.mjs → query-feedback-GPeVGvIa.mjs} +2 -2
  58. package/dist/{query-feedback-VI7Cl-Lr.mjs.map → query-feedback-GPeVGvIa.mjs.map} +1 -1
  59. package/dist/{reranker-3lnggwgq.mjs → reranker-BTiOia8Y.mjs} +1 -1
  60. package/dist/{reranker-3lnggwgq.mjs.map → reranker-BTiOia8Y.mjs.map} +1 -1
  61. package/dist/{reranker-CZ2mP4cf.mjs → reranker-YwDT2n5e.mjs} +1 -1
  62. package/dist/{router-dfjlaOMI.mjs → router-Bk_ySIZT.mjs} +2 -2
  63. package/dist/{router-dfjlaOMI.mjs.map → router-Bk_ySIZT.mjs.map} +1 -1
  64. package/dist/router-DyiZNI6o.mjs +3 -0
  65. package/dist/{run-DPwQkRwh.mjs → run-BZNdpUGQ.mjs} +126 -1795
  66. package/dist/run-BZNdpUGQ.mjs.map +1 -0
  67. package/dist/run-env-Cd0Son-Y.mjs +115 -0
  68. package/dist/run-env-Cd0Son-Y.mjs.map +1 -0
  69. package/dist/{runtime-paths-BiEi_gaG.mjs → runtime-paths-DkZoxofH.mjs} +1 -1
  70. package/dist/{runtime-paths-BiEi_gaG.mjs.map → runtime-paths-DkZoxofH.mjs.map} +1 -1
  71. package/dist/{search-CNAGTiJP.mjs → search-cPDj-9fO.mjs} +2 -2
  72. package/dist/{search-CNAGTiJP.mjs.map → search-cPDj-9fO.mjs.map} +1 -1
  73. package/dist/{sources-BxXbzpAJ.mjs → sources-CvvzNZad.mjs} +1 -1
  74. package/dist/{sources-BxXbzpAJ.mjs.map → sources-CvvzNZad.mjs.map} +1 -1
  75. package/dist/{stop-words-DtxaTWU_.mjs → stop-words-LtkS-Pej.mjs} +1 -1
  76. package/dist/{stop-words-DtxaTWU_.mjs.map → stop-words-LtkS-Pej.mjs.map} +1 -1
  77. package/dist/worktree-CwMRTa0H.mjs +1578 -0
  78. package/dist/worktree-CwMRTa0H.mjs.map +1 -0
  79. package/dist/{zettelkasten-wh2Yy1nS.mjs → zettelkasten-DSIe6wpF.mjs} +4 -4
  80. package/dist/{zettelkasten-wh2Yy1nS.mjs.map → zettelkasten-DSIe6wpF.mjs.map} +1 -1
  81. package/dist/{zettelkasten-CzcPzEsL.mjs → zettelkasten-DtkAvA18.mjs} +5 -5
  82. package/package.json +1 -1
  83. package/dist/auto-route-CboBqsN3.mjs +0 -3
  84. package/dist/context-handover-cache-Bb-H_mTa.mjs.map +0 -1
  85. package/dist/daemon-BXTn2EgV.mjs +0 -19
  86. package/dist/factory-XrH66erA.mjs +0 -9
  87. package/dist/main-resolver-CobQGm2B.mjs +0 -7
  88. package/dist/pai-marker-DXVpFsYz.mjs.map +0 -1
  89. package/dist/postgres-TedJP3yF.mjs +0 -7
  90. package/dist/program-BXns_5kO.mjs.map +0 -1
  91. package/dist/router-CKn9e0fo.mjs +0 -3
  92. package/dist/run-DPwQkRwh.mjs.map +0 -1
  93. package/dist/run-env-CkAmx3lb.mjs.map +0 -1
@@ -0,0 +1,115 @@
1
+ import { D as resolveModelCapability, O as resolveProviderKey } from "./config-Dpe2JlbY.mjs";
2
+ import { existsSync } from "node:fs";
3
+ import { delimiter, join } from "node:path";
4
+
5
+ //#region src/workers/run-env.ts
6
+ /**
7
+ * run-env.ts — the provider environment every Claude Code spawn must run with.
8
+ *
9
+ * Extracted from run.ts so daemon-side background spawns (session summaries,
10
+ * context handovers, KG extraction) go through the exact same env the worker
11
+ * runner uses, instead of importing the whole runner into the daemon.
12
+ */
13
+ /**
14
+ * Session-identity variables the SPAWNING session leaves in the environment.
15
+ * Inherited, they make a headless child attach to the spawner's messaging
16
+ * socket instead of standing on its own — and a worktree-mode child then
17
+ * starts with its core tools (Bash/Read/…) deferred out of its reach, left
18
+ * with nothing but the tool-registry search (2026-09-18). The child is its
19
+ * own session, so none of these may cross the spawn boundary.
20
+ */
21
+ const SESSION_IDENTITY_VARS = [
22
+ "CLAUDE_CODE_MESSAGING_SOCKET",
23
+ "CLAUDE_CODE_MESSAGING_TOKEN",
24
+ "CLAUDE_CODE_SESSION_ID",
25
+ "CLAUDE_CODE_CHILD_SESSION",
26
+ "CLAUDE_PID",
27
+ "CLAUDECODE"
28
+ ];
29
+ /**
30
+ * Harness settings the spawner's environment carries that a headless worker
31
+ * must not keep. ENABLE_TOOL_SEARCH reaches every pai process through the
32
+ * user settings' env block, so a pai spawned from inside a Claude session
33
+ * passes it on to worker children; inherited, the headless child starts with
34
+ * every core tool deferred out of reach — only ToolSearch remains callable,
35
+ * granted tools included (2026-09-18, verified by stripping exactly this one
36
+ * var). Interactive runs set it deliberately below.
37
+ */
38
+ const HEADLESS_STRIP_VARS = ["ENABLE_TOOL_SEARCH"];
39
+ /**
40
+ * Provider-specific env vars a native-Anthropic run must never carry — not
41
+ * set by this function, and stripped even when a parent shell exported them
42
+ * (e.g. this worker was itself spawned from a GLM-configured environment).
43
+ */
44
+ const NATIVE_STRIP_VARS = [
45
+ "ANTHROPIC_BASE_URL",
46
+ "ANTHROPIC_AUTH_TOKEN",
47
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL",
48
+ "ANTHROPIC_DEFAULT_SONNET_MODEL",
49
+ "ANTHROPIC_DEFAULT_OPUS_MODEL"
50
+ ];
51
+ /** Environment for a run through `provider`. Caller's env minus the Anthropic key. */
52
+ function buildRunEnv(provider, headless, proxyUrl) {
53
+ const env = { ...process.env };
54
+ delete env.ANTHROPIC_API_KEY;
55
+ if (headless) {
56
+ for (const k of SESSION_IDENTITY_VARS) delete env[k];
57
+ for (const k of HEADLESS_STRIP_VARS) delete env[k];
58
+ }
59
+ if (provider.native) {
60
+ for (const k of NATIVE_STRIP_VARS) delete env[k];
61
+ env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
62
+ if (headless) env.PAI_WORKER = "1";
63
+ else env.ENABLE_TOOL_SEARCH = "true";
64
+ return env;
65
+ }
66
+ let token = "local";
67
+ if (proxyUrl) env.ANTHROPIC_BASE_URL = proxyUrl;
68
+ else {
69
+ const resolved = resolveProviderKey(provider);
70
+ if (resolved) token = resolved;
71
+ env.ANTHROPIC_BASE_URL = provider.baseUrl;
72
+ }
73
+ env.ANTHROPIC_AUTH_TOKEN = token;
74
+ env.ANTHROPIC_DEFAULT_HAIKU_MODEL = resolveModelCapability(provider, "fast");
75
+ env.ANTHROPIC_DEFAULT_SONNET_MODEL = provider.models.default;
76
+ env.ANTHROPIC_DEFAULT_OPUS_MODEL = provider.models.default;
77
+ for (const [k, v] of Object.entries(provider.env)) env[k] = v;
78
+ env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
79
+ if (headless) env.PAI_WORKER = "1";
80
+ else env.ENABLE_TOOL_SEARCH = "true";
81
+ return env;
82
+ }
83
+ /** First-party route a native run is pinned to when caveman is off. */
84
+ const ANTHROPIC_FIRST_PARTY_URL = "https://api.anthropic.com";
85
+ /**
86
+ * The argv head every Claude Code spawn starts from, for an env built by
87
+ * buildRunEnv. The user settings' env block outranks the process env
88
+ * (measured 2026-09-22: an unreachable ANTHROPIC_BASE_URL in the process env
89
+ * was ignored while settings.json carried a caveman proxy route, and a glm
90
+ * worker's bearer token ended at Anthropic with a 401), so a route set in
91
+ * `env` is not enough once a machine-wide proxy is installed — it is pinned
92
+ * again with `--settings`, which outranks user settings. A native run goes
93
+ * through `caveman claude` when workers.caveman is on and the CLI is on
94
+ * PATH; off, it is pinned to api.anthropic.com so a global route cannot
95
+ * pull it in either.
96
+ */
97
+ function claudeCommand(env, caveman) {
98
+ const route = env.ANTHROPIC_BASE_URL;
99
+ if (!route && caveman) {
100
+ if (onPath("caveman", env)) return ["caveman", "claude"];
101
+ process.stderr.write("pai: workers.caveman is on but `caveman` is not on PATH; running claude directly\n");
102
+ }
103
+ return [
104
+ "claude",
105
+ "--settings",
106
+ JSON.stringify({ env: { ANTHROPIC_BASE_URL: route || ANTHROPIC_FIRST_PARTY_URL } })
107
+ ];
108
+ }
109
+ function onPath(bin, env) {
110
+ return (env.PATH ?? "").split(delimiter).some((d) => d && existsSync(join(d, bin)));
111
+ }
112
+
113
+ //#endregion
114
+ export { claudeCommand as n, buildRunEnv as t };
115
+ //# sourceMappingURL=run-env-Cd0Son-Y.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-env-Cd0Son-Y.mjs","names":[],"sources":["../src/workers/run-env.ts"],"sourcesContent":["/**\n * run-env.ts — the provider environment every Claude Code spawn must run with.\n *\n * Extracted from run.ts so daemon-side background spawns (session summaries,\n * context handovers, KG extraction) go through the exact same env the worker\n * runner uses, instead of importing the whole runner into the daemon.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { delimiter, join } from \"node:path\";\n\nimport { resolveModelCapability, resolveProviderKey, type WorkerProvider } from \"./config.js\";\n\n/**\n * Session-identity variables the SPAWNING session leaves in the environment.\n * Inherited, they make a headless child attach to the spawner's messaging\n * socket instead of standing on its own — and a worktree-mode child then\n * starts with its core tools (Bash/Read/…) deferred out of its reach, left\n * with nothing but the tool-registry search (2026-09-18). The child is its\n * own session, so none of these may cross the spawn boundary.\n */\nconst SESSION_IDENTITY_VARS = [\n \"CLAUDE_CODE_MESSAGING_SOCKET\",\n \"CLAUDE_CODE_MESSAGING_TOKEN\",\n \"CLAUDE_CODE_SESSION_ID\",\n \"CLAUDE_CODE_CHILD_SESSION\",\n \"CLAUDE_PID\",\n \"CLAUDECODE\",\n] as const;\n\n/**\n * Harness settings the spawner's environment carries that a headless worker\n * must not keep. ENABLE_TOOL_SEARCH reaches every pai process through the\n * user settings' env block, so a pai spawned from inside a Claude session\n * passes it on to worker children; inherited, the headless child starts with\n * every core tool deferred out of reach — only ToolSearch remains callable,\n * granted tools included (2026-09-18, verified by stripping exactly this one\n * var). Interactive runs set it deliberately below.\n */\nconst HEADLESS_STRIP_VARS = [\"ENABLE_TOOL_SEARCH\"] as const;\n\n/**\n * Provider-specific env vars a native-Anthropic run must never carry — not\n * set by this function, and stripped even when a parent shell exported them\n * (e.g. this worker was itself spawned from a GLM-configured environment).\n */\nconst NATIVE_STRIP_VARS = [\n \"ANTHROPIC_BASE_URL\",\n \"ANTHROPIC_AUTH_TOKEN\",\n \"ANTHROPIC_DEFAULT_HAIKU_MODEL\",\n \"ANTHROPIC_DEFAULT_SONNET_MODEL\",\n \"ANTHROPIC_DEFAULT_OPUS_MODEL\",\n] as const;\n\n/** Environment for a run through `provider`. Caller's env minus the Anthropic key. */\nexport function buildRunEnv(\n provider: WorkerProvider,\n headless: boolean,\n proxyUrl?: string\n): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = { ...process.env };\n delete env.ANTHROPIC_API_KEY;\n if (headless) {\n for (const k of SESSION_IDENTITY_VARS) delete env[k];\n for (const k of HEADLESS_STRIP_VARS) delete env[k];\n }\n\n if (provider.native) {\n // Plain Claude Code on its own OAuth/Max-plan login: no base URL, no\n // token, no provider-specific model pins — and none inherited from a\n // shell that had a different provider exported into it either.\n for (const k of NATIVE_STRIP_VARS) delete env[k];\n env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = \"1\";\n if (headless) {\n env.PAI_WORKER = \"1\";\n } else {\n env.ENABLE_TOOL_SEARCH = \"true\";\n }\n return env;\n }\n\n let token = \"local\";\n if (proxyUrl) {\n // openai-protocol provider: the proxy holds the real key; the runner only\n // needs a placeholder so Claude Code sends an auth header at all\n env.ANTHROPIC_BASE_URL = proxyUrl;\n } else {\n const resolved = resolveProviderKey(provider);\n if (resolved) token = resolved;\n env.ANTHROPIC_BASE_URL = provider.baseUrl;\n }\n\n env.ANTHROPIC_AUTH_TOKEN = token;\n env.ANTHROPIC_DEFAULT_HAIKU_MODEL = resolveModelCapability(provider, \"fast\");\n env.ANTHROPIC_DEFAULT_SONNET_MODEL = provider.models.default;\n env.ANTHROPIC_DEFAULT_OPUS_MODEL = provider.models.default;\n for (const [k, v] of Object.entries(provider.env)) env[k] = v;\n env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = \"1\";\n if (headless) {\n env.PAI_WORKER = \"1\";\n } else {\n env.ENABLE_TOOL_SEARCH = \"true\";\n }\n return env;\n}\n\n/** First-party route a native run is pinned to when caveman is off. */\nexport const ANTHROPIC_FIRST_PARTY_URL = \"https://api.anthropic.com\";\n\n/**\n * The argv head every Claude Code spawn starts from, for an env built by\n * buildRunEnv. The user settings' env block outranks the process env\n * (measured 2026-09-22: an unreachable ANTHROPIC_BASE_URL in the process env\n * was ignored while settings.json carried a caveman proxy route, and a glm\n * worker's bearer token ended at Anthropic with a 401), so a route set in\n * `env` is not enough once a machine-wide proxy is installed — it is pinned\n * again with `--settings`, which outranks user settings. A native run goes\n * through `caveman claude` when workers.caveman is on and the CLI is on\n * PATH; off, it is pinned to api.anthropic.com so a global route cannot\n * pull it in either.\n */\nexport function claudeCommand(env: NodeJS.ProcessEnv, caveman: boolean): string[] {\n const route = env.ANTHROPIC_BASE_URL;\n if (!route && caveman) {\n if (onPath(\"caveman\", env)) return [\"caveman\", \"claude\"];\n process.stderr.write(\"pai: workers.caveman is on but `caveman` is not on PATH; running claude directly\\n\");\n }\n return [\"claude\", \"--settings\", JSON.stringify({ env: { ANTHROPIC_BASE_URL: route || ANTHROPIC_FIRST_PARTY_URL } })];\n}\n\nfunction onPath(bin: string, env: NodeJS.ProcessEnv): boolean {\n return (env.PATH ?? \"\").split(delimiter).some((d) => d && existsSync(join(d, bin)));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAqBA,MAAM,wBAAwB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACD;;;;;;;;;;AAWD,MAAM,sBAAsB,CAAC,qBAAqB;;;;;;AAOlD,MAAM,oBAAoB;CACxB;CACA;CACA;CACA;CACA;CACD;;AAGD,SAAgB,YACd,UACA,UACA,UACmB;CACnB,MAAM,MAAyB,EAAE,GAAG,QAAQ,KAAK;AACjD,QAAO,IAAI;AACX,KAAI,UAAU;AACZ,OAAK,MAAM,KAAK,sBAAuB,QAAO,IAAI;AAClD,OAAK,MAAM,KAAK,oBAAqB,QAAO,IAAI;;AAGlD,KAAI,SAAS,QAAQ;AAInB,OAAK,MAAM,KAAK,kBAAmB,QAAO,IAAI;AAC9C,MAAI,2CAA2C;AAC/C,MAAI,SACF,KAAI,aAAa;MAEjB,KAAI,qBAAqB;AAE3B,SAAO;;CAGT,IAAI,QAAQ;AACZ,KAAI,SAGF,KAAI,qBAAqB;MACpB;EACL,MAAM,WAAW,mBAAmB,SAAS;AAC7C,MAAI,SAAU,SAAQ;AACtB,MAAI,qBAAqB,SAAS;;AAGpC,KAAI,uBAAuB;AAC3B,KAAI,gCAAgC,uBAAuB,UAAU,OAAO;AAC5E,KAAI,iCAAiC,SAAS,OAAO;AACrD,KAAI,+BAA+B,SAAS,OAAO;AACnD,MAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,SAAS,IAAI,CAAE,KAAI,KAAK;AAC5D,KAAI,2CAA2C;AAC/C,KAAI,SACF,KAAI,aAAa;KAEjB,KAAI,qBAAqB;AAE3B,QAAO;;;AAIT,MAAa,4BAA4B;;;;;;;;;;;;;AAczC,SAAgB,cAAc,KAAwB,SAA4B;CAChF,MAAM,QAAQ,IAAI;AAClB,KAAI,CAAC,SAAS,SAAS;AACrB,MAAI,OAAO,WAAW,IAAI,CAAE,QAAO,CAAC,WAAW,SAAS;AACxD,UAAQ,OAAO,MAAM,qFAAqF;;AAE5G,QAAO;EAAC;EAAU;EAAc,KAAK,UAAU,EAAE,KAAK,EAAE,oBAAoB,SAAS,2BAA2B,EAAE,CAAC;EAAC;;AAGtH,SAAS,OAAO,KAAa,KAAiC;AAC5D,SAAQ,IAAI,QAAQ,IAAI,MAAM,UAAU,CAAC,MAAM,MAAM,KAAK,WAAW,KAAK,GAAG,IAAI,CAAC,CAAC"}
@@ -209,4 +209,4 @@ function schedulerLogPath() {
209
209
 
210
210
  //#endregion
211
211
  export { schedulerLogPath as a, migratePaiFile as c, resolvePaiFile as d, paiSocketPath as i, paiHomeDir as l, daemonLogPath as n, PaiFileMigrationError as o, daemonPidPath as r, migratePaiDir as s, aibrokerSocketPath as t, paiHomePath as u };
212
- //# sourceMappingURL=runtime-paths-BiEi_gaG.mjs.map
212
+ //# sourceMappingURL=runtime-paths-DkZoxofH.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-paths-BiEi_gaG.mjs","names":[],"sources":["../src/config/pai-home.ts","../src/runtime-paths.ts"],"sourcesContent":["/**\n * pai-home.ts — the PAI_HOME namespace directory and generic per-user file\n * migration into it.\n *\n * Every PAI-owned per-user file (workers.yaml, config.json, whisper-rules.md,\n * advisor-mode.json, ...) lives under PAI_HOME (~/.claude/pai by default) so\n * nothing PAI writes can ever collide with a file Claude Code itself\n * introduces under ~/.claude. Decided 2026-09-19 — see docs/workers-config.md.\n */\n\nimport { existsSync, mkdirSync, readFileSync, copyFileSync, renameSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, dirname } from \"node:path\";\nimport { execFileSync } from \"node:child_process\";\n\n/**\n * Byte-identical check that works for files of any size. `cmp` compares by\n * streaming rather than loading either file into memory, unlike a\n * readFileSync + Buffer.compare — which throws (\"File size is greater than\n * 2 GiB\") on anything past Node's 2GiB single-read ceiling, a real limit hit\n * migrating a multi-gigabyte legacy federation database file.\n */\nfunction filesByteIdentical(a: string, b: string): boolean {\n try {\n execFileSync(\"cmp\", [\"-s\", a, b], { stdio: \"pipe\" });\n return true;\n } catch (e) {\n if (e && typeof e === \"object\" && \"status\" in e && (e as { status: number }).status === 1) return false;\n // cmp missing or errored for an unrelated reason — fall back to an\n // in-memory compare (fine for the small files this path normally sees).\n return Buffer.compare(readFileSync(a), readFileSync(b)) === 0;\n }\n}\n\n/** PAI_HOME (test isolation, power users) overrides the default namespace dir. */\nexport function paiHomeDir(): string {\n return process.env.PAI_HOME || join(homedir(), \".claude\", \"pai\");\n}\n\n/** A path under PAI_HOME, e.g. `paiHomePath(\"config.json\")`. */\nexport function paiHomePath(...segments: string[]): string {\n return join(paiHomeDir(), ...segments);\n}\n\nconst noticesPrinted = new Set<string>();\n\n/**\n * Resolve a per-user PAI file: the new PAI_HOME path if it exists, else the\n * first existing entry in `oldCandidates` (checked in order — most recent\n * old location first), else the new path (the target a first write\n * creates). Prints one stderr notice per process per new-path when a\n * fallback location is actually used.\n */\nexport function resolvePaiFile(newPath: string, oldCandidates: string[], migrateHint: string): string {\n if (existsSync(newPath)) return newPath;\n for (const old of oldCandidates) {\n if (existsSync(old)) {\n if (!noticesPrinted.has(newPath)) {\n noticesPrinted.add(newPath);\n process.stderr.write(\n `pai: ${old} is at an old location — run \\`${migrateHint}\\` to move it to ${newPath}\\n`\n );\n }\n return old;\n }\n }\n return newPath;\n}\n\nexport class PaiFileMigrationError extends Error {}\n\nexport interface MigrateFileResult {\n fromPath: string | null;\n toPath: string;\n dryRun: boolean;\n note?: string;\n}\n\n/**\n * Move a per-user PAI file to its new PAI_HOME location: copy, verify\n * byte-identical, then rename the source to `<name>.migrated-<YYYYMMDD>`\n * (never deleted). Idempotent: if the new path already holds bytes\n * identical to the found source, the source is just renamed aside; if it\n * differs, this refuses rather than overwrite silently.\n */\nexport function migratePaiFile(\n newPath: string,\n oldCandidates: string[],\n opts: { dryRun?: boolean } = {}\n): MigrateFileResult {\n const fromPath = oldCandidates.find((p) => existsSync(p)) ?? null;\n if (!fromPath) {\n const note = existsSync(newPath) ? \"already at new location\" : \"nothing to migrate — file does not exist yet\";\n return { fromPath: null, toPath: newPath, dryRun: !!opts.dryRun, note };\n }\n if (opts.dryRun) return { fromPath, toPath: newPath, dryRun: true };\n\n const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, \"\");\n\n if (existsSync(newPath)) {\n if (filesByteIdentical(fromPath, newPath)) {\n renameSync(fromPath, `${fromPath}.migrated-${stamp}`);\n return { fromPath, toPath: newPath, dryRun: false, note: \"identical — old file renamed aside\" };\n }\n throw new PaiFileMigrationError(\n `${newPath} already exists and differs from ${fromPath} — resolve manually, nothing changed`\n );\n }\n\n const dir = dirname(newPath);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n copyFileSync(fromPath, newPath);\n\n if (!filesByteIdentical(fromPath, newPath)) {\n throw new PaiFileMigrationError(\n `${newPath}: copy did not match ${fromPath} byte-for-byte — aborting, old file left in place`\n );\n }\n\n renameSync(fromPath, `${fromPath}.migrated-${stamp}`);\n return { fromPath, toPath: newPath, dryRun: false };\n}\n\nexport interface MigrateDirResult {\n fromDir: string | null;\n toDir: string;\n dryRun: boolean;\n movedCount?: number;\n note?: string;\n}\n\n/**\n * Move a per-user PAI directory (queries/, session-state/, ...) into\n * PAI_HOME: move every entry from the first found old candidate into the\n * new dir (never overwriting an existing entry there), then rename the now-\n * empty old dir aside to `<name>.migrated-<YYYYMMDD>` (never deleted).\n * Idempotent — an already-migrated dir has nothing left to find.\n */\nexport function migratePaiDir(\n newDir: string,\n oldCandidates: string[],\n opts: { dryRun?: boolean } = {}\n): MigrateDirResult {\n const fromDir = oldCandidates.find((p) => existsSync(p) && statSync(p).isDirectory()) ?? null;\n if (!fromDir) {\n const note = existsSync(newDir) ? \"already at new location\" : \"nothing to migrate — directory does not exist yet\";\n return { fromDir: null, toDir: newDir, dryRun: !!opts.dryRun, note };\n }\n\n const entries = readdirSync(fromDir);\n if (opts.dryRun) return { fromDir, toDir: newDir, dryRun: true, movedCount: entries.length };\n\n if (!existsSync(newDir)) mkdirSync(newDir, { recursive: true });\n\n let moved = 0;\n const collided: string[] = [];\n for (const entry of entries) {\n const src = join(fromDir, entry);\n const dst = join(newDir, entry);\n if (existsSync(dst)) {\n collided.push(entry);\n continue;\n }\n renameSync(src, dst);\n moved++;\n }\n\n const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, \"\");\n const remaining = readdirSync(fromDir);\n const note = collided.length\n ? `${collided.length} entrie(s) left in ${fromDir} — name already existed in ${newDir}`\n : undefined;\n\n if (remaining.length === 0) {\n renameSync(fromDir, `${fromDir}.migrated-${stamp}`);\n }\n\n return { fromDir, toDir: newDir, dryRun: false, movedCount: moved, note };\n}\n","/**\n * runtime-paths.ts — the shared files PAI talks to the running daemon through.\n *\n * Sockets, logs and pidfiles live at fixed absolute paths under /tmp, and every\n * one of them is shared with a *live* daemon. That makes them the same hazard\n * as any other piece of real user state a test can reach: writing one from a\n * test does not fail, it corrupts something already running, silently.\n *\n * The home guard in test/setup-home-guard.ts does not cover them. It works by\n * redirecting HOME, so it protects paths *derived* from the home directory and\n * nothing else — a hardcoded \"/tmp/pai.sock\" walks straight past it. Verified\n * on 2026-08-01 by snapshotting all five and running the suite: nothing writes\n * to them today. But \"no test does this yet\" is not a property, and the next\n * daemon test to be written would clobber the live socket with nothing to stop\n * it.\n *\n * So each path is read from an environment variable with the historical /tmp\n * value as its default. Behaviour is unchanged for every real invocation; the\n * test setup overrides the variables and the whole class becomes unreachable\n * rather than merely absent, the same way the home guard works.\n *\n * Read at call time, not at module load: the test setup file runs before any\n * module is imported, but resolving these eagerly would still bake in whatever\n * the environment held at import and make the override order matter.\n */\n\nimport { paiHomePath } from \"./config/pai-home.js\";\n\n/** IPC socket for the PAI daemon. */\nexport function paiSocketPath(): string {\n return process.env.PAI_SOCKET_PATH ?? \"/tmp/pai.sock\";\n}\n\n/** IPC socket for the AIBroker daemon — read by PAI, owned by AIBroker. */\nexport function aibrokerSocketPath(): string {\n return process.env.PAI_AIBROKER_SOCKET_PATH ?? \"/tmp/aibroker.sock\";\n}\n\n/** Where the daemon's stdout and stderr are collected. */\nexport function daemonLogPath(): string {\n return process.env.PAI_DAEMON_LOG_PATH ?? \"/tmp/pai-daemon.log\";\n}\n\n/** Pidfile for the running daemon. */\nexport function daemonPidPath(): string {\n return process.env.PAI_DAEMON_PID_PATH ?? paiHomePath(\"daemon.pid\");\n}\n\n/** Where the launchd scheduler tick writes its output. */\nexport function schedulerLogPath(): string {\n return process.env.PAI_SCHEDULER_LOG_PATH ?? \"/tmp/pai-scheduler.log\";\n}\n\n/**\n * Where the status line caches what it may reuse between renders: the\n * per-provider plan-usage snapshots and the Claude Code version. Shared with\n * every live session's status line, so a test writing here would be feeding\n * fixtures to the real thing. Read by statusline-command.sh as PAI_CACHE_DIR.\n */\nexport function paiCacheDir(): string {\n return process.env.PAI_CACHE_DIR ?? \"/tmp/claude\";\n}\n\n/**\n * Every runtime path, for the test guard to redirect in one place.\n *\n * Listed here rather than in the guard so that adding a path and forgetting to\n * protect it is not possible: the guard iterates this.\n */\nexport const RUNTIME_PATH_ENV_VARS = [\n \"PAI_SOCKET_PATH\",\n \"PAI_AIBROKER_SOCKET_PATH\",\n \"PAI_DAEMON_LOG_PATH\",\n \"PAI_DAEMON_PID_PATH\",\n \"PAI_SCHEDULER_LOG_PATH\",\n \"PAI_CACHE_DIR\",\n] as const;\n"],"mappings":"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"}
1
+ {"version":3,"file":"runtime-paths-DkZoxofH.mjs","names":[],"sources":["../src/config/pai-home.ts","../src/runtime-paths.ts"],"sourcesContent":["/**\n * pai-home.ts — the PAI_HOME namespace directory and generic per-user file\n * migration into it.\n *\n * Every PAI-owned per-user file (workers.yaml, config.json, whisper-rules.md,\n * advisor-mode.json, ...) lives under PAI_HOME (~/.claude/pai by default) so\n * nothing PAI writes can ever collide with a file Claude Code itself\n * introduces under ~/.claude. Decided 2026-09-19 — see docs/workers-config.md.\n */\n\nimport { existsSync, mkdirSync, readFileSync, copyFileSync, renameSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, dirname } from \"node:path\";\nimport { execFileSync } from \"node:child_process\";\n\n/**\n * Byte-identical check that works for files of any size. `cmp` compares by\n * streaming rather than loading either file into memory, unlike a\n * readFileSync + Buffer.compare — which throws (\"File size is greater than\n * 2 GiB\") on anything past Node's 2GiB single-read ceiling, a real limit hit\n * migrating a multi-gigabyte legacy federation database file.\n */\nfunction filesByteIdentical(a: string, b: string): boolean {\n try {\n execFileSync(\"cmp\", [\"-s\", a, b], { stdio: \"pipe\" });\n return true;\n } catch (e) {\n if (e && typeof e === \"object\" && \"status\" in e && (e as { status: number }).status === 1) return false;\n // cmp missing or errored for an unrelated reason — fall back to an\n // in-memory compare (fine for the small files this path normally sees).\n return Buffer.compare(readFileSync(a), readFileSync(b)) === 0;\n }\n}\n\n/** PAI_HOME (test isolation, power users) overrides the default namespace dir. */\nexport function paiHomeDir(): string {\n return process.env.PAI_HOME || join(homedir(), \".claude\", \"pai\");\n}\n\n/** A path under PAI_HOME, e.g. `paiHomePath(\"config.json\")`. */\nexport function paiHomePath(...segments: string[]): string {\n return join(paiHomeDir(), ...segments);\n}\n\nconst noticesPrinted = new Set<string>();\n\n/**\n * Resolve a per-user PAI file: the new PAI_HOME path if it exists, else the\n * first existing entry in `oldCandidates` (checked in order — most recent\n * old location first), else the new path (the target a first write\n * creates). Prints one stderr notice per process per new-path when a\n * fallback location is actually used.\n */\nexport function resolvePaiFile(newPath: string, oldCandidates: string[], migrateHint: string): string {\n if (existsSync(newPath)) return newPath;\n for (const old of oldCandidates) {\n if (existsSync(old)) {\n if (!noticesPrinted.has(newPath)) {\n noticesPrinted.add(newPath);\n process.stderr.write(\n `pai: ${old} is at an old location — run \\`${migrateHint}\\` to move it to ${newPath}\\n`\n );\n }\n return old;\n }\n }\n return newPath;\n}\n\nexport class PaiFileMigrationError extends Error {}\n\nexport interface MigrateFileResult {\n fromPath: string | null;\n toPath: string;\n dryRun: boolean;\n note?: string;\n}\n\n/**\n * Move a per-user PAI file to its new PAI_HOME location: copy, verify\n * byte-identical, then rename the source to `<name>.migrated-<YYYYMMDD>`\n * (never deleted). Idempotent: if the new path already holds bytes\n * identical to the found source, the source is just renamed aside; if it\n * differs, this refuses rather than overwrite silently.\n */\nexport function migratePaiFile(\n newPath: string,\n oldCandidates: string[],\n opts: { dryRun?: boolean } = {}\n): MigrateFileResult {\n const fromPath = oldCandidates.find((p) => existsSync(p)) ?? null;\n if (!fromPath) {\n const note = existsSync(newPath) ? \"already at new location\" : \"nothing to migrate — file does not exist yet\";\n return { fromPath: null, toPath: newPath, dryRun: !!opts.dryRun, note };\n }\n if (opts.dryRun) return { fromPath, toPath: newPath, dryRun: true };\n\n const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, \"\");\n\n if (existsSync(newPath)) {\n if (filesByteIdentical(fromPath, newPath)) {\n renameSync(fromPath, `${fromPath}.migrated-${stamp}`);\n return { fromPath, toPath: newPath, dryRun: false, note: \"identical — old file renamed aside\" };\n }\n throw new PaiFileMigrationError(\n `${newPath} already exists and differs from ${fromPath} — resolve manually, nothing changed`\n );\n }\n\n const dir = dirname(newPath);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n copyFileSync(fromPath, newPath);\n\n if (!filesByteIdentical(fromPath, newPath)) {\n throw new PaiFileMigrationError(\n `${newPath}: copy did not match ${fromPath} byte-for-byte — aborting, old file left in place`\n );\n }\n\n renameSync(fromPath, `${fromPath}.migrated-${stamp}`);\n return { fromPath, toPath: newPath, dryRun: false };\n}\n\nexport interface MigrateDirResult {\n fromDir: string | null;\n toDir: string;\n dryRun: boolean;\n movedCount?: number;\n note?: string;\n}\n\n/**\n * Move a per-user PAI directory (queries/, session-state/, ...) into\n * PAI_HOME: move every entry from the first found old candidate into the\n * new dir (never overwriting an existing entry there), then rename the now-\n * empty old dir aside to `<name>.migrated-<YYYYMMDD>` (never deleted).\n * Idempotent — an already-migrated dir has nothing left to find.\n */\nexport function migratePaiDir(\n newDir: string,\n oldCandidates: string[],\n opts: { dryRun?: boolean } = {}\n): MigrateDirResult {\n const fromDir = oldCandidates.find((p) => existsSync(p) && statSync(p).isDirectory()) ?? null;\n if (!fromDir) {\n const note = existsSync(newDir) ? \"already at new location\" : \"nothing to migrate — directory does not exist yet\";\n return { fromDir: null, toDir: newDir, dryRun: !!opts.dryRun, note };\n }\n\n const entries = readdirSync(fromDir);\n if (opts.dryRun) return { fromDir, toDir: newDir, dryRun: true, movedCount: entries.length };\n\n if (!existsSync(newDir)) mkdirSync(newDir, { recursive: true });\n\n let moved = 0;\n const collided: string[] = [];\n for (const entry of entries) {\n const src = join(fromDir, entry);\n const dst = join(newDir, entry);\n if (existsSync(dst)) {\n collided.push(entry);\n continue;\n }\n renameSync(src, dst);\n moved++;\n }\n\n const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, \"\");\n const remaining = readdirSync(fromDir);\n const note = collided.length\n ? `${collided.length} entrie(s) left in ${fromDir} — name already existed in ${newDir}`\n : undefined;\n\n if (remaining.length === 0) {\n renameSync(fromDir, `${fromDir}.migrated-${stamp}`);\n }\n\n return { fromDir, toDir: newDir, dryRun: false, movedCount: moved, note };\n}\n","/**\n * runtime-paths.ts — the shared files PAI talks to the running daemon through.\n *\n * Sockets, logs and pidfiles live at fixed absolute paths under /tmp, and every\n * one of them is shared with a *live* daemon. That makes them the same hazard\n * as any other piece of real user state a test can reach: writing one from a\n * test does not fail, it corrupts something already running, silently.\n *\n * The home guard in test/setup-home-guard.ts does not cover them. It works by\n * redirecting HOME, so it protects paths *derived* from the home directory and\n * nothing else — a hardcoded \"/tmp/pai.sock\" walks straight past it. Verified\n * on 2026-08-01 by snapshotting all five and running the suite: nothing writes\n * to them today. But \"no test does this yet\" is not a property, and the next\n * daemon test to be written would clobber the live socket with nothing to stop\n * it.\n *\n * So each path is read from an environment variable with the historical /tmp\n * value as its default. Behaviour is unchanged for every real invocation; the\n * test setup overrides the variables and the whole class becomes unreachable\n * rather than merely absent, the same way the home guard works.\n *\n * Read at call time, not at module load: the test setup file runs before any\n * module is imported, but resolving these eagerly would still bake in whatever\n * the environment held at import and make the override order matter.\n */\n\nimport { paiHomePath } from \"./config/pai-home.js\";\n\n/** IPC socket for the PAI daemon. */\nexport function paiSocketPath(): string {\n return process.env.PAI_SOCKET_PATH ?? \"/tmp/pai.sock\";\n}\n\n/** IPC socket for the AIBroker daemon — read by PAI, owned by AIBroker. */\nexport function aibrokerSocketPath(): string {\n return process.env.PAI_AIBROKER_SOCKET_PATH ?? \"/tmp/aibroker.sock\";\n}\n\n/** Where the daemon's stdout and stderr are collected. */\nexport function daemonLogPath(): string {\n return process.env.PAI_DAEMON_LOG_PATH ?? \"/tmp/pai-daemon.log\";\n}\n\n/** Pidfile for the running daemon. */\nexport function daemonPidPath(): string {\n return process.env.PAI_DAEMON_PID_PATH ?? paiHomePath(\"daemon.pid\");\n}\n\n/** Where the launchd scheduler tick writes its output. */\nexport function schedulerLogPath(): string {\n return process.env.PAI_SCHEDULER_LOG_PATH ?? \"/tmp/pai-scheduler.log\";\n}\n\n/**\n * Where the status line caches what it may reuse between renders: the\n * per-provider plan-usage snapshots and the Claude Code version. Shared with\n * every live session's status line, so a test writing here would be feeding\n * fixtures to the real thing. Read by statusline-command.sh as PAI_CACHE_DIR.\n */\nexport function paiCacheDir(): string {\n return process.env.PAI_CACHE_DIR ?? \"/tmp/claude\";\n}\n\n/**\n * Every runtime path, for the test guard to redirect in one place.\n *\n * Listed here rather than in the guard so that adding a path and forgetting to\n * protect it is not possible: the guard iterates this.\n */\nexport const RUNTIME_PATH_ENV_VARS = [\n \"PAI_SOCKET_PATH\",\n \"PAI_AIBROKER_SOCKET_PATH\",\n \"PAI_DAEMON_LOG_PATH\",\n \"PAI_DAEMON_PID_PATH\",\n \"PAI_SCHEDULER_LOG_PATH\",\n \"PAI_CACHE_DIR\",\n] as const;\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { t as STOP_WORDS } from "./stop-words-DtxaTWU_.mjs";
1
+ import { t as STOP_WORDS } from "./stop-words-LtkS-Pej.mjs";
2
2
 
3
3
  //#region src/memory/search.ts
4
4
  /**
@@ -104,4 +104,4 @@ function applyRecencyBoost(results, halfLifeDays = 90) {
104
104
 
105
105
  //#endregion
106
106
  export { populateSlugs as i, buildFtsQuery as n, isQuerySyntaxError as r, applyRecencyBoost as t };
107
- //# sourceMappingURL=search-CNAGTiJP.mjs.map
107
+ //# sourceMappingURL=search-cPDj-9fO.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"search-CNAGTiJP.mjs","names":[],"sources":["../src/memory/search.ts"],"sourcesContent":["/**\n * Query building and result post-processing for the PAI federation memory\n * index — everything here is pure logic (no SQL). The raw FTS5/vector\n * queries live under src/storage/{sqlite,postgres}/search.ts, which call\n * back into buildFtsQuery()/isQuerySyntaxError() from this module.\n */\n\nimport { STOP_WORDS } from \"../utils/stop-words.js\";\nimport type { RegistryBackend } from \"../storage/registry-interface.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SearchResult {\n projectId: number;\n projectSlug?: string; // populated from registry after search when available\n path: string;\n startLine: number;\n endLine: number;\n snippet: string;\n score: number; // raw BM25 score (lower = more relevant in FTS5)\n tier: string;\n source: string;\n updatedAt?: number; // Unix ms from memory_chunks.updated_at\n lastAccessedAt?: number; // Unix ms from memory_chunks.last_accessed_at (QW2)\n chunkId?: string; // chunk ID for last_accessed_at update (QW2)\n}\n\nexport interface SearchOptions {\n /** Restrict search to these project IDs. */\n projectIds?: number[];\n /** Restrict to 'memory' or 'notes' sources. */\n sources?: string[];\n /** Restrict to specific tier(s): 'evergreen' | 'daily' | 'topic' | 'session' */\n tiers?: string[];\n /** Maximum number of results to return. Default 10. */\n maxResults?: number;\n /** Minimum BM25 score threshold (FTS5 scores are negative; 0.0 means no filter). */\n minScore?: number;\n}\n\n// STOP_WORDS imported from utils/stop-words.ts\n\n// ---------------------------------------------------------------------------\n// Query builder\n// ---------------------------------------------------------------------------\n\n/**\n * Convert a free-text query into an FTS5 query string.\n *\n * Strategy:\n * 1. Tokenise by whitespace and punctuation\n * 2. Remove stop words and tokens shorter than 2 characters\n * 3. Double-quote each remaining token (exact word form)\n * 4. Join with OR so that any matching token returns a result\n *\n * Using OR instead of AND is critical for multi-word queries: the words rarely\n * all appear in the same chunk, so AND would return zero results. FTS5 BM25\n * scoring naturally ranks chunks where more terms match higher, so the most\n * relevant chunks still surface at the top.\n *\n * Example: \"Synchrotech interview follow-up Gilles\"\n * → `\"synchrotech\" OR \"interview\" OR \"follow\" OR \"gilles\"`\n * → chunks matching any term, ranked by how many terms match\n */\n/**\n * Did SQLite reject the QUERY, or fail at the STORE?\n *\n * Only the first justifies an empty result. FTS5 reports a bad MATCH expression\n * with a recognisable message; a missing table, a corrupt index or a locked\n * database do not, and must not be reported as \"nothing found\".\n *\n * Matching on message text is unlovely, and the alternative — treating every\n * failure as empty — is what produced a confidently wrong answer to a human.\n */\nexport function isQuerySyntaxError(e: unknown): boolean {\n const msg = e instanceof Error ? e.message : String(e);\n return /fts5|malformed MATCH|syntax error|unterminated string|no such column/i.test(msg);\n}\n\nexport function buildFtsQuery(query: string): string {\n const tokens = query\n .toLowerCase()\n .split(/[\\s\\p{P}]+/u)\n .filter(Boolean)\n .filter((t) => t.length >= 2)\n .filter((t) => !STOP_WORDS.has(t))\n // Escape any double-quotes inside the token (FTS5 uses them as delimiters)\n .map((t) => `\"${t.replace(/\"/g, '\"\"')}\"`)\n\n if (tokens.length === 0) {\n // Fallback: use original query as a raw string (may produce no results)\n return `\"${query.replace(/\"/g, '\"\"')}\"`;\n }\n\n return tokens.join(\" OR \");\n}\n\n// The raw searchMemory()/searchMemorySemantic()/searchMemoryHybrid()/\n// touchChunksLastAccessed() implementations live in storage/sqlite/search.ts\n// (SQLite) and storage/postgres/search.ts + backend.ts (Postgres).\n\n// ---------------------------------------------------------------------------\n// Slug lookup helper\n// ---------------------------------------------------------------------------\n\n/**\n * Populate the projectSlug field on search results by looking up project IDs\n * in the registry database.\n */\nexport async function populateSlugs(\n results: SearchResult[],\n registry: RegistryBackend,\n): Promise<SearchResult[]> {\n if (results.length === 0) return results;\n\n const ids = [...new Set(results.map((r) => r.projectId))];\n // No batch \"projects by ids\" method on RegistryBackend yet (flagged in the\n // report) — loops getProjectById, bounded by the distinct project count in\n // an already-capped result set.\n const slugMap = new Map<number, string>();\n for (const id of ids) {\n const project = await registry.getProjectById(id);\n if (project) slugMap.set(id, project.slug);\n }\n\n return results.map((r) => ({\n ...r,\n projectSlug: slugMap.get(r.projectId),\n }));\n}\n\n// ---------------------------------------------------------------------------\n// Recency boost\n// ---------------------------------------------------------------------------\n\n/**\n * Apply exponential recency boost to search scores.\n *\n * Scores are first min-max normalized to [0,1], then multiplied by an\n * exponential decay factor based on chunk age. Normalization is required\n * because the cross-encoder reranker produces negative logit scores — naive\n * multiplication of a negative score by a decay factor (0 < d ≤ 1) would\n * make the score *less* negative, effectively boosting old results instead\n * of penalizing them.\n *\n * Formula: score_final = normalized * exp(-lambda * age_days)\n * where lambda = ln(2) / halfLifeDays, normalized ∈ [0,1]\n *\n * With default halfLifeDays=90, a 3-month-old chunk retains 50% of its\n * normalized score, a 6-month-old retains 25%, and a 1-year-old ~6%.\n *\n * Results without an updatedAt timestamp receive no decay penalty.\n * Results are re-sorted by the boosted score after application.\n *\n * @param results Search results with optional updatedAt timestamps.\n * @param halfLifeDays Score halves every N days. Default 90 (~3 months).\n * @returns New array sorted by decayed normalized score (descending).\n */\nexport function applyRecencyBoost(\n results: SearchResult[],\n halfLifeDays = 90,\n): SearchResult[] {\n if (halfLifeDays <= 0 || results.length === 0) return results;\n\n const lambda = Math.LN2 / halfLifeDays;\n const now = Date.now();\n\n // Min-max normalize scores to [0,1] so multiplicative decay works\n // correctly regardless of the raw score sign/scale.\n const scores = results.map((r) => r.score);\n const minScore = Math.min(...scores);\n const maxScore = Math.max(...scores);\n const range = maxScore - minScore;\n\n return results\n .map((r) => {\n const normalized = range === 0 ? 1 : (r.score - minScore) / range;\n // QW2: use the more recent of updated_at and last_accessed_at for recency decay\n const effectiveTs = r.updatedAt != null && r.lastAccessedAt != null\n ? Math.max(r.updatedAt, r.lastAccessedAt)\n : (r.lastAccessedAt ?? r.updatedAt);\n const decay = effectiveTs\n ? Math.exp(-lambda * Math.max(0, (now - effectiveTs) / 86_400_000))\n : 1; // no timestamp → no penalty\n return { ...r, score: normalized * decay };\n })\n .sort((a, b) => b.score - a.score);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4EA,SAAgB,mBAAmB,GAAqB;CACtD,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,QAAO,wEAAwE,KAAK,IAAI;;AAG1F,SAAgB,cAAc,OAAuB;CACnD,MAAM,SAAS,MACZ,aAAa,CACb,MAAM,cAAc,CACpB,OAAO,QAAQ,CACf,QAAQ,MAAM,EAAE,UAAU,EAAE,CAC5B,QAAQ,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC,CAEjC,KAAK,MAAM,IAAI,EAAE,QAAQ,MAAM,OAAK,CAAC,GAAG;AAE3C,KAAI,OAAO,WAAW,EAEpB,QAAO,IAAI,MAAM,QAAQ,MAAM,OAAK,CAAC;AAGvC,QAAO,OAAO,KAAK,OAAO;;;;;;AAe5B,eAAsB,cACpB,SACA,UACyB;AACzB,KAAI,QAAQ,WAAW,EAAG,QAAO;CAEjC,MAAM,MAAM,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,UAAU,CAAC,CAAC;CAIzD,MAAM,0BAAU,IAAI,KAAqB;AACzC,MAAK,MAAM,MAAM,KAAK;EACpB,MAAM,UAAU,MAAM,SAAS,eAAe,GAAG;AACjD,MAAI,QAAS,SAAQ,IAAI,IAAI,QAAQ,KAAK;;AAG5C,QAAO,QAAQ,KAAK,OAAO;EACzB,GAAG;EACH,aAAa,QAAQ,IAAI,EAAE,UAAU;EACtC,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;AA8BL,SAAgB,kBACd,SACA,eAAe,IACC;AAChB,KAAI,gBAAgB,KAAK,QAAQ,WAAW,EAAG,QAAO;CAEtD,MAAM,SAAS,KAAK,MAAM;CAC1B,MAAM,MAAM,KAAK,KAAK;CAItB,MAAM,SAAS,QAAQ,KAAK,MAAM,EAAE,MAAM;CAC1C,MAAM,WAAW,KAAK,IAAI,GAAG,OAAO;CAEpC,MAAM,QADW,KAAK,IAAI,GAAG,OAAO,GACX;AAEzB,QAAO,QACJ,KAAK,MAAM;EACV,MAAM,aAAa,UAAU,IAAI,KAAK,EAAE,QAAQ,YAAY;EAE5D,MAAM,cAAc,EAAE,aAAa,QAAQ,EAAE,kBAAkB,OAC3D,KAAK,IAAI,EAAE,WAAW,EAAE,eAAe,GACtC,EAAE,kBAAkB,EAAE;EAC3B,MAAM,QAAQ,cACV,KAAK,IAAI,CAAC,SAAS,KAAK,IAAI,IAAI,MAAM,eAAe,MAAW,CAAC,GACjE;AACJ,SAAO;GAAE,GAAG;GAAG,OAAO,aAAa;GAAO;GAC1C,CACD,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM"}
1
+ {"version":3,"file":"search-cPDj-9fO.mjs","names":[],"sources":["../src/memory/search.ts"],"sourcesContent":["/**\n * Query building and result post-processing for the PAI federation memory\n * index — everything here is pure logic (no SQL). The raw FTS5/vector\n * queries live under src/storage/{sqlite,postgres}/search.ts, which call\n * back into buildFtsQuery()/isQuerySyntaxError() from this module.\n */\n\nimport { STOP_WORDS } from \"../utils/stop-words.js\";\nimport type { RegistryBackend } from \"../storage/registry-interface.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SearchResult {\n projectId: number;\n projectSlug?: string; // populated from registry after search when available\n path: string;\n startLine: number;\n endLine: number;\n snippet: string;\n score: number; // raw BM25 score (lower = more relevant in FTS5)\n tier: string;\n source: string;\n updatedAt?: number; // Unix ms from memory_chunks.updated_at\n lastAccessedAt?: number; // Unix ms from memory_chunks.last_accessed_at (QW2)\n chunkId?: string; // chunk ID for last_accessed_at update (QW2)\n}\n\nexport interface SearchOptions {\n /** Restrict search to these project IDs. */\n projectIds?: number[];\n /** Restrict to 'memory' or 'notes' sources. */\n sources?: string[];\n /** Restrict to specific tier(s): 'evergreen' | 'daily' | 'topic' | 'session' */\n tiers?: string[];\n /** Maximum number of results to return. Default 10. */\n maxResults?: number;\n /** Minimum BM25 score threshold (FTS5 scores are negative; 0.0 means no filter). */\n minScore?: number;\n}\n\n// STOP_WORDS imported from utils/stop-words.ts\n\n// ---------------------------------------------------------------------------\n// Query builder\n// ---------------------------------------------------------------------------\n\n/**\n * Convert a free-text query into an FTS5 query string.\n *\n * Strategy:\n * 1. Tokenise by whitespace and punctuation\n * 2. Remove stop words and tokens shorter than 2 characters\n * 3. Double-quote each remaining token (exact word form)\n * 4. Join with OR so that any matching token returns a result\n *\n * Using OR instead of AND is critical for multi-word queries: the words rarely\n * all appear in the same chunk, so AND would return zero results. FTS5 BM25\n * scoring naturally ranks chunks where more terms match higher, so the most\n * relevant chunks still surface at the top.\n *\n * Example: \"Synchrotech interview follow-up Gilles\"\n * → `\"synchrotech\" OR \"interview\" OR \"follow\" OR \"gilles\"`\n * → chunks matching any term, ranked by how many terms match\n */\n/**\n * Did SQLite reject the QUERY, or fail at the STORE?\n *\n * Only the first justifies an empty result. FTS5 reports a bad MATCH expression\n * with a recognisable message; a missing table, a corrupt index or a locked\n * database do not, and must not be reported as \"nothing found\".\n *\n * Matching on message text is unlovely, and the alternative — treating every\n * failure as empty — is what produced a confidently wrong answer to a human.\n */\nexport function isQuerySyntaxError(e: unknown): boolean {\n const msg = e instanceof Error ? e.message : String(e);\n return /fts5|malformed MATCH|syntax error|unterminated string|no such column/i.test(msg);\n}\n\nexport function buildFtsQuery(query: string): string {\n const tokens = query\n .toLowerCase()\n .split(/[\\s\\p{P}]+/u)\n .filter(Boolean)\n .filter((t) => t.length >= 2)\n .filter((t) => !STOP_WORDS.has(t))\n // Escape any double-quotes inside the token (FTS5 uses them as delimiters)\n .map((t) => `\"${t.replace(/\"/g, '\"\"')}\"`)\n\n if (tokens.length === 0) {\n // Fallback: use original query as a raw string (may produce no results)\n return `\"${query.replace(/\"/g, '\"\"')}\"`;\n }\n\n return tokens.join(\" OR \");\n}\n\n// The raw searchMemory()/searchMemorySemantic()/searchMemoryHybrid()/\n// touchChunksLastAccessed() implementations live in storage/sqlite/search.ts\n// (SQLite) and storage/postgres/search.ts + backend.ts (Postgres).\n\n// ---------------------------------------------------------------------------\n// Slug lookup helper\n// ---------------------------------------------------------------------------\n\n/**\n * Populate the projectSlug field on search results by looking up project IDs\n * in the registry database.\n */\nexport async function populateSlugs(\n results: SearchResult[],\n registry: RegistryBackend,\n): Promise<SearchResult[]> {\n if (results.length === 0) return results;\n\n const ids = [...new Set(results.map((r) => r.projectId))];\n // No batch \"projects by ids\" method on RegistryBackend yet (flagged in the\n // report) — loops getProjectById, bounded by the distinct project count in\n // an already-capped result set.\n const slugMap = new Map<number, string>();\n for (const id of ids) {\n const project = await registry.getProjectById(id);\n if (project) slugMap.set(id, project.slug);\n }\n\n return results.map((r) => ({\n ...r,\n projectSlug: slugMap.get(r.projectId),\n }));\n}\n\n// ---------------------------------------------------------------------------\n// Recency boost\n// ---------------------------------------------------------------------------\n\n/**\n * Apply exponential recency boost to search scores.\n *\n * Scores are first min-max normalized to [0,1], then multiplied by an\n * exponential decay factor based on chunk age. Normalization is required\n * because the cross-encoder reranker produces negative logit scores — naive\n * multiplication of a negative score by a decay factor (0 < d ≤ 1) would\n * make the score *less* negative, effectively boosting old results instead\n * of penalizing them.\n *\n * Formula: score_final = normalized * exp(-lambda * age_days)\n * where lambda = ln(2) / halfLifeDays, normalized ∈ [0,1]\n *\n * With default halfLifeDays=90, a 3-month-old chunk retains 50% of its\n * normalized score, a 6-month-old retains 25%, and a 1-year-old ~6%.\n *\n * Results without an updatedAt timestamp receive no decay penalty.\n * Results are re-sorted by the boosted score after application.\n *\n * @param results Search results with optional updatedAt timestamps.\n * @param halfLifeDays Score halves every N days. Default 90 (~3 months).\n * @returns New array sorted by decayed normalized score (descending).\n */\nexport function applyRecencyBoost(\n results: SearchResult[],\n halfLifeDays = 90,\n): SearchResult[] {\n if (halfLifeDays <= 0 || results.length === 0) return results;\n\n const lambda = Math.LN2 / halfLifeDays;\n const now = Date.now();\n\n // Min-max normalize scores to [0,1] so multiplicative decay works\n // correctly regardless of the raw score sign/scale.\n const scores = results.map((r) => r.score);\n const minScore = Math.min(...scores);\n const maxScore = Math.max(...scores);\n const range = maxScore - minScore;\n\n return results\n .map((r) => {\n const normalized = range === 0 ? 1 : (r.score - minScore) / range;\n // QW2: use the more recent of updated_at and last_accessed_at for recency decay\n const effectiveTs = r.updatedAt != null && r.lastAccessedAt != null\n ? Math.max(r.updatedAt, r.lastAccessedAt)\n : (r.lastAccessedAt ?? r.updatedAt);\n const decay = effectiveTs\n ? Math.exp(-lambda * Math.max(0, (now - effectiveTs) / 86_400_000))\n : 1; // no timestamp → no penalty\n return { ...r, score: normalized * decay };\n })\n .sort((a, b) => b.score - a.score);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4EA,SAAgB,mBAAmB,GAAqB;CACtD,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,QAAO,wEAAwE,KAAK,IAAI;;AAG1F,SAAgB,cAAc,OAAuB;CACnD,MAAM,SAAS,MACZ,aAAa,CACb,MAAM,cAAc,CACpB,OAAO,QAAQ,CACf,QAAQ,MAAM,EAAE,UAAU,EAAE,CAC5B,QAAQ,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC,CAEjC,KAAK,MAAM,IAAI,EAAE,QAAQ,MAAM,OAAK,CAAC,GAAG;AAE3C,KAAI,OAAO,WAAW,EAEpB,QAAO,IAAI,MAAM,QAAQ,MAAM,OAAK,CAAC;AAGvC,QAAO,OAAO,KAAK,OAAO;;;;;;AAe5B,eAAsB,cACpB,SACA,UACyB;AACzB,KAAI,QAAQ,WAAW,EAAG,QAAO;CAEjC,MAAM,MAAM,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,UAAU,CAAC,CAAC;CAIzD,MAAM,0BAAU,IAAI,KAAqB;AACzC,MAAK,MAAM,MAAM,KAAK;EACpB,MAAM,UAAU,MAAM,SAAS,eAAe,GAAG;AACjD,MAAI,QAAS,SAAQ,IAAI,IAAI,QAAQ,KAAK;;AAG5C,QAAO,QAAQ,KAAK,OAAO;EACzB,GAAG;EACH,aAAa,QAAQ,IAAI,EAAE,UAAU;EACtC,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;AA8BL,SAAgB,kBACd,SACA,eAAe,IACC;AAChB,KAAI,gBAAgB,KAAK,QAAQ,WAAW,EAAG,QAAO;CAEtD,MAAM,SAAS,KAAK,MAAM;CAC1B,MAAM,MAAM,KAAK,KAAK;CAItB,MAAM,SAAS,QAAQ,KAAK,MAAM,EAAE,MAAM;CAC1C,MAAM,WAAW,KAAK,IAAI,GAAG,OAAO;CAEpC,MAAM,QADW,KAAK,IAAI,GAAG,OAAO,GACX;AAEzB,QAAO,QACJ,KAAK,MAAM;EACV,MAAM,aAAa,UAAU,IAAI,KAAK,EAAE,QAAQ,YAAY;EAE5D,MAAM,cAAc,EAAE,aAAa,QAAQ,EAAE,kBAAkB,OAC3D,KAAK,IAAI,EAAE,WAAW,EAAE,eAAe,GACtC,EAAE,kBAAkB,EAAE;EAC3B,MAAM,QAAQ,cACV,KAAK,IAAI,CAAC,SAAS,KAAK,IAAI,IAAI,MAAM,eAAe,MAAW,CAAC,GACjE;AACJ,SAAO;GAAE,GAAG;GAAG,OAAO,aAAa;GAAO;GAC1C,CACD,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM"}
@@ -114,4 +114,4 @@ function tail(s, n) {
114
114
 
115
115
  //#endregion
116
116
  export { cmdMemorySources };
117
- //# sourceMappingURL=sources-BxXbzpAJ.mjs.map
117
+ //# sourceMappingURL=sources-CvvzNZad.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"sources-BxXbzpAJ.mjs","names":[],"sources":["../src/cli/commands/memory/sources.ts"],"sourcesContent":["/**\n * `pai memory sources` — what the indexer is actually taking in.\n *\n * The index had grown to ~1.5M chunks against a vault of ~2,700 notes, and\n * nothing in the CLI could show why. `memory status` refuses to report at all\n * when the backend is not SQLite, and `daemon status` gives three totals with no\n * composition — so answering \"what is it indexing, and why is it re-indexing\"\n * meant hand-writing aggregate SQL against the container.\n *\n * The cause, when it was finally measured, was visible in one breakdown: the\n * vault indexer follows symlinks out of the vault into cloud-synced trees, and\n * those trees have mtimes rewritten by the sync client on content that has not\n * changed. Each rewrite re-chunks the file, and re-chunking assigns new chunk\n * ids, which discards the embeddings — so the embedder was re-doing work\n * indefinitely while never catching up.\n *\n * Hence the four sections below. Each one exists because it was needed:\n * composition — the vault dwarfing everything else is the first clue\n * roots — where content enters from, which is how symlink leakage shows\n * heaviest — single files contributing thousands of chunks (attachments)\n * churn — chunks rewritten per day, which is what distinguishes a\n * backlog that will finish from a treadmill that never will\n */\n\nimport { ok, warn, dim, bold, header, renderTable } from \"../../utils.js\";\nimport type { StorageBackend } from \"../../../storage/interface.js\";\n\nconst num = (v: unknown): number => Number(v ?? 0);\nconst pct = (part: number, whole: number): string =>\n whole === 0 ? \"—\" : `${Math.round((part / whole) * 100)}%`;\n\n/** Group the leading path segments, which is where content enters the index. */\nexport function rootOf(path: string, depth = 2): string {\n const parts = path.split(\"/\").filter(Boolean);\n if (parts.length <= depth) return path;\n return parts.slice(0, depth).join(\"/\") + \"/…\";\n}\n\nexport async function cmdMemorySources(\n backend: StorageBackend,\n opts: { limit?: number } = {}\n): Promise<void> {\n const limit = opts.limit ?? 8;\n\n const report = await backend.getMemorySourcesReport();\n const comp = report.composition;\n\n if (comp.length === 0) {\n console.log();\n console.log(warn(` Nothing indexed yet, or the backend is unreachable.`));\n console.log(dim(` Backend: ${backend.backendType}`));\n console.log();\n return;\n }\n\n const totalChunks = comp.reduce((s, r) => s + num(r.chunks), 0);\n const totalEmbedded = comp.reduce((s, r) => s + num(r.embedded), 0);\n\n console.log();\n console.log(header(`What the indexer has taken in`));\n console.log();\n console.log(\n ` ${bold(totalChunks.toLocaleString())} chunks ` +\n `${totalEmbedded.toLocaleString()} embedded (${pct(totalEmbedded, totalChunks)}) ` +\n dim(`backend: ${backend.backendType}`)\n );\n console.log();\n\n console.log(\n renderTable(\n [\"source / tier\", \"chunks\", \"share\", \"embedded\"],\n comp.map((r) => [\n `${r.source} / ${r.tier}`,\n num(r.chunks).toLocaleString(),\n pct(num(r.chunks), totalChunks),\n `${num(r.embedded).toLocaleString()} (${pct(num(r.embedded), num(r.chunks))})`,\n ])\n )\n );\n\n // ---- where it enters from ---------------------------------------------\n const paths = report.paths;\n\n const byRoot = new Map<string, number>();\n for (const r of paths) {\n const k = rootOf(String(r.path ?? \"\"));\n byRoot.set(k, (byRoot.get(k) ?? 0) + num(r.chunks));\n }\n const roots = [...byRoot.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);\n\n console.log();\n console.log(header(`Where it comes from`));\n console.log(dim(` A root you did not expect here is usually a symlink leading out of the vault.`));\n console.log();\n console.log(\n renderTable(\n [\"root\", \"chunks\", \"share\"],\n roots.map(([k, v]) => [k, v.toLocaleString(), pct(v, totalChunks)])\n )\n );\n\n // ---- heaviest single files -------------------------------------------\n const heaviest = [...paths]\n .sort((a, b) => num(b.chunks) - num(a.chunks))\n .slice(0, limit);\n\n console.log();\n console.log(header(`Heaviest single files`));\n console.log();\n console.log(\n renderTable(\n [\"chunks\", \"path\"],\n heaviest.map((r) => [num(r.chunks).toLocaleString(), tail(String(r.path ?? \"\"), 62)])\n )\n );\n\n // ---- churn ------------------------------------------------------------\n // The section that distinguishes a backlog from a treadmill. A day with a\n // large chunk count and a small embedded count means those chunks were\n // rewritten and their embeddings thrown away.\n const churn = report.churn;\n\n console.log();\n console.log(header(`Rewritten per day`));\n console.log(\n dim(` Many chunks with few embedded means they were re-chunked and their`)\n );\n console.log(dim(` embeddings discarded — work the embedder has to redo.`));\n console.log();\n console.log(\n renderTable(\n [\"day\", \"chunks touched\", \"of those embedded\"],\n churn.map((r) => [\n String(r.day),\n num(r.chunks).toLocaleString(),\n `${num(r.embedded).toLocaleString()} (${pct(num(r.embedded), num(r.chunks))})`,\n ])\n )\n );\n\n const missing = totalChunks - totalEmbedded;\n console.log();\n if (missing > 0) {\n console.log(` ${bold(missing.toLocaleString())} chunks still need embedding.`);\n } else {\n console.log(ok(` Everything indexed is embedded.`));\n }\n console.log();\n}\n\n/** Keep the informative end of a long path. */\nfunction tail(s: string, n: number): string {\n return s.length <= n ? s : \"…\" + s.slice(-(n - 1));\n}\n\n"],"mappings":"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"}
1
+ {"version":3,"file":"sources-CvvzNZad.mjs","names":[],"sources":["../src/cli/commands/memory/sources.ts"],"sourcesContent":["/**\n * `pai memory sources` — what the indexer is actually taking in.\n *\n * The index had grown to ~1.5M chunks against a vault of ~2,700 notes, and\n * nothing in the CLI could show why. `memory status` refuses to report at all\n * when the backend is not SQLite, and `daemon status` gives three totals with no\n * composition — so answering \"what is it indexing, and why is it re-indexing\"\n * meant hand-writing aggregate SQL against the container.\n *\n * The cause, when it was finally measured, was visible in one breakdown: the\n * vault indexer follows symlinks out of the vault into cloud-synced trees, and\n * those trees have mtimes rewritten by the sync client on content that has not\n * changed. Each rewrite re-chunks the file, and re-chunking assigns new chunk\n * ids, which discards the embeddings — so the embedder was re-doing work\n * indefinitely while never catching up.\n *\n * Hence the four sections below. Each one exists because it was needed:\n * composition — the vault dwarfing everything else is the first clue\n * roots — where content enters from, which is how symlink leakage shows\n * heaviest — single files contributing thousands of chunks (attachments)\n * churn — chunks rewritten per day, which is what distinguishes a\n * backlog that will finish from a treadmill that never will\n */\n\nimport { ok, warn, dim, bold, header, renderTable } from \"../../utils.js\";\nimport type { StorageBackend } from \"../../../storage/interface.js\";\n\nconst num = (v: unknown): number => Number(v ?? 0);\nconst pct = (part: number, whole: number): string =>\n whole === 0 ? \"—\" : `${Math.round((part / whole) * 100)}%`;\n\n/** Group the leading path segments, which is where content enters the index. */\nexport function rootOf(path: string, depth = 2): string {\n const parts = path.split(\"/\").filter(Boolean);\n if (parts.length <= depth) return path;\n return parts.slice(0, depth).join(\"/\") + \"/…\";\n}\n\nexport async function cmdMemorySources(\n backend: StorageBackend,\n opts: { limit?: number } = {}\n): Promise<void> {\n const limit = opts.limit ?? 8;\n\n const report = await backend.getMemorySourcesReport();\n const comp = report.composition;\n\n if (comp.length === 0) {\n console.log();\n console.log(warn(` Nothing indexed yet, or the backend is unreachable.`));\n console.log(dim(` Backend: ${backend.backendType}`));\n console.log();\n return;\n }\n\n const totalChunks = comp.reduce((s, r) => s + num(r.chunks), 0);\n const totalEmbedded = comp.reduce((s, r) => s + num(r.embedded), 0);\n\n console.log();\n console.log(header(`What the indexer has taken in`));\n console.log();\n console.log(\n ` ${bold(totalChunks.toLocaleString())} chunks ` +\n `${totalEmbedded.toLocaleString()} embedded (${pct(totalEmbedded, totalChunks)}) ` +\n dim(`backend: ${backend.backendType}`)\n );\n console.log();\n\n console.log(\n renderTable(\n [\"source / tier\", \"chunks\", \"share\", \"embedded\"],\n comp.map((r) => [\n `${r.source} / ${r.tier}`,\n num(r.chunks).toLocaleString(),\n pct(num(r.chunks), totalChunks),\n `${num(r.embedded).toLocaleString()} (${pct(num(r.embedded), num(r.chunks))})`,\n ])\n )\n );\n\n // ---- where it enters from ---------------------------------------------\n const paths = report.paths;\n\n const byRoot = new Map<string, number>();\n for (const r of paths) {\n const k = rootOf(String(r.path ?? \"\"));\n byRoot.set(k, (byRoot.get(k) ?? 0) + num(r.chunks));\n }\n const roots = [...byRoot.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);\n\n console.log();\n console.log(header(`Where it comes from`));\n console.log(dim(` A root you did not expect here is usually a symlink leading out of the vault.`));\n console.log();\n console.log(\n renderTable(\n [\"root\", \"chunks\", \"share\"],\n roots.map(([k, v]) => [k, v.toLocaleString(), pct(v, totalChunks)])\n )\n );\n\n // ---- heaviest single files -------------------------------------------\n const heaviest = [...paths]\n .sort((a, b) => num(b.chunks) - num(a.chunks))\n .slice(0, limit);\n\n console.log();\n console.log(header(`Heaviest single files`));\n console.log();\n console.log(\n renderTable(\n [\"chunks\", \"path\"],\n heaviest.map((r) => [num(r.chunks).toLocaleString(), tail(String(r.path ?? \"\"), 62)])\n )\n );\n\n // ---- churn ------------------------------------------------------------\n // The section that distinguishes a backlog from a treadmill. A day with a\n // large chunk count and a small embedded count means those chunks were\n // rewritten and their embeddings thrown away.\n const churn = report.churn;\n\n console.log();\n console.log(header(`Rewritten per day`));\n console.log(\n dim(` Many chunks with few embedded means they were re-chunked and their`)\n );\n console.log(dim(` embeddings discarded — work the embedder has to redo.`));\n console.log();\n console.log(\n renderTable(\n [\"day\", \"chunks touched\", \"of those embedded\"],\n churn.map((r) => [\n String(r.day),\n num(r.chunks).toLocaleString(),\n `${num(r.embedded).toLocaleString()} (${pct(num(r.embedded), num(r.chunks))})`,\n ])\n )\n );\n\n const missing = totalChunks - totalEmbedded;\n console.log();\n if (missing > 0) {\n console.log(` ${bold(missing.toLocaleString())} chunks still need embedding.`);\n } else {\n console.log(ok(` Everything indexed is embedded.`));\n }\n console.log();\n}\n\n/** Keep the informative end of a long path. */\nfunction tail(s: string, n: number): string {\n return s.length <= n ? s : \"…\" + s.slice(-(n - 1));\n}\n\n"],"mappings":"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"}
@@ -323,4 +323,4 @@ const TITLE_STOP_WORDS = STOP_WORDS;
323
323
 
324
324
  //#endregion
325
325
  export { TITLE_STOP_WORDS as n, STOP_WORDS as t };
326
- //# sourceMappingURL=stop-words-DtxaTWU_.mjs.map
326
+ //# sourceMappingURL=stop-words-LtkS-Pej.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"stop-words-DtxaTWU_.mjs","names":[],"sources":["../src/utils/stop-words.ts"],"sourcesContent":["/**\n * Shared stop-word list used across memory search, slug generation, graph clustering,\n * and zettelkasten modules. This is the union of all per-file sets that previously\n * existed in 7 different files.\n */\n\nexport const STOP_WORDS = new Set([\n // Articles / determiners\n \"a\", \"an\", \"the\",\n // Conjunctions\n \"and\", \"or\", \"but\", \"if\", \"then\", \"else\", \"so\", \"because\", \"as\",\n \"although\", \"however\", \"therefore\", \"thus\", \"hence\", \"meanwhile\",\n \"moreover\", \"furthermore\", \"otherwise\", \"instead\", \"anyway\",\n // Prepositions\n \"at\", \"to\", \"for\", \"of\", \"with\", \"by\", \"from\", \"in\", \"on\", \"out\",\n \"off\", \"over\", \"under\", \"up\", \"into\", \"without\", \"per\", \"via\",\n // Pronouns\n \"i\", \"you\", \"we\", \"they\", \"he\", \"she\", \"it\", \"me\", \"us\", \"him\", \"her\",\n \"my\", \"your\", \"our\", \"their\", \"his\", \"its\", \"who\", \"whom\", \"what\",\n \"which\", \"this\", \"that\", \"these\", \"those\",\n // Common verbs\n \"is\", \"are\", \"was\", \"were\", \"be\", \"been\", \"being\",\n \"have\", \"has\", \"had\", \"do\", \"does\", \"did\",\n \"will\", \"would\", \"could\", \"should\", \"may\", \"might\", \"can\", \"shall\",\n \"want\", \"need\", \"know\", \"think\", \"see\", \"look\", \"make\", \"get\", \"go\",\n \"come\", \"take\", \"use\", \"find\", \"give\", \"tell\", \"say\", \"said\", \"try\",\n \"keep\", \"run\", \"set\", \"put\", \"add\", \"show\", \"check\", \"let\",\n // Negation / common function words\n \"not\", \"no\", \"yes\", \"just\", \"also\", \"very\", \"really\",\n \"about\", \"after\", \"before\",\n \"more\", \"most\", \"some\", \"any\", \"all\", \"each\", \"every\", \"both\", \"few\",\n \"many\", \"much\", \"other\", \"another\", \"such\", \"only\", \"own\", \"same\",\n \"than\", \"too\", \"ok\", \"okay\", \"sure\",\n \"please\", \"thanks\", \"thank\", \"here\", \"there\", \"now\", \"well\", \"like\",\n \"going\", \"done\", \"got\",\n // Tech/URL junk\n \"https\", \"http\", \"www\", \"com\", \"org\", \"net\", \"io\",\n \"null\", \"undefined\", \"true\", \"false\",\n // Contractions (de-apostrophe'd forms that appear after tokenisation)\n \"ll\", \"ve\", \"re\", \"don\", \"thats\", \"heres\", \"theres\",\n \"youre\", \"theyre\", \"didnt\", \"dont\", \"doesnt\", \"havent\", \"hasnt\",\n \"wont\", \"cant\", \"shouldnt\", \"wouldnt\", \"couldnt\", \"isnt\", \"arent\",\n \"wasnt\", \"werent\",\n // Filler adverbs\n \"never\", \"ever\", \"still\", \"already\", \"yet\", \"back\",\n \"away\", \"down\", \"right\", \"left\", \"next\", \"last\", \"first\", \"second\",\n \"third\",\n \"then\", \"again\", \"once\", \"twice\", \"since\", \"while\", \"though\",\n \"actually\", \"basically\", \"literally\", \"simply\", \"exactly\", \"probably\",\n \"possibly\", \"maybe\", \"perhaps\", \"certainly\", \"definitely\", \"absolutely\",\n \"completely\", \"totally\", \"quite\", \"rather\", \"fairly\", \"nearly\",\n \"almost\", \"barely\", \"hardly\", \"quickly\", \"slowly\", \"easily\", \"likely\",\n \"unlikely\",\n // Numbers (spelled out)\n \"one\", \"two\", \"three\", \"four\", \"five\", \"six\", \"seven\",\n \"eight\", \"nine\", \"ten\",\n // Common nouns (too generic for search/slug)\n \"time\", \"way\", \"thing\", \"something\", \"anything\", \"nothing\",\n \"everything\", \"someone\", \"anyone\", \"everyone\",\n // Zettelkasten / vault-specific noise\n \"new\", \"note\", \"untitled\", \"page\", \"file\", \"doc\", \"code\",\n \"session\", \"notes\", \"moc\", \"template\", \"content\", \"attachment\",\n // Misc abbreviations\n \"etc\", \"ie\", \"eg\", \"vs\",\n // French stop words (for multilingual vaults)\n \"les\", \"des\", \"une\", \"est\", \"que\", \"qui\", \"dans\", \"pour\", \"sur\",\n \"par\", \"pas\", \"son\", \"ses\", \"aux\", \"avec\", \"tout\", \"mais\",\n // German stop words (for multilingual vaults)\n \"und\", \"der\", \"die\", \"das\", \"ein\", \"eine\", \"ist\", \"den\", \"dem\",\n \"von\", \"mit\", \"auf\", \"nicht\", \"sich\", \"auch\", \"noch\", \"wie\",\n]);\n\n/**\n * Alias used by graph modules that need stop-word filtering specifically for\n * vault note titles (same set, just semantically named for clarity).\n */\nexport const TITLE_STOP_WORDS = STOP_WORDS;\n"],"mappings":";;;;;;AAMA,MAAa,aAAa,IAAI,IAAI;CAEhC;CAAK;CAAM;CAEX;CAAO;CAAM;CAAO;CAAM;CAAQ;CAAQ;CAAM;CAAW;CAC3D;CAAY;CAAW;CAAa;CAAQ;CAAS;CACrD;CAAY;CAAe;CAAa;CAAW;CAEnD;CAAM;CAAM;CAAO;CAAM;CAAQ;CAAM;CAAQ;CAAM;CAAM;CAC3D;CAAO;CAAQ;CAAS;CAAM;CAAQ;CAAW;CAAO;CAExD;CAAK;CAAO;CAAM;CAAQ;CAAM;CAAO;CAAM;CAAM;CAAM;CAAO;CAChE;CAAM;CAAQ;CAAO;CAAS;CAAO;CAAO;CAAO;CAAQ;CAC3D;CAAS;CAAQ;CAAQ;CAAS;CAElC;CAAM;CAAO;CAAO;CAAQ;CAAM;CAAQ;CAC1C;CAAQ;CAAO;CAAO;CAAM;CAAQ;CACpC;CAAQ;CAAS;CAAS;CAAU;CAAO;CAAS;CAAO;CAC3D;CAAQ;CAAQ;CAAQ;CAAS;CAAO;CAAQ;CAAQ;CAAO;CAC/D;CAAQ;CAAQ;CAAO;CAAQ;CAAQ;CAAQ;CAAO;CAAQ;CAC9D;CAAQ;CAAO;CAAO;CAAO;CAAO;CAAQ;CAAS;CAErD;CAAO;CAAM;CAAO;CAAQ;CAAQ;CAAQ;CAC5C;CAAS;CAAS;CAClB;CAAQ;CAAQ;CAAQ;CAAO;CAAO;CAAQ;CAAS;CAAQ;CAC/D;CAAQ;CAAQ;CAAS;CAAW;CAAQ;CAAQ;CAAO;CAC3D;CAAQ;CAAO;CAAM;CAAQ;CAC7B;CAAU;CAAU;CAAS;CAAQ;CAAS;CAAO;CAAQ;CAC7D;CAAS;CAAQ;CAEjB;CAAS;CAAQ;CAAO;CAAO;CAAO;CAAO;CAC7C;CAAQ;CAAa;CAAQ;CAE7B;CAAM;CAAM;CAAM;CAAO;CAAS;CAAS;CAC3C;CAAS;CAAU;CAAS;CAAQ;CAAU;CAAU;CACxD;CAAQ;CAAQ;CAAY;CAAW;CAAW;CAAQ;CAC1D;CAAS;CAET;CAAS;CAAQ;CAAS;CAAW;CAAO;CAC5C;CAAQ;CAAQ;CAAS;CAAQ;CAAQ;CAAQ;CAAS;CAC1D;CACA;CAAQ;CAAS;CAAQ;CAAS;CAAS;CAAS;CACpD;CAAY;CAAa;CAAa;CAAU;CAAW;CAC3D;CAAY;CAAS;CAAW;CAAa;CAAc;CAC3D;CAAc;CAAW;CAAS;CAAU;CAAU;CACtD;CAAU;CAAU;CAAU;CAAW;CAAU;CAAU;CAC7D;CAEA;CAAO;CAAO;CAAS;CAAQ;CAAQ;CAAO;CAC9C;CAAS;CAAQ;CAEjB;CAAQ;CAAO;CAAS;CAAa;CAAY;CACjD;CAAc;CAAW;CAAU;CAEnC;CAAO;CAAQ;CAAY;CAAQ;CAAQ;CAAO;CAClD;CAAW;CAAS;CAAO;CAAY;CAAW;CAElD;CAAO;CAAM;CAAM;CAEnB;CAAO;CAAO;CAAO;CAAO;CAAO;CAAO;CAAQ;CAAQ;CAC1D;CAAO;CAAO;CAAO;CAAO;CAAO;CAAQ;CAAQ;CAEnD;CAAO;CAAO;CAAO;CAAO;CAAO;CAAQ;CAAO;CAAO;CACzD;CAAO;CAAO;CAAO;CAAS;CAAQ;CAAQ;CAAQ;CACvD,CAAC;;;;;AAMF,MAAa,mBAAmB"}
1
+ {"version":3,"file":"stop-words-LtkS-Pej.mjs","names":[],"sources":["../src/utils/stop-words.ts"],"sourcesContent":["/**\n * Shared stop-word list used across memory search, slug generation, graph clustering,\n * and zettelkasten modules. This is the union of all per-file sets that previously\n * existed in 7 different files.\n */\n\nexport const STOP_WORDS = new Set([\n // Articles / determiners\n \"a\", \"an\", \"the\",\n // Conjunctions\n \"and\", \"or\", \"but\", \"if\", \"then\", \"else\", \"so\", \"because\", \"as\",\n \"although\", \"however\", \"therefore\", \"thus\", \"hence\", \"meanwhile\",\n \"moreover\", \"furthermore\", \"otherwise\", \"instead\", \"anyway\",\n // Prepositions\n \"at\", \"to\", \"for\", \"of\", \"with\", \"by\", \"from\", \"in\", \"on\", \"out\",\n \"off\", \"over\", \"under\", \"up\", \"into\", \"without\", \"per\", \"via\",\n // Pronouns\n \"i\", \"you\", \"we\", \"they\", \"he\", \"she\", \"it\", \"me\", \"us\", \"him\", \"her\",\n \"my\", \"your\", \"our\", \"their\", \"his\", \"its\", \"who\", \"whom\", \"what\",\n \"which\", \"this\", \"that\", \"these\", \"those\",\n // Common verbs\n \"is\", \"are\", \"was\", \"were\", \"be\", \"been\", \"being\",\n \"have\", \"has\", \"had\", \"do\", \"does\", \"did\",\n \"will\", \"would\", \"could\", \"should\", \"may\", \"might\", \"can\", \"shall\",\n \"want\", \"need\", \"know\", \"think\", \"see\", \"look\", \"make\", \"get\", \"go\",\n \"come\", \"take\", \"use\", \"find\", \"give\", \"tell\", \"say\", \"said\", \"try\",\n \"keep\", \"run\", \"set\", \"put\", \"add\", \"show\", \"check\", \"let\",\n // Negation / common function words\n \"not\", \"no\", \"yes\", \"just\", \"also\", \"very\", \"really\",\n \"about\", \"after\", \"before\",\n \"more\", \"most\", \"some\", \"any\", \"all\", \"each\", \"every\", \"both\", \"few\",\n \"many\", \"much\", \"other\", \"another\", \"such\", \"only\", \"own\", \"same\",\n \"than\", \"too\", \"ok\", \"okay\", \"sure\",\n \"please\", \"thanks\", \"thank\", \"here\", \"there\", \"now\", \"well\", \"like\",\n \"going\", \"done\", \"got\",\n // Tech/URL junk\n \"https\", \"http\", \"www\", \"com\", \"org\", \"net\", \"io\",\n \"null\", \"undefined\", \"true\", \"false\",\n // Contractions (de-apostrophe'd forms that appear after tokenisation)\n \"ll\", \"ve\", \"re\", \"don\", \"thats\", \"heres\", \"theres\",\n \"youre\", \"theyre\", \"didnt\", \"dont\", \"doesnt\", \"havent\", \"hasnt\",\n \"wont\", \"cant\", \"shouldnt\", \"wouldnt\", \"couldnt\", \"isnt\", \"arent\",\n \"wasnt\", \"werent\",\n // Filler adverbs\n \"never\", \"ever\", \"still\", \"already\", \"yet\", \"back\",\n \"away\", \"down\", \"right\", \"left\", \"next\", \"last\", \"first\", \"second\",\n \"third\",\n \"then\", \"again\", \"once\", \"twice\", \"since\", \"while\", \"though\",\n \"actually\", \"basically\", \"literally\", \"simply\", \"exactly\", \"probably\",\n \"possibly\", \"maybe\", \"perhaps\", \"certainly\", \"definitely\", \"absolutely\",\n \"completely\", \"totally\", \"quite\", \"rather\", \"fairly\", \"nearly\",\n \"almost\", \"barely\", \"hardly\", \"quickly\", \"slowly\", \"easily\", \"likely\",\n \"unlikely\",\n // Numbers (spelled out)\n \"one\", \"two\", \"three\", \"four\", \"five\", \"six\", \"seven\",\n \"eight\", \"nine\", \"ten\",\n // Common nouns (too generic for search/slug)\n \"time\", \"way\", \"thing\", \"something\", \"anything\", \"nothing\",\n \"everything\", \"someone\", \"anyone\", \"everyone\",\n // Zettelkasten / vault-specific noise\n \"new\", \"note\", \"untitled\", \"page\", \"file\", \"doc\", \"code\",\n \"session\", \"notes\", \"moc\", \"template\", \"content\", \"attachment\",\n // Misc abbreviations\n \"etc\", \"ie\", \"eg\", \"vs\",\n // French stop words (for multilingual vaults)\n \"les\", \"des\", \"une\", \"est\", \"que\", \"qui\", \"dans\", \"pour\", \"sur\",\n \"par\", \"pas\", \"son\", \"ses\", \"aux\", \"avec\", \"tout\", \"mais\",\n // German stop words (for multilingual vaults)\n \"und\", \"der\", \"die\", \"das\", \"ein\", \"eine\", \"ist\", \"den\", \"dem\",\n \"von\", \"mit\", \"auf\", \"nicht\", \"sich\", \"auch\", \"noch\", \"wie\",\n]);\n\n/**\n * Alias used by graph modules that need stop-word filtering specifically for\n * vault note titles (same set, just semantically named for clarity).\n */\nexport const TITLE_STOP_WORDS = STOP_WORDS;\n"],"mappings":";;;;;;AAMA,MAAa,aAAa,IAAI,IAAI;CAEhC;CAAK;CAAM;CAEX;CAAO;CAAM;CAAO;CAAM;CAAQ;CAAQ;CAAM;CAAW;CAC3D;CAAY;CAAW;CAAa;CAAQ;CAAS;CACrD;CAAY;CAAe;CAAa;CAAW;CAEnD;CAAM;CAAM;CAAO;CAAM;CAAQ;CAAM;CAAQ;CAAM;CAAM;CAC3D;CAAO;CAAQ;CAAS;CAAM;CAAQ;CAAW;CAAO;CAExD;CAAK;CAAO;CAAM;CAAQ;CAAM;CAAO;CAAM;CAAM;CAAM;CAAO;CAChE;CAAM;CAAQ;CAAO;CAAS;CAAO;CAAO;CAAO;CAAQ;CAC3D;CAAS;CAAQ;CAAQ;CAAS;CAElC;CAAM;CAAO;CAAO;CAAQ;CAAM;CAAQ;CAC1C;CAAQ;CAAO;CAAO;CAAM;CAAQ;CACpC;CAAQ;CAAS;CAAS;CAAU;CAAO;CAAS;CAAO;CAC3D;CAAQ;CAAQ;CAAQ;CAAS;CAAO;CAAQ;CAAQ;CAAO;CAC/D;CAAQ;CAAQ;CAAO;CAAQ;CAAQ;CAAQ;CAAO;CAAQ;CAC9D;CAAQ;CAAO;CAAO;CAAO;CAAO;CAAQ;CAAS;CAErD;CAAO;CAAM;CAAO;CAAQ;CAAQ;CAAQ;CAC5C;CAAS;CAAS;CAClB;CAAQ;CAAQ;CAAQ;CAAO;CAAO;CAAQ;CAAS;CAAQ;CAC/D;CAAQ;CAAQ;CAAS;CAAW;CAAQ;CAAQ;CAAO;CAC3D;CAAQ;CAAO;CAAM;CAAQ;CAC7B;CAAU;CAAU;CAAS;CAAQ;CAAS;CAAO;CAAQ;CAC7D;CAAS;CAAQ;CAEjB;CAAS;CAAQ;CAAO;CAAO;CAAO;CAAO;CAC7C;CAAQ;CAAa;CAAQ;CAE7B;CAAM;CAAM;CAAM;CAAO;CAAS;CAAS;CAC3C;CAAS;CAAU;CAAS;CAAQ;CAAU;CAAU;CACxD;CAAQ;CAAQ;CAAY;CAAW;CAAW;CAAQ;CAC1D;CAAS;CAET;CAAS;CAAQ;CAAS;CAAW;CAAO;CAC5C;CAAQ;CAAQ;CAAS;CAAQ;CAAQ;CAAQ;CAAS;CAC1D;CACA;CAAQ;CAAS;CAAQ;CAAS;CAAS;CAAS;CACpD;CAAY;CAAa;CAAa;CAAU;CAAW;CAC3D;CAAY;CAAS;CAAW;CAAa;CAAc;CAC3D;CAAc;CAAW;CAAS;CAAU;CAAU;CACtD;CAAU;CAAU;CAAU;CAAW;CAAU;CAAU;CAC7D;CAEA;CAAO;CAAO;CAAS;CAAQ;CAAQ;CAAO;CAC9C;CAAS;CAAQ;CAEjB;CAAQ;CAAO;CAAS;CAAa;CAAY;CACjD;CAAc;CAAW;CAAU;CAEnC;CAAO;CAAQ;CAAY;CAAQ;CAAQ;CAAO;CAClD;CAAW;CAAS;CAAO;CAAY;CAAW;CAElD;CAAO;CAAM;CAAM;CAEnB;CAAO;CAAO;CAAO;CAAO;CAAO;CAAO;CAAQ;CAAQ;CAC1D;CAAO;CAAO;CAAO;CAAO;CAAO;CAAQ;CAAQ;CAEnD;CAAO;CAAO;CAAO;CAAO;CAAO;CAAQ;CAAO;CAAO;CACzD;CAAO;CAAO;CAAO;CAAS;CAAQ;CAAQ;CAAQ;CACvD,CAAC;;;;;AAMF,MAAa,mBAAmB"}