@rubytech/create-realagent 1.0.817 → 1.0.819

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 (61) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts +1 -1
  3. package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.d.ts +2 -0
  4. package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.d.ts.map +1 -0
  5. package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js +168 -0
  6. package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js.map +1 -0
  7. package/payload/platform/lib/graph-write/dist/index.d.ts +30 -2
  8. package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
  9. package/payload/platform/lib/graph-write/dist/index.js +111 -3
  10. package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
  11. package/payload/platform/lib/graph-write/src/__tests__/action-provenance-gate.test.ts +191 -0
  12. package/payload/platform/lib/graph-write/src/index.ts +112 -6
  13. package/payload/platform/neo4j/edge-annotations.json +0 -8
  14. package/payload/platform/neo4j/migrations/004-prune-alien-accounts.ts +3 -4
  15. package/payload/platform/neo4j/migrations/005-removed-review-feature.ts +102 -0
  16. package/payload/platform/neo4j/schema.cypher +25 -22
  17. package/payload/platform/plugins/admin/PLUGIN.md +1 -8
  18. package/payload/platform/plugins/admin/mcp/dist/index.js +6 -44
  19. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  20. package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +24 -6
  21. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -1
  22. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +2 -3
  23. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -1
  24. package/payload/platform/plugins/cloudflare/mcp/dist/lib/setup-orchestrator.js +1 -1
  25. package/payload/platform/plugins/cloudflare/mcp/dist/lib/setup-orchestrator.js.map +1 -1
  26. package/payload/platform/plugins/docs/references/internals.md +16 -0
  27. package/payload/platform/plugins/docs/references/memory-guide.md +1 -1
  28. package/payload/platform/plugins/memory/PLUGIN.md +6 -0
  29. package/payload/platform/plugins/memory/mcp/dist/index.js +7 -2
  30. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  31. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +1 -1
  32. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -1
  33. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts +8 -0
  34. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
  35. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +26 -2
  36. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  37. package/payload/platform/plugins/memory/references/schema-base.md +8 -0
  38. package/payload/platform/plugins/tasks/PLUGIN.md +2 -2
  39. package/payload/platform/plugins/tasks/mcp/dist/index.js +10 -5
  40. package/payload/platform/plugins/tasks/mcp/dist/index.js.map +1 -1
  41. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts +27 -1
  42. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts.map +1 -1
  43. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js +45 -2
  44. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js.map +1 -1
  45. package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.d.ts +20 -1
  46. package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.d.ts.map +1 -1
  47. package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.js +46 -6
  48. package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.js.map +1 -1
  49. package/payload/platform/scripts/logs-read.sh +8 -38
  50. package/payload/server/chunk-AJLGI7Y3.js +10067 -0
  51. package/payload/server/chunk-ON3LBL2Y.js +1114 -0
  52. package/payload/server/chunk-PXQA2MA3.js +2518 -0
  53. package/payload/server/client-pool-GBY5I2KQ.js +31 -0
  54. package/payload/server/maxy-edge.js +3 -3
  55. package/payload/server/neo4j-migrations-STCKDWAL.js +364 -0
  56. package/payload/server/public/assets/{admin-2w0XSMC6.js → admin-CdVYoqKD.js} +1 -1
  57. package/payload/server/public/assets/{graph-C4-jEPDE.js → graph-DeH6ulGh.js} +1 -1
  58. package/payload/server/public/assets/{page-zuI00fuC.js → page-WIAWD2Oi.js} +1 -1
  59. package/payload/server/public/graph.html +2 -2
  60. package/payload/server/public/index.html +2 -2
  61. package/payload/server/server.js +790 -1896
@@ -51,7 +51,7 @@ import {
51
51
  vncLog,
52
52
  waitForExit,
53
53
  writeChromiumWrapper
54
- } from "./chunk-P3HTEK33.js";
54
+ } from "./chunk-AJLGI7Y3.js";
55
55
  import {
56
56
  agentLogStream,
57
57
  clearSessionHistory,
@@ -79,7 +79,7 @@ import {
79
79
  sigtermFlushStreamLogs,
80
80
  unregisterSession,
81
81
  validateSession
82
- } from "./chunk-UYLZDEMC.js";
82
+ } from "./chunk-ON3LBL2Y.js";
83
83
  import {
84
84
  ACCOUNTS_DIR,
85
85
  GREETING_DIRECTIVE,
@@ -119,10 +119,227 @@ import {
119
119
  verifyAndGetConversationUpdatedAt,
120
120
  verifyConversationOwnership,
121
121
  writeAdminUserAndPerson
122
- } from "./chunk-TQTMKIW6.js";
122
+ } from "./chunk-PXQA2MA3.js";
123
123
 
124
- // ../lib/graph-trash/dist/index.js
124
+ // ../lib/graph-write/dist/audit.js
125
+ var require_audit = __commonJS({
126
+ "../lib/graph-write/dist/audit.js"(exports) {
127
+ "use strict";
128
+ Object.defineProperty(exports, "__esModule", { value: true });
129
+ exports.auditCypherWrite = auditCypherWrite;
130
+ exports.formatAuditLine = formatAuditLine;
131
+ var EDGE_PATTERN = /\[[^\]]*?:([A-Z_][A-Za-z0-9_]*(?:\|[A-Z_][A-Za-z0-9_]*)*)[^\]]*?\]/g;
132
+ var CREATE_OR_MERGE_NODE = /\b(?:CREATE|MERGE)\s*\(\s*[A-Za-z_][A-Za-z0-9_]*\s*:\s*[A-Z]/g;
133
+ var PROVENANCE_TOKEN = /\bcreatedBy(?:Agent|Tool|Session|Source)\b/g;
134
+ function stripStringLiterals(cypher) {
135
+ return cypher.replace(/'[^']*'|"[^"]*"/g, '""');
136
+ }
137
+ function extractEdgeTypes(cleaned) {
138
+ const out = /* @__PURE__ */ new Set();
139
+ for (const m of cleaned.matchAll(EDGE_PATTERN)) {
140
+ for (const t of m[1].split("|")) {
141
+ const clean = t.trim();
142
+ if (clean)
143
+ out.add(clean);
144
+ }
145
+ }
146
+ return out;
147
+ }
148
+ function countCreateOrMergeNodes(cleaned) {
149
+ const matches = cleaned.match(CREATE_OR_MERGE_NODE);
150
+ return matches ? matches.length : 0;
151
+ }
152
+ function countProvenanceStamps(cleaned) {
153
+ const matches = cleaned.match(PROVENANCE_TOKEN);
154
+ return matches ? matches.length : 0;
155
+ }
156
+ function auditCypherWrite(input) {
157
+ const warnings = [];
158
+ const cleaned = stripStringLiterals(input.cypher);
159
+ const referencedTypes = extractEdgeTypes(cleaned);
160
+ for (const t of referencedTypes) {
161
+ if (!input.schema.relationshipTypes.has(t)) {
162
+ warnings.push({ kind: "unknown-type-warning", type: t });
163
+ }
164
+ }
165
+ if (input.nodesCreated > 0) {
166
+ const createOrMergeNodes = countCreateOrMergeNodes(cleaned);
167
+ if (createOrMergeNodes > 0) {
168
+ const stamps = countProvenanceStamps(cleaned);
169
+ if (stamps < createOrMergeNodes) {
170
+ warnings.push({
171
+ kind: "missing-provenance-warning",
172
+ created: createOrMergeNodes,
173
+ stamped: stamps
174
+ });
175
+ }
176
+ }
177
+ }
178
+ if (input.orphanIds.length > 0) {
179
+ warnings.push({ kind: "orphan-warning", orphanIds: input.orphanIds });
180
+ }
181
+ return warnings;
182
+ }
183
+ function formatAuditLine(line) {
184
+ const prefixField = `query="${line.cypherPrefix.replace(/"/g, "'")}"`;
185
+ switch (line.kind) {
186
+ case "accepted":
187
+ return `[graph-cypher-write] accepted ${prefixField} nodesCreated=${line.nodesCreated} relsCreated=${line.relsCreated} agentName=${line.agentName} sessionId=${line.sessionId}`;
188
+ case "orphan-warning":
189
+ return `[graph-cypher-write] orphan-warning ${prefixField} orphanIds=${line.orphanIds.join(",")}`;
190
+ case "unknown-type-warning":
191
+ return `[graph-cypher-write] unknown-type-warning ${prefixField} type=${line.type}`;
192
+ case "missing-provenance-warning":
193
+ return `[graph-cypher-write] missing-provenance-warning ${prefixField} created=${line.created} stamped=${line.stamped}`;
194
+ }
195
+ }
196
+ }
197
+ });
198
+
199
+ // ../lib/graph-write/dist/index.js
125
200
  var require_dist = __commonJS({
201
+ "../lib/graph-write/dist/index.js"(exports) {
202
+ "use strict";
203
+ var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
204
+ if (k2 === void 0) k2 = k;
205
+ var desc = Object.getOwnPropertyDescriptor(m, k);
206
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
207
+ desc = { enumerable: true, get: function() {
208
+ return m[k];
209
+ } };
210
+ }
211
+ Object.defineProperty(o, k2, desc);
212
+ }) : (function(o, m, k, k2) {
213
+ if (k2 === void 0) k2 = k;
214
+ o[k2] = m[k];
215
+ }));
216
+ var __exportStar = exports && exports.__exportStar || function(m, exports2) {
217
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
218
+ };
219
+ Object.defineProperty(exports, "__esModule", { value: true });
220
+ exports.ACTION_PROVENANCE_LABELS = void 0;
221
+ exports.stampCreatedBy = stampCreatedBy;
222
+ exports.writeNodeWithEdges = writeNodeWithEdges2;
223
+ __exportStar(require_audit(), exports);
224
+ exports.ACTION_PROVENANCE_LABELS = /* @__PURE__ */ new Set([
225
+ "Person",
226
+ "UserProfile",
227
+ "AdminUser",
228
+ "Organization",
229
+ "LocalBusiness",
230
+ "CloudflareTunnel",
231
+ "CloudflareHostname"
232
+ ]);
233
+ function requiresActionProvenance(labels) {
234
+ for (const label of labels) {
235
+ if (exports.ACTION_PROVENANCE_LABELS.has(label))
236
+ return true;
237
+ }
238
+ return false;
239
+ }
240
+ function findProducedFromTaskCandidates(relationships) {
241
+ return relationships.filter((r) => r.type === "PRODUCED" && r.direction === "incoming");
242
+ }
243
+ function stampCreatedBy(props, createdBy) {
244
+ return {
245
+ ...props,
246
+ createdByAgent: createdBy.agent ?? "unknown",
247
+ createdBySession: createdBy.session ?? "unknown",
248
+ createdByTool: createdBy.tool ?? null,
249
+ createdBySource: createdBy.source ?? null
250
+ };
251
+ }
252
+ async function writeNodeWithEdges2(params) {
253
+ const { session, labels, props, relationships, createdBy } = params;
254
+ const agentLabel = createdBy.agent ?? createdBy.source ?? "unknown";
255
+ const labelCsv = labels.join(",");
256
+ const reviewDigestActionTool = typeof props.actionTool === "string" && props.actionTool === "review-digest-compose";
257
+ if (labels.includes("ReviewAlert") || reviewDigestActionTool) {
258
+ const actionToolField = reviewDigestActionTool ? "review-digest-compose" : "n/a";
259
+ process.stderr.write(`[graph-write] reject reason=removed-feature labels=${labelCsv} actionTool=${actionToolField} agent=${agentLabel}
260
+ `);
261
+ throw new Error("Write doctrine violated: review-detector feature removed (Task 884) \u2014 `:ReviewAlert` and `:Event {actionTool:'review-digest-compose'}` writes are not allowed.");
262
+ }
263
+ if (!relationships || relationships.length < 1) {
264
+ process.stderr.write(`[graph-write] reject reason=zero-relationships labels=${labelCsv} agent=${agentLabel}
265
+ `);
266
+ throw new Error("Write doctrine violated: a node must be created with at least one relationship. See .docs/neo4j.md (Write doctrine).");
267
+ }
268
+ const labelStr = labels.map((l) => `\`${l.replace(/`/g, "")}\``).join(":");
269
+ const nodeProps = stampCreatedBy(props, createdBy);
270
+ return await session.executeWrite(async (tx) => {
271
+ const targetIds = relationships.map((r) => r.targetNodeId);
272
+ const check = await tx.run(`UNWIND $ids AS id MATCH (t) WHERE elementId(t) = id RETURN elementId(t) AS id, labels(t) AS labels`, { ids: targetIds });
273
+ const labelsByTarget = /* @__PURE__ */ new Map();
274
+ for (const rec of check.records) {
275
+ labelsByTarget.set(rec.get("id"), rec.get("labels"));
276
+ }
277
+ const found = labelsByTarget.size;
278
+ const uniqueRequested = new Set(targetIds).size;
279
+ if (found !== uniqueRequested) {
280
+ process.stderr.write(`[graph-write] reject reason=unresolved-target labels=${labelCsv} agent=${agentLabel} requested=${uniqueRequested} found=${found}
281
+ `);
282
+ throw new Error(`Write doctrine violated: ${uniqueRequested - found} of ${uniqueRequested} relationship target(s) did not resolve (elementId mismatch). No node created.`);
283
+ }
284
+ let producedByTaskId = null;
285
+ if (requiresActionProvenance(labels) && (createdBy.agent ?? "") !== "system") {
286
+ const candidates = findProducedFromTaskCandidates(relationships);
287
+ const taskCandidates = candidates.filter((r) => {
288
+ const lbls = labelsByTarget.get(r.targetNodeId);
289
+ return Array.isArray(lbls) && lbls.includes("Task");
290
+ });
291
+ if (taskCandidates.length === 0) {
292
+ process.stderr.write(`[graph-write] reject reason=missing-action-provenance labels=${labelCsv} agent=${agentLabel}
293
+ `);
294
+ throw new Error(`Process provenance doctrine violated: write to ${labelCsv} requires an inbound :PRODUCED edge from a :Task (createdBy.agent='${agentLabel}'). See .docs/neo4j.md (Process provenance doctrine).`);
295
+ }
296
+ producedByTaskId = taskCandidates[0].targetNodeId;
297
+ }
298
+ let nodeRes;
299
+ try {
300
+ nodeRes = await tx.run(`CREATE (n:${labelStr} $props) RETURN elementId(n) AS nodeId, labels(n) AS nodeLabels`, { props: nodeProps });
301
+ } catch (err) {
302
+ const code = err?.code ?? "";
303
+ if (code === "Neo.ClientError.Schema.ConstraintValidationFailed" && labels.includes("UserProfile")) {
304
+ const accountIdProp = nodeProps.accountId;
305
+ const userIdProp = nodeProps.userId;
306
+ const acctSlice = typeof accountIdProp === "string" ? accountIdProp.slice(0, 8) : "unknown";
307
+ const userSlice = typeof userIdProp === "string" ? userIdProp.slice(0, 8) : "unknown";
308
+ process.stderr.write(`[graph-write] reject reason=user-profile-uniqueness-violation accountId=${acctSlice} userId=${userSlice} writer=${agentLabel}
309
+ `);
310
+ }
311
+ throw err;
312
+ }
313
+ const nodeId = nodeRes.records[0].get("nodeId");
314
+ const nodeLabels = nodeRes.records[0].get("nodeLabels");
315
+ let edgesCreated = 0;
316
+ for (const rel of relationships) {
317
+ const type = rel.type.replace(/`/g, "");
318
+ const q = rel.direction === "outgoing" ? `MATCH (a), (b) WHERE elementId(a) = $from AND elementId(b) = $to CREATE (a)-[:\`${type}\`]->(b)` : `MATCH (a), (b) WHERE elementId(a) = $from AND elementId(b) = $to CREATE (b)-[:\`${type}\`]->(a)`;
319
+ const r = await tx.run(q, { from: nodeId, to: rel.targetNodeId });
320
+ const created = r.summary.counters.updates().relationshipsCreated;
321
+ if (created === 0) {
322
+ process.stderr.write(`[graph-write] reject reason=unresolved-target-on-create labels=${labelCsv} agent=${agentLabel} relType=${rel.type} targetId=${rel.targetNodeId}
323
+ `);
324
+ throw new Error(`Write doctrine violated: relationship CREATE to target ${rel.targetNodeId} produced 0 edges (target likely deleted concurrently after pre-check). Transaction rolled back.`);
325
+ }
326
+ edgesCreated += created;
327
+ }
328
+ if (edgesCreated !== relationships.length) {
329
+ process.stderr.write(`[graph-write] reject reason=edge-count-mismatch labels=${labelCsv} agent=${agentLabel} requested=${relationships.length} created=${edgesCreated}
330
+ `);
331
+ throw new Error(`Write doctrine violated: expected ${relationships.length} edges, created ${edgesCreated}. Transaction rolled back.`);
332
+ }
333
+ process.stderr.write(`[graph-write] accepted labels=${labelCsv} edges=${edgesCreated} createdByAgent=${createdBy.agent ?? "unknown"} createdByTool=${createdBy.tool ?? createdBy.source ?? "unknown"} producedByTask=${producedByTaskId ?? "none"}
334
+ `);
335
+ return { nodeId, labels: nodeLabels, edgesCreated };
336
+ });
337
+ }
338
+ }
339
+ });
340
+
341
+ // ../lib/graph-trash/dist/index.js
342
+ var require_dist2 = __commonJS({
126
343
  "../lib/graph-trash/dist/index.js"(exports) {
127
344
  "use strict";
128
345
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -618,15 +835,15 @@ var serveStatic = (options = { root: "" }) => {
618
835
  };
619
836
 
620
837
  // server/index.ts
621
- import { readFileSync as readFileSync18, existsSync as existsSync24, watchFile } from "fs";
622
- import { resolve as resolve25, join as join10, basename as basename7 } from "path";
838
+ import { readFileSync as readFileSync16, existsSync as existsSync22, watchFile } from "fs";
839
+ import { resolve as resolve21, join as join9, basename as basename5 } from "path";
623
840
  import { homedir as homedir2 } from "os";
624
841
 
625
842
  // app/lib/agent-slug-pattern.ts
626
843
  var AGENT_SLUG_PATTERN = /^\/([a-z][a-z0-9-]{2,49})$/;
627
844
 
628
845
  // server/routes/health.ts
629
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
846
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
630
847
  import { createConnection } from "net";
631
848
 
632
849
  // app/lib/network.ts
@@ -647,1562 +864,6 @@ function getLanIp() {
647
864
  return fallback;
648
865
  }
649
866
 
650
- // app/lib/review-detector/boot.ts
651
- import { basename as basename2 } from "path";
652
-
653
- // app/lib/review-detector/rules.ts
654
- import { readFileSync, writeFileSync, existsSync as existsSync2, statSync as statSync2, mkdirSync, renameSync } from "fs";
655
- import { resolve, dirname } from "path";
656
- var DEFAULT_SCAN_INTERVAL_MS = 5e3;
657
- var RATE_LIMIT_PATTERN = "rate[- ]?limit(?:ed| reached| hit)|(?:HTTP|status)[^a-z]{0,3}429|too many requests";
658
- var RATE_LIMIT_PATTERN_V1 = "\\b429\\b|rate.?limit|too.?many.?requests";
659
- var VALID_TYPES = /* @__PURE__ */ new Set([
660
- "reconnect-loop",
661
- "repeated-error",
662
- "silent-catch",
663
- "file-write-storm",
664
- "stale-log",
665
- "rate-limit",
666
- "absent-followup"
667
- ]);
668
- var MAX_FOLLOWUP_WINDOW_MS = 6e5;
669
- var VALID_SOURCES = /* @__PURE__ */ new Set([
670
- "any",
671
- "server",
672
- "vnc",
673
- "system",
674
- "error",
675
- "session",
676
- "public",
677
- "mcp",
678
- "cloudflared",
679
- "config-dir"
680
- ]);
681
- var VALID_SCOPES = /* @__PURE__ */ new Set(["global", "session"]);
682
- function defaultRules() {
683
- return [
684
- {
685
- id: "whatsapp-reconnect-loop",
686
- name: "WhatsApp Baileys reconnect loop",
687
- type: "reconnect-loop",
688
- logSource: "server",
689
- pattern: "\\[whatsapp:baileys\\] ERROR",
690
- thresholdCount: 5,
691
- thresholdWindowMinutes: 10,
692
- suggestedAction: "Check Baileys init-queries error. Run `/investigate` on the WhatsApp subsystem, or pause WhatsApp and re-pair the account from chat."
693
- },
694
- {
695
- id: "repeated-error-generic",
696
- name: "Repeated error signature",
697
- type: "repeated-error",
698
- // Match generic ERROR lines with a bracketed prefix. Fingerprinting by
699
- // first 80 chars happens in the evaluator.
700
- logSource: "server",
701
- pattern: "\\] ERROR ",
702
- thresholdCount: 20,
703
- thresholdWindowMinutes: 60,
704
- suggestedAction: "Repeated error burst. Tail the relevant log via `logs-read` and identify the upstream cause."
705
- },
706
- {
707
- id: "silent-catch-fingerprint",
708
- name: "Silent catch-block fingerprint",
709
- type: "silent-catch",
710
- // Patterns that historically indicated a silent catch. Every match is
711
- // worth an alert — never suppressed by frequency.
712
- logSource: "any",
713
- pattern: "(UnhandledPromiseRejection|catch.*ignored|swallowed error|silent failure)",
714
- thresholdCount: 0,
715
- thresholdWindowMinutes: 0,
716
- suggestedAction: "A silent-failure fingerprint appeared in logs. Read the surrounding context and turn the catch into loud failure per CLAUDE.md rule 'Observability Is Non-Negotiable'."
717
- },
718
- {
719
- id: "credentials-write-storm",
720
- name: "Credentials directory write storm",
721
- type: "file-write-storm",
722
- logSource: "config-dir",
723
- pattern: "",
724
- watchPath: "credentials",
725
- thresholdCount: 5,
726
- thresholdWindowMinutes: 5,
727
- suggestedAction: "The credentials directory is being rewritten repeatedly. This usually means a subsystem is stuck in an auth-retry loop. Check which file is being rewritten and trace the caller."
728
- },
729
- // The stale-log rule type is fully supported by the evaluator but the
730
- // default seed does not ship an instance of it. Choosing the right file
731
- // to watch is subsystem-specific (e.g. a plugin-specific log that stops
732
- // when the plugin dies); server.log is the wrong target because it is
733
- // written continuously by the detector's own cycle events, so it never
734
- // goes stale. Users can add a targeted stale-log rule via
735
- // `review-rules-add` when they know which file matters for their
736
- // subsystem.
737
- {
738
- id: "http-rate-limit-429",
739
- name: "HTTP 429 rate-limit hit",
740
- type: "rate-limit",
741
- logSource: "any",
742
- pattern: RATE_LIMIT_PATTERN,
743
- thresholdCount: 0,
744
- thresholdWindowMinutes: 0,
745
- suggestedAction: "An external API returned 429 (rate-limited). Identify the caller and add backoff, or check the quota on the relevant API key."
746
- },
747
- {
748
- id: "approval-bypass-detected",
749
- name: "Approval gating bypass",
750
- type: "repeated-error",
751
- logSource: "error",
752
- pattern: "\\[persist\\].*(?:email-send|email-reply|whatsapp-send|whatsapp-send-document|message|contact-erase).*approval=auto-executed",
753
- thresholdCount: 0,
754
- thresholdWindowMinutes: 0,
755
- suggestedAction: "An external-facing tool executed with auto-executed approval state. Check account.json approvalPolicy \u2014 if this tool should require review, the gating hook may have been bypassed or the policy was changed."
756
- },
757
- {
758
- // Task 530: catches the bridgeai-style class where a single conversation
759
- // sees the same tool error repeatedly and the agent silently falls back.
760
- // Session-scoped so cross-conversation coincidence doesn't trigger it.
761
- id: "tool-result-recurring-errors",
762
- name: "Tool result errors recurring in a conversation",
763
- type: "repeated-error",
764
- logSource: "system",
765
- pattern: "\\[tool-result\\].*error=true",
766
- thresholdCount: 2,
767
- thresholdWindowMinutes: 5,
768
- scope: "session",
769
- suggestedAction: 'The same conversation has logged multiple tool failures. Use the admin `logs-read` MCP tool with `type: "system"` (or the `logs-read.sh` script with the conversationId) to retrieve the adjacent [tool-failure-diag] lines and identify whether the cause is DNS, TCP, HTTP, or the tool\'s internal pipeline \u2014 then adapt the next attempt to match. Never retry the same tool against the same target without diagnostic-grounded reasoning.'
770
- },
771
- {
772
- // Task 532: closes the "60-second black-box tool wait" class. Fires when
773
- // a tool hits the 30-second mark of the mid-flight heartbeat. The
774
- // pattern anchors on elapsed=30s specifically (the tool-wait tick emits
775
- // one line per 5s per tool, so matching every tick would be noisy) and
776
- // excludes tools whose long runtime is expected: `Task`/`Agent` subagent
777
- // dispatch, `Bash` with explicit long timeouts.
778
- id: "tool-wait-long-stall",
779
- name: "Tool wait exceeds 30 seconds (possible stall)",
780
- type: "repeated-error",
781
- logSource: "system",
782
- pattern: "\\[tool-wait\\][^\\n]*name=(?!Task\\b|Agent\\b|Bash\\b)[A-Za-z0-9_]+[^\\n]*elapsed=30s",
783
- thresholdCount: 0,
784
- thresholdWindowMinutes: 0,
785
- scope: "session",
786
- suggestedAction: "A tool call has been pending for 30 seconds without a result. Read the adjacent [tool-wait-diag] and [tool-wait-proc] lines in the conversation's stream log to determine whether the network remained healthy, the subprocess held active sockets, and the HTTP request reached the wire. If diag shows a healthy network but the subprocess has no [subproc-stderr] UNDICI/HTTP activity during the wait window, the tool's internal pipeline is stalled \u2014 do not retry the same request against the same target without a change in approach."
787
- },
788
- {
789
- // Task 536: detect agents ignoring the WEBFETCH_CANNOT_READ_JS_SPA
790
- // structured failure. A single SPA short-circuit per conversation is
791
- // expected — the hook is doing its job. Two or more in the same
792
- // conversation within 5 minutes means either (a) the agent retried
793
- // WebFetch on the same SPA URL despite the directive, or (b) the
794
- // owner is asking about multiple SPA URLs in one session and the
795
- // pattern needs surfacing as a recurring class. Both signal that the
796
- // IDENTITY.md "Tool Failure Discipline" guidance is not landing in the
797
- // prompt — revise the copy rather than add mechanical enforcement.
798
- id: "webfetch-spa-short-circuit-recurring",
799
- name: "WebFetch JS-SPA short-circuit fired repeatedly in conversation",
800
- type: "repeated-error",
801
- logSource: "system",
802
- pattern: "WEBFETCH_CANNOT_READ_JS_SPA",
803
- thresholdCount: 2,
804
- thresholdWindowMinutes: 5,
805
- scope: "session",
806
- suggestedAction: "The WebFetch SPA preflight has fired more than once in this conversation. Either the agent is ignoring the loud-failure directive (retrying WebFetch after seeing WEBFETCH_CANNOT_READ_JS_SPA), or multiple SPA URLs are being asked about. Read the conversation's stream log for the [tool-use] / [tool-result] sequence around each occurrence \u2014 if the agent dispatched WebFetch on the same URL or substituted Playwright silently, revisit the IDENTITY.md `Tool Failure Discipline` paragraph that names structured-error handling."
807
- },
808
- {
809
- // Task 538: fires when a [spawn] line appears in a conversation's stream
810
- // log but no subprocess-lifecycle marker follows within 10s. The three
811
- // acceptable followups are Task 535's contract — at least one must be
812
- // emitted immediately at every spawn site. Their absence means
813
- // `teeProcStderrToStreamLog` regressed, the markers drifted, or the
814
- // spawn site was added without wiring them up. Session scope so a single
815
- // broken conversation fires exactly once, not N times for every spawn.
816
- id: "subproc-tee-silent-spawn",
817
- name: "Subprocess spawn without a stderr-tee lifecycle marker",
818
- type: "absent-followup",
819
- logSource: "system",
820
- pattern: "\\[spawn\\] pid=\\d+",
821
- followupPattern: "\\[subproc-stderr-tee-attached\\]|\\[subproc-debug-unavailable\\]|\\[subproc-stderr-skip\\]",
822
- followupWindowMs: 1e4,
823
- thresholdCount: 0,
824
- thresholdWindowMinutes: 0,
825
- scope: "session",
826
- suggestedAction: "The main-subprocess tee infrastructure has regressed \u2014 a spawn produced no lifecycle marker. Re-check `teeProcStderrToStreamLog` is invoked at the spawn site and that `[subproc-debug-unavailable]` or `[subproc-stderr-skip]` is written immediately when the tee cannot attach (Task 535 contract)."
827
- },
828
- {
829
- // Task 533: surface every Cloudflare-plugin refusal. The plugin emits
830
- // exactly one [cloudflare:refuse] line per refusal with a structured
831
- // reason field; any single occurrence on a previously-clean device
832
- // means the bound Cloudflare account does not match the operator's
833
- // intent (or the post-flight FQDN drifted) and the operator needs to
834
- // act in the dashboard.
835
- id: "cloudflare-refuse",
836
- name: "Cloudflare plugin refusal",
837
- type: "silent-catch",
838
- logSource: "any",
839
- pattern: "\\[cloudflare:refuse\\]|\\[cloudflare:post-flight-mismatch\\]",
840
- thresholdCount: 0,
841
- thresholdWindowMinutes: 0,
842
- suggestedAction: "The Cloudflare plugin refused an operation. Read the refusal `reason` field in the adjacent log line. For `account-drift` or `unbound-device`, run `tunnel-login force=true` while the operator is signed into the correct Cloudflare account in the browser. For `hostname-zone-not-routable`, the domain is not on Cloudflare yet \u2014 guide the operator to add it via the Cloudflare dashboard. For `post-flight-fqdn-mismatch` or `bound-account-does-not-own-hostname`, the laptop is signed into the wrong Cloudflare account \u2014 guide the operator to switch accounts in the dashboard, then re-run `tunnel-login force=true`."
843
- },
844
- {
845
- // Task 540: the single highest-priority refusal — surface it immediately
846
- // and independently of the generic cloudflare-refuse rule so the admin
847
- // agent sees it on the very next turn. This is the exact class that
848
- // burned the operator for 8 days across 9+ sessions (Apr 11–18, 2026):
849
- // tunnel running locally, dashboard serving the wrong account, nothing
850
- // from the internet reaches the laptop, and no prior telemetry surfaced
851
- // it in time for the agent to self-correct.
852
- id: "cloudflare-bound-account-mismatch",
853
- name: "Cloudflare bound account does not own the configured hostnames",
854
- type: "silent-catch",
855
- logSource: "any",
856
- pattern: '"reason":"bound-account-does-not-own-hostname"',
857
- thresholdCount: 0,
858
- thresholdWindowMinutes: 0,
859
- suggestedAction: "This laptop is signed into a Cloudflare account that does not own the hostnames the tunnel is configured to serve. Run `tunnel-status` to confirm, then tell the operator verbatim: 'The tunnel is running on this laptop but nothing from the internet is reaching it. The Cloudflare account this laptop is signed into doesn't own your domain. Open Cloudflare in your browser \u2014 is the account name in the top-left the one that owns your domain? If not, switch to the correct one, then tell me and I will re-sign-in.' When the operator confirms the correct account is selected, run `tunnel-login force=true`."
860
- },
861
- {
862
- // Task 545: tunnel-login's terminal-failure class — cloudflared's
863
- // login process died without writing cert.pem. Covers every reason
864
- // the handler emits on the `failed` branch: either an unknown exit
865
- // (`-without-cert`), an exit preceded by the courtesy browser-launch
866
- // marker (`-with-marker`), auth URL never produced (`-timeout`), or
867
- // crashed before producing it at all. Task 541's original pattern
868
- // matched `reason=browser-launch-fetch-error` — Task 545 retired
869
- // that reason because the marker alone is no longer terminal
870
- // (cloudflared keeps its OAuth-callback loop alive after emitting
871
- // it). Use this pattern for any new terminal reason the handler
872
- // gains: extend the alternation rather than adding parallel rules.
873
- id: "cloudflare-tunnel-login-failed",
874
- name: "Cloudflare tunnel-login process terminated without writing cert",
875
- type: "silent-catch",
876
- logSource: "any",
877
- pattern: "\\[cloudflare:tunnel-login:failed\\] reason=(login-process-exited-without-cert|login-process-exited-with-marker|auth-url-timeout|process-exited-before-auth-url)",
878
- thresholdCount: 0,
879
- thresholdWindowMinutes: 0,
880
- suggestedAction: "The cloudflared login process died before cert.pem landed on disk. For `login-process-exited-*` reasons the tunnel-login tool detected the dead process on the next call and has already respawned it \u2014 relay the new sign-in URL and wait for the operator to authorize in the VNC browser. For `auth-url-timeout` / `process-exited-before-auth-url`, the tool's most recent call returned an error and did not respawn \u2014 call `tunnel-login` again to spawn a fresh attempt. Never open the Cloudflare dashboard in any other surface; the only auth path is the sign-in URL the tool produces."
881
- },
882
- {
883
- // Task 545: non-terminal advisory. cloudflared's browser-launch
884
- // subcommand failed (DISPLAY unreachable, xdg-open absent, etc.) but
885
- // its OAuth-callback listener is still running — the login can still
886
- // complete if a human opens the URL. This rule fires so the admin
887
- // agent can relay "open the URL yourself" to the operator the moment
888
- // the condition appears, rather than waiting for the operator to
889
- // notice nothing is happening in their browser. Task 546 will
890
- // obsolete the advisory by rendering auth URLs that auto-open in
891
- // the VNC browser.
892
- id: "cloudflare-tunnel-login-browser-launch-failed",
893
- name: "cloudflared couldn't open the sign-in URL (login still live)",
894
- type: "silent-catch",
895
- logSource: "any",
896
- pattern: "\\[cloudflare:tunnel-login:browser-launch-failed\\]",
897
- thresholdCount: 0,
898
- thresholdWindowMinutes: 0,
899
- suggestedAction: "cloudflared's browser-launch courtesy failed on this laptop, but the sign-in URL is still live \u2014 the process is waiting for the OAuth callback. Tell the operator to open the sign-in URL in the VNC browser themselves and complete the authorization. The tunnel-login tool already includes the URL and the advisory in its most recent response; do not restart the login (that would invalidate the URL the operator is about to open)."
900
- },
901
- {
902
- // Task 545: raw-log surface. cloudflared-login.log is now read by
903
- // the review-detector's `cloudflared` source (see sources.ts). The
904
- // literal "Failed to fetch resource" is emitted by cloudflared
905
- // itself when its browser-launch subcommand can't reach a display.
906
- // This rule catches the cloudflared-side event even if the MCP
907
- // handler's classification drifts or the platform is restarting
908
- // mid-login and the advisory log line is not yet written. Keeping
909
- // this rule on a different source (log-line, not MCP stderr) makes
910
- // the detection redundant in the "defence in depth" sense — if the
911
- // MCP classification ever regresses, this still fires.
912
- id: "cloudflared-login-browser-launch-failed-raw",
913
- name: "cloudflared login \u2014 browser-launch fetch error in log",
914
- type: "silent-catch",
915
- logSource: "cloudflared",
916
- pattern: "Failed to fetch resource",
917
- thresholdCount: 0,
918
- thresholdWindowMinutes: 0,
919
- suggestedAction: "cloudflared-login.log shows the browser-launch courtesy failed. The sign-in URL from the last tunnel-login call is still live \u2014 the cloudflared process waits for the OAuth callback regardless of whether it could open the browser itself. Tell the operator to open the sign-in URL manually in the VNC browser on the device."
920
- },
921
- {
922
- // Task 540: cloudflared.log is the one file most likely to carry the
923
- // "tunnel is having real-world connectivity problems" signal — QUIC
924
- // connection failures, connector drops, edge unreachability. Prior to
925
- // this rule it was written but never read (the review-detector had no
926
- // rule coverage for it). A single ERR line is worth surfacing; the
927
- // tee'd output is typically noise-free.
928
- // Task 862: setup-tunnel.sh emits `[script:setup-tunnel]
929
- // step=onboarding-persist result=skipped reason=no-account-dir` via
930
- // phase_line when ACCOUNT_DIR is unset. Pre-Task-862, the form-driven
931
- // action runner threaded STREAM_LOG_PATH but not ACCOUNT_DIR, so the
932
- // line landed in the agent's stream log (system source) and the user
933
- // looped on currentStep=6 indefinitely.
934
- // The agent-via-Bash path also threads STREAM_LOG_PATH; operator-SSH
935
- // does not — that's the disambiguator. If this pattern reappears in
936
- // a system log, a future invocation surface forgot to declare
937
- // ACCOUNT_DIR. Fix at action-runner.ts WHITELIST['cloudflare-setup'],
938
- // not in the script.
939
- id: "cloudflare-setup-account-dir-missing",
940
- name: "cloudflare-setup ran without ACCOUNT_DIR \u2014 onboarding step-7 will not persist",
941
- type: "silent-catch",
942
- logSource: "system",
943
- pattern: "\\[script:setup-tunnel\\] step=onboarding-persist result=skipped reason=no-account-dir",
944
- thresholdCount: 0,
945
- thresholdWindowMinutes: 0,
946
- suggestedAction: "An invocation surface for setup-tunnel.sh failed to declare ACCOUNT_DIR. Without it the script's step-7 persist block (Task 562) skips, leaving OnboardingState.currentStep=6 forever. Inspect platform/ui/server/lib/action-runner.ts WHITELIST['cloudflare-setup'].build (Task 862 thread) and platform/ui/app/lib/claude-agent/spawn-env.ts buildSpawnEnv (Task 562 thread) \u2014 confirm ACCOUNT_DIR is in the env map for whichever surface the action-id prefix indicates. Do NOT change setup-tunnel.sh; the skipped branch is correct for operator-SSH reconfigure flows."
947
- },
948
- {
949
- id: "cloudflared-edge-errors",
950
- name: "cloudflared edge connectivity errors",
951
- type: "silent-catch",
952
- logSource: "cloudflared",
953
- pattern: "^\\S+ ERR (Failed to refresh protocol|no more connections active|Failed to dial a quic connection)",
954
- thresholdCount: 0,
955
- thresholdWindowMinutes: 0,
956
- suggestedAction: "cloudflared is reporting edge connectivity errors in its daemon log. Read the last 20 lines of `cloudflared.log` to see the surrounding context. Transient QUIC drops are normal; sustained failures (more than a handful in a minute) point to either a network issue on this laptop or an edge-side routing problem. Run `tunnel-status` to check whether end-to-end probing still succeeds."
957
- },
958
- {
959
- // Task 543: fires when an agent turn contains an opposing-axis choice-fork
960
- // question ("Want me to X, or Y?"). Every occurrence is a violation of the
961
- // IDENTITY § Questions rule — Rule A (one-sided questions) catches them
962
- // all, and the sharper sub-class (Rule B — no menu when a tool returned a
963
- // deterministic signal) is isolated by Task 544's `preceded-by` tightening.
964
- // logSource is "any" so the rule catches violations on both the admin
965
- // stream (claude-agent-stream-*) and the public agent stream
966
- // (public-agent-stream-*); the regex is agent-phrasing-specific and has
967
- // not been observed in other log contexts. Session scope groups matches
968
- // by conversationId so a single offending turn fires exactly once.
969
- id: "agent-choice-fork",
970
- name: "Agent emitted a choice-fork question",
971
- type: "repeated-error",
972
- logSource: "any",
973
- pattern: "Want me to [^\\n]+, or [^\\n]+\\?",
974
- thresholdCount: 1,
975
- thresholdWindowMinutes: 60,
976
- scope: "session",
977
- suggestedAction: 'Agent emitted a choice-fork question ("Want me to X, or Y?") instead of a one-sided question or the prescribed action. Review Task 543 IDENTITY \xA7 Questions \u2014 the agent asked an opposing-axis question, or degraded a deterministic tool signal into a menu. The log sample shows the offending turn verbatim.'
978
- },
979
- {
980
- // Task 546: fires when the operator clicks a device-bound URL affordance
981
- // and the chat UI cannot drive the device browser — either CDP is
982
- // unreachable, the navigation timed out, or CDP returned an error. The
983
- // log line carries intent, hostname, and navigateResult so the admin
984
- // agent can name the affected flow and hostname verbatim on its next
985
- // turn. Every occurrence is worth surfacing (thresholdCount: 0) because
986
- // this is the exact class of silent failure Task 546 exists to close:
987
- // the operator clicked, nothing happened on the device, and if we don't
988
- // review the click telemetry the agent has no way to know the flow is
989
- // stuck.
990
- id: "device-url-click-failed",
991
- name: "Device-bound URL click failed to drive the VNC browser",
992
- type: "silent-catch",
993
- logSource: "server",
994
- // Enumerate the NavigateResult union explicitly rather than relying
995
- // on a (?!ok) negative lookahead anchored to a specific token order.
996
- // If a new member is added to the union in cdp-client.ts, this rule
997
- // must be updated in the same commit — the pattern is order-agnostic
998
- // (browser= and navigateResult= can appear in either order) and the
999
- // enumerated list compile-fails the source if it ever drifts from
1000
- // the shared type in device-url-schema.ts.
1001
- pattern: "\\[device-url:click\\][^\\n]*(?:browser=fallback|navigateResult=(?:timeout|cdp-unreachable|error))",
1002
- thresholdCount: 0,
1003
- thresholdWindowMinutes: 0,
1004
- suggestedAction: "A device-bound URL click failed to drive Chromium on the device's VNC display. Identify the `intent` and `hostname` from the log line, then check the VNC surface: read `vnc-boot.log` and confirm Chromium on :99 is responding on CDP port 9222. If CDP is unreachable, the operator needs to restart the VNC stack; if CDP is reachable but navigation errored, the URL itself may be malformed upstream \u2014 grep the stream log for the originating `[device-url:render]` line."
1005
- },
1006
- {
1007
- // Task 554: fires when an agent turn emits a synthesized localhost/127.0.0.1
1008
- // `__remote-auth/setup` URL. The only legitimate source for that URL is
1009
- // `remote-auth-status`, which emits it inside a `maxy-device-url` affordance
1010
- // block with the device's real hostname. A synthesized localhost variant is
1011
- // the exact failure mode Task 554 closed: the removed "direct the user to
1012
- // /__remote-auth/setup" clause in the password-setter's tool description
1013
- // invited URL synthesis from a partial path. Session scope groups matches
1014
- // by conversationId so a single offending turn fires exactly once, even if
1015
- // the URL appears multiple times in the streamed response.
1016
- id: "invented-remote-auth-url",
1017
- name: "Invented localhost/127.0.0.1 remote-auth setup URL",
1018
- type: "silent-catch",
1019
- logSource: "any",
1020
- pattern: "http(s)?://(localhost|127\\.0\\.0\\.1)[:\\w/.-]*__remote-auth/setup",
1021
- thresholdCount: 0,
1022
- thresholdWindowMinutes: 0,
1023
- scope: "session",
1024
- suggestedAction: "[Task 554 regression] An assistant turn emitted a synthesized localhost/127.0.0.1 remote-auth setup URL. The only authority for the browser-setup URL is `remote-auth-status`, which emits a `maxy-device-url` affordance block with the device's real hostname. Review the offending turn, confirm the `remote-auth-set-password` tool description and onboarding SKILL.md step 3 still lack URL-synthesis language, and patch whichever prose surface invited the synthesis."
1025
- },
1026
- {
1027
- // Task 553: fires when `anthropic-setup` auto-resets a revoked API key.
1028
- // The auto-reset is silent to the agent (the tool falls through to
1029
- // awaiting_signin in the same call), but it is operator-visible — the
1030
- // user's previously-stored key was just deleted because Anthropic
1031
- // rejected it. Surfacing the event lets the admin agent explain on the
1032
- // next turn why the user is being asked to sign in again instead of
1033
- // continuing normally. The matching string is the exact stable prefix
1034
- // emitted by the state machine immediately before `deleteKey()` is
1035
- // invoked.
1036
- id: "anthropic-setup-auth-error-auto-reset",
1037
- name: "Anthropic API key auto-reset on auth_error",
1038
- type: "silent-catch",
1039
- logSource: "any",
1040
- pattern: "\\[anthropic-setup\\] auth_error",
1041
- thresholdCount: 0,
1042
- thresholdWindowMinutes: 0,
1043
- suggestedAction: "The stored Anthropic API key was rejected by console.anthropic.com (invalid, revoked, or expired) and `anthropic-setup` auto-deleted it. On the next operator interaction, explain that the key was cleared and walk them through sign-in again via the onboarding skill \u2014 the tool already returned `awaiting_signin` with the correct `browser_evaluate` action on the same call."
1044
- },
1045
- {
1046
- // Task 562: setup-tunnel.sh persists step-7 completion to a filesystem
1047
- // flag before arming the service restart. The flag is consumed by the
1048
- // next session's `loadOnboardingStep` and `getOnboardingState` calls,
1049
- // so any runtime failure to write it means the next admin session
1050
- // will re-ask the Cloudflare question the user just answered. Every
1051
- // occurrence is loud-surface-worthy: the cause is either a permission
1052
- // issue on the onboarding directory or a filesystem problem on the
1053
- // device, and the recovery is deterministic (the operator can re-run
1054
- // setup-tunnel.sh or call `onboarding-complete-step` explicitly).
1055
- id: "setup-tunnel-onboarding-persist-failed",
1056
- name: "setup-tunnel.sh failed to persist step-7 completion",
1057
- type: "silent-catch",
1058
- logSource: "any",
1059
- pattern: "\\[script:setup-tunnel\\] step=onboarding-persist result=error",
1060
- thresholdCount: 0,
1061
- thresholdWindowMinutes: 0,
1062
- suggestedAction: "[Task 562] setup-tunnel.sh could not persist step-7 completion before arming the service restart. Read the `reason` field on the failing phase line: `fs-flag-dir-failed` means `${ACCOUNT_DIR}/onboarding/` is not writable (chmod issue or disk full); `fs-flag-write-failed` means the flag file itself could not be written. Without the flag, the next admin session will re-ask the Cloudflare question the user just answered. Recovery: fix the permission/disk issue, then either re-run `~/setup-tunnel.sh` (the flag-write is idempotent) or call `onboarding-complete-step` with step 7 from the admin chat to mark step 7 complete explicitly."
1063
- },
1064
- {
1065
- // Task 570: every `[llm-call] adminModel resolution FAILED:` line
1066
- // is a workflow LLM step that could not resolve a model because
1067
- // readAdminModel returned null. The stderr line carries `reason=`
1068
- // (enoent/eacces/fs_error/parse_error/field_missing/
1069
- // field_wrong_type/field_empty) so the admin agent can act on the
1070
- // specific failure mode rather than reading the generic persisted
1071
- // error and misdiagnosing. Every occurrence is worth surfacing —
1072
- // the workflow already failed when this line was emitted.
1073
- id: "workflow-admin-model-resolution-failed",
1074
- name: "Workflow readAdminModel returned null (reason code in log)",
1075
- type: "silent-catch",
1076
- logSource: "any",
1077
- pattern: "\\[llm-call\\] adminModel resolution FAILED:",
1078
- thresholdCount: 0,
1079
- thresholdWindowMinutes: 0,
1080
- suggestedAction: "A workflow LLM step could not resolve the account's adminModel. Read the log line's `reason=` field: `enoent` means the account.json path is wrong or the account is unprovisioned \u2014 inspect `path=` and confirm it exists. `eacces` means permission drift \u2014 check file ownership. `parse_error` means the file is malformed, likely mid-write or truncated \u2014 read the file and repair. `field_missing` / `field_wrong_type` / `field_empty` mean the `adminModel` field needs to be set to a valid model ID. Never edit the persisted WorkflowRun.error string; fix the underlying file or permissions and re-run the workflow."
1081
- },
1082
- {
1083
- // Task 561: fires when an admin-agent Bash tool call installs
1084
- // `bind9-dnsutils` at runtime. Post-Task-561 the Maxy installer
1085
- // (platform/scripts/setup.sh) provisions the package on every fresh
1086
- // and upgrade install, so `dig` is always in PATH before
1087
- // setup-tunnel.sh runs. An admin apt-install of bind9-dnsutils is
1088
- // therefore evidence the installer regressed (or the host is on a
1089
- // pre-Task-561 image); in either case the fix is to re-run the
1090
- // installer, not to patch apt-state from chat. Session scope so a
1091
- // single offending turn fires exactly once.
1092
- id: "admin-agent-apt-bind9-install",
1093
- name: "Admin agent installed bind9-dnsutils at runtime (installer regression)",
1094
- type: "repeated-error",
1095
- logSource: "any",
1096
- pattern: "\\[tool-use\\][^\\n]*name=Bash[^\\n]*apt-get install[^\\n]*bind9-dnsutils",
1097
- thresholdCount: 1,
1098
- thresholdWindowMinutes: 60,
1099
- scope: "session",
1100
- suggestedAction: "[Task 561 regression] The admin agent ran `apt-get install bind9-dnsutils` \u2014 the exact workaround Task 561 eliminated. The Maxy installer provisions `bind9-dnsutils` in `platform/scripts/setup.sh` so `dig` is always in PATH before `setup-tunnel.sh` runs. Do not patch apt-state from chat \u2014 re-run the installer (`npx -y @rubytech/create-maxy`) and verify the installer's apt-get line still includes `bind9-dnsutils`. If the package is present in setup.sh but missing on the device, the installer did not re-run step 1 on the current image."
1101
- }
1102
- ];
1103
- }
1104
- function rulesFilePath(configDir2) {
1105
- return resolve(configDir2, "review-rules.json");
1106
- }
1107
- function ensureRulesFile(configDir2) {
1108
- const path2 = rulesFilePath(configDir2);
1109
- if (existsSync2(path2)) return { created: false, path: path2 };
1110
- mkdirSync(dirname(path2), { recursive: true });
1111
- const body = {
1112
- scanIntervalMs: DEFAULT_SCAN_INTERVAL_MS,
1113
- rules: defaultRules()
1114
- };
1115
- atomicWriteJson(path2, body);
1116
- return { created: true, path: path2 };
1117
- }
1118
- function loadRules(configDir2) {
1119
- const path2 = rulesFilePath(configDir2);
1120
- if (!existsSync2(path2)) {
1121
- throw new Error(`rules file missing at ${path2}`);
1122
- }
1123
- const raw = readFileSync(path2, "utf-8");
1124
- let parsed;
1125
- try {
1126
- parsed = JSON.parse(raw);
1127
- } catch (err) {
1128
- throw new Error(`rules file ${path2} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
1129
- }
1130
- return validateRulesFile(parsed, path2);
1131
- }
1132
- function rulesFileMtime(configDir2) {
1133
- const path2 = rulesFilePath(configDir2);
1134
- try {
1135
- return statSync2(path2).mtimeMs;
1136
- } catch {
1137
- return null;
1138
- }
1139
- }
1140
- function saveRules(configDir2, file) {
1141
- validateRulesFile(file, rulesFilePath(configDir2));
1142
- atomicWriteJson(rulesFilePath(configDir2), file);
1143
- }
1144
- function atomicWriteJson(path2, body) {
1145
- const tmp = `${path2}.tmp.${process.pid}.${Date.now()}`;
1146
- writeFileSync(tmp, JSON.stringify(body, null, 2) + "\n", "utf-8");
1147
- renameSync(tmp, path2);
1148
- }
1149
- function validateRulesFile(input, sourceLabel) {
1150
- if (!input || typeof input !== "object") {
1151
- throw new Error(`${sourceLabel}: top-level must be an object`);
1152
- }
1153
- const obj = input;
1154
- const scanIntervalMs = obj.scanIntervalMs;
1155
- if (typeof scanIntervalMs !== "number" || scanIntervalMs < 500 || scanIntervalMs > 3e5) {
1156
- throw new Error(`${sourceLabel}: scanIntervalMs must be a number between 500 and 300000`);
1157
- }
1158
- const rulesRaw = obj.rules;
1159
- if (!Array.isArray(rulesRaw)) {
1160
- throw new Error(`${sourceLabel}: rules must be an array`);
1161
- }
1162
- const ids = /* @__PURE__ */ new Set();
1163
- const rules = rulesRaw.map((r, i) => validateRule(r, `${sourceLabel}#rules[${i}]`, ids));
1164
- return { scanIntervalMs, rules };
1165
- }
1166
- function addMissingDefaultRules(rulesFile) {
1167
- const existingIds = new Set(rulesFile.rules.map((r) => r.id));
1168
- const defaults = defaultRules();
1169
- let mutated = false;
1170
- for (const rule of defaults) {
1171
- if (!existingIds.has(rule.id)) {
1172
- rulesFile.rules.push(rule);
1173
- mutated = true;
1174
- }
1175
- }
1176
- return mutated;
1177
- }
1178
- function migrateRateLimitPattern(rulesFile) {
1179
- const rule = rulesFile.rules.find((r) => r.id === "http-rate-limit-429");
1180
- if (!rule) return false;
1181
- if (rule.pattern !== RATE_LIMIT_PATTERN_V1) return false;
1182
- rule.pattern = RATE_LIMIT_PATTERN;
1183
- return true;
1184
- }
1185
- function validateRule(input, label, seenIds) {
1186
- if (!input || typeof input !== "object") {
1187
- throw new Error(`${label}: rule must be an object`);
1188
- }
1189
- const r = input;
1190
- const id = r.id;
1191
- if (typeof id !== "string" || id.length === 0) {
1192
- throw new Error(`${label}: id must be a non-empty string`);
1193
- }
1194
- if (seenIds.has(id)) {
1195
- throw new Error(`${label}: duplicate rule id "${id}"`);
1196
- }
1197
- seenIds.add(id);
1198
- const name = r.name;
1199
- if (typeof name !== "string" || name.length === 0) {
1200
- throw new Error(`${label}: name must be a non-empty string`);
1201
- }
1202
- const type = r.type;
1203
- if (typeof type !== "string" || !VALID_TYPES.has(type)) {
1204
- throw new Error(`${label}: type must be one of ${[...VALID_TYPES].join(", ")}`);
1205
- }
1206
- const logSource = r.logSource;
1207
- if (typeof logSource !== "string" || !VALID_SOURCES.has(logSource)) {
1208
- throw new Error(`${label}: logSource must be one of ${[...VALID_SOURCES].join(", ")}`);
1209
- }
1210
- const pattern = r.pattern;
1211
- if (typeof pattern !== "string") {
1212
- throw new Error(`${label}: pattern must be a string (may be empty for stale-log/file-write-storm)`);
1213
- }
1214
- if (pattern.length > 0) {
1215
- try {
1216
- new RegExp(pattern);
1217
- } catch (err) {
1218
- throw new Error(`${label}: pattern is not a valid regex: ${err instanceof Error ? err.message : String(err)}`);
1219
- }
1220
- }
1221
- const thresholdCount = r.thresholdCount;
1222
- if (typeof thresholdCount !== "number" || thresholdCount < 0) {
1223
- throw new Error(`${label}: thresholdCount must be a non-negative number`);
1224
- }
1225
- const thresholdWindowMinutes = r.thresholdWindowMinutes;
1226
- if (typeof thresholdWindowMinutes !== "number" || thresholdWindowMinutes < 0) {
1227
- throw new Error(`${label}: thresholdWindowMinutes must be a non-negative number`);
1228
- }
1229
- const suggestedAction = r.suggestedAction;
1230
- if (typeof suggestedAction !== "string" || suggestedAction.length === 0) {
1231
- throw new Error(`${label}: suggestedAction must be a non-empty string`);
1232
- }
1233
- const rule = {
1234
- id,
1235
- name,
1236
- type,
1237
- logSource,
1238
- pattern,
1239
- thresholdCount,
1240
- thresholdWindowMinutes,
1241
- suggestedAction
1242
- };
1243
- if (typeof r.watchPath === "string") rule.watchPath = r.watchPath;
1244
- if (typeof r.staleHours === "number") rule.staleHours = r.staleHours;
1245
- if (typeof r.followupPattern === "string") {
1246
- if (r.followupPattern.length > 0) {
1247
- try {
1248
- new RegExp(r.followupPattern);
1249
- } catch (err) {
1250
- throw new Error(`${label}: followupPattern is not a valid regex: ${err instanceof Error ? err.message : String(err)}`);
1251
- }
1252
- }
1253
- rule.followupPattern = r.followupPattern;
1254
- }
1255
- if (typeof r.followupWindowMs === "number") rule.followupWindowMs = r.followupWindowMs;
1256
- if (typeof r.suppressedUntil === "string") rule.suppressedUntil = r.suppressedUntil;
1257
- if (r.scope !== void 0) {
1258
- if (typeof r.scope !== "string" || !VALID_SCOPES.has(r.scope)) {
1259
- throw new Error(`${label}: scope must be one of ${[...VALID_SCOPES].join(", ")}`);
1260
- }
1261
- rule.scope = r.scope;
1262
- }
1263
- if (rule.type === "file-write-storm" || rule.type === "stale-log") {
1264
- if (!rule.watchPath) {
1265
- throw new Error(`${label}: ${rule.type} rules require watchPath`);
1266
- }
1267
- }
1268
- if (rule.type === "stale-log") {
1269
- if (typeof rule.staleHours !== "number" || rule.staleHours <= 0) {
1270
- throw new Error(`${label}: stale-log rules require a positive staleHours`);
1271
- }
1272
- }
1273
- if (rule.type === "absent-followup") {
1274
- if (rule.pattern.length === 0) {
1275
- throw new Error(`${label}: absent-followup rules require a non-empty pattern`);
1276
- }
1277
- if (typeof rule.followupPattern !== "string" || rule.followupPattern.length === 0) {
1278
- throw new Error(`${label}: absent-followup rules require a non-empty followupPattern`);
1279
- }
1280
- if (typeof rule.followupWindowMs !== "number" || rule.followupWindowMs <= 0 || rule.followupWindowMs > MAX_FOLLOWUP_WINDOW_MS) {
1281
- throw new Error(`${label}: absent-followup rules require followupWindowMs in (0, ${MAX_FOLLOWUP_WINDOW_MS}]`);
1282
- }
1283
- }
1284
- return rule;
1285
- }
1286
-
1287
- // app/lib/review-detector/sources.ts
1288
- import { existsSync as existsSync3, readdirSync, statSync as statSync3, writeFileSync as writeFileSync2, renameSync as renameSync2, mkdirSync as mkdirSync2, openSync, readSync, closeSync, readFileSync as readFileSync2 } from "fs";
1289
- import { resolve as resolve2, join as join2, basename, dirname as dirname2 } from "path";
1290
- function tailStatePath(configDir2) {
1291
- return resolve2(configDir2, "review-state.json");
1292
- }
1293
- function loadTailState(configDir2) {
1294
- const path2 = tailStatePath(configDir2);
1295
- if (!existsSync3(path2)) return {};
1296
- try {
1297
- const raw = readFileSync2(path2, "utf-8");
1298
- const parsed = JSON.parse(raw);
1299
- if (!parsed || typeof parsed !== "object") return {};
1300
- const clean = {};
1301
- for (const [key, value] of Object.entries(parsed)) {
1302
- const entry = value;
1303
- if (entry && typeof entry.offset === "number" && typeof entry.size === "number" && typeof entry.inode === "number") {
1304
- clean[key] = entry;
1305
- }
1306
- }
1307
- return clean;
1308
- } catch (err) {
1309
- console.error(`[review] tail state corrupt at ${path2}, starting fresh: ${err instanceof Error ? err.message : String(err)}`);
1310
- return {};
1311
- }
1312
- }
1313
- function saveTailState(configDir2, state) {
1314
- const path2 = tailStatePath(configDir2);
1315
- mkdirSync2(dirname2(path2), { recursive: true });
1316
- const tmp = `${path2}.tmp.${process.pid}.${Date.now()}`;
1317
- writeFileSync2(tmp, JSON.stringify(state, null, 2) + "\n", "utf-8");
1318
- renameSync2(tmp, path2);
1319
- }
1320
- function discoverSourceFiles(configDir2, accountLogDir2, logicalSource) {
1321
- if (logicalSource === "server") {
1322
- const p = resolve2(configDir2, "logs", "server.log");
1323
- return existsSync3(p) ? [{ logicalSource: "server", filepath: p }] : [];
1324
- }
1325
- if (logicalSource === "vnc") {
1326
- const p = resolve2(configDir2, "logs", "vnc-boot.log");
1327
- return existsSync3(p) ? [{ logicalSource: "vnc", filepath: p }] : [];
1328
- }
1329
- if (logicalSource === "cloudflared") {
1330
- const files2 = [];
1331
- const daemon = resolve2(configDir2, "logs", "cloudflared.log");
1332
- if (existsSync3(daemon)) files2.push({ logicalSource: "cloudflared", filepath: daemon });
1333
- const login = resolve2(configDir2, "logs", "cloudflared-login.log");
1334
- if (existsSync3(login)) files2.push({ logicalSource: "cloudflared", filepath: login });
1335
- return files2;
1336
- }
1337
- const prefix = {
1338
- system: "claude-agent-stream-",
1339
- error: "claude-agent-stderr-",
1340
- session: "sse-events-",
1341
- public: "public-agent-stream-",
1342
- mcp: "mcp-"
1343
- }[logicalSource];
1344
- if (!existsSync3(accountLogDir2)) return [];
1345
- const files = [];
1346
- let scanned = 0;
1347
- let skippedPrefixMismatch = 0;
1348
- let skippedNotLog = 0;
1349
- for (const entry of readdirSync(accountLogDir2)) {
1350
- scanned += 1;
1351
- const matchesPrefix = entry.startsWith(prefix);
1352
- const isLog = entry.endsWith(".log");
1353
- if (matchesPrefix && isLog) {
1354
- files.push({ logicalSource, filepath: join2(accountLogDir2, entry) });
1355
- } else if (!matchesPrefix) {
1356
- skippedPrefixMismatch += 1;
1357
- } else {
1358
- skippedNotLog += 1;
1359
- }
1360
- }
1361
- files.sort((a, b) => {
1362
- try {
1363
- return statSync3(b.filepath).mtimeMs - statSync3(a.filepath).mtimeMs;
1364
- } catch {
1365
- return a.filepath.localeCompare(b.filepath);
1366
- }
1367
- });
1368
- if (skippedPrefixMismatch > 0 || skippedNotLog > 0) {
1369
- console.error(`[review-scan-skip] dir=${accountLogDir2} source=${logicalSource} prefix=${prefix} scanned=${scanned} matched=${files.length} skipped_prefix=${skippedPrefixMismatch} skipped_non_log=${skippedNotLog}`);
1370
- }
1371
- return files;
1372
- }
1373
- function discoverAllSources(configDir2, accountLogDir2) {
1374
- return [
1375
- ...discoverSourceFiles(configDir2, accountLogDir2, "server"),
1376
- ...discoverSourceFiles(configDir2, accountLogDir2, "vnc"),
1377
- ...discoverSourceFiles(configDir2, accountLogDir2, "system"),
1378
- ...discoverSourceFiles(configDir2, accountLogDir2, "error"),
1379
- ...discoverSourceFiles(configDir2, accountLogDir2, "session"),
1380
- ...discoverSourceFiles(configDir2, accountLogDir2, "public"),
1381
- ...discoverSourceFiles(configDir2, accountLogDir2, "mcp"),
1382
- ...discoverSourceFiles(configDir2, accountLogDir2, "cloudflared")
1383
- ];
1384
- }
1385
- function readNewLines(filepath, prev) {
1386
- if (!existsSync3(filepath)) return null;
1387
- const stat7 = statSync3(filepath);
1388
- const size = stat7.size;
1389
- const inode = stat7.ino;
1390
- let startOffset = 0;
1391
- let rotated = false;
1392
- let truncated = false;
1393
- if (prev) {
1394
- if (prev.inode !== inode) {
1395
- rotated = true;
1396
- startOffset = 0;
1397
- } else if (size < prev.offset) {
1398
- truncated = true;
1399
- startOffset = 0;
1400
- } else {
1401
- startOffset = prev.offset;
1402
- }
1403
- }
1404
- if (startOffset >= size) {
1405
- return {
1406
- lines: [],
1407
- entry: { offset: size, size, inode },
1408
- rotated,
1409
- truncated
1410
- };
1411
- }
1412
- const fd = openSync(filepath, "r");
1413
- try {
1414
- const bufSize = Math.max(0, size - startOffset);
1415
- const buf = Buffer.alloc(bufSize);
1416
- if (bufSize > 0) {
1417
- readSync(fd, buf, 0, bufSize, startOffset);
1418
- }
1419
- const text = buf.toString("utf-8");
1420
- const lines = text.length > 0 ? text.split("\n") : [];
1421
- let newOffset = size;
1422
- if (lines.length > 0 && !text.endsWith("\n")) {
1423
- const partial = lines.pop();
1424
- newOffset = size - Buffer.byteLength(partial, "utf-8");
1425
- } else if (lines.length > 0 && text.endsWith("\n")) {
1426
- if (lines[lines.length - 1] === "") lines.pop();
1427
- }
1428
- return {
1429
- lines,
1430
- entry: { offset: newOffset, size, inode },
1431
- rotated,
1432
- truncated
1433
- };
1434
- } finally {
1435
- closeSync(fd);
1436
- }
1437
- }
1438
- function countRecentWrites(dir, sinceMs) {
1439
- if (!existsSync3(dir)) return 0;
1440
- let count = 0;
1441
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1442
- if (!entry.isFile()) continue;
1443
- try {
1444
- const st = statSync3(join2(dir, entry.name));
1445
- if (st.mtimeMs >= sinceMs) count += 1;
1446
- } catch {
1447
- }
1448
- }
1449
- return count;
1450
- }
1451
- function fileLastWriteMs(path2) {
1452
- if (!existsSync3(path2)) return null;
1453
- try {
1454
- return statSync3(path2).mtimeMs;
1455
- } catch {
1456
- return null;
1457
- }
1458
- }
1459
- function accountLogDir(accountDir) {
1460
- return resolve2(accountDir, "logs");
1461
- }
1462
- function sourceKey(file) {
1463
- return `${file.logicalSource}:${basename(file.filepath)}`;
1464
- }
1465
-
1466
- // app/lib/review-detector/writer.ts
1467
- import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync3, statSync as statSync4 } from "fs";
1468
- import { resolve as resolve3, dirname as dirname3 } from "path";
1469
- import { randomUUID } from "crypto";
1470
- function reviewLogPath(configDir2) {
1471
- return resolve3(configDir2, "logs", "review.log");
1472
- }
1473
- function pendingAlertsPath(configDir2) {
1474
- return resolve3(configDir2, "review-pending-alerts.jsonl");
1475
- }
1476
- function reviewLog(configDir2, event) {
1477
- const path2 = reviewLogPath(configDir2);
1478
- try {
1479
- mkdirSync3(dirname3(path2), { recursive: true });
1480
- const line = `${new Date(
1481
- typeof event.ts === "number" ? event.ts : Date.now()
1482
- ).toISOString()} [review] ${JSON.stringify(event)}
1483
- `;
1484
- appendFileSync(path2, line, "utf-8");
1485
- } catch (err) {
1486
- console.error(`[review] failed to write review log at ${path2}: ${err instanceof Error ? err.message : String(err)}`);
1487
- }
1488
- }
1489
- async function ensureReviewAlertIndex() {
1490
- const session = getSession();
1491
- try {
1492
- await session.run(
1493
- `CREATE INDEX review_alert_lookup IF NOT EXISTS
1494
- FOR (a:ReviewAlert)
1495
- ON (a.accountId, a.resolvedAt, a.lastMatchAt)`
1496
- );
1497
- } finally {
1498
- await session.close();
1499
- }
1500
- }
1501
- async function ensureReviewDigestSchedule(accountId) {
1502
- const eventId = `review-digest-${accountId}`;
1503
- const now = (/* @__PURE__ */ new Date()).toISOString();
1504
- const next = /* @__PURE__ */ new Date();
1505
- if (next.getHours() >= 8) {
1506
- next.setDate(next.getDate() + 1);
1507
- }
1508
- next.setHours(8, 0, 0, 0);
1509
- const nextRun = next.toISOString();
1510
- const session = getSession();
1511
- try {
1512
- await session.run(
1513
- `MERGE (e:Event { eventId: $eventId })
1514
- ON CREATE SET
1515
- e.accountId = $accountId,
1516
- e.name = 'Daily review digest',
1517
- e.description = 'Task 385 \u2014 composes the review cadence digest from the last 24 hours of review.log and active ReviewAlert records. Scheduled via check-due-events.',
1518
- e.startDate = $now,
1519
- e.eventStatus = 'scheduled',
1520
- e.recurrence = '0 8 * * *',
1521
- e.nextRun = datetime($nextRun),
1522
- e.sourcePlugin = 'admin',
1523
- e.actionPlugin = 'admin',
1524
- e.actionTool = 'review-digest-compose',
1525
- e.actionArgs = '{}',
1526
- e.createdAt = $now,
1527
- e.updatedAt = $now
1528
- ON MATCH SET
1529
- e.actionPlugin = 'admin',
1530
- e.actionTool = 'review-digest-compose',
1531
- e.updatedAt = $now`,
1532
- { eventId, accountId, now, nextRun }
1533
- );
1534
- } finally {
1535
- await session.close();
1536
- }
1537
- }
1538
- async function countActiveReviewAlerts(accountId) {
1539
- const session = getSession();
1540
- try {
1541
- const result = await session.run(
1542
- `MATCH (a:ReviewAlert {accountId: $accountId})
1543
- WHERE a.resolvedAt IS NULL
1544
- AND (a.suppressedUntil IS NULL OR a.suppressedUntil < datetime())
1545
- RETURN count(a) AS n`,
1546
- { accountId }
1547
- );
1548
- const n = result.records[0]?.get("n");
1549
- if (typeof n === "number") return n;
1550
- if (n && typeof n.toNumber === "function") {
1551
- return n.toNumber();
1552
- }
1553
- return 0;
1554
- } finally {
1555
- await session.close();
1556
- }
1557
- }
1558
- async function upsertReviewAlert(accountId, match) {
1559
- const session = getSession();
1560
- try {
1561
- await session.run(
1562
- `MERGE (a:ReviewAlert { ruleId: $ruleId, accountId: $accountId })
1563
- ON CREATE SET
1564
- a.alertId = $alertId,
1565
- a.ruleName = $ruleName,
1566
- a.firstMatchAt = datetime($matchedAt),
1567
- a.lastMatchAt = datetime($matchedAt),
1568
- a.cumulativeMatchCount = 1,
1569
- a.sampleEvidence = $sampleEvidence,
1570
- a.suggestedAction = $suggestedAction,
1571
- a.suppressedUntil = null,
1572
- a.resolvedAt = null
1573
- ON MATCH SET
1574
- a.lastMatchAt = datetime($matchedAt),
1575
- a.cumulativeMatchCount = a.cumulativeMatchCount + 1,
1576
- a.sampleEvidence = $sampleEvidence,
1577
- a.suggestedAction = $suggestedAction,
1578
- a.resolvedAt = null`,
1579
- {
1580
- ruleId: match.ruleId,
1581
- accountId,
1582
- alertId: randomUUID(),
1583
- ruleName: match.ruleName,
1584
- matchedAt: new Date(match.matchedAt).toISOString(),
1585
- sampleEvidence: match.sampleEvidence,
1586
- suggestedAction: match.suggestedAction
1587
- }
1588
- );
1589
- } finally {
1590
- await session.close();
1591
- }
1592
- }
1593
- function queueAlert(configDir2, accountId, match) {
1594
- const path2 = pendingAlertsPath(configDir2);
1595
- try {
1596
- mkdirSync3(dirname3(path2), { recursive: true });
1597
- const line = JSON.stringify({ accountId, match }) + "\n";
1598
- appendFileSync(path2, line, "utf-8");
1599
- } catch (err) {
1600
- console.error(`[review] failed to queue alert at ${path2}: ${err instanceof Error ? err.message : String(err)}`);
1601
- }
1602
- }
1603
- async function drainPendingAlerts(configDir2) {
1604
- const path2 = pendingAlertsPath(configDir2);
1605
- if (!existsSync4(path2)) return { drained: 0, remaining: 0 };
1606
- const raw = readFileSync3(path2, "utf-8");
1607
- const lines = raw.split("\n").filter((l) => l.trim().length > 0);
1608
- if (lines.length === 0) return { drained: 0, remaining: 0 };
1609
- const remaining = [];
1610
- let drained = 0;
1611
- for (const line of lines) {
1612
- let entry = null;
1613
- try {
1614
- entry = JSON.parse(line);
1615
- } catch {
1616
- continue;
1617
- }
1618
- try {
1619
- await upsertReviewAlert(entry.accountId, entry.match);
1620
- drained += 1;
1621
- } catch {
1622
- remaining.push(line);
1623
- }
1624
- }
1625
- const tmp = `${path2}.tmp.${process.pid}.${Date.now()}`;
1626
- if (remaining.length > 0) {
1627
- writeFileSync3(tmp, remaining.join("\n") + "\n", "utf-8");
1628
- } else {
1629
- writeFileSync3(tmp, "", "utf-8");
1630
- }
1631
- renameSync3(tmp, path2);
1632
- return { drained, remaining: remaining.length };
1633
- }
1634
-
1635
- // app/lib/review-detector/boot.ts
1636
- async function bootDetector() {
1637
- const account = resolveAccount();
1638
- if (!account) {
1639
- console.error("[review] boot: no account resolved \u2014 detector not starting (Phase 0 expects exactly one account)");
1640
- return null;
1641
- }
1642
- const configDir2 = MAXY_DIR;
1643
- const accountId = account.accountId;
1644
- const accountDir = account.accountDir;
1645
- const ensured = ensureRulesFile(configDir2);
1646
- if (ensured.created) {
1647
- reviewLog(configDir2, { event: "rules-defaults-created", path: ensured.path });
1648
- }
1649
- let rulesFile;
1650
- try {
1651
- rulesFile = loadRules(configDir2);
1652
- } catch (err) {
1653
- reviewLog(configDir2, {
1654
- event: "boot-failed",
1655
- reason: "rules-invalid",
1656
- error: err instanceof Error ? err.message : String(err)
1657
- });
1658
- console.error(`[review] boot: rules file invalid \u2014 ${err instanceof Error ? err.message : String(err)}`);
1659
- return null;
1660
- }
1661
- if (addMissingDefaultRules(rulesFile)) {
1662
- saveRules(configDir2, rulesFile);
1663
- reviewLog(configDir2, { event: "rules-updated", update: "added-missing-defaults" });
1664
- console.error("[review] boot: added missing default rules to existing rules file");
1665
- }
1666
- if (migrateRateLimitPattern(rulesFile)) {
1667
- saveRules(configDir2, rulesFile);
1668
- reviewLog(configDir2, { event: "rules-migrated", migration: "rate-limit-pattern-v2" });
1669
- console.error("[review] boot: migrated http-rate-limit-429 pattern to v2 (Task 408)");
1670
- }
1671
- try {
1672
- await ensureReviewAlertIndex();
1673
- reviewLog(configDir2, { event: "neo4j-index-ensured" });
1674
- } catch (err) {
1675
- reviewLog(configDir2, {
1676
- event: "neo4j-index-failed",
1677
- error: err instanceof Error ? err.message : String(err)
1678
- });
1679
- console.error(`[review] boot: ensureReviewAlertIndex failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
1680
- }
1681
- try {
1682
- await ensureReviewDigestSchedule(accountId);
1683
- reviewLog(configDir2, { event: "digest-schedule-ensured" });
1684
- } catch (err) {
1685
- reviewLog(configDir2, {
1686
- event: "digest-schedule-failed",
1687
- error: err instanceof Error ? err.message : String(err)
1688
- });
1689
- console.error(`[review] boot: ensureReviewDigestSchedule failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
1690
- }
1691
- const tailState = loadTailState(configDir2);
1692
- const logDir = accountLogDir(accountDir);
1693
- const initialSources = discoverAllSources(configDir2, logDir);
1694
- const snapshot = {
1695
- state: "running",
1696
- startedAt: Date.now(),
1697
- lastScanAt: null,
1698
- lastScanDurationMs: null,
1699
- scanCycles: 0,
1700
- rulesLoaded: rulesFile.rules.length,
1701
- sourcesTracked: initialSources.length,
1702
- activeAlerts: 0,
1703
- lastError: null
1704
- };
1705
- reviewLog(configDir2, {
1706
- event: "detector-started",
1707
- configDir: basename2(configDir2),
1708
- accountId: accountId.slice(0, 8),
1709
- rulesLoaded: rulesFile.rules.length,
1710
- sourcesDiscovered: initialSources.length,
1711
- tailStateEntries: Object.keys(tailState).length,
1712
- scanIntervalMs: rulesFile.scanIntervalMs
1713
- });
1714
- const runtime = {
1715
- configDir: configDir2,
1716
- accountId,
1717
- accountDir,
1718
- rulesFile,
1719
- rulesFileMtimeMs: rulesFileMtime(configDir2) ?? Date.now(),
1720
- ruleState: /* @__PURE__ */ new Map(),
1721
- tailState,
1722
- snapshot,
1723
- stopped: false
1724
- };
1725
- return runtime;
1726
- }
1727
-
1728
- // app/lib/review-detector/scan-loop.ts
1729
- import { resolve as resolve4 } from "path";
1730
-
1731
- // app/lib/review-detector/evaluator.ts
1732
- var SAMPLE_MAX_CHARS = 500;
1733
- var CONV_ID_REGEX = /conversationId=([a-f0-9]{8})/;
1734
- function scopeKeyFor(rule, line) {
1735
- if (rule.scope !== "session") return "";
1736
- const m = CONV_ID_REGEX.exec(line);
1737
- return m ? m[1] : "";
1738
- }
1739
- var compiledRegexCache = /* @__PURE__ */ new Map();
1740
- function compileRegex(pattern) {
1741
- let re = compiledRegexCache.get(pattern);
1742
- if (re === void 0) {
1743
- re = new RegExp(pattern, "i");
1744
- compiledRegexCache.set(pattern, re);
1745
- }
1746
- return re;
1747
- }
1748
- function isSuppressed(rule, nowMs) {
1749
- if (!rule.suppressedUntil) return false;
1750
- const until = Date.parse(rule.suppressedUntil);
1751
- if (Number.isNaN(until)) return false;
1752
- return nowMs < until;
1753
- }
1754
- function toSample(line) {
1755
- if (line.length <= SAMPLE_MAX_CHARS) return line;
1756
- return line.slice(0, SAMPLE_MAX_CHARS) + "\u2026";
1757
- }
1758
- function newRuleState() {
1759
- return {
1760
- matchTimestamps: [],
1761
- matchTimestampsByScope: /* @__PURE__ */ new Map(),
1762
- lastAlertAt: null,
1763
- cumulativeSinceLastAlert: 0,
1764
- lastSeenAt: null,
1765
- pendingFollowups: []
1766
- };
1767
- }
1768
- function evaluateTextRule(rule, lines, state, nowMs) {
1769
- if (isSuppressed(rule, nowMs)) return { match: null, state };
1770
- if (rule.pattern.length === 0) return { match: null, state };
1771
- const regex = compileRegex(rule.pattern);
1772
- const matchesByScope = /* @__PURE__ */ new Map();
1773
- let firstSample = null;
1774
- for (const line of lines) {
1775
- if (!regex.test(line)) continue;
1776
- if (!firstSample) firstSample = line;
1777
- const key = scopeKeyFor(rule, line);
1778
- const existing = matchesByScope.get(key) ?? [];
1779
- existing.push(line);
1780
- matchesByScope.set(key, existing);
1781
- }
1782
- if (matchesByScope.size === 0) return { match: null, state };
1783
- const windowStart = rule.thresholdWindowMinutes > 0 ? nowMs - rule.thresholdWindowMinutes * 6e4 : -Infinity;
1784
- const updated = {
1785
- ...state,
1786
- matchTimestamps: [...state.matchTimestamps],
1787
- matchTimestampsByScope: new Map(state.matchTimestampsByScope ?? [])
1788
- };
1789
- let firingSample = null;
1790
- let fires = false;
1791
- const isCountZero = rule.thresholdCount === 0;
1792
- if (rule.scope === "session") {
1793
- for (const [key, hits] of matchesByScope) {
1794
- const prior = updated.matchTimestampsByScope.get(key) ?? [];
1795
- const merged = [...prior, ...hits.map(() => nowMs)].filter((t) => t >= windowStart);
1796
- if (merged.length === 0) {
1797
- updated.matchTimestampsByScope.delete(key);
1798
- } else {
1799
- updated.matchTimestampsByScope.set(key, merged);
1800
- }
1801
- if (!fires && (isCountZero || merged.length >= rule.thresholdCount)) {
1802
- fires = true;
1803
- firingSample = hits[0];
1804
- }
1805
- }
1806
- } else {
1807
- for (const hits of matchesByScope.values()) {
1808
- for (const _ of hits) updated.matchTimestamps.push(nowMs);
1809
- }
1810
- updated.matchTimestamps = updated.matchTimestamps.filter((t) => t >= windowStart);
1811
- fires = isCountZero || updated.matchTimestamps.length >= rule.thresholdCount;
1812
- if (fires) firingSample = firstSample;
1813
- }
1814
- if (!fires) {
1815
- return { match: null, state: updated };
1816
- }
1817
- const match = {
1818
- ruleId: rule.id,
1819
- ruleName: rule.name,
1820
- matchedAt: nowMs,
1821
- sampleEvidence: toSample(firingSample ?? firstSample ?? lines[0] ?? ""),
1822
- suggestedAction: rule.suggestedAction
1823
- };
1824
- return { match, state: updated };
1825
- }
1826
- function evaluateFileWriteStormRule(rule, recentWriteCount, state, nowMs) {
1827
- if (isSuppressed(rule, nowMs)) return { match: null, state };
1828
- if (recentWriteCount < rule.thresholdCount) {
1829
- return { match: null, state };
1830
- }
1831
- const match = {
1832
- ruleId: rule.id,
1833
- ruleName: rule.name,
1834
- matchedAt: nowMs,
1835
- sampleEvidence: `${recentWriteCount} file writes in ${rule.thresholdWindowMinutes}m at ${rule.watchPath}`,
1836
- suggestedAction: rule.suggestedAction
1837
- };
1838
- return { match, state };
1839
- }
1840
- function evaluateStaleLogRule(rule, lastMtimeMs, state, nowMs) {
1841
- if (isSuppressed(rule, nowMs)) return { match: null, state };
1842
- let lastSeenAt = state.lastSeenAt;
1843
- if (lastMtimeMs !== null) {
1844
- lastSeenAt = Math.max(lastSeenAt ?? 0, lastMtimeMs);
1845
- }
1846
- const updated = { ...state, lastSeenAt };
1847
- if (lastMtimeMs === null || lastSeenAt === null) {
1848
- return { match: null, state: updated };
1849
- }
1850
- const staleMs = (rule.staleHours ?? 24) * 60 * 60 * 1e3;
1851
- if (nowMs - lastSeenAt < staleMs) {
1852
- return { match: null, state: updated };
1853
- }
1854
- const match = {
1855
- ruleId: rule.id,
1856
- ruleName: rule.name,
1857
- matchedAt: nowMs,
1858
- sampleEvidence: `last write at ${new Date(lastSeenAt).toISOString()} (${rule.watchPath})`,
1859
- suggestedAction: rule.suggestedAction
1860
- };
1861
- return { match, state: updated };
1862
- }
1863
- function evaluateAbsentFollowupRule(rule, lines, state, nowMs) {
1864
- if (isSuppressed(rule, nowMs)) return { matches: [], state };
1865
- if (!rule.pattern || !rule.followupPattern || !rule.followupWindowMs) {
1866
- return { matches: [], state };
1867
- }
1868
- const triggerRegex = compileRegex(rule.pattern);
1869
- const followupRegex = compileRegex(rule.followupPattern);
1870
- const pending = [...state.pendingFollowups ?? []];
1871
- for (const line of lines) {
1872
- if (triggerRegex.test(line)) {
1873
- pending.push({
1874
- scope: scopeKeyFor(rule, line),
1875
- timestamp: nowMs,
1876
- line,
1877
- fulfilled: false
1878
- });
1879
- continue;
1880
- }
1881
- if (followupRegex.test(line)) {
1882
- const scope = scopeKeyFor(rule, line);
1883
- for (const entry of pending) {
1884
- if (!entry.fulfilled && entry.scope === scope) {
1885
- entry.fulfilled = true;
1886
- break;
1887
- }
1888
- }
1889
- }
1890
- }
1891
- const matches = [];
1892
- const kept = [];
1893
- for (const entry of pending) {
1894
- const age = nowMs - entry.timestamp;
1895
- if (age >= rule.followupWindowMs) {
1896
- if (!entry.fulfilled) {
1897
- matches.push({
1898
- ruleId: rule.id,
1899
- ruleName: rule.name,
1900
- matchedAt: nowMs,
1901
- sampleEvidence: toSample(entry.line),
1902
- suggestedAction: rule.suggestedAction,
1903
- missedForMs: age
1904
- });
1905
- }
1906
- continue;
1907
- }
1908
- kept.push(entry);
1909
- }
1910
- return {
1911
- matches,
1912
- state: { ...state, pendingFollowups: kept }
1913
- };
1914
- }
1915
- var ALERT_WINDOW_MS = 60 * 60 * 1e3;
1916
- function rateLimitDecision(state, nowMs) {
1917
- const since = state.lastAlertAt === null ? Infinity : nowMs - state.lastAlertAt;
1918
- if (since >= ALERT_WINDOW_MS) {
1919
- return {
1920
- surface: true,
1921
- state: { ...state, lastAlertAt: nowMs, cumulativeSinceLastAlert: 0 }
1922
- };
1923
- }
1924
- return {
1925
- surface: false,
1926
- state: { ...state, cumulativeSinceLastAlert: state.cumulativeSinceLastAlert + 1 }
1927
- };
1928
- }
1929
-
1930
- // app/lib/review-detector/scan-loop.ts
1931
- async function runScanCycle(runtime) {
1932
- const cycleStart = Date.now();
1933
- runtime.snapshot.scanCycles += 1;
1934
- try {
1935
- const currentMtime = rulesFileMtime(runtime.configDir);
1936
- if (currentMtime !== null && currentMtime !== runtime.rulesFileMtimeMs) {
1937
- try {
1938
- runtime.rulesFile = loadRules(runtime.configDir);
1939
- runtime.rulesFileMtimeMs = currentMtime;
1940
- runtime.snapshot.rulesLoaded = runtime.rulesFile.rules.length;
1941
- reviewLog(runtime.configDir, {
1942
- event: "rules-reloaded",
1943
- rulesLoaded: runtime.rulesFile.rules.length,
1944
- mtime: new Date(currentMtime).toISOString()
1945
- });
1946
- } catch (err) {
1947
- reviewLog(runtime.configDir, {
1948
- event: "rules-reload-failed",
1949
- error: err instanceof Error ? err.message : String(err)
1950
- });
1951
- runtime.snapshot.state = "degraded";
1952
- runtime.snapshot.lastError = err instanceof Error ? err.message : String(err);
1953
- }
1954
- }
1955
- const logDir = accountLogDir(runtime.accountDir);
1956
- const files = discoverAllSources(runtime.configDir, logDir);
1957
- runtime.snapshot.sourcesTracked = files.length;
1958
- const linesBySource = /* @__PURE__ */ new Map();
1959
- for (const file of files) {
1960
- const key = sourceKey(file);
1961
- const prev = runtime.tailState[key];
1962
- const result = readNewLines(file.filepath, prev);
1963
- if (result === null) {
1964
- if (prev) {
1965
- reviewLog(runtime.configDir, {
1966
- event: "source-vanished",
1967
- source: file.logicalSource,
1968
- filepath: file.filepath
1969
- });
1970
- delete runtime.tailState[key];
1971
- }
1972
- continue;
1973
- }
1974
- if (result.rotated) {
1975
- reviewLog(runtime.configDir, {
1976
- event: "source-rotated",
1977
- source: file.logicalSource,
1978
- filepath: file.filepath
1979
- });
1980
- }
1981
- if (result.truncated) {
1982
- reviewLog(runtime.configDir, {
1983
- event: "source-truncated",
1984
- source: file.logicalSource,
1985
- filepath: file.filepath
1986
- });
1987
- }
1988
- runtime.tailState[key] = result.entry;
1989
- if (result.lines.length > 0) {
1990
- const bucket = linesBySource.get(file.logicalSource) ?? [];
1991
- bucket.push(...result.lines);
1992
- linesBySource.set(file.logicalSource, bucket);
1993
- }
1994
- }
1995
- const matches = [];
1996
- for (const rule of runtime.rulesFile.rules) {
1997
- let state = runtime.ruleState.get(rule.id) ?? newRuleState();
1998
- if (isSuppressed(rule, cycleStart)) {
1999
- reviewLog(runtime.configDir, {
2000
- event: "rule-suppressed",
2001
- ruleId: rule.id,
2002
- suppressedUntil: rule.suppressedUntil
2003
- });
2004
- runtime.ruleState.set(rule.id, state);
2005
- continue;
2006
- }
2007
- let match = null;
2008
- if (rule.type === "reconnect-loop" || rule.type === "repeated-error" || rule.type === "silent-catch" || rule.type === "rate-limit") {
2009
- let inputLines = [];
2010
- if (rule.logSource === "any") {
2011
- for (const [src, lines] of linesBySource.entries()) {
2012
- if (src !== "config-dir") inputLines.push(...lines);
2013
- }
2014
- } else {
2015
- inputLines = linesBySource.get(rule.logSource) ?? [];
2016
- }
2017
- if (inputLines.length > 0) {
2018
- const result = evaluateTextRule(rule, inputLines, state, cycleStart);
2019
- state = result.state;
2020
- match = result.match;
2021
- }
2022
- } else if (rule.type === "file-write-storm") {
2023
- const dir = resolve4(runtime.configDir, rule.watchPath ?? "");
2024
- const sinceMs = cycleStart - rule.thresholdWindowMinutes * 6e4;
2025
- const count = countRecentWrites(dir, sinceMs);
2026
- const result = evaluateFileWriteStormRule(rule, count, state, cycleStart);
2027
- state = result.state;
2028
- match = result.match;
2029
- } else if (rule.type === "stale-log") {
2030
- const trackedPath = resolve4(runtime.configDir, rule.watchPath ?? "");
2031
- const lastMs = fileLastWriteMs(trackedPath);
2032
- const result = evaluateStaleLogRule(rule, lastMs, state, cycleStart);
2033
- state = result.state;
2034
- match = result.match;
2035
- } else if (rule.type === "absent-followup") {
2036
- let inputLines = [];
2037
- if (rule.logSource === "any") {
2038
- for (const [src, lines] of linesBySource.entries()) {
2039
- if (src !== "config-dir") inputLines.push(...lines);
2040
- }
2041
- } else {
2042
- inputLines = linesBySource.get(rule.logSource) ?? [];
2043
- }
2044
- const result = evaluateAbsentFollowupRule(rule, inputLines, state, cycleStart);
2045
- state = result.state;
2046
- for (const m of result.matches) {
2047
- const safeTrigger = m.sampleEvidence.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
2048
- console.error(
2049
- `[review-detector] absent-followup rule=${rule.id} trigger="${safeTrigger}" missed_for_ms=${m.missedForMs ?? ""}`
2050
- );
2051
- matches.push(m);
2052
- }
2053
- }
2054
- runtime.ruleState.set(rule.id, state);
2055
- if (match) matches.push(match);
2056
- }
2057
- for (const match of matches) {
2058
- const state = runtime.ruleState.get(match.ruleId) ?? newRuleState();
2059
- const decision = rateLimitDecision(state, cycleStart);
2060
- runtime.ruleState.set(match.ruleId, decision.state);
2061
- if (!decision.surface) {
2062
- reviewLog(runtime.configDir, {
2063
- event: "rate-limit-deferred",
2064
- ruleId: match.ruleId,
2065
- cumulativeSinceLastAlert: decision.state.cumulativeSinceLastAlert
2066
- });
2067
- continue;
2068
- }
2069
- reviewLog(runtime.configDir, {
2070
- event: "match",
2071
- ruleId: match.ruleId,
2072
- ruleName: match.ruleName,
2073
- sampleEvidence: match.sampleEvidence,
2074
- suggestedAction: match.suggestedAction
2075
- });
2076
- try {
2077
- await upsertReviewAlert(runtime.accountId, match);
2078
- reviewLog(runtime.configDir, { event: "alert-persisted", ruleId: match.ruleId });
2079
- } catch (err) {
2080
- queueAlert(runtime.configDir, runtime.accountId, match);
2081
- reviewLog(runtime.configDir, {
2082
- event: "alert-queued",
2083
- ruleId: match.ruleId,
2084
- error: err instanceof Error ? err.message : String(err)
2085
- });
2086
- }
2087
- }
2088
- try {
2089
- const drain = await drainPendingAlerts(runtime.configDir);
2090
- if (drain.drained > 0 || drain.remaining > 0) {
2091
- reviewLog(runtime.configDir, {
2092
- event: "queue-drain",
2093
- drained: drain.drained,
2094
- remaining: drain.remaining
2095
- });
2096
- }
2097
- } catch (err) {
2098
- reviewLog(runtime.configDir, {
2099
- event: "queue-drain-failed",
2100
- error: err instanceof Error ? err.message : String(err)
2101
- });
2102
- }
2103
- try {
2104
- runtime.snapshot.activeAlerts = await countActiveReviewAlerts(runtime.accountId);
2105
- } catch (err) {
2106
- reviewLog(runtime.configDir, {
2107
- event: "active-alerts-count-failed",
2108
- error: err instanceof Error ? err.message : String(err)
2109
- });
2110
- }
2111
- saveTailState(runtime.configDir, runtime.tailState);
2112
- const cycleDuration = Date.now() - cycleStart;
2113
- runtime.snapshot.lastScanAt = cycleStart;
2114
- runtime.snapshot.lastScanDurationMs = cycleDuration;
2115
- if (runtime.snapshot.state !== "degraded" && runtime.snapshot.state !== "failed") {
2116
- runtime.snapshot.state = "running";
2117
- }
2118
- runtime.snapshot.lastError = null;
2119
- reviewLog(runtime.configDir, {
2120
- event: "cycle",
2121
- cycles: runtime.snapshot.scanCycles,
2122
- sources: files.length,
2123
- rules: runtime.rulesFile.rules.length,
2124
- matches: matches.length,
2125
- durationMs: cycleDuration
2126
- });
2127
- } catch (err) {
2128
- runtime.snapshot.state = "degraded";
2129
- runtime.snapshot.lastError = err instanceof Error ? err.message : String(err);
2130
- reviewLog(runtime.configDir, {
2131
- event: "cycle-failed",
2132
- error: err instanceof Error ? err.message : String(err)
2133
- });
2134
- }
2135
- }
2136
- function startScanLoop(runtime) {
2137
- let inFlight = null;
2138
- const interval = setInterval(async () => {
2139
- if (runtime.stopped) return;
2140
- if (inFlight) return;
2141
- inFlight = runScanCycle(runtime);
2142
- try {
2143
- await inFlight;
2144
- } finally {
2145
- inFlight = null;
2146
- }
2147
- }, runtime.rulesFile.scanIntervalMs);
2148
- inFlight = runScanCycle(runtime);
2149
- return async () => {
2150
- runtime.stopped = true;
2151
- clearInterval(interval);
2152
- if (inFlight) {
2153
- try {
2154
- await inFlight;
2155
- } catch {
2156
- }
2157
- }
2158
- runtime.snapshot.state = "stopped";
2159
- reviewLog(runtime.configDir, { event: "detector-stopped", cycles: runtime.snapshot.scanCycles });
2160
- };
2161
- }
2162
-
2163
- // app/lib/review-detector/index.ts
2164
- var activeRuntime = null;
2165
- var stopFn = null;
2166
- async function startReviewDetector() {
2167
- if (stopFn) {
2168
- console.error("[review] startReviewDetector called twice \u2014 ignoring second call");
2169
- return;
2170
- }
2171
- try {
2172
- activeRuntime = await bootDetector();
2173
- if (!activeRuntime) return;
2174
- stopFn = startScanLoop(activeRuntime);
2175
- } catch (err) {
2176
- console.error(`[review] detector start failed: ${err instanceof Error ? err.message : String(err)}`);
2177
- }
2178
- }
2179
- async function shutdownReviewDetector() {
2180
- if (!stopFn) return;
2181
- try {
2182
- await stopFn();
2183
- } catch (err) {
2184
- console.error(`[review] detector shutdown error: ${err instanceof Error ? err.message : String(err)}`);
2185
- }
2186
- stopFn = null;
2187
- activeRuntime = null;
2188
- }
2189
- function getReviewDetectorSnapshot() {
2190
- if (!activeRuntime) {
2191
- return {
2192
- state: "stopped",
2193
- startedAt: null,
2194
- lastScanAt: null,
2195
- lastScanDurationMs: null,
2196
- scanCycles: 0,
2197
- rulesLoaded: 0,
2198
- sourcesTracked: 0,
2199
- activeAlerts: 0,
2200
- lastError: null
2201
- };
2202
- }
2203
- return activeRuntime.snapshot;
2204
- }
2205
-
2206
867
  // app/lib/whatsapp/schema.ts
2207
868
  import { z } from "zod";
2208
869
  var DmPolicySchema = z.enum(["open", "allowlist", "disabled"]);
@@ -2267,20 +928,20 @@ var WhatsAppConfigSchema = z.object({
2267
928
  });
2268
929
 
2269
930
  // app/lib/whatsapp/config-persist.ts
2270
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
2271
- import { resolve as resolve5, join as join3 } from "path";
931
+ import { readFileSync, writeFileSync, existsSync as existsSync2 } from "fs";
932
+ import { resolve, join as join2 } from "path";
2272
933
  var TAG = "[whatsapp:config]";
2273
934
  function configPath(accountDir) {
2274
- return resolve5(accountDir, "account.json");
935
+ return resolve(accountDir, "account.json");
2275
936
  }
2276
937
  function readConfig(accountDir) {
2277
938
  const path2 = configPath(accountDir);
2278
- if (!existsSync5(path2)) throw new Error(`account.json not found at ${path2}`);
2279
- return JSON.parse(readFileSync4(path2, "utf-8"));
939
+ if (!existsSync2(path2)) throw new Error(`account.json not found at ${path2}`);
940
+ return JSON.parse(readFileSync(path2, "utf-8"));
2280
941
  }
2281
942
  function writeConfig(accountDir, config) {
2282
943
  const path2 = configPath(accountDir);
2283
- writeFileSync4(path2, JSON.stringify(config, null, 2) + "\n", "utf-8");
944
+ writeFileSync(path2, JSON.stringify(config, null, 2) + "\n", "utf-8");
2284
945
  }
2285
946
  function reloadManagerConfig(accountDir) {
2286
947
  try {
@@ -2450,8 +1111,8 @@ function setPublicAgent(accountDir, slug) {
2450
1111
  if (!trimmed) {
2451
1112
  return { ok: false, error: "Agent slug cannot be empty." };
2452
1113
  }
2453
- const agentConfigPath = join3(accountDir, "agents", trimmed, "config.json");
2454
- if (!existsSync5(agentConfigPath)) {
1114
+ const agentConfigPath = join2(accountDir, "agents", trimmed, "config.json");
1115
+ if (!existsSync2(agentConfigPath)) {
2455
1116
  return { ok: false, error: `Agent "${trimmed}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
2456
1117
  }
2457
1118
  try {
@@ -2514,8 +1175,8 @@ function setGroupPublicAgent(accountDir, accountId, groupJid, slug) {
2514
1175
  if (!trimmedSlug) return { ok: false, error: "Agent slug cannot be empty." };
2515
1176
  if (!trimmedGroup) return { ok: false, error: "Group JID cannot be empty." };
2516
1177
  if (!trimmedAccount) return { ok: false, error: "Account ID cannot be empty." };
2517
- const agentConfigPath = join3(accountDir, "agents", trimmedSlug, "config.json");
2518
- if (!existsSync5(agentConfigPath)) {
1178
+ const agentConfigPath = join2(accountDir, "agents", trimmedSlug, "config.json");
1179
+ if (!existsSync2(agentConfigPath)) {
2519
1180
  return { ok: false, error: `Agent "${trimmedSlug}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
2520
1181
  }
2521
1182
  try {
@@ -2744,7 +1405,7 @@ function listCredentialAccountIds(configDir2) {
2744
1405
  }
2745
1406
 
2746
1407
  // app/lib/whatsapp/session.ts
2747
- import { randomUUID as randomUUID2 } from "crypto";
1408
+ import { randomUUID } from "crypto";
2748
1409
  import fsSync2 from "fs";
2749
1410
  import fs2 from "fs/promises";
2750
1411
  import { inspect } from "util";
@@ -2819,7 +1480,7 @@ var credsSaveQueue = Promise.resolve();
2819
1480
  async function drainCredsSaveQueue(timeoutMs = 5e3) {
2820
1481
  console.error(`${TAG3} draining credential save queue\u2026`);
2821
1482
  const timer2 = new Promise(
2822
- (resolve26) => setTimeout(() => resolve26("timeout"), timeoutMs)
1483
+ (resolve22) => setTimeout(() => resolve22("timeout"), timeoutMs)
2823
1484
  );
2824
1485
  const result = await Promise.race([
2825
1486
  credsSaveQueue.then(() => "drained"),
@@ -2947,11 +1608,11 @@ async function createWaSocket(opts) {
2947
1608
  return sock;
2948
1609
  }
2949
1610
  async function waitForConnection(sock) {
2950
- return new Promise((resolve26, reject) => {
1611
+ return new Promise((resolve22, reject) => {
2951
1612
  const handler = (update) => {
2952
1613
  if (update.connection === "open") {
2953
1614
  sock.ev.off("connection.update", handler);
2954
- resolve26();
1615
+ resolve22();
2955
1616
  }
2956
1617
  if (update.connection === "close") {
2957
1618
  sock.ev.off("connection.update", handler);
@@ -3065,14 +1726,14 @@ ${inspected}`;
3065
1726
  return inspect2(err, INSPECT_OPTS2);
3066
1727
  }
3067
1728
  function withTimeout(label, promise, timeoutMs) {
3068
- return new Promise((resolve26, reject) => {
1729
+ return new Promise((resolve22, reject) => {
3069
1730
  const timer2 = setTimeout(() => {
3070
1731
  reject(new Error(`${label} timed out after ${timeoutMs}ms`));
3071
1732
  }, timeoutMs);
3072
1733
  promise.then(
3073
1734
  (value) => {
3074
1735
  clearTimeout(timer2);
3075
- resolve26(value);
1736
+ resolve22(value);
3076
1737
  },
3077
1738
  (err) => {
3078
1739
  clearTimeout(timer2);
@@ -3607,8 +2268,8 @@ async function persistWhatsAppMessage(input) {
3607
2268
  const { givenName, familyName } = splitName(input.pushName);
3608
2269
  const prev = sessionWriteLocks.get(input.sessionKey);
3609
2270
  let release;
3610
- const mine = new Promise((resolve26) => {
3611
- release = resolve26;
2271
+ const mine = new Promise((resolve22) => {
2272
+ release = resolve22;
3612
2273
  });
3613
2274
  const chained = (prev ?? Promise.resolve()).then(() => mine);
3614
2275
  sessionWriteLocks.set(input.sessionKey, chained);
@@ -3793,8 +2454,8 @@ async function ensureWhatsAppConversation(input) {
3793
2454
  }
3794
2455
 
3795
2456
  // app/lib/whatsapp/platform-account-id.ts
3796
- import { readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
3797
- import { resolve as resolve6 } from "path";
2457
+ import { readdirSync, readFileSync as readFileSync2 } from "fs";
2458
+ import { resolve as resolve2 } from "path";
3798
2459
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3799
2460
  var cached = null;
3800
2461
  var cachedAccountsDir = null;
@@ -3818,7 +2479,7 @@ function resolvePlatformAccountId(accountsDir = ACCOUNTS_DIR) {
3818
2479
  function enumerateValidAccountIds(accountsDir) {
3819
2480
  let names;
3820
2481
  try {
3821
- names = readdirSync2(accountsDir);
2482
+ names = readdirSync(accountsDir);
3822
2483
  } catch (err) {
3823
2484
  if (err.code === "ENOENT") return [];
3824
2485
  throw err;
@@ -3826,9 +2487,9 @@ function enumerateValidAccountIds(accountsDir) {
3826
2487
  const valid = [];
3827
2488
  for (const name of names) {
3828
2489
  if (!UUID_RE.test(name)) continue;
3829
- const configPath2 = resolve6(accountsDir, name, "account.json");
2490
+ const configPath2 = resolve2(accountsDir, name, "account.json");
3830
2491
  try {
3831
- JSON.parse(readFileSync5(configPath2, "utf-8"));
2492
+ JSON.parse(readFileSync2(configPath2, "utf-8"));
3832
2493
  valid.push(name);
3833
2494
  } catch (err) {
3834
2495
  const code = err.code;
@@ -3839,9 +2500,9 @@ function enumerateValidAccountIds(accountsDir) {
3839
2500
  }
3840
2501
 
3841
2502
  // app/lib/whatsapp/inbound/media.ts
3842
- import { randomUUID as randomUUID3 } from "crypto";
2503
+ import { randomUUID as randomUUID2 } from "crypto";
3843
2504
  import { writeFile, mkdir } from "fs/promises";
3844
- import { join as join4 } from "path";
2505
+ import { join as join3 } from "path";
3845
2506
  import {
3846
2507
  downloadMediaMessage,
3847
2508
  downloadContentFromMessage,
@@ -3926,8 +2587,8 @@ async function downloadInboundMedia(msg, sock, opts) {
3926
2587
  }
3927
2588
  await mkdir(MEDIA_DIR, { recursive: true });
3928
2589
  const ext = mimeToExt(mimetype ?? "application/octet-stream");
3929
- const filename = `${randomUUID3()}.${ext}`;
3930
- const filePath = join4(MEDIA_DIR, filename);
2590
+ const filename = `${randomUUID2()}.${ext}`;
2591
+ const filePath = join3(MEDIA_DIR, filename);
3931
2592
  await writeFile(filePath, buffer);
3932
2593
  const sizeKB = (buffer.length / 1024).toFixed(0);
3933
2594
  console.error(`${TAG9} media downloaded type=${mimetype ?? "unknown"} size=${sizeKB}KB path=${filePath}`);
@@ -4615,11 +3276,11 @@ async function connectWithReconnect(conn) {
4615
3276
  console.error(
4616
3277
  `${TAG13} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${decision.nextAttempts}/${maxAttempts})`
4617
3278
  );
4618
- await new Promise((resolve26) => {
4619
- const timer2 = setTimeout(resolve26, delay);
3279
+ await new Promise((resolve22) => {
3280
+ const timer2 = setTimeout(resolve22, delay);
4620
3281
  conn.abortController.signal.addEventListener("abort", () => {
4621
3282
  clearTimeout(timer2);
4622
- resolve26();
3283
+ resolve22();
4623
3284
  }, { once: true });
4624
3285
  });
4625
3286
  }
@@ -4627,16 +3288,16 @@ async function connectWithReconnect(conn) {
4627
3288
  }
4628
3289
  }
4629
3290
  function waitForDisconnectEvent(conn) {
4630
- return new Promise((resolve26) => {
3291
+ return new Promise((resolve22) => {
4631
3292
  if (!conn.sock) {
4632
- resolve26();
3293
+ resolve22();
4633
3294
  return;
4634
3295
  }
4635
3296
  const sock = conn.sock;
4636
3297
  const handler = (update) => {
4637
3298
  if (update.connection === "close") {
4638
3299
  sock.ev.off("connection.update", handler);
4639
- resolve26();
3300
+ resolve22();
4640
3301
  }
4641
3302
  };
4642
3303
  sock.ev.on("connection.update", handler);
@@ -4897,8 +3558,8 @@ async function handleInboundMessage(conn, msg) {
4897
3558
  const conversationKey = isGroup ? remoteJid : senderPhone;
4898
3559
  const debounceKey = `${conn.accountId}:${conversationKey}:${senderPhone}`;
4899
3560
  let resolvePending;
4900
- const sttPending = new Promise((resolve26) => {
4901
- resolvePending = resolve26;
3561
+ const sttPending = new Promise((resolve22) => {
3562
+ resolvePending = resolve22;
4902
3563
  });
4903
3564
  if (conn.debouncer) conn.debouncer.registerPending(debounceKey, sttPending);
4904
3565
  try {
@@ -5019,20 +3680,20 @@ async function probeApiKey() {
5019
3680
  return result.status;
5020
3681
  }
5021
3682
  function checkPort(port2, timeoutMs = 500) {
5022
- return new Promise((resolve26) => {
3683
+ return new Promise((resolve22) => {
5023
3684
  const socket = createConnection(port2, "127.0.0.1");
5024
3685
  socket.setTimeout(timeoutMs);
5025
3686
  socket.once("connect", () => {
5026
3687
  socket.destroy();
5027
- resolve26(true);
3688
+ resolve22(true);
5028
3689
  });
5029
3690
  socket.once("error", () => {
5030
3691
  socket.destroy();
5031
- resolve26(false);
3692
+ resolve22(false);
5032
3693
  });
5033
3694
  socket.once("timeout", () => {
5034
3695
  socket.destroy();
5035
- resolve26(false);
3696
+ resolve22(false);
5036
3697
  });
5037
3698
  });
5038
3699
  }
@@ -5041,8 +3702,8 @@ app.get("/", async (c) => {
5041
3702
  const browserTransport = resolveBrowserTransport(c.req.raw, c.env?.incoming?.socket?.remoteAddress);
5042
3703
  let pinConfigured = false;
5043
3704
  try {
5044
- if (existsSync6(USERS_FILE)) {
5045
- const raw = readFileSync6(USERS_FILE, "utf-8").trim();
3705
+ if (existsSync3(USERS_FILE)) {
3706
+ const raw = readFileSync3(USERS_FILE, "utf-8").trim();
5046
3707
  if (raw) {
5047
3708
  const users = JSON.parse(raw);
5048
3709
  pinConfigured = Array.isArray(users) && users.length > 0;
@@ -5061,7 +3722,7 @@ app.get("/", async (c) => {
5061
3722
  const vncRunning = await checkPort(6080);
5062
3723
  let apiKeyConfigured = false;
5063
3724
  try {
5064
- apiKeyConfigured = existsSync6(keyFilePath());
3725
+ apiKeyConfigured = existsSync3(keyFilePath());
5065
3726
  } catch {
5066
3727
  }
5067
3728
  let apiKeyStatus = "missing";
@@ -5094,7 +3755,6 @@ app.get("/", async (c) => {
5094
3755
  const step = await loadOnboardingStep(account.accountId);
5095
3756
  if (step !== null) onboardingComplete = step >= 6;
5096
3757
  }
5097
- const reviewDetector = getReviewDetectorSnapshot();
5098
3758
  return c.json({
5099
3759
  pin_configured: pinConfigured,
5100
3760
  claude_authenticated: claudeAuthenticated,
@@ -5110,31 +3770,20 @@ app.get("/", async (c) => {
5110
3770
  any_connected: whatsappAnyConnected,
5111
3771
  any_stuck: whatsappAnyStuck,
5112
3772
  accounts: whatsappAccounts
5113
- },
5114
- review_detector: {
5115
- state: reviewDetector.state,
5116
- started_at: reviewDetector.startedAt,
5117
- last_scan_at: reviewDetector.lastScanAt,
5118
- last_scan_duration_ms: reviewDetector.lastScanDurationMs,
5119
- scan_cycles: reviewDetector.scanCycles,
5120
- rules_loaded: reviewDetector.rulesLoaded,
5121
- sources_tracked: reviewDetector.sourcesTracked,
5122
- active_alerts: reviewDetector.activeAlerts,
5123
- last_error: reviewDetector.lastError
5124
3773
  }
5125
3774
  });
5126
3775
  });
5127
3776
  var health_default = app;
5128
3777
 
5129
3778
  // server/routes/session.ts
5130
- import { resolve as resolve7 } from "path";
5131
- import { existsSync as existsSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4 } from "fs";
3779
+ import { resolve as resolve3 } from "path";
3780
+ import { existsSync as existsSync4, writeFileSync as writeFileSync2, mkdirSync } from "fs";
5132
3781
  var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5133
3782
  function writeBrandingCache(accountId, agentSlug, branding) {
5134
3783
  try {
5135
- const cacheDir = resolve7(MAXY_DIR, "branding-cache", accountId);
5136
- mkdirSync4(cacheDir, { recursive: true });
5137
- writeFileSync5(resolve7(cacheDir, `${agentSlug}.json`), JSON.stringify(branding), "utf-8");
3784
+ const cacheDir = resolve3(MAXY_DIR, "branding-cache", accountId);
3785
+ mkdirSync(cacheDir, { recursive: true });
3786
+ writeFileSync2(resolve3(cacheDir, `${agentSlug}.json`), JSON.stringify(branding), "utf-8");
5138
3787
  } catch (err) {
5139
3788
  console.error(`[branding] cache write failed: ${err instanceof Error ? err.message : String(err)}`);
5140
3789
  }
@@ -5204,9 +3853,9 @@ app2.post("/", async (c) => {
5204
3853
  }
5205
3854
  let agentConfig = null;
5206
3855
  if (account) {
5207
- const agentDir = resolve7(account.accountDir, "agents", agentSlug);
5208
- const agentConfigPath = resolve7(agentDir, "config.json");
5209
- if (!existsSync7(agentDir) || !existsSync7(agentConfigPath)) {
3856
+ const agentDir = resolve3(account.accountDir, "agents", agentSlug);
3857
+ const agentConfigPath = resolve3(agentDir, "config.json");
3858
+ if (!existsSync4(agentDir) || !existsSync4(agentConfigPath)) {
5210
3859
  return c.json({ error: "Agent not found" }, 404);
5211
3860
  }
5212
3861
  agentConfig = resolveAgentConfig(account.accountDir, agentSlug);
@@ -5456,12 +4105,12 @@ ${raw}`;
5456
4105
  }
5457
4106
 
5458
4107
  // app/lib/attachments.ts
5459
- import { randomUUID as randomUUID4 } from "crypto";
4108
+ import { randomUUID as randomUUID3 } from "crypto";
5460
4109
  import { mkdir as mkdir2, readFile, stat as stat2, writeFile as writeFile2 } from "fs/promises";
5461
4110
  import { realpathSync } from "fs";
5462
- import { resolve as resolve8, extname, basename as basename3 } from "path";
5463
- var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ?? resolve8(process.cwd(), "../platform");
5464
- var ATTACHMENTS_ROOT = resolve8(PLATFORM_ROOT2, "..", "data/uploads");
4111
+ import { resolve as resolve4, extname, basename } from "path";
4112
+ var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ?? resolve4(process.cwd(), "../platform");
4113
+ var ATTACHMENTS_ROOT = resolve4(PLATFORM_ROOT2, "..", "data/uploads");
5465
4114
  var SUPPORTED_MIME_TYPES = /* @__PURE__ */ new Set([
5466
4115
  "image/jpeg",
5467
4116
  "image/png",
@@ -5487,12 +4136,12 @@ function assertSupportedMime(mimeType) {
5487
4136
  }
5488
4137
  }
5489
4138
  async function writeAttachment(scope, filename, mimeType, sizeBytes, buffer) {
5490
- const attachmentId = randomUUID4();
5491
- const dir = resolve8(ATTACHMENTS_ROOT, scope, attachmentId);
4139
+ const attachmentId = randomUUID3();
4140
+ const dir = resolve4(ATTACHMENTS_ROOT, scope, attachmentId);
5492
4141
  await mkdir2(dir, { recursive: true });
5493
4142
  const ext = extname(filename) || "";
5494
- const storagePath = resolve8(dir, `${attachmentId}${ext}`);
5495
- const metaPath = resolve8(dir, `${attachmentId}.meta.json`);
4143
+ const storagePath = resolve4(dir, `${attachmentId}${ext}`);
4144
+ const metaPath = resolve4(dir, `${attachmentId}.meta.json`);
5496
4145
  const meta = {
5497
4146
  attachmentId,
5498
4147
  scope,
@@ -5566,7 +4215,7 @@ async function storeGeneratedFile(accountId, filePath) {
5566
4215
  `File exceeds the 20 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB).`
5567
4216
  );
5568
4217
  }
5569
- const filename = basename3(filePath);
4218
+ const filename = basename(filePath);
5570
4219
  const mimeType = detectMimeType(filePath);
5571
4220
  const buffer = await readFile(filePath);
5572
4221
  return writeAttachment(accountId, filename, mimeType, fileStat.size, buffer);
@@ -5575,7 +4224,7 @@ async function storeGeneratedFile(accountId, filePath) {
5575
4224
  // app/lib/stt/voice-note.ts
5576
4225
  import { writeFile as writeFile3, mkdtemp, rm } from "fs/promises";
5577
4226
  import { tmpdir } from "os";
5578
- import { join as join5 } from "path";
4227
+ import { join as join4 } from "path";
5579
4228
  var TAG14 = "[voice]";
5580
4229
  var AUDIO_MIME_TYPES = /* @__PURE__ */ new Set([
5581
4230
  "audio/ogg",
@@ -5613,9 +4262,9 @@ async function transcribeVoiceNote(file, source) {
5613
4262
  let tempDir;
5614
4263
  let tempPath;
5615
4264
  try {
5616
- tempDir = await mkdtemp(join5(tmpdir(), "voice-"));
4265
+ tempDir = await mkdtemp(join4(tmpdir(), "voice-"));
5617
4266
  const ext = audioExtension(mimeType);
5618
- tempPath = join5(tempDir, `recording${ext}`);
4267
+ tempPath = join4(tempDir, `recording${ext}`);
5619
4268
  const buffer = Buffer.from(await file.arrayBuffer());
5620
4269
  await writeFile3(tempPath, buffer);
5621
4270
  } catch (err) {
@@ -6170,16 +4819,16 @@ var group_default = app4;
6170
4819
 
6171
4820
  // app/lib/access-gate.ts
6172
4821
  import neo4j from "neo4j-driver";
6173
- import { readFileSync as readFileSync7 } from "fs";
6174
- import { resolve as resolve9 } from "path";
6175
- import { randomUUID as randomUUID5, randomInt } from "crypto";
6176
- var PLATFORM_ROOT3 = process.env.MAXY_PLATFORM_ROOT ?? resolve9(process.cwd(), "..");
4822
+ import { readFileSync as readFileSync4 } from "fs";
4823
+ import { resolve as resolve5 } from "path";
4824
+ import { randomUUID as randomUUID4, randomInt } from "crypto";
4825
+ var PLATFORM_ROOT3 = process.env.MAXY_PLATFORM_ROOT ?? resolve5(process.cwd(), "..");
6177
4826
  var driver = null;
6178
4827
  function readPassword() {
6179
4828
  if (process.env.NEO4J_PASSWORD) return process.env.NEO4J_PASSWORD;
6180
- const passwordFile = resolve9(PLATFORM_ROOT3, "config/.neo4j-password");
4829
+ const passwordFile = resolve5(PLATFORM_ROOT3, "config/.neo4j-password");
6181
4830
  try {
6182
- return readFileSync7(passwordFile, "utf-8").trim();
4831
+ return readFileSync4(passwordFile, "utf-8").trim();
6183
4832
  } catch {
6184
4833
  throw new Error(
6185
4834
  `Neo4j password not found. Expected at ${passwordFile} or in NEO4J_PASSWORD env var.`
@@ -6428,7 +5077,7 @@ async function setGrantPassword(grantId, passwordHash) {
6428
5077
  }
6429
5078
  }
6430
5079
  async function generateNewMagicToken(grantId) {
6431
- const token = randomUUID5();
5080
+ const token = randomUUID4();
6432
5081
  const session = getSession2();
6433
5082
  try {
6434
5083
  await session.run(
@@ -6490,19 +5139,19 @@ async function findActiveGrantByContact(contactValue, agentSlug, accountId) {
6490
5139
  }
6491
5140
 
6492
5141
  // app/lib/brevo-sms.ts
6493
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync8, chmodSync } from "fs";
6494
- import { dirname as dirname4 } from "path";
6495
- import { resolve as resolve10 } from "path";
6496
- var BREVO_API_KEY_FILE = resolve10(MAXY_DIR, ".brevo-api-key");
5142
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync5, chmodSync } from "fs";
5143
+ import { dirname } from "path";
5144
+ import { resolve as resolve6 } from "path";
5145
+ var BREVO_API_KEY_FILE = resolve6(MAXY_DIR, ".brevo-api-key");
6497
5146
  var BREVO_API_URL = "https://api.brevo.com/v3/transactionalSMS/sms";
6498
5147
  var BREVO_TIMEOUT_MS = 1e4;
6499
5148
  var BREVO_SENDER = "Maxy";
6500
5149
  var platformRoot = process.env.MAXY_PLATFORM_ROOT;
6501
5150
  if (platformRoot) {
6502
5151
  try {
6503
- const brandPath = resolve10(platformRoot, "config", "brand.json");
6504
- if (existsSync8(brandPath)) {
6505
- const brand = JSON.parse(readFileSync8(brandPath, "utf-8"));
5152
+ const brandPath = resolve6(platformRoot, "config", "brand.json");
5153
+ if (existsSync5(brandPath)) {
5154
+ const brand = JSON.parse(readFileSync5(brandPath, "utf-8"));
6506
5155
  if (brand.productName) BREVO_SENDER = brand.productName;
6507
5156
  }
6508
5157
  } catch {
@@ -6510,7 +5159,7 @@ if (platformRoot) {
6510
5159
  }
6511
5160
  function readBrevoApiKey() {
6512
5161
  try {
6513
- const key = readFileSync8(BREVO_API_KEY_FILE, "utf-8").trim();
5162
+ const key = readFileSync5(BREVO_API_KEY_FILE, "utf-8").trim();
6514
5163
  if (!key) {
6515
5164
  throw new Error(`Brevo API key file is empty: ${BREVO_API_KEY_FILE}`);
6516
5165
  }
@@ -6525,7 +5174,7 @@ function readBrevoApiKey() {
6525
5174
  }
6526
5175
  }
6527
5176
  function hasBrevoApiKey() {
6528
- return existsSync8(BREVO_API_KEY_FILE);
5177
+ return existsSync5(BREVO_API_KEY_FILE);
6529
5178
  }
6530
5179
  async function sendSms(recipient, content, opts) {
6531
5180
  let apiKey;
@@ -6941,7 +5590,7 @@ app5.post("/send-otp", async (c) => {
6941
5590
  var access_default = app5;
6942
5591
 
6943
5592
  // server/routes/telegram.ts
6944
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
5593
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
6945
5594
  import { timingSafeEqual } from "crypto";
6946
5595
 
6947
5596
  // app/lib/telegram/access-control.ts
@@ -6978,8 +5627,8 @@ var TELEGRAM_API = "https://api.telegram.org";
6978
5627
  function getWebhookSecret(botType) {
6979
5628
  const filePath = botType === "admin" ? TELEGRAM_ADMIN_WEBHOOK_SECRET_FILE : TELEGRAM_WEBHOOK_SECRET_FILE;
6980
5629
  try {
6981
- if (!existsSync9(filePath)) return null;
6982
- const secret = readFileSync9(filePath, "utf-8").trim();
5630
+ if (!existsSync6(filePath)) return null;
5631
+ const secret = readFileSync6(filePath, "utf-8").trim();
6983
5632
  return secret || null;
6984
5633
  } catch {
6985
5634
  return null;
@@ -7137,12 +5786,12 @@ app6.post("/webhook", async (c) => {
7137
5786
  var telegram_default = app6;
7138
5787
 
7139
5788
  // server/routes/whatsapp.ts
7140
- import { join as join6, resolve as resolve11, basename as basename4 } from "path";
5789
+ import { join as join5, resolve as resolve7, basename as basename2 } from "path";
7141
5790
  import { readFile as readFile2, stat as stat3 } from "fs/promises";
7142
- import { realpathSync as realpathSync2, readdirSync as readdirSync3, readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
5791
+ import { realpathSync as realpathSync2, readdirSync as readdirSync2, readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
7143
5792
 
7144
5793
  // app/lib/whatsapp/login.ts
7145
- import { randomUUID as randomUUID6 } from "crypto";
5794
+ import { randomUUID as randomUUID5 } from "crypto";
7146
5795
  var TAG17 = "[whatsapp:login]";
7147
5796
  var ACTIVE_LOGIN_TTL_MS = 3 * 6e4;
7148
5797
  var activeLogins = /* @__PURE__ */ new Map();
@@ -7245,8 +5894,8 @@ async function startLogin(opts) {
7245
5894
  resetActiveLogin(accountId);
7246
5895
  let resolveQr = null;
7247
5896
  let rejectQr = null;
7248
- const qrPromise = new Promise((resolve26, reject) => {
7249
- resolveQr = resolve26;
5897
+ const qrPromise = new Promise((resolve22, reject) => {
5898
+ resolveQr = resolve22;
7250
5899
  rejectQr = reject;
7251
5900
  });
7252
5901
  const qrTimer = setTimeout(
@@ -7281,7 +5930,7 @@ async function startLogin(opts) {
7281
5930
  const login = {
7282
5931
  accountId,
7283
5932
  authDir,
7284
- id: randomUUID6(),
5933
+ id: randomUUID5(),
7285
5934
  sock,
7286
5935
  startedAt: Date.now(),
7287
5936
  connected: false
@@ -7480,7 +6129,7 @@ app7.post("/login/start", async (c) => {
7480
6129
  const body = await c.req.json().catch(() => ({}));
7481
6130
  const accountId = validateAccountId(body.accountId);
7482
6131
  const force = body.force ?? false;
7483
- const authDir = join6(MAXY_DIR, "credentials", "whatsapp", accountId);
6132
+ const authDir = join5(MAXY_DIR, "credentials", "whatsapp", accountId);
7484
6133
  const result = await startLogin({ accountId, authDir, force });
7485
6134
  console.error(`${TAG18} login/start result account=${accountId} hasQr=${!!result.qrRaw}${result.selfPhone ? ` phone=${result.selfPhone}` : ""}`);
7486
6135
  return c.json(result);
@@ -7640,17 +6289,17 @@ app7.post("/config", async (c) => {
7640
6289
  return c.json(result, result.ok ? 200 : 400);
7641
6290
  }
7642
6291
  case "list-public-agents": {
7643
- const agentsDir = resolve11(account.accountDir, "agents");
6292
+ const agentsDir = resolve7(account.accountDir, "agents");
7644
6293
  const agents = [];
7645
- if (existsSync10(agentsDir)) {
6294
+ if (existsSync7(agentsDir)) {
7646
6295
  try {
7647
- const entries = readdirSync3(agentsDir, { withFileTypes: true });
6296
+ const entries = readdirSync2(agentsDir, { withFileTypes: true });
7648
6297
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
7649
6298
  if (!entry.isDirectory() || entry.name === "admin") continue;
7650
- const configPath2 = resolve11(agentsDir, entry.name, "config.json");
7651
- if (!existsSync10(configPath2)) continue;
6299
+ const configPath2 = resolve7(agentsDir, entry.name, "config.json");
6300
+ if (!existsSync7(configPath2)) continue;
7652
6301
  try {
7653
- const config = JSON.parse(readFileSync10(configPath2, "utf-8"));
6302
+ const config = JSON.parse(readFileSync7(configPath2, "utf-8"));
7654
6303
  agents.push({ slug: entry.name, displayName: config.displayName ?? entry.name });
7655
6304
  } catch {
7656
6305
  console.error(`${TAG18} config action=list-public-agents error="failed to parse config.json for agent ${entry.name}" \u2014 skipping`);
@@ -7725,7 +6374,7 @@ app7.post("/send-document", async (c) => {
7725
6374
  if (!maxyAccountId || !PLATFORM_ROOT4) {
7726
6375
  return c.json({ error: "Cannot validate file path: missing account or platform context" }, 400);
7727
6376
  }
7728
- const accountDir = resolve11(PLATFORM_ROOT4, "..", "data/accounts", maxyAccountId);
6377
+ const accountDir = resolve7(PLATFORM_ROOT4, "..", "data/accounts", maxyAccountId);
7729
6378
  let resolvedPath;
7730
6379
  try {
7731
6380
  resolvedPath = realpathSync2(filePath);
@@ -7749,7 +6398,7 @@ app7.post("/send-document", async (c) => {
7749
6398
  return c.json({ error: `File exceeds 20 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB)` }, 400);
7750
6399
  }
7751
6400
  const buffer = Buffer.from(await readFile2(resolvedPath));
7752
- const filename = basename4(resolvedPath);
6401
+ const filename = basename2(resolvedPath);
7753
6402
  const mimetype = detectMimeType(resolvedPath);
7754
6403
  const sock = getSocket(accountId);
7755
6404
  if (!sock) {
@@ -7949,16 +6598,16 @@ var whatsapp_default = app7;
7949
6598
 
7950
6599
  // server/routes/onboarding.ts
7951
6600
  import { spawn, execFileSync } from "child_process";
7952
- import { openSync as openSync2, closeSync as closeSync2, writeFileSync as writeFileSync7, writeSync, existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync11, unlinkSync } from "fs";
7953
- import { resolve as resolve12, dirname as dirname5 } from "path";
7954
- import { createHash, randomUUID as randomUUID7 } from "crypto";
6601
+ import { openSync, closeSync, writeFileSync as writeFileSync4, writeSync, existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync } from "fs";
6602
+ import { resolve as resolve8, dirname as dirname2 } from "path";
6603
+ import { createHash, randomUUID as randomUUID6 } from "crypto";
7955
6604
  var PLATFORM_ROOT5 = process.env.MAXY_PLATFORM_ROOT || "";
7956
6605
  function hashPin(pin) {
7957
6606
  return createHash("sha256").update(pin).digest("hex");
7958
6607
  }
7959
6608
  function readUsersFile() {
7960
- if (!existsSync11(USERS_FILE)) return null;
7961
- const raw = readFileSync11(USERS_FILE, "utf-8").trim();
6609
+ if (!existsSync8(USERS_FILE)) return null;
6610
+ const raw = readFileSync8(USERS_FILE, "utf-8").trim();
7962
6611
  if (!raw) return [];
7963
6612
  return JSON.parse(raw);
7964
6613
  }
@@ -8024,11 +6673,11 @@ app8.post("/claude-auth", async (c) => {
8024
6673
  if (!vncReady) return c.json({ error: "VNC display failed to start" }, 500);
8025
6674
  }
8026
6675
  await ensureCdp(transport);
8027
- writeFileSync7(logPath("claude-auth"), "");
6676
+ writeFileSync4(logPath("claude-auth"), "");
8028
6677
  const chromiumWrapper = writeChromiumWrapper();
8029
6678
  const x11Env = buildX11Env(chromiumWrapper, transport);
8030
6679
  vncLog("claude-auth", { action: "start", transport });
8031
- const claudeAuthLogFd = openSync2(logPath("claude-auth"), "a");
6680
+ const claudeAuthLogFd = openSync(logPath("claude-auth"), "a");
8032
6681
  const claudeProc = spawn("claude", ["auth", "login"], {
8033
6682
  env: x11Env,
8034
6683
  stdio: ["ignore", "pipe", "pipe"]
@@ -8037,7 +6686,7 @@ app8.post("/claude-auth", async (c) => {
8037
6686
  const onClaudeOutput = (chunk) => writeSync(claudeAuthLogFd, chunk);
8038
6687
  claudeProc.stdout?.on("data", onClaudeOutput);
8039
6688
  claudeProc.stderr?.on("data", onClaudeOutput);
8040
- claudeProc.once("close", () => closeSync2(claudeAuthLogFd));
6689
+ claudeProc.once("close", () => closeSync(claudeAuthLogFd));
8041
6690
  await waitForAuthPage(2e4);
8042
6691
  return c.json({ started: true, transport });
8043
6692
  });
@@ -8068,22 +6717,22 @@ app8.post("/set-pin", async (c) => {
8068
6717
  const hash = hashPin(body.pin);
8069
6718
  const account = resolveAccount();
8070
6719
  const existingOwnerUserId = account?.config.admins?.find((a) => a.role === "owner")?.userId;
8071
- const userId = existingOwnerUserId ?? randomUUID7();
6720
+ const userId = existingOwnerUserId ?? randomUUID6();
8072
6721
  if (existingOwnerUserId) {
8073
6722
  console.log(`[set-pin] reusing existing owner userId=${userId.slice(0, 8)}\u2026 (change-PIN preserves identity)`);
8074
6723
  } else {
8075
6724
  console.log(`[set-pin] minted new userId=${userId.slice(0, 8)}\u2026 (first-time install)`);
8076
6725
  }
8077
- mkdirSync6(dirname5(USERS_FILE), { recursive: true });
8078
- writeFileSync7(USERS_FILE, JSON.stringify([{ userId, pin: hash }]), { mode: 384 });
6726
+ mkdirSync3(dirname2(USERS_FILE), { recursive: true });
6727
+ writeFileSync4(USERS_FILE, JSON.stringify([{ userId, pin: hash }]), { mode: 384 });
8079
6728
  console.log(`[set-pin] wrote users.json: userId=${userId.slice(0, 8)}\u2026 hash=${hash.slice(0, 8)}\u2026`);
8080
6729
  if (account) {
8081
6730
  try {
8082
- const config = JSON.parse(readFileSync11(`${account.accountDir}/account.json`, "utf-8"));
6731
+ const config = JSON.parse(readFileSync8(`${account.accountDir}/account.json`, "utf-8"));
8083
6732
  if (!config.admins) config.admins = [];
8084
6733
  if (!config.admins.some((a) => a.userId === userId)) {
8085
6734
  config.admins.push({ userId, role: "owner" });
8086
- writeFileSync7(`${account.accountDir}/account.json`, JSON.stringify(config, null, 2) + "\n");
6735
+ writeFileSync4(`${account.accountDir}/account.json`, JSON.stringify(config, null, 2) + "\n");
8087
6736
  console.log(`[set-pin] added userId=${userId.slice(0, 8)}\u2026 to account.json admins`);
8088
6737
  }
8089
6738
  } catch (err) {
@@ -8136,7 +6785,7 @@ app8.delete("/set-pin", async (c) => {
8136
6785
  unlinkSync(USERS_FILE);
8137
6786
  console.log(`[set-pin] cleared users.json (last entry removed): userId=${matchedUser.userId.slice(0, 8)}\u2026`);
8138
6787
  } else {
8139
- writeFileSync7(USERS_FILE, JSON.stringify(remaining), { mode: 384 });
6788
+ writeFileSync4(USERS_FILE, JSON.stringify(remaining), { mode: 384 });
8140
6789
  console.log(`[set-pin] removed entry from users.json: userId=${matchedUser.userId.slice(0, 8)}\u2026 remaining=${remaining.length}`);
8141
6790
  }
8142
6791
  return c.json({ ok: true });
@@ -8155,19 +6804,19 @@ app8.post("/skip", async (c) => {
8155
6804
  }
8156
6805
  const { accountId, accountDir } = account;
8157
6806
  let agentName = "Maxy";
8158
- const brandPath = PLATFORM_ROOT5 ? resolve12(PLATFORM_ROOT5, "config", "brand.json") : "";
8159
- if (brandPath && existsSync11(brandPath)) {
6807
+ const brandPath = PLATFORM_ROOT5 ? resolve8(PLATFORM_ROOT5, "config", "brand.json") : "";
6808
+ if (brandPath && existsSync8(brandPath)) {
8160
6809
  try {
8161
- const brand = JSON.parse(readFileSync11(brandPath, "utf-8"));
6810
+ const brand = JSON.parse(readFileSync8(brandPath, "utf-8"));
8162
6811
  if (brand.productName) agentName = brand.productName;
8163
6812
  } catch (err) {
8164
6813
  console.error(`[onboarding-skip] brand.json read failed: ${err instanceof Error ? err.message : String(err)}`);
8165
6814
  }
8166
6815
  }
8167
- const soulPath = resolve12(accountDir, "agents", "admin", "SOUL.md");
6816
+ const soulPath = resolve8(accountDir, "agents", "admin", "SOUL.md");
8168
6817
  try {
8169
- mkdirSync6(dirname5(soulPath), { recursive: true });
8170
- writeFileSync7(soulPath, `You are ${agentName}, an AI operations manager.
6818
+ mkdirSync3(dirname2(soulPath), { recursive: true });
6819
+ writeFileSync4(soulPath, `You are ${agentName}, an AI operations manager.
8171
6820
  `);
8172
6821
  console.log(`[onboarding-skip] wrote SOUL.md: ${soulPath}`);
8173
6822
  } catch (err) {
@@ -8211,9 +6860,9 @@ app8.post("/skip", async (c) => {
8211
6860
  var onboarding_default = app8;
8212
6861
 
8213
6862
  // server/routes/client-error.ts
8214
- import { appendFileSync as appendFileSync2, existsSync as existsSync12, renameSync as renameSync4, statSync as statSync5 } from "fs";
8215
- import { join as join7 } from "path";
8216
- var CLIENT_ERRORS_LOG = join7(LOG_DIR, "client-errors.log");
6863
+ import { appendFileSync, existsSync as existsSync9, renameSync, statSync as statSync2 } from "fs";
6864
+ import { join as join6 } from "path";
6865
+ var CLIENT_ERRORS_LOG = join6(LOG_DIR, "client-errors.log");
8217
6866
  var MAX_LOG_SIZE = 10 * 1024 * 1024;
8218
6867
  var MAX_BODY_SIZE = 8 * 1024;
8219
6868
  var MAX_STACK_LEN = 2e3;
@@ -8256,10 +6905,10 @@ function stackHead(stack) {
8256
6905
  }
8257
6906
  function rotateIfNeeded() {
8258
6907
  try {
8259
- if (!existsSync12(CLIENT_ERRORS_LOG)) return;
8260
- const stats = statSync5(CLIENT_ERRORS_LOG);
6908
+ if (!existsSync9(CLIENT_ERRORS_LOG)) return;
6909
+ const stats = statSync2(CLIENT_ERRORS_LOG);
8261
6910
  if (stats.size < MAX_LOG_SIZE) return;
8262
- renameSync4(CLIENT_ERRORS_LOG, CLIENT_ERRORS_LOG + ".1");
6911
+ renameSync(CLIENT_ERRORS_LOG, CLIENT_ERRORS_LOG + ".1");
8263
6912
  } catch (err) {
8264
6913
  console.error(`[client-error] log rotation failed: ${err instanceof Error ? err.message : String(err)}`);
8265
6914
  }
@@ -8360,7 +7009,7 @@ app9.post("/", async (c) => {
8360
7009
  tag: typeof body.tag === "string" ? truncate(body.tag, 32) : void 0,
8361
7010
  status: typeof body.status === "number" ? body.status : void 0
8362
7011
  };
8363
- appendFileSync2(CLIENT_ERRORS_LOG, JSON.stringify(payload) + "\n", "utf-8");
7012
+ appendFileSync(CLIENT_ERRORS_LOG, JSON.stringify(payload) + "\n", "utf-8");
8364
7013
  } catch (err) {
8365
7014
  console.error(`[client-error] append failed: ${err instanceof Error ? err.message : String(err)}`);
8366
7015
  }
@@ -8370,15 +7019,15 @@ app9.post("/", async (c) => {
8370
7019
  var client_error_default = app9;
8371
7020
 
8372
7021
  // server/routes/admin/session.ts
8373
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync13 } from "fs";
7022
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
8374
7023
  import { createHash as createHash2 } from "crypto";
8375
7024
  var deprecationLogged = /* @__PURE__ */ new Set();
8376
7025
  function hashPin2(pin) {
8377
7026
  return createHash2("sha256").update(pin).digest("hex");
8378
7027
  }
8379
7028
  function readUsersFile2() {
8380
- if (!existsSync13(USERS_FILE)) return null;
8381
- const raw = readFileSync12(USERS_FILE, "utf-8").trim();
7029
+ if (!existsSync10(USERS_FILE)) return null;
7030
+ const raw = readFileSync9(USERS_FILE, "utf-8").trim();
8382
7031
  if (!raw) return [];
8383
7032
  return JSON.parse(raw);
8384
7033
  }
@@ -8395,7 +7044,7 @@ function stripLegacyNameField(users) {
8395
7044
  }
8396
7045
  }
8397
7046
  try {
8398
- writeFileSync8(USERS_FILE, JSON.stringify(users), { mode: 384 });
7047
+ writeFileSync5(USERS_FILE, JSON.stringify(users), { mode: 384 });
8399
7048
  } catch (err) {
8400
7049
  console.error(`[admin-identity] users-json strip failed: ${err instanceof Error ? err.message : String(err)}`);
8401
7050
  }
@@ -8552,13 +7201,13 @@ app10.post("/", async (c) => {
8552
7201
  var session_default2 = app10;
8553
7202
 
8554
7203
  // server/routes/admin/chat.ts
8555
- import { resolve as resolve13 } from "path";
8556
- import { appendFileSync as appendFileSync4 } from "fs";
7204
+ import { resolve as resolve9 } from "path";
7205
+ import { appendFileSync as appendFileSync3 } from "fs";
8557
7206
 
8558
7207
  // app/lib/script-stream-tailer.ts
8559
7208
  import * as childProcess from "child_process";
8560
- import { appendFileSync as appendFileSync3, createReadStream as createReadStream2, mkdirSync as mkdirSync7, statSync as statSync6 } from "fs";
8561
- import { dirname as dirname6 } from "path";
7209
+ import { appendFileSync as appendFileSync2, createReadStream as createReadStream2, mkdirSync as mkdirSync4, statSync as statSync3 } from "fs";
7210
+ import { dirname as dirname3 } from "path";
8562
7211
  import { StringDecoder } from "string_decoder";
8563
7212
  var SCRIPT_STREAM_RE = /^\[([^\]]+)\] \[script:([a-z][a-z0-9-]*)((?::[a-z0-9:_-]+)?)\] (.*)$/;
8564
7213
  function parseLine(line) {
@@ -8576,7 +7225,7 @@ function startScriptStreamTailer(opts) {
8576
7225
  const { path: path2, onEvent, onError } = opts;
8577
7226
  let offset;
8578
7227
  try {
8579
- offset = statSync6(path2).size;
7228
+ offset = statSync3(path2).size;
8580
7229
  } catch {
8581
7230
  offset = 0;
8582
7231
  }
@@ -8595,7 +7244,7 @@ function startScriptStreamTailer(opts) {
8595
7244
  try {
8596
7245
  let size;
8597
7246
  try {
8598
- size = statSync6(path2).size;
7247
+ size = statSync3(path2).size;
8599
7248
  } catch {
8600
7249
  return;
8601
7250
  }
@@ -8667,8 +7316,8 @@ function writeRouteMilestone(streamLogPath, scope, line) {
8667
7316
  }
8668
7317
  const ts = (/* @__PURE__ */ new Date()).toISOString();
8669
7318
  try {
8670
- mkdirSync7(dirname6(streamLogPath), { recursive: true });
8671
- appendFileSync3(streamLogPath, `[${ts}] [script:${scope}] ${line}
7319
+ mkdirSync4(dirname3(streamLogPath), { recursive: true });
7320
+ appendFileSync2(streamLogPath, `[${ts}] [script:${scope}] ${line}
8672
7321
  `);
8673
7322
  } catch (err) {
8674
7323
  console.error(
@@ -8843,7 +7492,7 @@ var app11 = new Hono();
8843
7492
  app11.post("/cancel", requireAdminSession, async (c) => {
8844
7493
  const session_key = c.var.sessionKey;
8845
7494
  try {
8846
- const { interruptClient: interruptClient2 } = await import("./client-pool-BMPFHXHB.js");
7495
+ const { interruptClient: interruptClient2 } = await import("./client-pool-GBY5I2KQ.js");
8847
7496
  await interruptClient2(session_key);
8848
7497
  return c.json({ ok: true });
8849
7498
  } catch (err) {
@@ -9007,10 +7656,10 @@ app11.post("/", requireAdminSession, async (c) => {
9007
7656
  function resolveTeeStreamLogPath() {
9008
7657
  const liveConvId = getConversationIdForSession(session_key);
9009
7658
  const key = liveConvId ?? preflushStreamLogKey(session_key);
9010
- return resolve13(account.accountDir, "logs", `claude-agent-stream-${key}.log`);
7659
+ return resolve9(account.accountDir, "logs", `claude-agent-stream-${key}.log`);
9011
7660
  }
9012
7661
  try {
9013
- appendFileSync4(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [chat-route-version=task606-tee-path-resolve] sessionKey=${session_key.slice(0, 12)}\u2026
7662
+ appendFileSync3(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [chat-route-version=task606-tee-path-resolve] sessionKey=${session_key.slice(0, 12)}\u2026
9014
7663
  `);
9015
7664
  } catch {
9016
7665
  }
@@ -9019,7 +7668,7 @@ app11.post("/", requireAdminSession, async (c) => {
9019
7668
  incoming.on("close", () => {
9020
7669
  const tsClose = (/* @__PURE__ */ new Date()).toISOString();
9021
7670
  try {
9022
- appendFileSync4(resolveTeeStreamLogPath(), `[${tsClose}] [incoming-close] sessionKey=${session_key.slice(0, 12)}\u2026 complete=${incoming.complete}
7671
+ appendFileSync3(resolveTeeStreamLogPath(), `[${tsClose}] [incoming-close] sessionKey=${session_key.slice(0, 12)}\u2026 complete=${incoming.complete}
9023
7672
  `);
9024
7673
  } catch {
9025
7674
  }
@@ -9027,7 +7676,7 @@ app11.post("/", requireAdminSession, async (c) => {
9027
7676
  console.error(`[${tsClose}] [incoming-close] DISCONNECT sessionKey=${session_key.slice(0, 12)}\u2026`);
9028
7677
  interruptClient(session_key).catch((err) => {
9029
7678
  try {
9030
- appendFileSync4(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [incoming-close] interrupt-failed: ${err instanceof Error ? err.message : String(err)}
7679
+ appendFileSync3(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [incoming-close] interrupt-failed: ${err instanceof Error ? err.message : String(err)}
9031
7680
  `);
9032
7681
  } catch {
9033
7682
  }
@@ -9036,7 +7685,7 @@ app11.post("/", requireAdminSession, async (c) => {
9036
7685
  });
9037
7686
  } else {
9038
7687
  try {
9039
- appendFileSync4(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [incoming-close] UNAVAILABLE \u2014 c.env.incoming is not an EventEmitter
7688
+ appendFileSync3(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [incoming-close] UNAVAILABLE \u2014 c.env.incoming is not an EventEmitter
9040
7689
  `);
9041
7690
  } catch {
9042
7691
  }
@@ -9048,7 +7697,7 @@ app11.post("/", requireAdminSession, async (c) => {
9048
7697
  } catch {
9049
7698
  }
9050
7699
  try {
9051
- appendFileSync4(resolveTeeStreamLogPath(), line);
7700
+ appendFileSync3(resolveTeeStreamLogPath(), line);
9052
7701
  } catch {
9053
7702
  }
9054
7703
  return true;
@@ -9109,7 +7758,7 @@ app11.post("/", requireAdminSession, async (c) => {
9109
7758
  try {
9110
7759
  registerAdminSSE(sseEntry);
9111
7760
  if (sseConvId) {
9112
- const streamLogPath = resolve13(account.accountDir, "logs", `claude-agent-stream-${sseConvId}.log`);
7761
+ const streamLogPath = resolve9(account.accountDir, "logs", `claude-agent-stream-${sseConvId}.log`);
9113
7762
  tailer = startScriptStreamTailer({
9114
7763
  path: streamLogPath,
9115
7764
  onEvent: (event) => {
@@ -9238,22 +7887,22 @@ app12.post("/", requireAdminSession, async (c) => {
9238
7887
  var compact_default = app12;
9239
7888
 
9240
7889
  // server/routes/admin/logs.ts
9241
- import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as readFileSync13, statSync as statSync7 } from "fs";
9242
- import { resolve as resolve14, basename as basename5 } from "path";
7890
+ import { existsSync as existsSync12, readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync4 } from "fs";
7891
+ import { resolve as resolve10, basename as basename3 } from "path";
9243
7892
 
9244
7893
  // app/lib/logs-read-resolve.ts
9245
- import { existsSync as existsSync14 } from "fs";
9246
- import { join as join8 } from "path";
7894
+ import { existsSync as existsSync11 } from "fs";
7895
+ import { join as join7 } from "path";
9247
7896
  function resolveConversationLogPaths(fullFilename, preflushFilename, logDirs) {
9248
7897
  const tried = [fullFilename, preflushFilename];
9249
7898
  const hits = [];
9250
7899
  const stalePreflushPaths = [];
9251
7900
  for (const dir of logDirs) {
9252
- const fullPath = join8(dir, fullFilename);
9253
- if (existsSync14(fullPath)) {
7901
+ const fullPath = join7(dir, fullFilename);
7902
+ if (existsSync11(fullPath)) {
9254
7903
  hits.push({ path: fullPath, shape: "full", dir });
9255
- const preflushSibling = join8(dir, preflushFilename);
9256
- if (existsSync14(preflushSibling)) {
7904
+ const preflushSibling = join7(dir, preflushFilename);
7905
+ if (existsSync11(preflushSibling)) {
9257
7906
  stalePreflushPaths.push(preflushSibling);
9258
7907
  }
9259
7908
  }
@@ -9262,8 +7911,8 @@ function resolveConversationLogPaths(fullFilename, preflushFilename, logDirs) {
9262
7911
  return { hits, stalePreflushPaths, tried };
9263
7912
  }
9264
7913
  for (const dir of logDirs) {
9265
- const preflushPath = join8(dir, preflushFilename);
9266
- if (existsSync14(preflushPath)) {
7914
+ const preflushPath = join7(dir, preflushFilename);
7915
+ if (existsSync11(preflushPath)) {
9267
7916
  hits.push({ path: preflushPath, shape: "preflush", dir });
9268
7917
  }
9269
7918
  }
@@ -9283,19 +7932,19 @@ app13.get("/", async (c) => {
9283
7932
  const sessionKeyParam = c.req.query("sessionKey");
9284
7933
  const download = c.req.query("download") === "1";
9285
7934
  const account = resolveAccount();
9286
- const accountLogDir2 = account ? resolve14(account.accountDir, "logs") : null;
7935
+ const accountLogDir = account ? resolve10(account.accountDir, "logs") : null;
9287
7936
  const logDirs = [];
9288
- if (accountLogDir2) logDirs.push(accountLogDir2);
7937
+ if (accountLogDir) logDirs.push(accountLogDir);
9289
7938
  logDirs.push(LOG_DIR);
9290
7939
  if (fileParam) {
9291
- const safe = basename5(fileParam);
7940
+ const safe = basename3(fileParam);
9292
7941
  const searched = [];
9293
7942
  for (const dir of logDirs) {
9294
- const filePath = resolve14(dir, safe);
7943
+ const filePath = resolve10(dir, safe);
9295
7944
  searched.push(filePath);
9296
7945
  try {
9297
- const buffer = readFileSync13(filePath);
9298
- const onDiskBytes = statSync7(filePath).size;
7946
+ const buffer = readFileSync10(filePath);
7947
+ const onDiskBytes = statSync4(filePath).size;
9299
7948
  const headers = {
9300
7949
  "Content-Type": "text/plain; charset=utf-8",
9301
7950
  "Content-Length": String(buffer.byteLength)
@@ -9367,9 +8016,9 @@ app13.get("/", async (c) => {
9367
8016
  const hit = hits[0];
9368
8017
  console.info(`[admin/logs] resolved sessionKey=${sessionKeySlice} conversationId=${conversationIdSlice} shape=${hit.shape} stalePreflushCount=${stalePreflushCount}`);
9369
8018
  try {
9370
- const filename = basename5(hit.path);
8019
+ const filename = basename3(hit.path);
9371
8020
  if (stalePreflushCount > 0 && !download) {
9372
- const content = readFileSync13(hit.path, "utf-8");
8021
+ const content = readFileSync10(hit.path, "utf-8");
9373
8022
  return c.json({
9374
8023
  log: content,
9375
8024
  filename,
@@ -9377,8 +8026,8 @@ app13.get("/", async (c) => {
9377
8026
  warnings: stalePreflushPaths.map((path2) => ({ kind: "stale-preflush", path: path2 }))
9378
8027
  });
9379
8028
  }
9380
- const buffer = readFileSync13(hit.path);
9381
- const onDiskBytes = statSync7(hit.path).size;
8029
+ const buffer = readFileSync10(hit.path);
8030
+ const onDiskBytes = statSync4(hit.path).size;
9382
8031
  const headers = {
9383
8032
  "Content-Type": "text/plain; charset=utf-8",
9384
8033
  "Content-Length": String(buffer.byteLength)
@@ -9411,19 +8060,19 @@ app13.get("/", async (c) => {
9411
8060
  const seen = /* @__PURE__ */ new Set();
9412
8061
  const logs = {};
9413
8062
  for (const dir of logDirs) {
9414
- if (!existsSync15(dir)) continue;
8063
+ if (!existsSync12(dir)) continue;
9415
8064
  let files;
9416
8065
  try {
9417
- files = readdirSync4(dir).filter((f) => f.endsWith(".log"));
8066
+ files = readdirSync3(dir).filter((f) => f.endsWith(".log"));
9418
8067
  } catch (err) {
9419
8068
  const reason = err instanceof Error ? err.message : String(err);
9420
8069
  console.warn(`[admin/logs] readdir-fail dir=${dir} reason=${reason}`);
9421
8070
  continue;
9422
8071
  }
9423
- files.filter((f) => !seen.has(f)).map((f) => ({ name: f, mtime: statSync7(resolve14(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime).forEach(({ name }) => {
8072
+ files.filter((f) => !seen.has(f)).map((f) => ({ name: f, mtime: statSync4(resolve10(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime).forEach(({ name }) => {
9424
8073
  seen.add(name);
9425
8074
  try {
9426
- const content = readFileSync13(resolve14(dir, name));
8075
+ const content = readFileSync10(resolve10(dir, name));
9427
8076
  const tail = content.length > TAIL_BYTES ? content.subarray(content.length - TAIL_BYTES).toString("utf-8") : content.toString("utf-8");
9428
8077
  logs[name] = tail.trim() || "(empty)";
9429
8078
  } catch (err) {
@@ -9462,8 +8111,8 @@ var claude_info_default = app14;
9462
8111
 
9463
8112
  // server/routes/admin/attachment.ts
9464
8113
  import { readFile as readFile3, readdir } from "fs/promises";
9465
- import { existsSync as existsSync16 } from "fs";
9466
- import { resolve as resolve15 } from "path";
8114
+ import { existsSync as existsSync13 } from "fs";
8115
+ import { resolve as resolve11 } from "path";
9467
8116
  var app15 = new Hono();
9468
8117
  app15.get("/:attachmentId", requireAdminSession, async (c) => {
9469
8118
  const attachmentId = c.req.param("attachmentId");
@@ -9475,12 +8124,12 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
9475
8124
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(attachmentId)) {
9476
8125
  return new Response("Not found", { status: 404 });
9477
8126
  }
9478
- const dir = resolve15(ATTACHMENTS_ROOT, accountId, attachmentId);
9479
- if (!existsSync16(dir)) {
8127
+ const dir = resolve11(ATTACHMENTS_ROOT, accountId, attachmentId);
8128
+ if (!existsSync13(dir)) {
9480
8129
  return new Response("Not found", { status: 404 });
9481
8130
  }
9482
- const metaPath = resolve15(dir, `${attachmentId}.meta.json`);
9483
- if (!existsSync16(metaPath)) {
8131
+ const metaPath = resolve11(dir, `${attachmentId}.meta.json`);
8132
+ if (!existsSync13(metaPath)) {
9484
8133
  return new Response("Not found", { status: 404 });
9485
8134
  }
9486
8135
  let meta;
@@ -9494,7 +8143,7 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
9494
8143
  if (!dataFile) {
9495
8144
  return new Response("Not found", { status: 404 });
9496
8145
  }
9497
- const filePath = resolve15(dir, dataFile);
8146
+ const filePath = resolve11(dir, dataFile);
9498
8147
  const buffer = await readFile3(filePath);
9499
8148
  return new Response(new Uint8Array(buffer), {
9500
8149
  headers: {
@@ -9507,24 +8156,24 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
9507
8156
  var attachment_default = app15;
9508
8157
 
9509
8158
  // server/routes/admin/agents.ts
9510
- import { resolve as resolve16 } from "path";
9511
- import { readdirSync as readdirSync5, readFileSync as readFileSync14, existsSync as existsSync17, rmSync } from "fs";
8159
+ import { resolve as resolve12 } from "path";
8160
+ import { readdirSync as readdirSync4, readFileSync as readFileSync11, existsSync as existsSync14, rmSync } from "fs";
9512
8161
  var app16 = new Hono();
9513
8162
  app16.get("/", (c) => {
9514
8163
  const account = resolveAccount();
9515
8164
  if (!account) return c.json({ agents: [] });
9516
- const agentsDir = resolve16(account.accountDir, "agents");
9517
- if (!existsSync17(agentsDir)) return c.json({ agents: [] });
8165
+ const agentsDir = resolve12(account.accountDir, "agents");
8166
+ if (!existsSync14(agentsDir)) return c.json({ agents: [] });
9518
8167
  const agents = [];
9519
8168
  try {
9520
- const entries = readdirSync5(agentsDir, { withFileTypes: true });
8169
+ const entries = readdirSync4(agentsDir, { withFileTypes: true });
9521
8170
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
9522
8171
  if (!entry.isDirectory()) continue;
9523
8172
  if (entry.name === "admin") continue;
9524
- const configPath2 = resolve16(agentsDir, entry.name, "config.json");
9525
- if (!existsSync17(configPath2)) continue;
8173
+ const configPath2 = resolve12(agentsDir, entry.name, "config.json");
8174
+ if (!existsSync14(configPath2)) continue;
9526
8175
  try {
9527
- const config = JSON.parse(readFileSync14(configPath2, "utf-8"));
8176
+ const config = JSON.parse(readFileSync11(configPath2, "utf-8"));
9528
8177
  agents.push({
9529
8178
  slug: entry.name,
9530
8179
  displayName: config.displayName ?? entry.name,
@@ -9550,8 +8199,8 @@ app16.delete("/:slug", async (c) => {
9550
8199
  if (slug.includes("/") || slug.includes("..") || slug.includes("\\")) {
9551
8200
  return c.json({ error: "Invalid agent slug" }, 400);
9552
8201
  }
9553
- const agentDir = resolve16(account.accountDir, "agents", slug);
9554
- if (!existsSync17(agentDir)) {
8202
+ const agentDir = resolve12(account.accountDir, "agents", slug);
8203
+ if (!existsSync14(agentDir)) {
9555
8204
  return c.json({ error: "Agent not found" }, 404);
9556
8205
  }
9557
8206
  try {
@@ -9580,8 +8229,8 @@ app16.post("/:slug/project", async (c) => {
9580
8229
  if (slug.includes("/") || slug.includes("..") || slug.includes("\\")) {
9581
8230
  return c.json({ error: "Invalid agent slug" }, 400);
9582
8231
  }
9583
- const agentDir = resolve16(account.accountDir, "agents", slug);
9584
- if (!existsSync17(agentDir)) {
8232
+ const agentDir = resolve12(account.accountDir, "agents", slug);
8233
+ if (!existsSync14(agentDir)) {
9585
8234
  return c.json({ error: "Agent not found on disk" }, 404);
9586
8235
  }
9587
8236
  try {
@@ -9597,7 +8246,7 @@ var agents_default = app16;
9597
8246
  // server/routes/admin/sessions.ts
9598
8247
  import crypto2 from "crypto";
9599
8248
  import { resolve as resolvePath } from "path";
9600
- import { appendFileSync as appendFileSync5, existsSync as existsSync18 } from "fs";
8249
+ import { appendFileSync as appendFileSync4, existsSync as existsSync15 } from "fs";
9601
8250
  function validateAndShapeAttachments(raws, conversationAccountId, conversationId, messageId, streamLogPath) {
9602
8251
  const chips = [];
9603
8252
  let valid = 0;
@@ -9606,11 +8255,11 @@ function validateAndShapeAttachments(raws, conversationAccountId, conversationId
9606
8255
  let reason = null;
9607
8256
  if (!a.attachmentId || !a.filename || !a.mimeType || !a.storagePath) reason = "schema-fail";
9608
8257
  else if (a.accountId !== conversationAccountId) reason = "account-mismatch";
9609
- else if (!existsSync18(a.storagePath)) reason = "missing-file";
8258
+ else if (!existsSync15(a.storagePath)) reason = "missing-file";
9610
8259
  if (reason) {
9611
8260
  invalid++;
9612
8261
  try {
9613
- appendFileSync5(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [attachment-rehydrate-invalid] conversationId=${conversationId.slice(0, 8)} messageId=${messageId.slice(0, 8)} attachmentId=${(a.attachmentId || "").slice(0, 8)} reason=${reason}
8262
+ appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [attachment-rehydrate-invalid] conversationId=${conversationId.slice(0, 8)} messageId=${messageId.slice(0, 8)} attachmentId=${(a.attachmentId || "").slice(0, 8)} reason=${reason}
9614
8263
  `);
9615
8264
  } catch {
9616
8265
  }
@@ -9673,7 +8322,7 @@ function reconstructAssistantEvents(content, components, conversationId, message
9673
8322
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [component-rehydrate-invalid] conversationId=${conversationId.slice(0, 8)} messageId=${messageId.slice(0, 8)} name=${c.name || "<unnamed>"} reason=${invalidReason}
9674
8323
  `;
9675
8324
  try {
9676
- appendFileSync5(streamLogPath, line);
8325
+ appendFileSync4(streamLogPath, line);
9677
8326
  } catch {
9678
8327
  }
9679
8328
  continue;
@@ -9876,14 +8525,14 @@ app17.post("/:id/resume", async (c) => {
9876
8525
  const userMessageCount = rehydrated.filter((m) => m.role !== "assistant").length;
9877
8526
  const reason = bridged ? "post-restart" : "page-refresh";
9878
8527
  try {
9879
- appendFileSync5(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [admin-resume] reason=${reason} sessionKey=${sessionKey.slice(0, 8)} conversationId=${conversationId.slice(0, 8)} ${tag} loadedMessages=${messages.length} componentCount=${totalComponents} userAttachmentCount=${totalAttachments}
8528
+ appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [admin-resume] reason=${reason} sessionKey=${sessionKey.slice(0, 8)} conversationId=${conversationId.slice(0, 8)} ${tag} loadedMessages=${messages.length} componentCount=${totalComponents} userAttachmentCount=${totalAttachments}
9880
8529
  `);
9881
8530
  if (totalComponents > 0) {
9882
- appendFileSync5(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [component-rehydrate] conversationId=${conversationId.slice(0, 8)} count=${totalComponents} valid=${totalValid} invalid=${totalInvalid} textRuns=${textRuns}
8531
+ appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [component-rehydrate] conversationId=${conversationId.slice(0, 8)} count=${totalComponents} valid=${totalValid} invalid=${totalInvalid} textRuns=${textRuns}
9883
8532
  `);
9884
8533
  }
9885
8534
  if (totalAttachments > 0 || totalAttachmentInvalid > 0) {
9886
- appendFileSync5(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [attachment-rehydrate] conversationId=${conversationId.slice(0, 8)} userMessages=${userMessageCount} attachments=${totalAttachments} invalid=${totalAttachmentInvalid}
8535
+ appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [attachment-rehydrate] conversationId=${conversationId.slice(0, 8)} userMessages=${userMessageCount} attachments=${totalAttachments} invalid=${totalAttachmentInvalid}
9887
8536
  `);
9888
8537
  }
9889
8538
  } catch {
@@ -10147,8 +8796,8 @@ var events_default = app20;
10147
8796
 
10148
8797
  // server/routes/admin/cloudflare.ts
10149
8798
  import { homedir } from "os";
10150
- import { resolve as resolve18 } from "path";
10151
- import { readFileSync as readFileSync16 } from "fs";
8799
+ import { resolve as resolve14 } from "path";
8800
+ import { readFileSync as readFileSync14 } from "fs";
10152
8801
 
10153
8802
  // app/lib/dns-label.ts
10154
8803
  var VALID_LABEL = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -10164,14 +8813,14 @@ function isValidDomain(value) {
10164
8813
  }
10165
8814
 
10166
8815
  // app/lib/alias-domains.ts
10167
- import { existsSync as existsSync19, mkdirSync as mkdirSync8, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
10168
- import { dirname as dirname7 } from "path";
10169
- import { resolve as resolve17 } from "path";
10170
- var ALIAS_DOMAINS_PATH = resolve17(MAXY_DIR, "alias-domains.json");
8816
+ import { existsSync as existsSync16, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
8817
+ import { dirname as dirname4 } from "path";
8818
+ import { resolve as resolve13 } from "path";
8819
+ var ALIAS_DOMAINS_PATH = resolve13(MAXY_DIR, "alias-domains.json");
10171
8820
  function readExisting() {
10172
- if (!existsSync19(ALIAS_DOMAINS_PATH)) return /* @__PURE__ */ new Set();
8821
+ if (!existsSync16(ALIAS_DOMAINS_PATH)) return /* @__PURE__ */ new Set();
10173
8822
  try {
10174
- const parsed = JSON.parse(readFileSync15(ALIAS_DOMAINS_PATH, "utf-8"));
8823
+ const parsed = JSON.parse(readFileSync12(ALIAS_DOMAINS_PATH, "utf-8"));
10175
8824
  if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
10176
8825
  return new Set(parsed.filter((h) => typeof h === "string"));
10177
8826
  } catch {
@@ -10182,18 +8831,226 @@ function addAliasDomain(hostname2) {
10182
8831
  const existing = readExisting();
10183
8832
  if (existing.has(hostname2)) return;
10184
8833
  existing.add(hostname2);
10185
- mkdirSync8(dirname7(ALIAS_DOMAINS_PATH), { recursive: true });
10186
- writeFileSync9(ALIAS_DOMAINS_PATH, JSON.stringify([...existing], null, 2) + "\n", "utf-8");
8834
+ mkdirSync5(dirname4(ALIAS_DOMAINS_PATH), { recursive: true });
8835
+ writeFileSync6(ALIAS_DOMAINS_PATH, JSON.stringify([...existing], null, 2) + "\n", "utf-8");
8836
+ }
8837
+
8838
+ // app/lib/cloudflare-task-tracker.ts
8839
+ import { readFileSync as readFileSync13, existsSync as existsSync17 } from "fs";
8840
+ import { randomUUID as randomUUID7 } from "crypto";
8841
+ var import_dist = __toESM(require_dist(), 1);
8842
+ var CREATED_BY_AGENT = "cloudflare-setup-endpoint";
8843
+ var TASK_KIND = "cloudflare-tunnel-login";
8844
+ async function openCloudflareTask(params) {
8845
+ const { accountId, conversationKey, inputsProvided } = params;
8846
+ const taskId = randomUUID7();
8847
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8848
+ const session = getSession();
8849
+ try {
8850
+ const conv = await session.run(
8851
+ `MATCH (c:Conversation {sessionKey: $conversationKey, accountId: $accountId}) RETURN elementId(c) AS id LIMIT 1`,
8852
+ { conversationKey, accountId }
8853
+ );
8854
+ if (conv.records.length === 0) {
8855
+ throw new Error(
8856
+ `cloudflare-task-tracker: no Conversation with sessionKey=${conversationKey.slice(-8)} for accountId \u2014 refusing to create an orphan Task`
8857
+ );
8858
+ }
8859
+ const conversationElementId = conv.records[0].get("id");
8860
+ const props = {
8861
+ taskId,
8862
+ accountId,
8863
+ name: "Cloudflare tunnel login + setup",
8864
+ description: `Deterministic cloudflare setup invoked by /api/admin/cloudflare/setup. inputsProvided=[${inputsProvided.join(", ")}]`,
8865
+ status: "running",
8866
+ priority: "normal",
8867
+ kind: TASK_KIND,
8868
+ inputsProvided,
8869
+ startedAt: now,
8870
+ createdAt: now,
8871
+ updatedAt: now
8872
+ };
8873
+ const relationships = [
8874
+ {
8875
+ type: "RAISED_DURING",
8876
+ direction: "outgoing",
8877
+ targetNodeId: conversationElementId
8878
+ }
8879
+ ];
8880
+ const result = await (0, import_dist.writeNodeWithEdges)({
8881
+ session,
8882
+ labels: ["Task"],
8883
+ props,
8884
+ relationships,
8885
+ createdBy: {
8886
+ agent: CREATED_BY_AGENT,
8887
+ session: conversationKey,
8888
+ tool: "cloudflare-setup-endpoint"
8889
+ }
8890
+ });
8891
+ process.stderr.write(
8892
+ `[task] action-start kind=${TASK_KIND} taskId=${taskId} raisedDuring=${conversationKey.slice(-8)}
8893
+ `
8894
+ );
8895
+ return { taskId, taskElementId: result.nodeId };
8896
+ } finally {
8897
+ await session.close();
8898
+ }
8899
+ }
8900
+ async function appendCloudflareSteps(taskId, accountId, streamLogPath) {
8901
+ if (!existsSync17(streamLogPath)) return [];
8902
+ let content;
8903
+ try {
8904
+ content = readFileSync13(streamLogPath, "utf-8");
8905
+ } catch {
8906
+ return [];
8907
+ }
8908
+ const steps = [];
8909
+ for (const line of content.split(/\r?\n/)) {
8910
+ const m = line.match(/\bphase_line\s+setup-tunnel\s+step=(\S+)/);
8911
+ if (m) steps.push(m[1]);
8912
+ }
8913
+ if (steps.length === 0) return [];
8914
+ const session = getSession();
8915
+ try {
8916
+ await session.run(
8917
+ `MATCH (t:Task {taskId: $taskId, accountId: $accountId})
8918
+ SET t.steps = coalesce(t.steps, []) + $steps,
8919
+ t.updatedAt = $updatedAt`,
8920
+ { taskId, accountId, steps, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
8921
+ );
8922
+ for (const step of steps) {
8923
+ process.stderr.write(
8924
+ `[task] action-step kind=${TASK_KIND} taskId=${taskId} step=${step}
8925
+ `
8926
+ );
8927
+ }
8928
+ return steps;
8929
+ } finally {
8930
+ await session.close();
8931
+ }
8932
+ }
8933
+ async function completeCloudflareTask(params) {
8934
+ const { taskId, taskElementId, accountId, conversationKey, tunnelId, tunnelName, hostnames, status, errorMessage } = params;
8935
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8936
+ if (status === "failed" && (!errorMessage || errorMessage.trim().length === 0)) {
8937
+ throw new Error(
8938
+ "cloudflare-task-tracker: errorMessage is required when status='failed' (Task 885 process-provenance contract)."
8939
+ );
8940
+ }
8941
+ const session = getSession();
8942
+ try {
8943
+ if (status === "completed" && tunnelId && tunnelName) {
8944
+ const conv = await session.run(
8945
+ `MATCH (c:Conversation {sessionKey: $conversationKey, accountId: $accountId}) RETURN elementId(c) AS id LIMIT 1`,
8946
+ { conversationKey, accountId }
8947
+ );
8948
+ const conversationElementId = conv.records.length > 0 ? conv.records[0].get("id") : null;
8949
+ const tunnelProps = {
8950
+ accountId,
8951
+ tunnelId,
8952
+ tunnelName,
8953
+ createdAt: now,
8954
+ updatedAt: now
8955
+ };
8956
+ const tunnelRels = [
8957
+ { type: "PRODUCED", direction: "incoming", targetNodeId: taskElementId }
8958
+ ];
8959
+ if (conversationElementId) {
8960
+ tunnelRels.push({
8961
+ type: "RAISED_DURING",
8962
+ direction: "outgoing",
8963
+ targetNodeId: conversationElementId
8964
+ });
8965
+ }
8966
+ const tunnelWrite = await (0, import_dist.writeNodeWithEdges)({
8967
+ session,
8968
+ labels: ["CloudflareTunnel"],
8969
+ props: tunnelProps,
8970
+ relationships: tunnelRels,
8971
+ createdBy: {
8972
+ agent: CREATED_BY_AGENT,
8973
+ session: conversationKey,
8974
+ tool: "cloudflare-setup-endpoint"
8975
+ }
8976
+ });
8977
+ for (const h of hostnames ?? []) {
8978
+ const hostRels = [
8979
+ { type: "PRODUCED", direction: "incoming", targetNodeId: taskElementId },
8980
+ {
8981
+ type: "ROUTES_TO",
8982
+ direction: "outgoing",
8983
+ targetNodeId: tunnelWrite.nodeId
8984
+ }
8985
+ ];
8986
+ await (0, import_dist.writeNodeWithEdges)({
8987
+ session,
8988
+ labels: ["CloudflareHostname"],
8989
+ props: {
8990
+ accountId,
8991
+ hostnameValue: h.hostnameValue,
8992
+ tunnelId,
8993
+ isApex: h.isApex,
8994
+ createdAt: now,
8995
+ updatedAt: now
8996
+ },
8997
+ relationships: hostRels,
8998
+ createdBy: {
8999
+ agent: CREATED_BY_AGENT,
9000
+ session: conversationKey,
9001
+ tool: "cloudflare-setup-endpoint"
9002
+ }
9003
+ });
9004
+ }
9005
+ }
9006
+ const setClauses = [
9007
+ "t.status = $status",
9008
+ "t.completedAt = $now",
9009
+ "t.updatedAt = $now"
9010
+ ];
9011
+ const queryParams = { taskId, accountId, status, now };
9012
+ if (status === "failed" && errorMessage) {
9013
+ setClauses.push("t.errorMessage = $errorMessage");
9014
+ queryParams.errorMessage = errorMessage;
9015
+ }
9016
+ const updateRes = await session.run(
9017
+ `MATCH (t:Task {taskId: $taskId, accountId: $accountId})
9018
+ SET ${setClauses.join(", ")}
9019
+ RETURN size(coalesce(t.steps, [])) AS stepsCount`,
9020
+ queryParams
9021
+ );
9022
+ const stepsCount = updateRes.records[0]?.get("stepsCount")?.toNumber?.() ?? 0;
9023
+ process.stderr.write(
9024
+ `[task] action-done kind=${TASK_KIND} taskId=${taskId} status=${status} stepsCount=${stepsCount}
9025
+ `
9026
+ );
9027
+ } finally {
9028
+ await session.close();
9029
+ }
9030
+ }
9031
+ function readTunnelState(brandConfigDir) {
9032
+ const statePath = `${process.env.HOME ?? ""}/${brandConfigDir}/cloudflared/tunnel.state`;
9033
+ if (!existsSync17(statePath)) return null;
9034
+ try {
9035
+ const parsed = JSON.parse(readFileSync13(statePath, "utf-8"));
9036
+ const tunnelId = typeof parsed.tunnelId === "string" ? parsed.tunnelId : null;
9037
+ const tunnelName = typeof parsed.tunnelName === "string" ? parsed.tunnelName : null;
9038
+ const domain = typeof parsed.domain === "string" ? parsed.domain : null;
9039
+ if (!tunnelId || !tunnelName || !domain) return null;
9040
+ return { tunnelId, tunnelName, domain };
9041
+ } catch {
9042
+ return null;
9043
+ }
10187
9044
  }
10188
9045
 
10189
9046
  // server/routes/admin/cloudflare.ts
10190
9047
  var SETUP_TIMEOUT_MS = 10 * 60 * 1e3;
10191
9048
  var DOMAINS_TIMEOUT_MS = 40 * 1e3;
10192
9049
  function loadBrandInfo() {
10193
- const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ?? resolve18(process.cwd(), "..");
10194
- const brandPath = resolve18(platformRoot2, "config", "brand.json");
9050
+ const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ?? resolve14(process.cwd(), "..");
9051
+ const brandPath = resolve14(platformRoot2, "config", "brand.json");
10195
9052
  try {
10196
- const parsed = JSON.parse(readFileSync16(brandPath, "utf-8"));
9053
+ const parsed = JSON.parse(readFileSync14(brandPath, "utf-8"));
10197
9054
  const hostname2 = typeof parsed.hostname === "string" && parsed.hostname ? parsed.hostname : "maxy";
10198
9055
  const configDir2 = typeof parsed.configDir === "string" && parsed.configDir ? parsed.configDir : ".maxy";
10199
9056
  return { hostname: hostname2, configDir: configDir2 };
@@ -10296,7 +9153,7 @@ app21.get("/domains", requireAdminSession, async (c) => {
10296
9153
  streamLogPath = streamLogPathFor(accountId, correlationId).streamLogPath;
10297
9154
  log(`phase=stream-log-resolved path=${streamLogPath}`);
10298
9155
  const brand = loadBrandInfo();
10299
- const scriptPath = resolve18(homedir(), "list-cf-domains.sh");
9156
+ const scriptPath = resolve14(homedir(), "list-cf-domains.sh");
10300
9157
  const result = await runFormSpawn({
10301
9158
  scriptPath,
10302
9159
  args: [brand.hostname],
@@ -10435,6 +9292,17 @@ app21.post("/setup", requireAdminSession, async (c) => {
10435
9292
  if (await isActionActive("cloudflare-setup")) {
10436
9293
  return err("request", "Another Cloudflare setup is already running. Wait for it to finish before starting a new one.");
10437
9294
  }
9295
+ let cloudflareTask;
9296
+ try {
9297
+ cloudflareTask = await openCloudflareTask({
9298
+ accountId,
9299
+ conversationKey: sessionKey,
9300
+ inputsProvided: ["adminLabel", "adminDomain", publicFqdn ? "publicLabel" : null, apex ? "apex" : null, "password"].filter((s) => typeof s === "string")
9301
+ });
9302
+ log(`phase=task-opened taskId=${cloudflareTask.taskId}`);
9303
+ } catch (e) {
9304
+ return err("script", `Failed to open process-provenance Task record: ${e instanceof Error ? e.message : String(e)}`);
9305
+ }
10438
9306
  let actionId;
10439
9307
  let unit;
10440
9308
  try {
@@ -10447,8 +9315,27 @@ app21.post("/setup", requireAdminSession, async (c) => {
10447
9315
  log(`phase=action-launched id=${actionId} unit=${unit}`);
10448
9316
  void (async () => {
10449
9317
  const status = await waitForExit(unit, SETUP_TIMEOUT_MS);
9318
+ try {
9319
+ const appendedSteps = await appendCloudflareSteps(cloudflareTask.taskId, accountId, streamLogPath);
9320
+ log(`phase=task-steps-appended count=${appendedSteps.length}`);
9321
+ } catch (e) {
9322
+ logErr(`phase=task-steps-append-failed reason="${e instanceof Error ? e.message : String(e)}"`);
9323
+ }
10450
9324
  if (!status || status.execMainStatus !== 0) {
10451
9325
  logErr(`phase=post-exit-skipped reason=${status ? `exit=${status.execMainStatus}` : "unit-gone"} id=${actionId}`);
9326
+ try {
9327
+ const exitReason = status ? `script exited with status ${status.execMainStatus}` : "systemd unit vanished before exit recorded";
9328
+ await completeCloudflareTask({
9329
+ taskId: cloudflareTask.taskId,
9330
+ taskElementId: cloudflareTask.taskElementId,
9331
+ accountId,
9332
+ conversationKey: sessionKey,
9333
+ status: "failed",
9334
+ errorMessage: exitReason
9335
+ });
9336
+ } catch (e) {
9337
+ logErr(`phase=task-close-failed status=failed reason="${e instanceof Error ? e.message : String(e)}"`);
9338
+ }
10452
9339
  return;
10453
9340
  }
10454
9341
  const candidates = [publicFqdn, apex].filter((h) => typeof h === "string" && h.length > 0);
@@ -10468,6 +9355,30 @@ app21.post("/setup", requireAdminSession, async (c) => {
10468
9355
  logErr(`phase=alias-domain-write host=${host} result=error reason="${e instanceof Error ? e.message : String(e)}"`);
10469
9356
  }
10470
9357
  }
9358
+ try {
9359
+ const tunnelState = readTunnelState(brand.configDir);
9360
+ const allHostnames = [adminFqdn, publicFqdn, apex].filter(
9361
+ (h) => typeof h === "string" && h.length > 0
9362
+ );
9363
+ const hostnameRecords = allHostnames.map((value) => ({
9364
+ hostnameValue: value,
9365
+ // Apex heuristic: exactly one dot in the FQDN. Mirrors setup-tunnel.sh:332.
9366
+ isApex: (value.match(/\./g)?.length ?? 0) === 1
9367
+ }));
9368
+ await completeCloudflareTask({
9369
+ taskId: cloudflareTask.taskId,
9370
+ taskElementId: cloudflareTask.taskElementId,
9371
+ accountId,
9372
+ conversationKey: sessionKey,
9373
+ tunnelId: tunnelState?.tunnelId,
9374
+ tunnelName: tunnelState?.tunnelName,
9375
+ hostnames: tunnelState ? hostnameRecords : void 0,
9376
+ status: "completed"
9377
+ });
9378
+ log(`phase=task-closed status=completed tunnelId=${tunnelState?.tunnelId ?? "absent"} hostnames=${allHostnames.length}`);
9379
+ } catch (e) {
9380
+ logErr(`phase=task-close-failed status=completed reason="${e instanceof Error ? e.message : String(e)}"`);
9381
+ }
10471
9382
  log(`phase=done action=${actionId}`);
10472
9383
  })().catch((e) => logErr(`post-exit handler threw: ${e}`));
10473
9384
  const total = Date.now() - started;
@@ -10495,17 +9406,17 @@ var cloudflare_default = app21;
10495
9406
  import { createReadStream as createReadStream3 } from "fs";
10496
9407
  import { readdir as readdir2, readFile as readFile4, stat as stat4, mkdir as mkdir3, writeFile as writeFile4, unlink as unlink2 } from "fs/promises";
10497
9408
  import { realpathSync as realpathSync4 } from "fs";
10498
- import { basename as basename6, dirname as dirname8, join as join9, resolve as resolve20, sep as sep2 } from "path";
9409
+ import { basename as basename4, dirname as dirname5, join as join8, resolve as resolve16, sep as sep2 } from "path";
10499
9410
  import { Readable as Readable2 } from "stream";
10500
9411
 
10501
9412
  // app/lib/data-path.ts
10502
9413
  import { realpathSync as realpathSync3 } from "fs";
10503
- import { resolve as resolve19, normalize, sep, relative } from "path";
10504
- var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ?? resolve19(process.cwd(), "../platform");
10505
- var DATA_ROOT = resolve19(PLATFORM_ROOT6, "..", "data");
9414
+ import { resolve as resolve15, normalize, sep, relative } from "path";
9415
+ var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ?? resolve15(process.cwd(), "../platform");
9416
+ var DATA_ROOT = resolve15(PLATFORM_ROOT6, "..", "data");
10506
9417
  function resolveDataPath(raw) {
10507
9418
  const cleaned = normalize("/" + (raw ?? "").replace(/\\/g, "/")).replace(/^\/+/, "");
10508
- const absolute = resolve19(DATA_ROOT, cleaned);
9419
+ const absolute = resolve15(DATA_ROOT, cleaned);
10509
9420
  let dataRootReal;
10510
9421
  try {
10511
9422
  dataRootReal = realpathSync3(DATA_ROOT);
@@ -10853,7 +9764,7 @@ async function cascadeDeleteDocument(params) {
10853
9764
  var UUID_RE4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10854
9765
  async function readMeta(absDir, baseName) {
10855
9766
  try {
10856
- const raw = await readFile4(join9(absDir, `${baseName}.meta.json`), "utf8");
9767
+ const raw = await readFile4(join8(absDir, `${baseName}.meta.json`), "utf8");
10857
9768
  const parsed = JSON.parse(raw);
10858
9769
  if (typeof parsed?.filename === "string") {
10859
9770
  return { filename: parsed.filename, mimeType: typeof parsed.mimeType === "string" ? parsed.mimeType : void 0 };
@@ -10864,7 +9775,7 @@ async function readMeta(absDir, baseName) {
10864
9775
  }
10865
9776
  async function readAccountNames() {
10866
9777
  const map = /* @__PURE__ */ new Map();
10867
- const accountsDir = resolve20(DATA_ROOT, "accounts");
9778
+ const accountsDir = resolve16(DATA_ROOT, "accounts");
10868
9779
  let names;
10869
9780
  try {
10870
9781
  names = await readdir2(accountsDir);
@@ -10873,7 +9784,7 @@ async function readAccountNames() {
10873
9784
  }
10874
9785
  for (const name of names) {
10875
9786
  if (!UUID_RE4.test(name)) continue;
10876
- const configPath2 = resolve20(accountsDir, name, "account.json");
9787
+ const configPath2 = resolve16(accountsDir, name, "account.json");
10877
9788
  try {
10878
9789
  const raw = await readFile4(configPath2, "utf8");
10879
9790
  const parsed = JSON.parse(raw);
@@ -10891,7 +9802,7 @@ async function readAccountNames() {
10891
9802
  }
10892
9803
  async function enrich(absolute, entry, accountNames) {
10893
9804
  if (entry.kind === "directory" && UUID_RE4.test(entry.name)) {
10894
- const meta = await readMeta(join9(absolute, entry.name), entry.name);
9805
+ const meta = await readMeta(join8(absolute, entry.name), entry.name);
10895
9806
  if (meta?.filename) {
10896
9807
  entry.displayName = meta.filename;
10897
9808
  entry.mimeType = meta.mimeType;
@@ -10950,7 +9861,7 @@ app22.get("/", requireAdminSession, async (c) => {
10950
9861
  continue;
10951
9862
  }
10952
9863
  try {
10953
- const entryPath = join9(absolute, name);
9864
+ const entryPath = join8(absolute, name);
10954
9865
  const s = await stat4(entryPath);
10955
9866
  entries.push({
10956
9867
  name,
@@ -11005,7 +9916,7 @@ app22.get("/download", requireAdminSession, async (c) => {
11005
9916
  if (!info.isFile()) {
11006
9917
  return c.json({ error: "Path is not a file" }, 400);
11007
9918
  }
11008
- const filename = basename6(absolute);
9919
+ const filename = basename4(absolute);
11009
9920
  const mimeType = detectMimeType(absolute);
11010
9921
  const nodeStream = createReadStream3(absolute);
11011
9922
  const webStream = Readable2.toWeb(nodeStream);
@@ -11062,10 +9973,10 @@ app22.post("/upload", requireAdminSession, async (c) => {
11062
9973
  error: `Unsupported file type: "${file.type}". Supported: ${[...SUPPORTED_MIME_TYPES].join(", ")}.`
11063
9974
  }, 422);
11064
9975
  }
11065
- const safeName = basename6(file.name).replace(/[\0/\\]/g, "_");
9976
+ const safeName = basename4(file.name).replace(/[\0/\\]/g, "_");
11066
9977
  const finalName = `${Date.now()}-${safeName}`;
11067
- const destDir = resolve20(DATA_ROOT, "uploads", accountId);
11068
- const destPath = resolve20(destDir, finalName);
9978
+ const destDir = resolve16(DATA_ROOT, "uploads", accountId);
9979
+ const destPath = resolve16(destDir, finalName);
11069
9980
  try {
11070
9981
  await mkdir3(destDir, { recursive: true });
11071
9982
  const dataRootReal = realpathSync4(DATA_ROOT);
@@ -11107,7 +10018,7 @@ app22.delete("/", requireAdminSession, async (c) => {
11107
10018
  return c.json({ error: resolution.error }, resolution.status);
11108
10019
  }
11109
10020
  const { absolute, relative: relPath2 } = resolution;
11110
- const base = basename6(absolute);
10021
+ const base = basename4(absolute);
11111
10022
  const segments = relPath2.split("/").filter(Boolean);
11112
10023
  if (base === "account.json" || segments.includes(".git")) {
11113
10024
  console.error(`[data] file-delete blocked path="${relPath2}" reason="protected"`);
@@ -11123,7 +10034,7 @@ app22.delete("/", requireAdminSession, async (c) => {
11123
10034
  }
11124
10035
  const dot = base.lastIndexOf(".");
11125
10036
  const stem = dot === -1 ? base : base.slice(0, dot);
11126
- const sidecarPath = UUID_RE4.test(stem) && base !== `${stem}.meta.json` ? join9(dirname8(absolute), `${stem}.meta.json`) : null;
10037
+ const sidecarPath = UUID_RE4.test(stem) && base !== `${stem}.meta.json` ? join8(dirname5(absolute), `${stem}.meta.json`) : null;
11127
10038
  await unlink2(absolute);
11128
10039
  if (sidecarPath) {
11129
10040
  try {
@@ -11160,7 +10071,7 @@ app22.delete("/", requireAdminSession, async (c) => {
11160
10071
  var files_default = app22;
11161
10072
 
11162
10073
  // ../lib/graph-search/src/index.ts
11163
- var import_dist = __toESM(require_dist());
10074
+ var import_dist2 = __toESM(require_dist2());
11164
10075
  import { int } from "neo4j-driver";
11165
10076
  var VECTOR_WEIGHT = 0.7;
11166
10077
  var BM25_WEIGHT = 0.3;
@@ -11215,7 +10126,7 @@ async function bm25Only(session, params) {
11215
10126
  ${scopeClause}
11216
10127
  ${agentClause}
11217
10128
  ${labelClause}
11218
- AND ${(0, import_dist.notTrashed)("node")}
10129
+ AND ${(0, import_dist2.notTrashed)("node")}
11219
10130
  ${kwClause}
11220
10131
  RETURN node, score, labels(node) AS nodeLabels, elementId(node) AS nodeId
11221
10132
  ORDER BY score DESC
@@ -11305,7 +10216,7 @@ async function hybrid(session, embed2, params) {
11305
10216
  WHERE node.accountId = $accountId
11306
10217
  ${scopeClause}
11307
10218
  ${agentClause}
11308
- AND ${(0, import_dist.notTrashed)("node")}
10219
+ AND ${(0, import_dist2.notTrashed)("node")}
11309
10220
  ${keywordClause}
11310
10221
  RETURN node, score, labels(node) AS nodeLabels, elementId(node) AS nodeId
11311
10222
  ORDER BY score DESC
@@ -11366,7 +10277,7 @@ async function hybrid(session, embed2, params) {
11366
10277
  const propResult = await session.run(
11367
10278
  `MATCH (node)
11368
10279
  WHERE node.accountId = $accountId
11369
- AND ${(0, import_dist.notTrashed)("node")}
10280
+ AND ${(0, import_dist2.notTrashed)("node")}
11370
10281
  AND node.keywords IS NOT NULL
11371
10282
  AND ANY(kw IN $kwSubs WHERE ANY(nk IN node.keywords WHERE toLower(nk) = kw))
11372
10283
  ${propScopeClause}
@@ -11423,7 +10334,7 @@ async function hybrid(session, embed2, params) {
11423
10334
  `UNWIND $nodeIds AS nid
11424
10335
  MATCH (n)-[r]-(related)
11425
10336
  WHERE elementId(n) = nid
11426
- AND ${(0, import_dist.notTrashed)("related")}
10337
+ AND ${(0, import_dist2.notTrashed)("related")}
11427
10338
  ${expandScopeClause}
11428
10339
  ${expandAgentClause}
11429
10340
  WITH nid, n, r, related
@@ -11674,11 +10585,6 @@ var GRAPH_LABEL_COLOURS = {
11674
10585
  // confusion doesn't apply, but kept distinguishable for the legend)
11675
10586
  Email: "#6F7F4A",
11676
10587
  EmailAccount: "#91A063",
11677
- // Review signals (Task 626 — previously written by review-detector but
11678
- // unregistered here, producing an `unknown label` 400 whenever the
11679
- // filter popover advertised the label. Muted brick — still reads as
11680
- // alert against the cream background but doesn't shout fire-engine red.)
11681
- ReviewAlert: "#A85C5C",
11682
10588
  // Public-agent projection (Task 837) — burnished bronze sits between
11683
10589
  // people-terracotta and email-moss but shares neither hue, signalling
11684
10590
  // "operator-defined persona that traverses to KnowledgeDocuments,
@@ -12603,8 +11509,8 @@ var adherence_default = app30;
12603
11509
  // server/routes/admin/sidebar-artefacts.ts
12604
11510
  import neo4j3 from "neo4j-driver";
12605
11511
  import { readFile as readFile5, readdir as readdir3, stat as stat5 } from "fs/promises";
12606
- import { resolve as resolve21, relative as relative2, isAbsolute } from "path";
12607
- import { existsSync as existsSync20 } from "fs";
11512
+ import { resolve as resolve17, relative as relative2, isAbsolute } from "path";
11513
+ import { existsSync as existsSync18 } from "fs";
12608
11514
  var LIMIT = 50;
12609
11515
  var TEXT_MIME_PREFIXES = ["text/", "application/json", "application/markdown"];
12610
11516
  var ADMIN_AGENT_FILES = ["IDENTITY.md", "SOUL.md", "KNOWLEDGE.md"];
@@ -12620,7 +11526,7 @@ app31.get("/", requireAdminSession, async (c) => {
12620
11526
  if (docs === null) {
12621
11527
  return c.json({ error: "Failed to load artefacts" }, 500);
12622
11528
  }
12623
- const accountDir = resolve21(ACCOUNTS_DIR, accountId);
11529
+ const accountDir = resolve17(ACCOUNTS_DIR, accountId);
12624
11530
  const agents = await fetchAgentTemplateRows(accountDir);
12625
11531
  const artefacts = [...docs, ...agents].sort(
12626
11532
  (a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "")
@@ -12683,8 +11589,8 @@ async function readArtefactContent(accountId, attachmentId, mimeType, displayNam
12683
11589
  logSkip(displayName, "non-text-mime", mimeType);
12684
11590
  return { content: "", skipReason: "non-text-mime" };
12685
11591
  }
12686
- const accountDir = resolve21(ATTACHMENTS_ROOT, accountId);
12687
- const dir = resolve21(accountDir, attachmentId);
11592
+ const accountDir = resolve17(ATTACHMENTS_ROOT, accountId);
11593
+ const dir = resolve17(accountDir, attachmentId);
12688
11594
  try {
12689
11595
  validateFilePathInAccount(dir, accountDir);
12690
11596
  } catch {
@@ -12698,7 +11604,7 @@ async function readArtefactContent(accountId, attachmentId, mimeType, displayNam
12698
11604
  logSkip(displayName, "missing-on-disk", mimeType);
12699
11605
  return { content: "", skipReason: "missing-on-disk" };
12700
11606
  }
12701
- return { content: await readFile5(resolve21(dir, dataFile), "utf-8"), skipReason: null };
11607
+ return { content: await readFile5(resolve17(dir, dataFile), "utf-8"), skipReason: null };
12702
11608
  } catch (err) {
12703
11609
  const message = err instanceof Error ? err.message : String(err);
12704
11610
  console.error(`[admin/sidebar-artefacts] read-failed attachmentId=${attachmentId.slice(0, 8)} error="${message}"`);
@@ -12714,8 +11620,8 @@ function logSkip(name, reason, mimeType) {
12714
11620
  async function fetchAgentTemplateRows(accountDir) {
12715
11621
  const rows = [];
12716
11622
  for (const filename of ADMIN_AGENT_FILES) {
12717
- const overridePath = resolve21(accountDir, "agents", "admin", filename);
12718
- const bundledPath = resolve21(PLATFORM_ROOT, "templates", "agents", "admin", filename);
11623
+ const overridePath = resolve17(accountDir, "agents", "admin", filename);
11624
+ const bundledPath = resolve17(PLATFORM_ROOT, "templates", "agents", "admin", filename);
12719
11625
  const labelStem = filename.replace(/\.md$/, "");
12720
11626
  const row = await readAgentTemplateRow({
12721
11627
  id: `agent-template:admin:${filename}`,
@@ -12729,12 +11635,12 @@ async function fetchAgentTemplateRows(accountDir) {
12729
11635
  });
12730
11636
  if (row) rows.push(row);
12731
11637
  }
12732
- const overrideDir = resolve21(accountDir, "specialists", "agents");
12733
- const bundledDir = resolve21(PLATFORM_ROOT, "templates", "specialists", "agents");
11638
+ const overrideDir = resolve17(accountDir, "specialists", "agents");
11639
+ const bundledDir = resolve17(PLATFORM_ROOT, "templates", "specialists", "agents");
12734
11640
  const specialistNames = await unionSpecialistFilenames(overrideDir, bundledDir);
12735
11641
  for (const filename of specialistNames) {
12736
- const overridePath = resolve21(overrideDir, filename);
12737
- const bundledPath = resolve21(bundledDir, filename);
11642
+ const overridePath = resolve17(overrideDir, filename);
11643
+ const bundledPath = resolve17(bundledDir, filename);
12738
11644
  const row = await readAgentTemplateRow({
12739
11645
  id: `agent-template:specialist:${filename}`,
12740
11646
  displayName: filename.replace(/\.md$/, ""),
@@ -12752,7 +11658,7 @@ async function fetchAgentTemplateRows(accountDir) {
12752
11658
  async function unionSpecialistFilenames(overrideDir, bundledDir) {
12753
11659
  const names = /* @__PURE__ */ new Set();
12754
11660
  for (const dir of [overrideDir, bundledDir]) {
12755
- if (!existsSync20(dir)) continue;
11661
+ if (!existsSync18(dir)) continue;
12756
11662
  try {
12757
11663
  const entries = await readdir3(dir);
12758
11664
  for (const entry of entries) {
@@ -12767,7 +11673,7 @@ async function unionSpecialistFilenames(overrideDir, bundledDir) {
12767
11673
  }
12768
11674
  async function readAgentTemplateRow(inp) {
12769
11675
  let chosenPath = null;
12770
- if (existsSync20(inp.overridePath)) {
11676
+ if (existsSync18(inp.overridePath)) {
12771
11677
  try {
12772
11678
  validateFilePathInAccount(inp.overridePath, inp.overrideRoot);
12773
11679
  chosenPath = inp.overridePath;
@@ -12778,7 +11684,7 @@ async function readAgentTemplateRow(inp) {
12778
11684
  );
12779
11685
  return null;
12780
11686
  }
12781
- } else if (existsSync20(inp.bundledPath)) {
11687
+ } else if (existsSync18(inp.bundledPath)) {
12782
11688
  if (!isWithin(inp.bundledPath, inp.bundledRoot)) {
12783
11689
  console.error(
12784
11690
  `[admin/sidebar-artefacts] agent-template-read-failed agent=${inp.displayName} kind=${inp.logName} error="bundled path outside PLATFORM_ROOT"`
@@ -12819,8 +11725,8 @@ var sidebar_artefacts_default = app31;
12819
11725
 
12820
11726
  // server/routes/admin/sidebar-artefact-save.ts
12821
11727
  import { mkdir as mkdir4, readdir as readdir4, stat as stat6, writeFile as writeFile5 } from "fs/promises";
12822
- import { resolve as resolve22 } from "path";
12823
- import { existsSync as existsSync21 } from "fs";
11728
+ import { resolve as resolve18 } from "path";
11729
+ import { existsSync as existsSync19 } from "fs";
12824
11730
  var ADMIN_AGENT_FILES2 = /* @__PURE__ */ new Set(["IDENTITY.md", "SOUL.md", "KNOWLEDGE.md"]);
12825
11731
  var UUID_RE5 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
12826
11732
  var app32 = new Hono();
@@ -12832,7 +11738,7 @@ app32.post("/", requireAdminSession, async (c) => {
12832
11738
  if (!body || typeof body.id !== "string" || typeof body.content !== "string") {
12833
11739
  return c.json({ error: "id and content required" }, 400);
12834
11740
  }
12835
- const accountDir = resolve22(ACCOUNTS_DIR, accountId);
11741
+ const accountDir = resolve18(ACCOUNTS_DIR, accountId);
12836
11742
  const resolved = await resolveSavePath(body.id, accountId, accountDir);
12837
11743
  if (resolved.kind === "reject") {
12838
11744
  console.error(
@@ -12873,22 +11779,22 @@ async function resolveSavePath(id, accountId, accountDir) {
12873
11779
  if (role !== "admin" || !ADMIN_AGENT_FILES2.has(filename)) {
12874
11780
  return { kind: "reject", status: 400, reason: "invalid-id" };
12875
11781
  }
12876
- const parent = resolve22(accountDir, "agents", "admin");
11782
+ const parent = resolve18(accountDir, "agents", "admin");
12877
11783
  await mkdir4(parent, { recursive: true });
12878
11784
  try {
12879
11785
  validateFilePathInAccount(parent, accountDir);
12880
11786
  } catch {
12881
11787
  return { kind: "reject", status: 400, reason: "containment-rejected" };
12882
11788
  }
12883
- return { kind: "admin-template", path: resolve22(parent, filename) };
11789
+ return { kind: "admin-template", path: resolve18(parent, filename) };
12884
11790
  }
12885
11791
  if (UUID_RE5.test(id)) {
12886
- const dir = resolve22(ATTACHMENTS_ROOT, accountId, id);
12887
- if (!existsSync21(dir)) {
11792
+ const dir = resolve18(ATTACHMENTS_ROOT, accountId, id);
11793
+ if (!existsSync19(dir)) {
12888
11794
  return { kind: "reject", status: 400, reason: "not-found" };
12889
11795
  }
12890
11796
  try {
12891
- validateFilePathInAccount(dir, resolve22(ATTACHMENTS_ROOT, accountId));
11797
+ validateFilePathInAccount(dir, resolve18(ATTACHMENTS_ROOT, accountId));
12892
11798
  } catch {
12893
11799
  return { kind: "reject", status: 400, reason: "containment-rejected" };
12894
11800
  }
@@ -12897,7 +11803,7 @@ async function resolveSavePath(id, accountId, accountDir) {
12897
11803
  if (!dataFile) {
12898
11804
  return { kind: "reject", status: 400, reason: "not-found" };
12899
11805
  }
12900
- return { kind: "knowledge-doc", path: resolve22(dir, dataFile) };
11806
+ return { kind: "knowledge-doc", path: resolve18(dir, dataFile) };
12901
11807
  }
12902
11808
  return { kind: "reject", status: 400, reason: "invalid-id" };
12903
11809
  }
@@ -12908,8 +11814,8 @@ var sidebar_artefact_save_default = app32;
12908
11814
 
12909
11815
  // server/routes/admin/sidebar-artefact-content.ts
12910
11816
  import { readFile as readFile6, readdir as readdir5 } from "fs/promises";
12911
- import { existsSync as existsSync22 } from "fs";
12912
- import { resolve as resolve23 } from "path";
11817
+ import { existsSync as existsSync20 } from "fs";
11818
+ import { resolve as resolve19 } from "path";
12913
11819
  var UUID_RE6 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
12914
11820
  var app33 = new Hono();
12915
11821
  app33.get("/", requireAdminSession, async (c) => {
@@ -12921,14 +11827,14 @@ app33.get("/", requireAdminSession, async (c) => {
12921
11827
  console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
12922
11828
  return new Response("Not found", { status: 404 });
12923
11829
  }
12924
- const dir = resolve23(ATTACHMENTS_ROOT, accountId, id);
12925
- if (!existsSync22(dir)) {
11830
+ const dir = resolve19(ATTACHMENTS_ROOT, accountId, id);
11831
+ if (!existsSync20(dir)) {
12926
11832
  console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
12927
11833
  return new Response("Not found", { status: 404 });
12928
11834
  }
12929
11835
  let meta;
12930
11836
  try {
12931
- meta = JSON.parse(await readFile6(resolve23(dir, `${id}.meta.json`), "utf-8"));
11837
+ meta = JSON.parse(await readFile6(resolve19(dir, `${id}.meta.json`), "utf-8"));
12932
11838
  } catch {
12933
11839
  console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
12934
11840
  return new Response("Not found", { status: 404 });
@@ -12940,7 +11846,7 @@ app33.get("/", requireAdminSession, async (c) => {
12940
11846
  return new Response("Not found", { status: 404 });
12941
11847
  }
12942
11848
  const start = Date.now();
12943
- const buffer = await readFile6(resolve23(dir, dataFile));
11849
+ const buffer = await readFile6(resolve19(dir, dataFile));
12944
11850
  const ms = Date.now() - start;
12945
11851
  console.log(
12946
11852
  `[admin/sidebar-artefact-content] account=${accountId} id=${id.slice(0, 8)} mime=${meta.mimeType} bytes=${buffer.length} ms=${ms}`
@@ -12984,8 +11890,8 @@ app34.route("/sidebar-artefact-content", sidebar_artefact_content_default);
12984
11890
  var admin_default = app34;
12985
11891
 
12986
11892
  // server/routes/sites.ts
12987
- import { existsSync as existsSync23, readFileSync as readFileSync17, realpathSync as realpathSync5, statSync as statSync8 } from "fs";
12988
- import { resolve as resolve24 } from "path";
11893
+ import { existsSync as existsSync21, readFileSync as readFileSync15, realpathSync as realpathSync5, statSync as statSync5 } from "fs";
11894
+ import { resolve as resolve20 } from "path";
12989
11895
  var SAFE_SEG_RE = /^[a-z0-9_][a-z0-9_.-]{0,99}$/i;
12990
11896
  var MIME = {
12991
11897
  ".html": "text/html; charset=utf-8",
@@ -13043,28 +11949,28 @@ app35.get("/:rel{.*}", (c) => {
13043
11949
  }
13044
11950
  segments.push(seg);
13045
11951
  }
13046
- const rootDir = resolve24(account.accountDir, "sites");
13047
- let filePath = segments.length === 0 ? rootDir : resolve24(rootDir, ...segments);
11952
+ const rootDir = resolve20(account.accountDir, "sites");
11953
+ let filePath = segments.length === 0 ? rootDir : resolve20(rootDir, ...segments);
13048
11954
  if (filePath !== rootDir && !filePath.startsWith(rootDir + "/")) {
13049
11955
  console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
13050
11956
  return c.text("Forbidden", 403);
13051
11957
  }
13052
11958
  let stat7;
13053
11959
  try {
13054
- stat7 = existsSync23(filePath) ? statSync8(filePath) : null;
11960
+ stat7 = existsSync21(filePath) ? statSync5(filePath) : null;
13055
11961
  } catch {
13056
11962
  stat7 = null;
13057
11963
  }
13058
11964
  if (stat7?.isDirectory()) {
13059
- filePath = resolve24(filePath, "index.html");
11965
+ filePath = resolve20(filePath, "index.html");
13060
11966
  } else if (stat7 === null && isDirRequest) {
13061
- filePath = resolve24(filePath, "index.html");
11967
+ filePath = resolve20(filePath, "index.html");
13062
11968
  }
13063
11969
  if (!filePath.startsWith(rootDir + "/")) {
13064
11970
  console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
13065
11971
  return c.text("Forbidden", 403);
13066
11972
  }
13067
- if (!existsSync23(filePath)) {
11973
+ if (!existsSync21(filePath)) {
13068
11974
  console.error(`[sites] not-found path=${reqPath} status=404`);
13069
11975
  return c.text("Not found", 404);
13070
11976
  }
@@ -13083,7 +11989,7 @@ app35.get("/:rel{.*}", (c) => {
13083
11989
  }
13084
11990
  let body;
13085
11991
  try {
13086
- body = readFileSync17(realPath);
11992
+ body = readFileSync15(realPath);
13087
11993
  } catch (err) {
13088
11994
  const code = err?.code;
13089
11995
  if (code === "EISDIR") {
@@ -13215,14 +12121,14 @@ function clientFrom(c) {
13215
12121
  );
13216
12122
  }
13217
12123
  var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT || "";
13218
- var BRAND_JSON_PATH = PLATFORM_ROOT7 ? join10(PLATFORM_ROOT7, "config", "brand.json") : "";
12124
+ var BRAND_JSON_PATH = PLATFORM_ROOT7 ? join9(PLATFORM_ROOT7, "config", "brand.json") : "";
13219
12125
  var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", domain: "getmaxy.com" };
13220
- if (BRAND_JSON_PATH && !existsSync24(BRAND_JSON_PATH)) {
12126
+ if (BRAND_JSON_PATH && !existsSync22(BRAND_JSON_PATH)) {
13221
12127
  console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
13222
12128
  }
13223
- if (BRAND_JSON_PATH && existsSync24(BRAND_JSON_PATH)) {
12129
+ if (BRAND_JSON_PATH && existsSync22(BRAND_JSON_PATH)) {
13224
12130
  try {
13225
- const parsed = JSON.parse(readFileSync18(BRAND_JSON_PATH, "utf-8"));
12131
+ const parsed = JSON.parse(readFileSync16(BRAND_JSON_PATH, "utf-8"));
13226
12132
  BRAND = { ...BRAND, ...parsed };
13227
12133
  } catch (err) {
13228
12134
  console.error(`[brand] Failed to parse brand.json: ${err.message}`);
@@ -13241,11 +12147,11 @@ var brandLoginOpts = {
13241
12147
  bodyFont: BRAND.defaultFonts?.body,
13242
12148
  logoContainsName: !!BRAND.logoContainsName
13243
12149
  };
13244
- var ALIAS_DOMAINS_PATH2 = join10(homedir2(), BRAND.configDir, "alias-domains.json");
12150
+ var ALIAS_DOMAINS_PATH2 = join9(homedir2(), BRAND.configDir, "alias-domains.json");
13245
12151
  function loadAliasDomains() {
13246
12152
  try {
13247
- if (!existsSync24(ALIAS_DOMAINS_PATH2)) return null;
13248
- const parsed = JSON.parse(readFileSync18(ALIAS_DOMAINS_PATH2, "utf-8"));
12153
+ if (!existsSync22(ALIAS_DOMAINS_PATH2)) return null;
12154
+ const parsed = JSON.parse(readFileSync16(ALIAS_DOMAINS_PATH2, "utf-8"));
13249
12155
  if (!Array.isArray(parsed)) {
13250
12156
  console.error("[alias-domains] malformed alias-domains.json \u2014 expected array");
13251
12157
  return null;
@@ -13585,20 +12491,20 @@ app36.get("/agent-assets/:slug/:filename", (c) => {
13585
12491
  console.error(`[agent-assets] no-account slug=${slug} file=${filename}`);
13586
12492
  return c.text("Not found", 404);
13587
12493
  }
13588
- const filePath = resolve25(account.accountDir, "agents", slug, "assets", filename);
13589
- const expectedDir = resolve25(account.accountDir, "agents", slug, "assets");
12494
+ const filePath = resolve21(account.accountDir, "agents", slug, "assets", filename);
12495
+ const expectedDir = resolve21(account.accountDir, "agents", slug, "assets");
13590
12496
  if (!filePath.startsWith(expectedDir + "/")) {
13591
12497
  console.error(`[agent-assets] path-traversal-rejected slug=${slug} file=${filename}`);
13592
12498
  return c.text("Forbidden", 403);
13593
12499
  }
13594
- if (!existsSync24(filePath)) {
12500
+ if (!existsSync22(filePath)) {
13595
12501
  console.error(`[agent-assets] serve slug=${slug} file=${filename} status=404`);
13596
12502
  return c.text("Not found", 404);
13597
12503
  }
13598
12504
  const ext = "." + filename.split(".").pop()?.toLowerCase();
13599
12505
  const contentType = IMAGE_MIME[ext] || "application/octet-stream";
13600
12506
  console.log(`[agent-assets] serve slug=${slug} file=${filename} status=200`);
13601
- const body = readFileSync18(filePath);
12507
+ const body = readFileSync16(filePath);
13602
12508
  return c.body(body, 200, {
13603
12509
  "Content-Type": contentType,
13604
12510
  "Cache-Control": "public, max-age=3600"
@@ -13615,20 +12521,20 @@ app36.get("/generated/:filename", (c) => {
13615
12521
  console.error(`[generated] serve file=${filename} status=404`);
13616
12522
  return c.text("Not found", 404);
13617
12523
  }
13618
- const filePath = resolve25(account.accountDir, "generated", filename);
13619
- const expectedDir = resolve25(account.accountDir, "generated");
12524
+ const filePath = resolve21(account.accountDir, "generated", filename);
12525
+ const expectedDir = resolve21(account.accountDir, "generated");
13620
12526
  if (!filePath.startsWith(expectedDir + "/")) {
13621
12527
  console.error(`[generated] serve file=${filename} status=403`);
13622
12528
  return c.text("Forbidden", 403);
13623
12529
  }
13624
- if (!existsSync24(filePath)) {
12530
+ if (!existsSync22(filePath)) {
13625
12531
  console.error(`[generated] serve file=${filename} status=404`);
13626
12532
  return c.text("Not found", 404);
13627
12533
  }
13628
12534
  const ext = "." + filename.split(".").pop()?.toLowerCase();
13629
12535
  const contentType = IMAGE_MIME[ext] || "application/octet-stream";
13630
12536
  console.log(`[generated] serve file=${filename} status=200`);
13631
- const body = readFileSync18(filePath);
12537
+ const body = readFileSync16(filePath);
13632
12538
  return c.body(body, 200, {
13633
12539
  "Content-Type": contentType,
13634
12540
  "Cache-Control": "public, max-age=86400"
@@ -13638,9 +12544,9 @@ app36.route("/sites", sites_default);
13638
12544
  var htmlCache = /* @__PURE__ */ new Map();
13639
12545
  var brandLogoPath = "/brand/maxy-monochrome.png";
13640
12546
  var brandIconPath = "/brand/maxy-monochrome.png";
13641
- if (BRAND_JSON_PATH && existsSync24(BRAND_JSON_PATH)) {
12547
+ if (BRAND_JSON_PATH && existsSync22(BRAND_JSON_PATH)) {
13642
12548
  try {
13643
- const fullBrand = JSON.parse(readFileSync18(BRAND_JSON_PATH, "utf-8"));
12549
+ const fullBrand = JSON.parse(readFileSync16(BRAND_JSON_PATH, "utf-8"));
13644
12550
  if (fullBrand.assets?.logo) brandLogoPath = `/brand/${fullBrand.assets.logo}`;
13645
12551
  brandIconPath = fullBrand.assets?.icon ? `/brand/${fullBrand.assets.icon}` : brandLogoPath;
13646
12552
  } catch {
@@ -13657,9 +12563,9 @@ var brandScript = `<script>window.__BRAND__=${JSON.stringify({
13657
12563
  function readInstalledVersion() {
13658
12564
  try {
13659
12565
  if (!PLATFORM_ROOT7) return "unknown";
13660
- const versionFile = join10(PLATFORM_ROOT7, "config", `.${BRAND.hostname}-version`);
13661
- if (!existsSync24(versionFile)) return "unknown";
13662
- const content = readFileSync18(versionFile, "utf-8").trim();
12566
+ const versionFile = join9(PLATFORM_ROOT7, "config", `.${BRAND.hostname}-version`);
12567
+ if (!existsSync22(versionFile)) return "unknown";
12568
+ const content = readFileSync16(versionFile, "utf-8").trim();
13663
12569
  return content || "unknown";
13664
12570
  } catch {
13665
12571
  return "unknown";
@@ -13700,7 +12606,7 @@ var clientErrorReporterScript = `<script>
13700
12606
  function cachedHtml(file) {
13701
12607
  let html = htmlCache.get(file);
13702
12608
  if (!html) {
13703
- html = readFileSync18(resolve25(process.cwd(), "public", file), "utf-8");
12609
+ html = readFileSync16(resolve21(process.cwd(), "public", file), "utf-8");
13704
12610
  const productNameEsc = escapeHtml(BRAND.productName);
13705
12611
  html = html.replace(/<title>([^<]*)<\/title>/, (_match, inner) => `<title>${escapeHtml(inner).replace(/Maxy/g, productNameEsc)}</title>`);
13706
12612
  html = html.replace('href="/favicon.ico"', `href="${escapeHtml(brandFaviconPath)}"`);
@@ -13716,26 +12622,26 @@ ${clientErrorReporterScript}
13716
12622
  }
13717
12623
  var brandedHtmlCache = /* @__PURE__ */ new Map();
13718
12624
  function loadBrandingCache(agentSlug) {
13719
- const configDir2 = join10(homedir2(), BRAND.configDir);
12625
+ const configDir2 = join9(homedir2(), BRAND.configDir);
13720
12626
  try {
13721
- const accountJsonPath = join10(configDir2, "account.json");
13722
- if (!existsSync24(accountJsonPath)) return null;
13723
- const account = JSON.parse(readFileSync18(accountJsonPath, "utf-8"));
12627
+ const accountJsonPath = join9(configDir2, "account.json");
12628
+ if (!existsSync22(accountJsonPath)) return null;
12629
+ const account = JSON.parse(readFileSync16(accountJsonPath, "utf-8"));
13724
12630
  const accountId = account.accountId;
13725
12631
  if (!accountId) return null;
13726
- const cachePath = join10(configDir2, "branding-cache", accountId, `${agentSlug}.json`);
13727
- if (!existsSync24(cachePath)) return null;
13728
- return JSON.parse(readFileSync18(cachePath, "utf-8"));
12632
+ const cachePath = join9(configDir2, "branding-cache", accountId, `${agentSlug}.json`);
12633
+ if (!existsSync22(cachePath)) return null;
12634
+ return JSON.parse(readFileSync16(cachePath, "utf-8"));
13729
12635
  } catch {
13730
12636
  return null;
13731
12637
  }
13732
12638
  }
13733
12639
  function resolveDefaultSlug() {
13734
12640
  try {
13735
- const configDir2 = join10(homedir2(), BRAND.configDir);
13736
- const accountJsonPath = join10(configDir2, "account.json");
13737
- if (!existsSync24(accountJsonPath)) return null;
13738
- const account = JSON.parse(readFileSync18(accountJsonPath, "utf-8"));
12641
+ const configDir2 = join9(homedir2(), BRAND.configDir);
12642
+ const accountJsonPath = join9(configDir2, "account.json");
12643
+ if (!existsSync22(accountJsonPath)) return null;
12644
+ const account = JSON.parse(readFileSync16(accountJsonPath, "utf-8"));
13739
12645
  return account.defaultAgent || null;
13740
12646
  } catch {
13741
12647
  return null;
@@ -13808,7 +12714,7 @@ app36.use("/vnc-popout.html", logViewerFetch);
13808
12714
  app36.get("/vnc-popout.html", (c) => {
13809
12715
  let html = htmlCache.get("vnc-popout.html");
13810
12716
  if (!html) {
13811
- html = readFileSync18(resolve25(process.cwd(), "public", "vnc-popout.html"), "utf-8");
12717
+ html = readFileSync16(resolve21(process.cwd(), "public", "vnc-popout.html"), "utf-8");
13812
12718
  const name = escapeHtml(BRAND.productName);
13813
12719
  html = html.replace("<title>Browser \u2014 Maxy</title>", `<title>${name}</title>`);
13814
12720
  html = html.replace("</head>", ` ${brandScript}
@@ -13898,8 +12804,8 @@ try {
13898
12804
  (async () => {
13899
12805
  try {
13900
12806
  let userId = "";
13901
- if (existsSync24(USERS_FILE)) {
13902
- const users = JSON.parse(readFileSync18(USERS_FILE, "utf-8").trim() || "[]");
12807
+ if (existsSync22(USERS_FILE)) {
12808
+ const users = JSON.parse(readFileSync16(USERS_FILE, "utf-8").trim() || "[]");
13903
12809
  userId = users[0]?.userId ?? "";
13904
12810
  }
13905
12811
  await backfillNullUserIdConversations(userId);
@@ -13914,15 +12820,8 @@ try {
13914
12820
  console.error(`[migration] runBootMigrations rejected: ${err instanceof Error ? err.message : String(err)}`);
13915
12821
  }
13916
12822
  })();
13917
- (async () => {
13918
- try {
13919
- await startReviewDetector();
13920
- } catch (err) {
13921
- console.error(`[review] startReviewDetector rejected: ${err instanceof Error ? err.message : String(err)}`);
13922
- }
13923
- })();
13924
12823
  startGraphHealthTimer();
13925
- var configDirForWhatsApp = basename7(MAXY_DIR) || ".maxy";
12824
+ var configDirForWhatsApp = basename5(MAXY_DIR) || ".maxy";
13926
12825
  var bootAccount = resolveAccount();
13927
12826
  var bootAccountConfig = bootAccount?.config;
13928
12827
  var bootPublicAgent = bootAccount ? resolvePublicAgent(bootAccount.accountDir, { accountId: bootAccount.accountId })?.slug ?? null : null;
@@ -13966,7 +12865,7 @@ if (bootAccountConfig?.whatsapp) {
13966
12865
  }
13967
12866
  init({
13968
12867
  configDir: configDirForWhatsApp,
13969
- platformRoot: resolve25(process.env.MAXY_PLATFORM_ROOT ?? join10(__dirname, "..")),
12868
+ platformRoot: resolve21(process.env.MAXY_PLATFORM_ROOT ?? join9(__dirname, "..")),
13970
12869
  accountConfig: bootAccountConfig,
13971
12870
  onMessage: async (msg) => {
13972
12871
  try {
@@ -14103,11 +13002,6 @@ process.on("SIGTERM", async () => {
14103
13002
  } catch (err) {
14104
13003
  console.error(`[server] shutdown error: ${String(err)}`);
14105
13004
  }
14106
- try {
14107
- await shutdownReviewDetector();
14108
- } catch (err) {
14109
- console.error(`[server] review detector shutdown error: ${String(err)}`);
14110
- }
14111
13005
  console.error("[server] graceful shutdown complete \u2014 exiting");
14112
13006
  process.exit(0);
14113
13007
  });