@rubytech/create-realagent 1.0.816 → 1.0.818

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 (39) 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/index.d.ts +5 -0
  4. package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
  5. package/payload/platform/lib/graph-write/dist/index.js +14 -0
  6. package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
  7. package/payload/platform/lib/graph-write/src/index.ts +25 -0
  8. package/payload/platform/neo4j/edge-annotations.json +0 -8
  9. package/payload/platform/neo4j/migrations/004-prune-alien-accounts.ts +3 -4
  10. package/payload/platform/neo4j/migrations/005-removed-review-feature.ts +102 -0
  11. package/payload/platform/neo4j/schema.cypher +1 -22
  12. package/payload/platform/plugins/admin/PLUGIN.md +1 -8
  13. package/payload/platform/plugins/admin/mcp/dist/index.js +6 -44
  14. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  15. package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +2 -2
  16. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -1
  17. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +2 -3
  18. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -1
  19. package/payload/platform/plugins/cloudflare/mcp/dist/lib/setup-orchestrator.js +1 -1
  20. package/payload/platform/plugins/cloudflare/mcp/dist/lib/setup-orchestrator.js.map +1 -1
  21. package/payload/platform/plugins/docs/references/cloudflare.md +1 -3
  22. package/payload/platform/plugins/docs/references/memory-guide.md +1 -1
  23. package/payload/platform/plugins/docs/references/troubleshooting.md +8 -12
  24. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +1 -1
  25. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -1
  26. package/payload/platform/scripts/logs-read.sh +8 -38
  27. package/payload/server/chunk-AJLGI7Y3.js +10067 -0
  28. package/payload/server/chunk-ON3LBL2Y.js +1114 -0
  29. package/payload/server/chunk-P3HTEK33.js +10074 -0
  30. package/payload/server/chunk-PXQA2MA3.js +2518 -0
  31. package/payload/server/client-pool-GBY5I2KQ.js +31 -0
  32. package/payload/server/maxy-edge.js +3 -3
  33. package/payload/server/neo4j-migrations-STCKDWAL.js +364 -0
  34. package/payload/server/public/assets/{admin-Cxtmv0wo.js → admin-CdVYoqKD.js} +20 -20
  35. package/payload/server/public/assets/{graph-C4-jEPDE.js → graph-DeH6ulGh.js} +1 -1
  36. package/payload/server/public/assets/{page-zuI00fuC.js → page-WIAWD2Oi.js} +1 -1
  37. package/payload/server/public/graph.html +2 -2
  38. package/payload/server/public/index.html +2 -2
  39. package/payload/server/server.js +326 -2236
@@ -6,7 +6,6 @@ import {
6
6
  TELEGRAM_ADMIN_WEBHOOK_SECRET_FILE,
7
7
  TELEGRAM_WEBHOOK_SECRET_FILE,
8
8
  USERS_FILE,
9
- actionLogPath,
10
9
  autoDeliverPremiumPlugins,
11
10
  buildX11Env,
12
11
  callOauthLlm,
@@ -30,7 +29,6 @@ import {
30
29
  launchAction,
31
30
  load,
32
31
  logPath,
33
- reconcileCloudflareSetupFromLog,
34
32
  recordFailedAttempt,
35
33
  render,
36
34
  renderLoginPage,
@@ -53,7 +51,7 @@ import {
53
51
  vncLog,
54
52
  waitForExit,
55
53
  writeChromiumWrapper
56
- } from "./chunk-Y3UQFQM7.js";
54
+ } from "./chunk-AJLGI7Y3.js";
57
55
  import {
58
56
  agentLogStream,
59
57
  clearSessionHistory,
@@ -81,7 +79,7 @@ import {
81
79
  sigtermFlushStreamLogs,
82
80
  unregisterSession,
83
81
  validateSession
84
- } from "./chunk-UYLZDEMC.js";
82
+ } from "./chunk-ON3LBL2Y.js";
85
83
  import {
86
84
  ACCOUNTS_DIR,
87
85
  GREETING_DIRECTIVE,
@@ -121,7 +119,7 @@ import {
121
119
  verifyAndGetConversationUpdatedAt,
122
120
  verifyConversationOwnership,
123
121
  writeAdminUserAndPerson
124
- } from "./chunk-TQTMKIW6.js";
122
+ } from "./chunk-PXQA2MA3.js";
125
123
 
126
124
  // ../lib/graph-trash/dist/index.js
127
125
  var require_dist = __commonJS({
@@ -620,15 +618,15 @@ var serveStatic = (options = { root: "" }) => {
620
618
  };
621
619
 
622
620
  // server/index.ts
623
- import { readFileSync as readFileSync19, existsSync as existsSync25, watchFile } from "fs";
624
- import { resolve as resolve25, join as join11, basename as basename7 } from "path";
621
+ import { readFileSync as readFileSync15, existsSync as existsSync21, watchFile } from "fs";
622
+ import { resolve as resolve21, join as join9, basename as basename5 } from "path";
625
623
  import { homedir as homedir2 } from "os";
626
624
 
627
625
  // app/lib/agent-slug-pattern.ts
628
626
  var AGENT_SLUG_PATTERN = /^\/([a-z][a-z0-9-]{2,49})$/;
629
627
 
630
628
  // server/routes/health.ts
631
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
629
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
632
630
  import { createConnection } from "net";
633
631
 
634
632
  // app/lib/network.ts
@@ -649,1608 +647,6 @@ function getLanIp() {
649
647
  return fallback;
650
648
  }
651
649
 
652
- // app/lib/review-detector/boot.ts
653
- import { basename as basename2 } from "path";
654
-
655
- // app/lib/review-detector/rules.ts
656
- import { readFileSync, writeFileSync, existsSync as existsSync2, statSync as statSync2, mkdirSync, renameSync } from "fs";
657
- import { resolve, dirname } from "path";
658
- var DEFAULT_SCAN_INTERVAL_MS = 5e3;
659
- var RATE_LIMIT_PATTERN = "rate[- ]?limit(?:ed| reached| hit)|(?:HTTP|status)[^a-z]{0,3}429|too many requests";
660
- var RATE_LIMIT_PATTERN_V1 = "\\b429\\b|rate.?limit|too.?many.?requests";
661
- var VALID_TYPES = /* @__PURE__ */ new Set([
662
- "reconnect-loop",
663
- "repeated-error",
664
- "silent-catch",
665
- "file-write-storm",
666
- "stale-log",
667
- "rate-limit",
668
- "absent-followup"
669
- ]);
670
- var MAX_FOLLOWUP_WINDOW_MS = 6e5;
671
- var VALID_SOURCES = /* @__PURE__ */ new Set([
672
- "any",
673
- "server",
674
- "vnc",
675
- "system",
676
- "error",
677
- "session",
678
- "public",
679
- "mcp",
680
- "cloudflared",
681
- "config-dir"
682
- ]);
683
- var VALID_SCOPES = /* @__PURE__ */ new Set(["global", "session"]);
684
- function defaultRules() {
685
- return [
686
- {
687
- id: "whatsapp-reconnect-loop",
688
- name: "WhatsApp Baileys reconnect loop",
689
- type: "reconnect-loop",
690
- logSource: "server",
691
- pattern: "\\[whatsapp:baileys\\] ERROR",
692
- thresholdCount: 5,
693
- thresholdWindowMinutes: 10,
694
- suggestedAction: "Check Baileys init-queries error. Run `/investigate` on the WhatsApp subsystem, or pause WhatsApp and re-pair the account from chat."
695
- },
696
- {
697
- id: "repeated-error-generic",
698
- name: "Repeated error signature",
699
- type: "repeated-error",
700
- // Match generic ERROR lines with a bracketed prefix. Fingerprinting by
701
- // first 80 chars happens in the evaluator.
702
- logSource: "server",
703
- pattern: "\\] ERROR ",
704
- thresholdCount: 20,
705
- thresholdWindowMinutes: 60,
706
- suggestedAction: "Repeated error burst. Tail the relevant log via `logs-read` and identify the upstream cause."
707
- },
708
- {
709
- id: "silent-catch-fingerprint",
710
- name: "Silent catch-block fingerprint",
711
- type: "silent-catch",
712
- // Patterns that historically indicated a silent catch. Every match is
713
- // worth an alert — never suppressed by frequency.
714
- logSource: "any",
715
- pattern: "(UnhandledPromiseRejection|catch.*ignored|swallowed error|silent failure)",
716
- thresholdCount: 0,
717
- thresholdWindowMinutes: 0,
718
- 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'."
719
- },
720
- {
721
- id: "credentials-write-storm",
722
- name: "Credentials directory write storm",
723
- type: "file-write-storm",
724
- logSource: "config-dir",
725
- pattern: "",
726
- watchPath: "credentials",
727
- thresholdCount: 5,
728
- thresholdWindowMinutes: 5,
729
- 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."
730
- },
731
- // The stale-log rule type is fully supported by the evaluator but the
732
- // default seed does not ship an instance of it. Choosing the right file
733
- // to watch is subsystem-specific (e.g. a plugin-specific log that stops
734
- // when the plugin dies); server.log is the wrong target because it is
735
- // written continuously by the detector's own cycle events, so it never
736
- // goes stale. Users can add a targeted stale-log rule via
737
- // `review-rules-add` when they know which file matters for their
738
- // subsystem.
739
- {
740
- id: "http-rate-limit-429",
741
- name: "HTTP 429 rate-limit hit",
742
- type: "rate-limit",
743
- logSource: "any",
744
- pattern: RATE_LIMIT_PATTERN,
745
- thresholdCount: 0,
746
- thresholdWindowMinutes: 0,
747
- suggestedAction: "An external API returned 429 (rate-limited). Identify the caller and add backoff, or check the quota on the relevant API key."
748
- },
749
- {
750
- id: "approval-bypass-detected",
751
- name: "Approval gating bypass",
752
- type: "repeated-error",
753
- logSource: "error",
754
- pattern: "\\[persist\\].*(?:email-send|email-reply|whatsapp-send|whatsapp-send-document|message|contact-erase).*approval=auto-executed",
755
- thresholdCount: 0,
756
- thresholdWindowMinutes: 0,
757
- 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."
758
- },
759
- {
760
- // Task 530: catches the bridgeai-style class where a single conversation
761
- // sees the same tool error repeatedly and the agent silently falls back.
762
- // Session-scoped so cross-conversation coincidence doesn't trigger it.
763
- id: "tool-result-recurring-errors",
764
- name: "Tool result errors recurring in a conversation",
765
- type: "repeated-error",
766
- logSource: "system",
767
- pattern: "\\[tool-result\\].*error=true",
768
- thresholdCount: 2,
769
- thresholdWindowMinutes: 5,
770
- scope: "session",
771
- 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.'
772
- },
773
- {
774
- // Task 532: closes the "60-second black-box tool wait" class. Fires when
775
- // a tool hits the 30-second mark of the mid-flight heartbeat. The
776
- // pattern anchors on elapsed=30s specifically (the tool-wait tick emits
777
- // one line per 5s per tool, so matching every tick would be noisy) and
778
- // excludes tools whose long runtime is expected: `Task`/`Agent` subagent
779
- // dispatch, `Bash` with explicit long timeouts.
780
- id: "tool-wait-long-stall",
781
- name: "Tool wait exceeds 30 seconds (possible stall)",
782
- type: "repeated-error",
783
- logSource: "system",
784
- pattern: "\\[tool-wait\\][^\\n]*name=(?!Task\\b|Agent\\b|Bash\\b)[A-Za-z0-9_]+[^\\n]*elapsed=30s",
785
- thresholdCount: 0,
786
- thresholdWindowMinutes: 0,
787
- scope: "session",
788
- 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."
789
- },
790
- {
791
- // Task 536: detect agents ignoring the WEBFETCH_CANNOT_READ_JS_SPA
792
- // structured failure. A single SPA short-circuit per conversation is
793
- // expected — the hook is doing its job. Two or more in the same
794
- // conversation within 5 minutes means either (a) the agent retried
795
- // WebFetch on the same SPA URL despite the directive, or (b) the
796
- // owner is asking about multiple SPA URLs in one session and the
797
- // pattern needs surfacing as a recurring class. Both signal that the
798
- // IDENTITY.md "Tool Failure Discipline" guidance is not landing in the
799
- // prompt — revise the copy rather than add mechanical enforcement.
800
- id: "webfetch-spa-short-circuit-recurring",
801
- name: "WebFetch JS-SPA short-circuit fired repeatedly in conversation",
802
- type: "repeated-error",
803
- logSource: "system",
804
- pattern: "WEBFETCH_CANNOT_READ_JS_SPA",
805
- thresholdCount: 2,
806
- thresholdWindowMinutes: 5,
807
- scope: "session",
808
- 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."
809
- },
810
- {
811
- // Task 867 — fires when setup-tunnel.sh emits step=done but no
812
- // `[persist] role=user … Cloudflare setup completed (actionId: <id>)`
813
- // line appears within 60s. Covers the three failure modes of the
814
- // action-relay-queue plumbing: queue-write failed, boot-drain consumer
815
- // skipped the record, or the agent's hoisted persist threw. The
816
- // followup pattern is intentionally narrow to the cloudflare-setup
817
- // relay shape (not a general "any user persist") so a benign user
818
- // typing in chat does not satisfy the followup. logSource=any so
819
- // the rule sees both the script tee (in stream logs / server.log)
820
- // and the [persist] line (server.log).
821
- id: "cloudflare-setup-relay-not-acknowledged",
822
- name: "Cloudflare-setup completed but the chat relay never acknowledged",
823
- type: "absent-followup",
824
- logSource: "any",
825
- pattern: "\\[script:setup-tunnel\\] step=done",
826
- followupPattern: "\\[persist\\] .*role=user.* Cloudflare setup completed \\(actionId:",
827
- followupWindowMs: 6e4,
828
- thresholdCount: 0,
829
- thresholdWindowMinutes: 0,
830
- suggestedAction: "[Task 867] cloudflare-setup completed but the post-action relay never reached the chat history. Check `[action-relay-queue] phase=enqueued` (queue write), `[action-completion-relay] phase=consumed` (boot-drain ran), and `[persist] role=user \u2026 Cloudflare setup completed` (graph write). One of those is missing; the matching grep recipe is in the Task 867 brief Observability section."
831
- },
832
- {
833
- // Task 879 §C — onboarding turn-completion contract: any assistant
834
- // turn that narrates a step transition with a non-question-terminated
835
- // phrase ("Moving to step 9 — operator persona...", "Step 8 done.")
836
- // must be followed within 30s by either a `render-component` call
837
- // (the next step's UI surfaces) or `onboarding-complete-step`
838
- // (deterministic step advance). The trigger pattern matches the
839
- // `[onboarding-step-narration]` line written by stream-parser at the
840
- // assistant text block emit site (only for non-question phrases).
841
- // Operator's symptom in Task 879's evidence: "Moving to step 9..."
842
- // followed by 4 minutes of silence until the operator nudged with
843
- // "yeah, so?" — exact failure shape this rule catches.
844
- id: "assistant-step-advance-deadend",
845
- name: "Onboarding step-advance narration without follow-up tool call",
846
- type: "absent-followup",
847
- logSource: "session",
848
- pattern: "\\[onboarding-step-narration\\]",
849
- followupPattern: "\\[render-component\\]|\\[tool-use\\][^\\n]*name=mcp__admin__onboarding-complete-step",
850
- followupWindowMs: 3e4,
851
- thresholdCount: 0,
852
- thresholdWindowMinutes: 0,
853
- scope: "session",
854
- suggestedAction: '[Task 879 \xA7C] An onboarding turn narrated a step transition (e.g. "Moving to step N", "Step N done.") but did not render a component or call `onboarding-complete-step` within 30s. The operator is left with a dead-end paragraph and no actionable surface. Check the `platform/plugins/admin/skills/onboarding/SKILL.md` Turn-completion contract section \u2014 the agent must end any step-advance turn with `render-component` or a `?`-terminated question, never bare prose.'
855
- },
856
- {
857
- // Task 538: fires when a [spawn] line appears in a conversation's stream
858
- // log but no subprocess-lifecycle marker follows within 10s. The three
859
- // acceptable followups are Task 535's contract — at least one must be
860
- // emitted immediately at every spawn site. Their absence means
861
- // `teeProcStderrToStreamLog` regressed, the markers drifted, or the
862
- // spawn site was added without wiring them up. Session scope so a single
863
- // broken conversation fires exactly once, not N times for every spawn.
864
- id: "subproc-tee-silent-spawn",
865
- name: "Subprocess spawn without a stderr-tee lifecycle marker",
866
- type: "absent-followup",
867
- logSource: "system",
868
- pattern: "\\[spawn\\] pid=\\d+",
869
- followupPattern: "\\[subproc-stderr-tee-attached\\]|\\[subproc-debug-unavailable\\]|\\[subproc-stderr-skip\\]",
870
- followupWindowMs: 1e4,
871
- thresholdCount: 0,
872
- thresholdWindowMinutes: 0,
873
- scope: "session",
874
- 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)."
875
- },
876
- {
877
- // Task 533: surface every Cloudflare-plugin refusal. The plugin emits
878
- // exactly one [cloudflare:refuse] line per refusal with a structured
879
- // reason field; any single occurrence on a previously-clean device
880
- // means the bound Cloudflare account does not match the operator's
881
- // intent (or the post-flight FQDN drifted) and the operator needs to
882
- // act in the dashboard.
883
- id: "cloudflare-refuse",
884
- name: "Cloudflare plugin refusal",
885
- type: "silent-catch",
886
- logSource: "any",
887
- pattern: "\\[cloudflare:refuse\\]|\\[cloudflare:post-flight-mismatch\\]",
888
- thresholdCount: 0,
889
- thresholdWindowMinutes: 0,
890
- 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`."
891
- },
892
- {
893
- // Task 540: the single highest-priority refusal — surface it immediately
894
- // and independently of the generic cloudflare-refuse rule so the admin
895
- // agent sees it on the very next turn. This is the exact class that
896
- // burned the operator for 8 days across 9+ sessions (Apr 11–18, 2026):
897
- // tunnel running locally, dashboard serving the wrong account, nothing
898
- // from the internet reaches the laptop, and no prior telemetry surfaced
899
- // it in time for the agent to self-correct.
900
- id: "cloudflare-bound-account-mismatch",
901
- name: "Cloudflare bound account does not own the configured hostnames",
902
- type: "silent-catch",
903
- logSource: "any",
904
- pattern: '"reason":"bound-account-does-not-own-hostname"',
905
- thresholdCount: 0,
906
- thresholdWindowMinutes: 0,
907
- 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`."
908
- },
909
- {
910
- // Task 545: tunnel-login's terminal-failure class — cloudflared's
911
- // login process died without writing cert.pem. Covers every reason
912
- // the handler emits on the `failed` branch: either an unknown exit
913
- // (`-without-cert`), an exit preceded by the courtesy browser-launch
914
- // marker (`-with-marker`), auth URL never produced (`-timeout`), or
915
- // crashed before producing it at all. Task 541's original pattern
916
- // matched `reason=browser-launch-fetch-error` — Task 545 retired
917
- // that reason because the marker alone is no longer terminal
918
- // (cloudflared keeps its OAuth-callback loop alive after emitting
919
- // it). Use this pattern for any new terminal reason the handler
920
- // gains: extend the alternation rather than adding parallel rules.
921
- id: "cloudflare-tunnel-login-failed",
922
- name: "Cloudflare tunnel-login process terminated without writing cert",
923
- type: "silent-catch",
924
- logSource: "any",
925
- pattern: "\\[cloudflare:tunnel-login:failed\\] reason=(login-process-exited-without-cert|login-process-exited-with-marker|auth-url-timeout|process-exited-before-auth-url)",
926
- thresholdCount: 0,
927
- thresholdWindowMinutes: 0,
928
- 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."
929
- },
930
- {
931
- // Task 545: non-terminal advisory. cloudflared's browser-launch
932
- // subcommand failed (DISPLAY unreachable, xdg-open absent, etc.) but
933
- // its OAuth-callback listener is still running — the login can still
934
- // complete if a human opens the URL. This rule fires so the admin
935
- // agent can relay "open the URL yourself" to the operator the moment
936
- // the condition appears, rather than waiting for the operator to
937
- // notice nothing is happening in their browser. Task 546 will
938
- // obsolete the advisory by rendering auth URLs that auto-open in
939
- // the VNC browser.
940
- id: "cloudflare-tunnel-login-browser-launch-failed",
941
- name: "cloudflared couldn't open the sign-in URL (login still live)",
942
- type: "silent-catch",
943
- logSource: "any",
944
- pattern: "\\[cloudflare:tunnel-login:browser-launch-failed\\]",
945
- thresholdCount: 0,
946
- thresholdWindowMinutes: 0,
947
- 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)."
948
- },
949
- {
950
- // Task 545: raw-log surface. cloudflared-login.log is now read by
951
- // the review-detector's `cloudflared` source (see sources.ts). The
952
- // literal "Failed to fetch resource" is emitted by cloudflared
953
- // itself when its browser-launch subcommand can't reach a display.
954
- // This rule catches the cloudflared-side event even if the MCP
955
- // handler's classification drifts or the platform is restarting
956
- // mid-login and the advisory log line is not yet written. Keeping
957
- // this rule on a different source (log-line, not MCP stderr) makes
958
- // the detection redundant in the "defence in depth" sense — if the
959
- // MCP classification ever regresses, this still fires.
960
- id: "cloudflared-login-browser-launch-failed-raw",
961
- name: "cloudflared login \u2014 browser-launch fetch error in log",
962
- type: "silent-catch",
963
- logSource: "cloudflared",
964
- pattern: "Failed to fetch resource",
965
- thresholdCount: 0,
966
- thresholdWindowMinutes: 0,
967
- 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."
968
- },
969
- {
970
- // Task 540: cloudflared.log is the one file most likely to carry the
971
- // "tunnel is having real-world connectivity problems" signal — QUIC
972
- // connection failures, connector drops, edge unreachability. Prior to
973
- // this rule it was written but never read (the review-detector had no
974
- // rule coverage for it). A single ERR line is worth surfacing; the
975
- // tee'd output is typically noise-free.
976
- // Task 862: setup-tunnel.sh emits `[script:setup-tunnel]
977
- // step=onboarding-persist result=skipped reason=no-account-dir` via
978
- // phase_line when ACCOUNT_DIR is unset. Pre-Task-862, the form-driven
979
- // action runner threaded STREAM_LOG_PATH but not ACCOUNT_DIR, so the
980
- // line landed in the agent's stream log (system source) and the user
981
- // looped on currentStep=6 indefinitely.
982
- // The agent-via-Bash path also threads STREAM_LOG_PATH; operator-SSH
983
- // does not — that's the disambiguator. If this pattern reappears in
984
- // a system log, a future invocation surface forgot to declare
985
- // ACCOUNT_DIR. Fix at action-runner.ts WHITELIST['cloudflare-setup'],
986
- // not in the script.
987
- id: "cloudflare-setup-account-dir-missing",
988
- name: "cloudflare-setup ran without ACCOUNT_DIR \u2014 onboarding step-7 will not persist",
989
- type: "silent-catch",
990
- logSource: "system",
991
- pattern: "\\[script:setup-tunnel\\] step=onboarding-persist result=skipped reason=no-account-dir",
992
- thresholdCount: 0,
993
- thresholdWindowMinutes: 0,
994
- 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."
995
- },
996
- {
997
- id: "cloudflared-edge-errors",
998
- name: "cloudflared edge connectivity errors",
999
- type: "silent-catch",
1000
- logSource: "cloudflared",
1001
- pattern: "^\\S+ ERR (Failed to refresh protocol|no more connections active|Failed to dial a quic connection)",
1002
- thresholdCount: 0,
1003
- thresholdWindowMinutes: 0,
1004
- 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."
1005
- },
1006
- {
1007
- // Task 543: fires when an agent turn contains an opposing-axis choice-fork
1008
- // question ("Want me to X, or Y?"). Every occurrence is a violation of the
1009
- // IDENTITY § Questions rule — Rule A (one-sided questions) catches them
1010
- // all, and the sharper sub-class (Rule B — no menu when a tool returned a
1011
- // deterministic signal) is isolated by Task 544's `preceded-by` tightening.
1012
- // logSource is "any" so the rule catches violations on both the admin
1013
- // stream (claude-agent-stream-*) and the public agent stream
1014
- // (public-agent-stream-*); the regex is agent-phrasing-specific and has
1015
- // not been observed in other log contexts. Session scope groups matches
1016
- // by conversationId so a single offending turn fires exactly once.
1017
- id: "agent-choice-fork",
1018
- name: "Agent emitted a choice-fork question",
1019
- type: "repeated-error",
1020
- logSource: "any",
1021
- pattern: "Want me to [^\\n]+, or [^\\n]+\\?",
1022
- thresholdCount: 1,
1023
- thresholdWindowMinutes: 60,
1024
- scope: "session",
1025
- 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.'
1026
- },
1027
- {
1028
- // Task 546: fires when the operator clicks a device-bound URL affordance
1029
- // and the chat UI cannot drive the device browser — either CDP is
1030
- // unreachable, the navigation timed out, or CDP returned an error. The
1031
- // log line carries intent, hostname, and navigateResult so the admin
1032
- // agent can name the affected flow and hostname verbatim on its next
1033
- // turn. Every occurrence is worth surfacing (thresholdCount: 0) because
1034
- // this is the exact class of silent failure Task 546 exists to close:
1035
- // the operator clicked, nothing happened on the device, and if we don't
1036
- // review the click telemetry the agent has no way to know the flow is
1037
- // stuck.
1038
- id: "device-url-click-failed",
1039
- name: "Device-bound URL click failed to drive the VNC browser",
1040
- type: "silent-catch",
1041
- logSource: "server",
1042
- // Enumerate the NavigateResult union explicitly rather than relying
1043
- // on a (?!ok) negative lookahead anchored to a specific token order.
1044
- // If a new member is added to the union in cdp-client.ts, this rule
1045
- // must be updated in the same commit — the pattern is order-agnostic
1046
- // (browser= and navigateResult= can appear in either order) and the
1047
- // enumerated list compile-fails the source if it ever drifts from
1048
- // the shared type in device-url-schema.ts.
1049
- pattern: "\\[device-url:click\\][^\\n]*(?:browser=fallback|navigateResult=(?:timeout|cdp-unreachable|error))",
1050
- thresholdCount: 0,
1051
- thresholdWindowMinutes: 0,
1052
- 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."
1053
- },
1054
- {
1055
- // Task 554: fires when an agent turn emits a synthesized localhost/127.0.0.1
1056
- // `__remote-auth/setup` URL. The only legitimate source for that URL is
1057
- // `remote-auth-status`, which emits it inside a `maxy-device-url` affordance
1058
- // block with the device's real hostname. A synthesized localhost variant is
1059
- // the exact failure mode Task 554 closed: the removed "direct the user to
1060
- // /__remote-auth/setup" clause in the password-setter's tool description
1061
- // invited URL synthesis from a partial path. Session scope groups matches
1062
- // by conversationId so a single offending turn fires exactly once, even if
1063
- // the URL appears multiple times in the streamed response.
1064
- id: "invented-remote-auth-url",
1065
- name: "Invented localhost/127.0.0.1 remote-auth setup URL",
1066
- type: "silent-catch",
1067
- logSource: "any",
1068
- pattern: "http(s)?://(localhost|127\\.0\\.0\\.1)[:\\w/.-]*__remote-auth/setup",
1069
- thresholdCount: 0,
1070
- thresholdWindowMinutes: 0,
1071
- scope: "session",
1072
- 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."
1073
- },
1074
- {
1075
- // Task 553: fires when `anthropic-setup` auto-resets a revoked API key.
1076
- // The auto-reset is silent to the agent (the tool falls through to
1077
- // awaiting_signin in the same call), but it is operator-visible — the
1078
- // user's previously-stored key was just deleted because Anthropic
1079
- // rejected it. Surfacing the event lets the admin agent explain on the
1080
- // next turn why the user is being asked to sign in again instead of
1081
- // continuing normally. The matching string is the exact stable prefix
1082
- // emitted by the state machine immediately before `deleteKey()` is
1083
- // invoked.
1084
- id: "anthropic-setup-auth-error-auto-reset",
1085
- name: "Anthropic API key auto-reset on auth_error",
1086
- type: "silent-catch",
1087
- logSource: "any",
1088
- pattern: "\\[anthropic-setup\\] auth_error",
1089
- thresholdCount: 0,
1090
- thresholdWindowMinutes: 0,
1091
- 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."
1092
- },
1093
- {
1094
- // Task 562: setup-tunnel.sh persists step-7 completion to a filesystem
1095
- // flag before arming the service restart. The flag is consumed by the
1096
- // next session's `loadOnboardingStep` and `getOnboardingState` calls,
1097
- // so any runtime failure to write it means the next admin session
1098
- // will re-ask the Cloudflare question the user just answered. Every
1099
- // occurrence is loud-surface-worthy: the cause is either a permission
1100
- // issue on the onboarding directory or a filesystem problem on the
1101
- // device, and the recovery is deterministic (the operator can re-run
1102
- // setup-tunnel.sh or call `onboarding-complete-step` explicitly).
1103
- id: "setup-tunnel-onboarding-persist-failed",
1104
- name: "setup-tunnel.sh failed to persist step-7 completion",
1105
- type: "silent-catch",
1106
- logSource: "any",
1107
- pattern: "\\[script:setup-tunnel\\] step=onboarding-persist result=error",
1108
- thresholdCount: 0,
1109
- thresholdWindowMinutes: 0,
1110
- 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."
1111
- },
1112
- {
1113
- // Task 570: every `[llm-call] adminModel resolution FAILED:` line
1114
- // is a workflow LLM step that could not resolve a model because
1115
- // readAdminModel returned null. The stderr line carries `reason=`
1116
- // (enoent/eacces/fs_error/parse_error/field_missing/
1117
- // field_wrong_type/field_empty) so the admin agent can act on the
1118
- // specific failure mode rather than reading the generic persisted
1119
- // error and misdiagnosing. Every occurrence is worth surfacing —
1120
- // the workflow already failed when this line was emitted.
1121
- id: "workflow-admin-model-resolution-failed",
1122
- name: "Workflow readAdminModel returned null (reason code in log)",
1123
- type: "silent-catch",
1124
- logSource: "any",
1125
- pattern: "\\[llm-call\\] adminModel resolution FAILED:",
1126
- thresholdCount: 0,
1127
- thresholdWindowMinutes: 0,
1128
- 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."
1129
- },
1130
- {
1131
- // Task 561: fires when an admin-agent Bash tool call installs
1132
- // `bind9-dnsutils` at runtime. Post-Task-561 the Maxy installer
1133
- // (platform/scripts/setup.sh) provisions the package on every fresh
1134
- // and upgrade install, so `dig` is always in PATH before
1135
- // setup-tunnel.sh runs. An admin apt-install of bind9-dnsutils is
1136
- // therefore evidence the installer regressed (or the host is on a
1137
- // pre-Task-561 image); in either case the fix is to re-run the
1138
- // installer, not to patch apt-state from chat. Session scope so a
1139
- // single offending turn fires exactly once.
1140
- id: "admin-agent-apt-bind9-install",
1141
- name: "Admin agent installed bind9-dnsutils at runtime (installer regression)",
1142
- type: "repeated-error",
1143
- logSource: "any",
1144
- pattern: "\\[tool-use\\][^\\n]*name=Bash[^\\n]*apt-get install[^\\n]*bind9-dnsutils",
1145
- thresholdCount: 1,
1146
- thresholdWindowMinutes: 60,
1147
- scope: "session",
1148
- 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."
1149
- }
1150
- ];
1151
- }
1152
- function rulesFilePath(configDir2) {
1153
- return resolve(configDir2, "review-rules.json");
1154
- }
1155
- function ensureRulesFile(configDir2) {
1156
- const path2 = rulesFilePath(configDir2);
1157
- if (existsSync2(path2)) return { created: false, path: path2 };
1158
- mkdirSync(dirname(path2), { recursive: true });
1159
- const body = {
1160
- scanIntervalMs: DEFAULT_SCAN_INTERVAL_MS,
1161
- rules: defaultRules()
1162
- };
1163
- atomicWriteJson(path2, body);
1164
- return { created: true, path: path2 };
1165
- }
1166
- function loadRules(configDir2) {
1167
- const path2 = rulesFilePath(configDir2);
1168
- if (!existsSync2(path2)) {
1169
- throw new Error(`rules file missing at ${path2}`);
1170
- }
1171
- const raw = readFileSync(path2, "utf-8");
1172
- let parsed;
1173
- try {
1174
- parsed = JSON.parse(raw);
1175
- } catch (err) {
1176
- throw new Error(`rules file ${path2} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
1177
- }
1178
- return validateRulesFile(parsed, path2);
1179
- }
1180
- function rulesFileMtime(configDir2) {
1181
- const path2 = rulesFilePath(configDir2);
1182
- try {
1183
- return statSync2(path2).mtimeMs;
1184
- } catch {
1185
- return null;
1186
- }
1187
- }
1188
- function saveRules(configDir2, file) {
1189
- validateRulesFile(file, rulesFilePath(configDir2));
1190
- atomicWriteJson(rulesFilePath(configDir2), file);
1191
- }
1192
- function atomicWriteJson(path2, body) {
1193
- const tmp = `${path2}.tmp.${process.pid}.${Date.now()}`;
1194
- writeFileSync(tmp, JSON.stringify(body, null, 2) + "\n", "utf-8");
1195
- renameSync(tmp, path2);
1196
- }
1197
- function validateRulesFile(input, sourceLabel) {
1198
- if (!input || typeof input !== "object") {
1199
- throw new Error(`${sourceLabel}: top-level must be an object`);
1200
- }
1201
- const obj = input;
1202
- const scanIntervalMs = obj.scanIntervalMs;
1203
- if (typeof scanIntervalMs !== "number" || scanIntervalMs < 500 || scanIntervalMs > 3e5) {
1204
- throw new Error(`${sourceLabel}: scanIntervalMs must be a number between 500 and 300000`);
1205
- }
1206
- const rulesRaw = obj.rules;
1207
- if (!Array.isArray(rulesRaw)) {
1208
- throw new Error(`${sourceLabel}: rules must be an array`);
1209
- }
1210
- const ids = /* @__PURE__ */ new Set();
1211
- const rules = rulesRaw.map((r, i) => validateRule(r, `${sourceLabel}#rules[${i}]`, ids));
1212
- return { scanIntervalMs, rules };
1213
- }
1214
- function addMissingDefaultRules(rulesFile) {
1215
- const existingIds = new Set(rulesFile.rules.map((r) => r.id));
1216
- const defaults = defaultRules();
1217
- let mutated = false;
1218
- for (const rule of defaults) {
1219
- if (!existingIds.has(rule.id)) {
1220
- rulesFile.rules.push(rule);
1221
- mutated = true;
1222
- }
1223
- }
1224
- return mutated;
1225
- }
1226
- function migrateRateLimitPattern(rulesFile) {
1227
- const rule = rulesFile.rules.find((r) => r.id === "http-rate-limit-429");
1228
- if (!rule) return false;
1229
- if (rule.pattern !== RATE_LIMIT_PATTERN_V1) return false;
1230
- rule.pattern = RATE_LIMIT_PATTERN;
1231
- return true;
1232
- }
1233
- function validateRule(input, label, seenIds) {
1234
- if (!input || typeof input !== "object") {
1235
- throw new Error(`${label}: rule must be an object`);
1236
- }
1237
- const r = input;
1238
- const id = r.id;
1239
- if (typeof id !== "string" || id.length === 0) {
1240
- throw new Error(`${label}: id must be a non-empty string`);
1241
- }
1242
- if (seenIds.has(id)) {
1243
- throw new Error(`${label}: duplicate rule id "${id}"`);
1244
- }
1245
- seenIds.add(id);
1246
- const name = r.name;
1247
- if (typeof name !== "string" || name.length === 0) {
1248
- throw new Error(`${label}: name must be a non-empty string`);
1249
- }
1250
- const type = r.type;
1251
- if (typeof type !== "string" || !VALID_TYPES.has(type)) {
1252
- throw new Error(`${label}: type must be one of ${[...VALID_TYPES].join(", ")}`);
1253
- }
1254
- const logSource = r.logSource;
1255
- if (typeof logSource !== "string" || !VALID_SOURCES.has(logSource)) {
1256
- throw new Error(`${label}: logSource must be one of ${[...VALID_SOURCES].join(", ")}`);
1257
- }
1258
- const pattern = r.pattern;
1259
- if (typeof pattern !== "string") {
1260
- throw new Error(`${label}: pattern must be a string (may be empty for stale-log/file-write-storm)`);
1261
- }
1262
- if (pattern.length > 0) {
1263
- try {
1264
- new RegExp(pattern);
1265
- } catch (err) {
1266
- throw new Error(`${label}: pattern is not a valid regex: ${err instanceof Error ? err.message : String(err)}`);
1267
- }
1268
- }
1269
- const thresholdCount = r.thresholdCount;
1270
- if (typeof thresholdCount !== "number" || thresholdCount < 0) {
1271
- throw new Error(`${label}: thresholdCount must be a non-negative number`);
1272
- }
1273
- const thresholdWindowMinutes = r.thresholdWindowMinutes;
1274
- if (typeof thresholdWindowMinutes !== "number" || thresholdWindowMinutes < 0) {
1275
- throw new Error(`${label}: thresholdWindowMinutes must be a non-negative number`);
1276
- }
1277
- const suggestedAction = r.suggestedAction;
1278
- if (typeof suggestedAction !== "string" || suggestedAction.length === 0) {
1279
- throw new Error(`${label}: suggestedAction must be a non-empty string`);
1280
- }
1281
- const rule = {
1282
- id,
1283
- name,
1284
- type,
1285
- logSource,
1286
- pattern,
1287
- thresholdCount,
1288
- thresholdWindowMinutes,
1289
- suggestedAction
1290
- };
1291
- if (typeof r.watchPath === "string") rule.watchPath = r.watchPath;
1292
- if (typeof r.staleHours === "number") rule.staleHours = r.staleHours;
1293
- if (typeof r.followupPattern === "string") {
1294
- if (r.followupPattern.length > 0) {
1295
- try {
1296
- new RegExp(r.followupPattern);
1297
- } catch (err) {
1298
- throw new Error(`${label}: followupPattern is not a valid regex: ${err instanceof Error ? err.message : String(err)}`);
1299
- }
1300
- }
1301
- rule.followupPattern = r.followupPattern;
1302
- }
1303
- if (typeof r.followupWindowMs === "number") rule.followupWindowMs = r.followupWindowMs;
1304
- if (typeof r.suppressedUntil === "string") rule.suppressedUntil = r.suppressedUntil;
1305
- if (r.scope !== void 0) {
1306
- if (typeof r.scope !== "string" || !VALID_SCOPES.has(r.scope)) {
1307
- throw new Error(`${label}: scope must be one of ${[...VALID_SCOPES].join(", ")}`);
1308
- }
1309
- rule.scope = r.scope;
1310
- }
1311
- if (rule.type === "file-write-storm" || rule.type === "stale-log") {
1312
- if (!rule.watchPath) {
1313
- throw new Error(`${label}: ${rule.type} rules require watchPath`);
1314
- }
1315
- }
1316
- if (rule.type === "stale-log") {
1317
- if (typeof rule.staleHours !== "number" || rule.staleHours <= 0) {
1318
- throw new Error(`${label}: stale-log rules require a positive staleHours`);
1319
- }
1320
- }
1321
- if (rule.type === "absent-followup") {
1322
- if (rule.pattern.length === 0) {
1323
- throw new Error(`${label}: absent-followup rules require a non-empty pattern`);
1324
- }
1325
- if (typeof rule.followupPattern !== "string" || rule.followupPattern.length === 0) {
1326
- throw new Error(`${label}: absent-followup rules require a non-empty followupPattern`);
1327
- }
1328
- if (typeof rule.followupWindowMs !== "number" || rule.followupWindowMs <= 0 || rule.followupWindowMs > MAX_FOLLOWUP_WINDOW_MS) {
1329
- throw new Error(`${label}: absent-followup rules require followupWindowMs in (0, ${MAX_FOLLOWUP_WINDOW_MS}]`);
1330
- }
1331
- }
1332
- return rule;
1333
- }
1334
-
1335
- // app/lib/review-detector/sources.ts
1336
- 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";
1337
- import { resolve as resolve2, join as join2, basename, dirname as dirname2 } from "path";
1338
- function tailStatePath(configDir2) {
1339
- return resolve2(configDir2, "review-state.json");
1340
- }
1341
- function loadTailState(configDir2) {
1342
- const path2 = tailStatePath(configDir2);
1343
- if (!existsSync3(path2)) return {};
1344
- try {
1345
- const raw = readFileSync2(path2, "utf-8");
1346
- const parsed = JSON.parse(raw);
1347
- if (!parsed || typeof parsed !== "object") return {};
1348
- const clean = {};
1349
- for (const [key, value] of Object.entries(parsed)) {
1350
- const entry = value;
1351
- if (entry && typeof entry.offset === "number" && typeof entry.size === "number" && typeof entry.inode === "number") {
1352
- clean[key] = entry;
1353
- }
1354
- }
1355
- return clean;
1356
- } catch (err) {
1357
- console.error(`[review] tail state corrupt at ${path2}, starting fresh: ${err instanceof Error ? err.message : String(err)}`);
1358
- return {};
1359
- }
1360
- }
1361
- function saveTailState(configDir2, state) {
1362
- const path2 = tailStatePath(configDir2);
1363
- mkdirSync2(dirname2(path2), { recursive: true });
1364
- const tmp = `${path2}.tmp.${process.pid}.${Date.now()}`;
1365
- writeFileSync2(tmp, JSON.stringify(state, null, 2) + "\n", "utf-8");
1366
- renameSync2(tmp, path2);
1367
- }
1368
- function discoverSourceFiles(configDir2, accountLogDir2, logicalSource) {
1369
- if (logicalSource === "server") {
1370
- const p = resolve2(configDir2, "logs", "server.log");
1371
- return existsSync3(p) ? [{ logicalSource: "server", filepath: p }] : [];
1372
- }
1373
- if (logicalSource === "vnc") {
1374
- const p = resolve2(configDir2, "logs", "vnc-boot.log");
1375
- return existsSync3(p) ? [{ logicalSource: "vnc", filepath: p }] : [];
1376
- }
1377
- if (logicalSource === "cloudflared") {
1378
- const files2 = [];
1379
- const daemon = resolve2(configDir2, "logs", "cloudflared.log");
1380
- if (existsSync3(daemon)) files2.push({ logicalSource: "cloudflared", filepath: daemon });
1381
- const login = resolve2(configDir2, "logs", "cloudflared-login.log");
1382
- if (existsSync3(login)) files2.push({ logicalSource: "cloudflared", filepath: login });
1383
- return files2;
1384
- }
1385
- const prefix = {
1386
- system: "claude-agent-stream-",
1387
- error: "claude-agent-stderr-",
1388
- session: "sse-events-",
1389
- public: "public-agent-stream-",
1390
- mcp: "mcp-"
1391
- }[logicalSource];
1392
- if (!existsSync3(accountLogDir2)) return [];
1393
- const files = [];
1394
- let scanned = 0;
1395
- let skippedPrefixMismatch = 0;
1396
- let skippedNotLog = 0;
1397
- for (const entry of readdirSync(accountLogDir2)) {
1398
- scanned += 1;
1399
- const matchesPrefix = entry.startsWith(prefix);
1400
- const isLog = entry.endsWith(".log");
1401
- if (matchesPrefix && isLog) {
1402
- files.push({ logicalSource, filepath: join2(accountLogDir2, entry) });
1403
- } else if (!matchesPrefix) {
1404
- skippedPrefixMismatch += 1;
1405
- } else {
1406
- skippedNotLog += 1;
1407
- }
1408
- }
1409
- files.sort((a, b) => {
1410
- try {
1411
- return statSync3(b.filepath).mtimeMs - statSync3(a.filepath).mtimeMs;
1412
- } catch {
1413
- return a.filepath.localeCompare(b.filepath);
1414
- }
1415
- });
1416
- if (skippedPrefixMismatch > 0 || skippedNotLog > 0) {
1417
- console.error(`[review-scan-skip] dir=${accountLogDir2} source=${logicalSource} prefix=${prefix} scanned=${scanned} matched=${files.length} skipped_prefix=${skippedPrefixMismatch} skipped_non_log=${skippedNotLog}`);
1418
- }
1419
- return files;
1420
- }
1421
- function discoverAllSources(configDir2, accountLogDir2) {
1422
- return [
1423
- ...discoverSourceFiles(configDir2, accountLogDir2, "server"),
1424
- ...discoverSourceFiles(configDir2, accountLogDir2, "vnc"),
1425
- ...discoverSourceFiles(configDir2, accountLogDir2, "system"),
1426
- ...discoverSourceFiles(configDir2, accountLogDir2, "error"),
1427
- ...discoverSourceFiles(configDir2, accountLogDir2, "session"),
1428
- ...discoverSourceFiles(configDir2, accountLogDir2, "public"),
1429
- ...discoverSourceFiles(configDir2, accountLogDir2, "mcp"),
1430
- ...discoverSourceFiles(configDir2, accountLogDir2, "cloudflared")
1431
- ];
1432
- }
1433
- function readNewLines(filepath, prev) {
1434
- if (!existsSync3(filepath)) return null;
1435
- const stat7 = statSync3(filepath);
1436
- const size = stat7.size;
1437
- const inode = stat7.ino;
1438
- let startOffset = 0;
1439
- let rotated = false;
1440
- let truncated = false;
1441
- if (prev) {
1442
- if (prev.inode !== inode) {
1443
- rotated = true;
1444
- startOffset = 0;
1445
- } else if (size < prev.offset) {
1446
- truncated = true;
1447
- startOffset = 0;
1448
- } else {
1449
- startOffset = prev.offset;
1450
- }
1451
- }
1452
- if (startOffset >= size) {
1453
- return {
1454
- lines: [],
1455
- entry: { offset: size, size, inode },
1456
- rotated,
1457
- truncated
1458
- };
1459
- }
1460
- const fd = openSync(filepath, "r");
1461
- try {
1462
- const bufSize = Math.max(0, size - startOffset);
1463
- const buf = Buffer.alloc(bufSize);
1464
- if (bufSize > 0) {
1465
- readSync(fd, buf, 0, bufSize, startOffset);
1466
- }
1467
- const text = buf.toString("utf-8");
1468
- const lines = text.length > 0 ? text.split("\n") : [];
1469
- let newOffset = size;
1470
- if (lines.length > 0 && !text.endsWith("\n")) {
1471
- const partial = lines.pop();
1472
- newOffset = size - Buffer.byteLength(partial, "utf-8");
1473
- } else if (lines.length > 0 && text.endsWith("\n")) {
1474
- if (lines[lines.length - 1] === "") lines.pop();
1475
- }
1476
- return {
1477
- lines,
1478
- entry: { offset: newOffset, size, inode },
1479
- rotated,
1480
- truncated
1481
- };
1482
- } finally {
1483
- closeSync(fd);
1484
- }
1485
- }
1486
- function countRecentWrites(dir, sinceMs) {
1487
- if (!existsSync3(dir)) return 0;
1488
- let count = 0;
1489
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1490
- if (!entry.isFile()) continue;
1491
- try {
1492
- const st = statSync3(join2(dir, entry.name));
1493
- if (st.mtimeMs >= sinceMs) count += 1;
1494
- } catch {
1495
- }
1496
- }
1497
- return count;
1498
- }
1499
- function fileLastWriteMs(path2) {
1500
- if (!existsSync3(path2)) return null;
1501
- try {
1502
- return statSync3(path2).mtimeMs;
1503
- } catch {
1504
- return null;
1505
- }
1506
- }
1507
- function accountLogDir(accountDir) {
1508
- return resolve2(accountDir, "logs");
1509
- }
1510
- function sourceKey(file) {
1511
- return `${file.logicalSource}:${basename(file.filepath)}`;
1512
- }
1513
-
1514
- // app/lib/review-detector/writer.ts
1515
- import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync3, statSync as statSync4 } from "fs";
1516
- import { resolve as resolve3, dirname as dirname3 } from "path";
1517
- import { randomUUID } from "crypto";
1518
- function reviewLogPath(configDir2) {
1519
- return resolve3(configDir2, "logs", "review.log");
1520
- }
1521
- function pendingAlertsPath(configDir2) {
1522
- return resolve3(configDir2, "review-pending-alerts.jsonl");
1523
- }
1524
- function reviewLog(configDir2, event) {
1525
- const path2 = reviewLogPath(configDir2);
1526
- try {
1527
- mkdirSync3(dirname3(path2), { recursive: true });
1528
- const line = `${new Date(
1529
- typeof event.ts === "number" ? event.ts : Date.now()
1530
- ).toISOString()} [review] ${JSON.stringify(event)}
1531
- `;
1532
- appendFileSync(path2, line, "utf-8");
1533
- } catch (err) {
1534
- console.error(`[review] failed to write review log at ${path2}: ${err instanceof Error ? err.message : String(err)}`);
1535
- }
1536
- }
1537
- async function ensureReviewAlertIndex() {
1538
- const session = getSession();
1539
- try {
1540
- await session.run(
1541
- `CREATE INDEX review_alert_lookup IF NOT EXISTS
1542
- FOR (a:ReviewAlert)
1543
- ON (a.accountId, a.resolvedAt, a.lastMatchAt)`
1544
- );
1545
- } finally {
1546
- await session.close();
1547
- }
1548
- }
1549
- async function ensureReviewDigestSchedule(accountId) {
1550
- const eventId = `review-digest-${accountId}`;
1551
- const now = (/* @__PURE__ */ new Date()).toISOString();
1552
- const next = /* @__PURE__ */ new Date();
1553
- if (next.getHours() >= 8) {
1554
- next.setDate(next.getDate() + 1);
1555
- }
1556
- next.setHours(8, 0, 0, 0);
1557
- const nextRun = next.toISOString();
1558
- const session = getSession();
1559
- try {
1560
- await session.run(
1561
- `MERGE (e:Event { eventId: $eventId })
1562
- ON CREATE SET
1563
- e.accountId = $accountId,
1564
- e.name = 'Daily review digest',
1565
- 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.',
1566
- e.startDate = $now,
1567
- e.eventStatus = 'scheduled',
1568
- e.recurrence = '0 8 * * *',
1569
- e.nextRun = datetime($nextRun),
1570
- e.sourcePlugin = 'admin',
1571
- e.actionPlugin = 'admin',
1572
- e.actionTool = 'review-digest-compose',
1573
- e.actionArgs = '{}',
1574
- e.createdAt = $now,
1575
- e.updatedAt = $now
1576
- ON MATCH SET
1577
- e.actionPlugin = 'admin',
1578
- e.actionTool = 'review-digest-compose',
1579
- e.updatedAt = $now`,
1580
- { eventId, accountId, now, nextRun }
1581
- );
1582
- } finally {
1583
- await session.close();
1584
- }
1585
- }
1586
- async function countActiveReviewAlerts(accountId) {
1587
- const session = getSession();
1588
- try {
1589
- const result = await session.run(
1590
- `MATCH (a:ReviewAlert {accountId: $accountId})
1591
- WHERE a.resolvedAt IS NULL
1592
- AND (a.suppressedUntil IS NULL OR a.suppressedUntil < datetime())
1593
- RETURN count(a) AS n`,
1594
- { accountId }
1595
- );
1596
- const n = result.records[0]?.get("n");
1597
- if (typeof n === "number") return n;
1598
- if (n && typeof n.toNumber === "function") {
1599
- return n.toNumber();
1600
- }
1601
- return 0;
1602
- } finally {
1603
- await session.close();
1604
- }
1605
- }
1606
- async function upsertReviewAlert(accountId, match) {
1607
- const session = getSession();
1608
- try {
1609
- await session.run(
1610
- `MERGE (a:ReviewAlert { ruleId: $ruleId, accountId: $accountId })
1611
- ON CREATE SET
1612
- a.alertId = $alertId,
1613
- a.ruleName = $ruleName,
1614
- a.firstMatchAt = datetime($matchedAt),
1615
- a.lastMatchAt = datetime($matchedAt),
1616
- a.cumulativeMatchCount = 1,
1617
- a.sampleEvidence = $sampleEvidence,
1618
- a.suggestedAction = $suggestedAction,
1619
- a.suppressedUntil = null,
1620
- a.resolvedAt = null
1621
- ON MATCH SET
1622
- a.lastMatchAt = datetime($matchedAt),
1623
- a.cumulativeMatchCount = a.cumulativeMatchCount + 1,
1624
- a.sampleEvidence = $sampleEvidence,
1625
- a.suggestedAction = $suggestedAction,
1626
- a.resolvedAt = null`,
1627
- {
1628
- ruleId: match.ruleId,
1629
- accountId,
1630
- alertId: randomUUID(),
1631
- ruleName: match.ruleName,
1632
- matchedAt: new Date(match.matchedAt).toISOString(),
1633
- sampleEvidence: match.sampleEvidence,
1634
- suggestedAction: match.suggestedAction
1635
- }
1636
- );
1637
- } finally {
1638
- await session.close();
1639
- }
1640
- }
1641
- function queueAlert(configDir2, accountId, match) {
1642
- const path2 = pendingAlertsPath(configDir2);
1643
- try {
1644
- mkdirSync3(dirname3(path2), { recursive: true });
1645
- const line = JSON.stringify({ accountId, match }) + "\n";
1646
- appendFileSync(path2, line, "utf-8");
1647
- } catch (err) {
1648
- console.error(`[review] failed to queue alert at ${path2}: ${err instanceof Error ? err.message : String(err)}`);
1649
- }
1650
- }
1651
- async function drainPendingAlerts(configDir2) {
1652
- const path2 = pendingAlertsPath(configDir2);
1653
- if (!existsSync4(path2)) return { drained: 0, remaining: 0 };
1654
- const raw = readFileSync3(path2, "utf-8");
1655
- const lines = raw.split("\n").filter((l) => l.trim().length > 0);
1656
- if (lines.length === 0) return { drained: 0, remaining: 0 };
1657
- const remaining = [];
1658
- let drained = 0;
1659
- for (const line of lines) {
1660
- let entry = null;
1661
- try {
1662
- entry = JSON.parse(line);
1663
- } catch {
1664
- continue;
1665
- }
1666
- try {
1667
- await upsertReviewAlert(entry.accountId, entry.match);
1668
- drained += 1;
1669
- } catch {
1670
- remaining.push(line);
1671
- }
1672
- }
1673
- const tmp = `${path2}.tmp.${process.pid}.${Date.now()}`;
1674
- if (remaining.length > 0) {
1675
- writeFileSync3(tmp, remaining.join("\n") + "\n", "utf-8");
1676
- } else {
1677
- writeFileSync3(tmp, "", "utf-8");
1678
- }
1679
- renameSync3(tmp, path2);
1680
- return { drained, remaining: remaining.length };
1681
- }
1682
-
1683
- // app/lib/review-detector/boot.ts
1684
- async function bootDetector() {
1685
- const account = resolveAccount();
1686
- if (!account) {
1687
- console.error("[review] boot: no account resolved \u2014 detector not starting (Phase 0 expects exactly one account)");
1688
- return null;
1689
- }
1690
- const configDir2 = MAXY_DIR;
1691
- const accountId = account.accountId;
1692
- const accountDir = account.accountDir;
1693
- const ensured = ensureRulesFile(configDir2);
1694
- if (ensured.created) {
1695
- reviewLog(configDir2, { event: "rules-defaults-created", path: ensured.path });
1696
- }
1697
- let rulesFile;
1698
- try {
1699
- rulesFile = loadRules(configDir2);
1700
- } catch (err) {
1701
- reviewLog(configDir2, {
1702
- event: "boot-failed",
1703
- reason: "rules-invalid",
1704
- error: err instanceof Error ? err.message : String(err)
1705
- });
1706
- console.error(`[review] boot: rules file invalid \u2014 ${err instanceof Error ? err.message : String(err)}`);
1707
- return null;
1708
- }
1709
- if (addMissingDefaultRules(rulesFile)) {
1710
- saveRules(configDir2, rulesFile);
1711
- reviewLog(configDir2, { event: "rules-updated", update: "added-missing-defaults" });
1712
- console.error("[review] boot: added missing default rules to existing rules file");
1713
- }
1714
- if (migrateRateLimitPattern(rulesFile)) {
1715
- saveRules(configDir2, rulesFile);
1716
- reviewLog(configDir2, { event: "rules-migrated", migration: "rate-limit-pattern-v2" });
1717
- console.error("[review] boot: migrated http-rate-limit-429 pattern to v2 (Task 408)");
1718
- }
1719
- try {
1720
- await ensureReviewAlertIndex();
1721
- reviewLog(configDir2, { event: "neo4j-index-ensured" });
1722
- } catch (err) {
1723
- reviewLog(configDir2, {
1724
- event: "neo4j-index-failed",
1725
- error: err instanceof Error ? err.message : String(err)
1726
- });
1727
- console.error(`[review] boot: ensureReviewAlertIndex failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
1728
- }
1729
- try {
1730
- await ensureReviewDigestSchedule(accountId);
1731
- reviewLog(configDir2, { event: "digest-schedule-ensured" });
1732
- } catch (err) {
1733
- reviewLog(configDir2, {
1734
- event: "digest-schedule-failed",
1735
- error: err instanceof Error ? err.message : String(err)
1736
- });
1737
- console.error(`[review] boot: ensureReviewDigestSchedule failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
1738
- }
1739
- const tailState = loadTailState(configDir2);
1740
- const logDir = accountLogDir(accountDir);
1741
- const initialSources = discoverAllSources(configDir2, logDir);
1742
- const snapshot = {
1743
- state: "running",
1744
- startedAt: Date.now(),
1745
- lastScanAt: null,
1746
- lastScanDurationMs: null,
1747
- scanCycles: 0,
1748
- rulesLoaded: rulesFile.rules.length,
1749
- sourcesTracked: initialSources.length,
1750
- activeAlerts: 0,
1751
- lastError: null
1752
- };
1753
- reviewLog(configDir2, {
1754
- event: "detector-started",
1755
- configDir: basename2(configDir2),
1756
- accountId: accountId.slice(0, 8),
1757
- rulesLoaded: rulesFile.rules.length,
1758
- sourcesDiscovered: initialSources.length,
1759
- tailStateEntries: Object.keys(tailState).length,
1760
- scanIntervalMs: rulesFile.scanIntervalMs
1761
- });
1762
- const runtime = {
1763
- configDir: configDir2,
1764
- accountId,
1765
- accountDir,
1766
- rulesFile,
1767
- rulesFileMtimeMs: rulesFileMtime(configDir2) ?? Date.now(),
1768
- ruleState: /* @__PURE__ */ new Map(),
1769
- tailState,
1770
- snapshot,
1771
- stopped: false
1772
- };
1773
- return runtime;
1774
- }
1775
-
1776
- // app/lib/review-detector/scan-loop.ts
1777
- import { resolve as resolve4 } from "path";
1778
-
1779
- // app/lib/review-detector/evaluator.ts
1780
- var SAMPLE_MAX_CHARS = 500;
1781
- var CONV_ID_REGEX = /conversationId=([a-f0-9]{8})/;
1782
- function scopeKeyFor(rule, line) {
1783
- if (rule.scope !== "session") return "";
1784
- const m = CONV_ID_REGEX.exec(line);
1785
- return m ? m[1] : "";
1786
- }
1787
- var compiledRegexCache = /* @__PURE__ */ new Map();
1788
- function compileRegex(pattern) {
1789
- let re = compiledRegexCache.get(pattern);
1790
- if (re === void 0) {
1791
- re = new RegExp(pattern, "i");
1792
- compiledRegexCache.set(pattern, re);
1793
- }
1794
- return re;
1795
- }
1796
- function isSuppressed(rule, nowMs) {
1797
- if (!rule.suppressedUntil) return false;
1798
- const until = Date.parse(rule.suppressedUntil);
1799
- if (Number.isNaN(until)) return false;
1800
- return nowMs < until;
1801
- }
1802
- function toSample(line) {
1803
- if (line.length <= SAMPLE_MAX_CHARS) return line;
1804
- return line.slice(0, SAMPLE_MAX_CHARS) + "\u2026";
1805
- }
1806
- function newRuleState() {
1807
- return {
1808
- matchTimestamps: [],
1809
- matchTimestampsByScope: /* @__PURE__ */ new Map(),
1810
- lastAlertAt: null,
1811
- cumulativeSinceLastAlert: 0,
1812
- lastSeenAt: null,
1813
- pendingFollowups: []
1814
- };
1815
- }
1816
- function evaluateTextRule(rule, lines, state, nowMs) {
1817
- if (isSuppressed(rule, nowMs)) return { match: null, state };
1818
- if (rule.pattern.length === 0) return { match: null, state };
1819
- const regex = compileRegex(rule.pattern);
1820
- const matchesByScope = /* @__PURE__ */ new Map();
1821
- let firstSample = null;
1822
- for (const line of lines) {
1823
- if (!regex.test(line)) continue;
1824
- if (!firstSample) firstSample = line;
1825
- const key = scopeKeyFor(rule, line);
1826
- const existing = matchesByScope.get(key) ?? [];
1827
- existing.push(line);
1828
- matchesByScope.set(key, existing);
1829
- }
1830
- if (matchesByScope.size === 0) return { match: null, state };
1831
- const windowStart = rule.thresholdWindowMinutes > 0 ? nowMs - rule.thresholdWindowMinutes * 6e4 : -Infinity;
1832
- const updated = {
1833
- ...state,
1834
- matchTimestamps: [...state.matchTimestamps],
1835
- matchTimestampsByScope: new Map(state.matchTimestampsByScope ?? [])
1836
- };
1837
- let firingSample = null;
1838
- let fires = false;
1839
- const isCountZero = rule.thresholdCount === 0;
1840
- if (rule.scope === "session") {
1841
- for (const [key, hits] of matchesByScope) {
1842
- const prior = updated.matchTimestampsByScope.get(key) ?? [];
1843
- const merged = [...prior, ...hits.map(() => nowMs)].filter((t) => t >= windowStart);
1844
- if (merged.length === 0) {
1845
- updated.matchTimestampsByScope.delete(key);
1846
- } else {
1847
- updated.matchTimestampsByScope.set(key, merged);
1848
- }
1849
- if (!fires && (isCountZero || merged.length >= rule.thresholdCount)) {
1850
- fires = true;
1851
- firingSample = hits[0];
1852
- }
1853
- }
1854
- } else {
1855
- for (const hits of matchesByScope.values()) {
1856
- for (const _ of hits) updated.matchTimestamps.push(nowMs);
1857
- }
1858
- updated.matchTimestamps = updated.matchTimestamps.filter((t) => t >= windowStart);
1859
- fires = isCountZero || updated.matchTimestamps.length >= rule.thresholdCount;
1860
- if (fires) firingSample = firstSample;
1861
- }
1862
- if (!fires) {
1863
- return { match: null, state: updated };
1864
- }
1865
- const match = {
1866
- ruleId: rule.id,
1867
- ruleName: rule.name,
1868
- matchedAt: nowMs,
1869
- sampleEvidence: toSample(firingSample ?? firstSample ?? lines[0] ?? ""),
1870
- suggestedAction: rule.suggestedAction
1871
- };
1872
- return { match, state: updated };
1873
- }
1874
- function evaluateFileWriteStormRule(rule, recentWriteCount, state, nowMs) {
1875
- if (isSuppressed(rule, nowMs)) return { match: null, state };
1876
- if (recentWriteCount < rule.thresholdCount) {
1877
- return { match: null, state };
1878
- }
1879
- const match = {
1880
- ruleId: rule.id,
1881
- ruleName: rule.name,
1882
- matchedAt: nowMs,
1883
- sampleEvidence: `${recentWriteCount} file writes in ${rule.thresholdWindowMinutes}m at ${rule.watchPath}`,
1884
- suggestedAction: rule.suggestedAction
1885
- };
1886
- return { match, state };
1887
- }
1888
- function evaluateStaleLogRule(rule, lastMtimeMs, state, nowMs) {
1889
- if (isSuppressed(rule, nowMs)) return { match: null, state };
1890
- let lastSeenAt = state.lastSeenAt;
1891
- if (lastMtimeMs !== null) {
1892
- lastSeenAt = Math.max(lastSeenAt ?? 0, lastMtimeMs);
1893
- }
1894
- const updated = { ...state, lastSeenAt };
1895
- if (lastMtimeMs === null || lastSeenAt === null) {
1896
- return { match: null, state: updated };
1897
- }
1898
- const staleMs = (rule.staleHours ?? 24) * 60 * 60 * 1e3;
1899
- if (nowMs - lastSeenAt < staleMs) {
1900
- return { match: null, state: updated };
1901
- }
1902
- const match = {
1903
- ruleId: rule.id,
1904
- ruleName: rule.name,
1905
- matchedAt: nowMs,
1906
- sampleEvidence: `last write at ${new Date(lastSeenAt).toISOString()} (${rule.watchPath})`,
1907
- suggestedAction: rule.suggestedAction
1908
- };
1909
- return { match, state: updated };
1910
- }
1911
- function evaluateAbsentFollowupRule(rule, lines, state, nowMs) {
1912
- if (isSuppressed(rule, nowMs)) return { matches: [], state };
1913
- if (!rule.pattern || !rule.followupPattern || !rule.followupWindowMs) {
1914
- return { matches: [], state };
1915
- }
1916
- const triggerRegex = compileRegex(rule.pattern);
1917
- const followupRegex = compileRegex(rule.followupPattern);
1918
- const pending = [...state.pendingFollowups ?? []];
1919
- for (const line of lines) {
1920
- if (triggerRegex.test(line)) {
1921
- pending.push({
1922
- scope: scopeKeyFor(rule, line),
1923
- timestamp: nowMs,
1924
- line,
1925
- fulfilled: false
1926
- });
1927
- continue;
1928
- }
1929
- if (followupRegex.test(line)) {
1930
- const scope = scopeKeyFor(rule, line);
1931
- for (const entry of pending) {
1932
- if (!entry.fulfilled && entry.scope === scope) {
1933
- entry.fulfilled = true;
1934
- break;
1935
- }
1936
- }
1937
- }
1938
- }
1939
- const matches = [];
1940
- const kept = [];
1941
- for (const entry of pending) {
1942
- const age = nowMs - entry.timestamp;
1943
- if (age >= rule.followupWindowMs) {
1944
- if (!entry.fulfilled) {
1945
- matches.push({
1946
- ruleId: rule.id,
1947
- ruleName: rule.name,
1948
- matchedAt: nowMs,
1949
- sampleEvidence: toSample(entry.line),
1950
- suggestedAction: rule.suggestedAction,
1951
- missedForMs: age
1952
- });
1953
- }
1954
- continue;
1955
- }
1956
- kept.push(entry);
1957
- }
1958
- return {
1959
- matches,
1960
- state: { ...state, pendingFollowups: kept }
1961
- };
1962
- }
1963
- var ALERT_WINDOW_MS = 60 * 60 * 1e3;
1964
- function rateLimitDecision(state, nowMs) {
1965
- const since = state.lastAlertAt === null ? Infinity : nowMs - state.lastAlertAt;
1966
- if (since >= ALERT_WINDOW_MS) {
1967
- return {
1968
- surface: true,
1969
- state: { ...state, lastAlertAt: nowMs, cumulativeSinceLastAlert: 0 }
1970
- };
1971
- }
1972
- return {
1973
- surface: false,
1974
- state: { ...state, cumulativeSinceLastAlert: state.cumulativeSinceLastAlert + 1 }
1975
- };
1976
- }
1977
-
1978
- // app/lib/review-detector/scan-loop.ts
1979
- async function runScanCycle(runtime) {
1980
- const cycleStart = Date.now();
1981
- runtime.snapshot.scanCycles += 1;
1982
- try {
1983
- const currentMtime = rulesFileMtime(runtime.configDir);
1984
- if (currentMtime !== null && currentMtime !== runtime.rulesFileMtimeMs) {
1985
- try {
1986
- runtime.rulesFile = loadRules(runtime.configDir);
1987
- runtime.rulesFileMtimeMs = currentMtime;
1988
- runtime.snapshot.rulesLoaded = runtime.rulesFile.rules.length;
1989
- reviewLog(runtime.configDir, {
1990
- event: "rules-reloaded",
1991
- rulesLoaded: runtime.rulesFile.rules.length,
1992
- mtime: new Date(currentMtime).toISOString()
1993
- });
1994
- } catch (err) {
1995
- reviewLog(runtime.configDir, {
1996
- event: "rules-reload-failed",
1997
- error: err instanceof Error ? err.message : String(err)
1998
- });
1999
- runtime.snapshot.state = "degraded";
2000
- runtime.snapshot.lastError = err instanceof Error ? err.message : String(err);
2001
- }
2002
- }
2003
- const logDir = accountLogDir(runtime.accountDir);
2004
- const files = discoverAllSources(runtime.configDir, logDir);
2005
- runtime.snapshot.sourcesTracked = files.length;
2006
- const linesBySource = /* @__PURE__ */ new Map();
2007
- for (const file of files) {
2008
- const key = sourceKey(file);
2009
- const prev = runtime.tailState[key];
2010
- const result = readNewLines(file.filepath, prev);
2011
- if (result === null) {
2012
- if (prev) {
2013
- reviewLog(runtime.configDir, {
2014
- event: "source-vanished",
2015
- source: file.logicalSource,
2016
- filepath: file.filepath
2017
- });
2018
- delete runtime.tailState[key];
2019
- }
2020
- continue;
2021
- }
2022
- if (result.rotated) {
2023
- reviewLog(runtime.configDir, {
2024
- event: "source-rotated",
2025
- source: file.logicalSource,
2026
- filepath: file.filepath
2027
- });
2028
- }
2029
- if (result.truncated) {
2030
- reviewLog(runtime.configDir, {
2031
- event: "source-truncated",
2032
- source: file.logicalSource,
2033
- filepath: file.filepath
2034
- });
2035
- }
2036
- runtime.tailState[key] = result.entry;
2037
- if (result.lines.length > 0) {
2038
- const bucket = linesBySource.get(file.logicalSource) ?? [];
2039
- bucket.push(...result.lines);
2040
- linesBySource.set(file.logicalSource, bucket);
2041
- }
2042
- }
2043
- const matches = [];
2044
- for (const rule of runtime.rulesFile.rules) {
2045
- let state = runtime.ruleState.get(rule.id) ?? newRuleState();
2046
- if (isSuppressed(rule, cycleStart)) {
2047
- reviewLog(runtime.configDir, {
2048
- event: "rule-suppressed",
2049
- ruleId: rule.id,
2050
- suppressedUntil: rule.suppressedUntil
2051
- });
2052
- runtime.ruleState.set(rule.id, state);
2053
- continue;
2054
- }
2055
- let match = null;
2056
- if (rule.type === "reconnect-loop" || rule.type === "repeated-error" || rule.type === "silent-catch" || rule.type === "rate-limit") {
2057
- let inputLines = [];
2058
- if (rule.logSource === "any") {
2059
- for (const [src, lines] of linesBySource.entries()) {
2060
- if (src !== "config-dir") inputLines.push(...lines);
2061
- }
2062
- } else {
2063
- inputLines = linesBySource.get(rule.logSource) ?? [];
2064
- }
2065
- if (inputLines.length > 0) {
2066
- const result = evaluateTextRule(rule, inputLines, state, cycleStart);
2067
- state = result.state;
2068
- match = result.match;
2069
- }
2070
- } else if (rule.type === "file-write-storm") {
2071
- const dir = resolve4(runtime.configDir, rule.watchPath ?? "");
2072
- const sinceMs = cycleStart - rule.thresholdWindowMinutes * 6e4;
2073
- const count = countRecentWrites(dir, sinceMs);
2074
- const result = evaluateFileWriteStormRule(rule, count, state, cycleStart);
2075
- state = result.state;
2076
- match = result.match;
2077
- } else if (rule.type === "stale-log") {
2078
- const trackedPath = resolve4(runtime.configDir, rule.watchPath ?? "");
2079
- const lastMs = fileLastWriteMs(trackedPath);
2080
- const result = evaluateStaleLogRule(rule, lastMs, state, cycleStart);
2081
- state = result.state;
2082
- match = result.match;
2083
- } else if (rule.type === "absent-followup") {
2084
- let inputLines = [];
2085
- if (rule.logSource === "any") {
2086
- for (const [src, lines] of linesBySource.entries()) {
2087
- if (src !== "config-dir") inputLines.push(...lines);
2088
- }
2089
- } else {
2090
- inputLines = linesBySource.get(rule.logSource) ?? [];
2091
- }
2092
- const result = evaluateAbsentFollowupRule(rule, inputLines, state, cycleStart);
2093
- state = result.state;
2094
- for (const m of result.matches) {
2095
- const safeTrigger = m.sampleEvidence.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
2096
- console.error(
2097
- `[review-detector] absent-followup rule=${rule.id} trigger="${safeTrigger}" missed_for_ms=${m.missedForMs ?? ""}`
2098
- );
2099
- matches.push(m);
2100
- }
2101
- }
2102
- runtime.ruleState.set(rule.id, state);
2103
- if (match) matches.push(match);
2104
- }
2105
- for (const match of matches) {
2106
- const state = runtime.ruleState.get(match.ruleId) ?? newRuleState();
2107
- const decision = rateLimitDecision(state, cycleStart);
2108
- runtime.ruleState.set(match.ruleId, decision.state);
2109
- if (!decision.surface) {
2110
- reviewLog(runtime.configDir, {
2111
- event: "rate-limit-deferred",
2112
- ruleId: match.ruleId,
2113
- cumulativeSinceLastAlert: decision.state.cumulativeSinceLastAlert
2114
- });
2115
- continue;
2116
- }
2117
- reviewLog(runtime.configDir, {
2118
- event: "match",
2119
- ruleId: match.ruleId,
2120
- ruleName: match.ruleName,
2121
- sampleEvidence: match.sampleEvidence,
2122
- suggestedAction: match.suggestedAction
2123
- });
2124
- try {
2125
- await upsertReviewAlert(runtime.accountId, match);
2126
- reviewLog(runtime.configDir, { event: "alert-persisted", ruleId: match.ruleId });
2127
- } catch (err) {
2128
- queueAlert(runtime.configDir, runtime.accountId, match);
2129
- reviewLog(runtime.configDir, {
2130
- event: "alert-queued",
2131
- ruleId: match.ruleId,
2132
- error: err instanceof Error ? err.message : String(err)
2133
- });
2134
- }
2135
- }
2136
- try {
2137
- const drain = await drainPendingAlerts(runtime.configDir);
2138
- if (drain.drained > 0 || drain.remaining > 0) {
2139
- reviewLog(runtime.configDir, {
2140
- event: "queue-drain",
2141
- drained: drain.drained,
2142
- remaining: drain.remaining
2143
- });
2144
- }
2145
- } catch (err) {
2146
- reviewLog(runtime.configDir, {
2147
- event: "queue-drain-failed",
2148
- error: err instanceof Error ? err.message : String(err)
2149
- });
2150
- }
2151
- try {
2152
- runtime.snapshot.activeAlerts = await countActiveReviewAlerts(runtime.accountId);
2153
- } catch (err) {
2154
- reviewLog(runtime.configDir, {
2155
- event: "active-alerts-count-failed",
2156
- error: err instanceof Error ? err.message : String(err)
2157
- });
2158
- }
2159
- saveTailState(runtime.configDir, runtime.tailState);
2160
- const cycleDuration = Date.now() - cycleStart;
2161
- runtime.snapshot.lastScanAt = cycleStart;
2162
- runtime.snapshot.lastScanDurationMs = cycleDuration;
2163
- if (runtime.snapshot.state !== "degraded" && runtime.snapshot.state !== "failed") {
2164
- runtime.snapshot.state = "running";
2165
- }
2166
- runtime.snapshot.lastError = null;
2167
- reviewLog(runtime.configDir, {
2168
- event: "cycle",
2169
- cycles: runtime.snapshot.scanCycles,
2170
- sources: files.length,
2171
- rules: runtime.rulesFile.rules.length,
2172
- matches: matches.length,
2173
- durationMs: cycleDuration
2174
- });
2175
- } catch (err) {
2176
- runtime.snapshot.state = "degraded";
2177
- runtime.snapshot.lastError = err instanceof Error ? err.message : String(err);
2178
- reviewLog(runtime.configDir, {
2179
- event: "cycle-failed",
2180
- error: err instanceof Error ? err.message : String(err)
2181
- });
2182
- }
2183
- }
2184
- function startScanLoop(runtime) {
2185
- let inFlight = null;
2186
- const interval = setInterval(async () => {
2187
- if (runtime.stopped) return;
2188
- if (inFlight) return;
2189
- inFlight = runScanCycle(runtime);
2190
- try {
2191
- await inFlight;
2192
- } finally {
2193
- inFlight = null;
2194
- }
2195
- }, runtime.rulesFile.scanIntervalMs);
2196
- inFlight = runScanCycle(runtime);
2197
- return async () => {
2198
- runtime.stopped = true;
2199
- clearInterval(interval);
2200
- if (inFlight) {
2201
- try {
2202
- await inFlight;
2203
- } catch {
2204
- }
2205
- }
2206
- runtime.snapshot.state = "stopped";
2207
- reviewLog(runtime.configDir, { event: "detector-stopped", cycles: runtime.snapshot.scanCycles });
2208
- };
2209
- }
2210
-
2211
- // app/lib/review-detector/index.ts
2212
- var activeRuntime = null;
2213
- var stopFn = null;
2214
- async function startReviewDetector() {
2215
- if (stopFn) {
2216
- console.error("[review] startReviewDetector called twice \u2014 ignoring second call");
2217
- return;
2218
- }
2219
- try {
2220
- activeRuntime = await bootDetector();
2221
- if (!activeRuntime) return;
2222
- stopFn = startScanLoop(activeRuntime);
2223
- } catch (err) {
2224
- console.error(`[review] detector start failed: ${err instanceof Error ? err.message : String(err)}`);
2225
- }
2226
- }
2227
- async function shutdownReviewDetector() {
2228
- if (!stopFn) return;
2229
- try {
2230
- await stopFn();
2231
- } catch (err) {
2232
- console.error(`[review] detector shutdown error: ${err instanceof Error ? err.message : String(err)}`);
2233
- }
2234
- stopFn = null;
2235
- activeRuntime = null;
2236
- }
2237
- function getReviewDetectorSnapshot() {
2238
- if (!activeRuntime) {
2239
- return {
2240
- state: "stopped",
2241
- startedAt: null,
2242
- lastScanAt: null,
2243
- lastScanDurationMs: null,
2244
- scanCycles: 0,
2245
- rulesLoaded: 0,
2246
- sourcesTracked: 0,
2247
- activeAlerts: 0,
2248
- lastError: null
2249
- };
2250
- }
2251
- return activeRuntime.snapshot;
2252
- }
2253
-
2254
650
  // app/lib/whatsapp/schema.ts
2255
651
  import { z } from "zod";
2256
652
  var DmPolicySchema = z.enum(["open", "allowlist", "disabled"]);
@@ -2315,20 +711,20 @@ var WhatsAppConfigSchema = z.object({
2315
711
  });
2316
712
 
2317
713
  // app/lib/whatsapp/config-persist.ts
2318
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
2319
- import { resolve as resolve5, join as join3 } from "path";
714
+ import { readFileSync, writeFileSync, existsSync as existsSync2 } from "fs";
715
+ import { resolve, join as join2 } from "path";
2320
716
  var TAG = "[whatsapp:config]";
2321
717
  function configPath(accountDir) {
2322
- return resolve5(accountDir, "account.json");
718
+ return resolve(accountDir, "account.json");
2323
719
  }
2324
720
  function readConfig(accountDir) {
2325
721
  const path2 = configPath(accountDir);
2326
- if (!existsSync5(path2)) throw new Error(`account.json not found at ${path2}`);
2327
- return JSON.parse(readFileSync4(path2, "utf-8"));
722
+ if (!existsSync2(path2)) throw new Error(`account.json not found at ${path2}`);
723
+ return JSON.parse(readFileSync(path2, "utf-8"));
2328
724
  }
2329
725
  function writeConfig(accountDir, config) {
2330
726
  const path2 = configPath(accountDir);
2331
- writeFileSync4(path2, JSON.stringify(config, null, 2) + "\n", "utf-8");
727
+ writeFileSync(path2, JSON.stringify(config, null, 2) + "\n", "utf-8");
2332
728
  }
2333
729
  function reloadManagerConfig(accountDir) {
2334
730
  try {
@@ -2498,8 +894,8 @@ function setPublicAgent(accountDir, slug) {
2498
894
  if (!trimmed) {
2499
895
  return { ok: false, error: "Agent slug cannot be empty." };
2500
896
  }
2501
- const agentConfigPath = join3(accountDir, "agents", trimmed, "config.json");
2502
- if (!existsSync5(agentConfigPath)) {
897
+ const agentConfigPath = join2(accountDir, "agents", trimmed, "config.json");
898
+ if (!existsSync2(agentConfigPath)) {
2503
899
  return { ok: false, error: `Agent "${trimmed}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
2504
900
  }
2505
901
  try {
@@ -2562,8 +958,8 @@ function setGroupPublicAgent(accountDir, accountId, groupJid, slug) {
2562
958
  if (!trimmedSlug) return { ok: false, error: "Agent slug cannot be empty." };
2563
959
  if (!trimmedGroup) return { ok: false, error: "Group JID cannot be empty." };
2564
960
  if (!trimmedAccount) return { ok: false, error: "Account ID cannot be empty." };
2565
- const agentConfigPath = join3(accountDir, "agents", trimmedSlug, "config.json");
2566
- if (!existsSync5(agentConfigPath)) {
961
+ const agentConfigPath = join2(accountDir, "agents", trimmedSlug, "config.json");
962
+ if (!existsSync2(agentConfigPath)) {
2567
963
  return { ok: false, error: `Agent "${trimmedSlug}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
2568
964
  }
2569
965
  try {
@@ -2792,7 +1188,7 @@ function listCredentialAccountIds(configDir2) {
2792
1188
  }
2793
1189
 
2794
1190
  // app/lib/whatsapp/session.ts
2795
- import { randomUUID as randomUUID2 } from "crypto";
1191
+ import { randomUUID } from "crypto";
2796
1192
  import fsSync2 from "fs";
2797
1193
  import fs2 from "fs/promises";
2798
1194
  import { inspect } from "util";
@@ -2867,7 +1263,7 @@ var credsSaveQueue = Promise.resolve();
2867
1263
  async function drainCredsSaveQueue(timeoutMs = 5e3) {
2868
1264
  console.error(`${TAG3} draining credential save queue\u2026`);
2869
1265
  const timer2 = new Promise(
2870
- (resolve26) => setTimeout(() => resolve26("timeout"), timeoutMs)
1266
+ (resolve22) => setTimeout(() => resolve22("timeout"), timeoutMs)
2871
1267
  );
2872
1268
  const result = await Promise.race([
2873
1269
  credsSaveQueue.then(() => "drained"),
@@ -2995,11 +1391,11 @@ async function createWaSocket(opts) {
2995
1391
  return sock;
2996
1392
  }
2997
1393
  async function waitForConnection(sock) {
2998
- return new Promise((resolve26, reject) => {
1394
+ return new Promise((resolve22, reject) => {
2999
1395
  const handler = (update) => {
3000
1396
  if (update.connection === "open") {
3001
1397
  sock.ev.off("connection.update", handler);
3002
- resolve26();
1398
+ resolve22();
3003
1399
  }
3004
1400
  if (update.connection === "close") {
3005
1401
  sock.ev.off("connection.update", handler);
@@ -3113,14 +1509,14 @@ ${inspected}`;
3113
1509
  return inspect2(err, INSPECT_OPTS2);
3114
1510
  }
3115
1511
  function withTimeout(label, promise, timeoutMs) {
3116
- return new Promise((resolve26, reject) => {
1512
+ return new Promise((resolve22, reject) => {
3117
1513
  const timer2 = setTimeout(() => {
3118
1514
  reject(new Error(`${label} timed out after ${timeoutMs}ms`));
3119
1515
  }, timeoutMs);
3120
1516
  promise.then(
3121
1517
  (value) => {
3122
1518
  clearTimeout(timer2);
3123
- resolve26(value);
1519
+ resolve22(value);
3124
1520
  },
3125
1521
  (err) => {
3126
1522
  clearTimeout(timer2);
@@ -3655,8 +2051,8 @@ async function persistWhatsAppMessage(input) {
3655
2051
  const { givenName, familyName } = splitName(input.pushName);
3656
2052
  const prev = sessionWriteLocks.get(input.sessionKey);
3657
2053
  let release;
3658
- const mine = new Promise((resolve26) => {
3659
- release = resolve26;
2054
+ const mine = new Promise((resolve22) => {
2055
+ release = resolve22;
3660
2056
  });
3661
2057
  const chained = (prev ?? Promise.resolve()).then(() => mine);
3662
2058
  sessionWriteLocks.set(input.sessionKey, chained);
@@ -3841,8 +2237,8 @@ async function ensureWhatsAppConversation(input) {
3841
2237
  }
3842
2238
 
3843
2239
  // app/lib/whatsapp/platform-account-id.ts
3844
- import { readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
3845
- import { resolve as resolve6 } from "path";
2240
+ import { readdirSync, readFileSync as readFileSync2 } from "fs";
2241
+ import { resolve as resolve2 } from "path";
3846
2242
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3847
2243
  var cached = null;
3848
2244
  var cachedAccountsDir = null;
@@ -3866,7 +2262,7 @@ function resolvePlatformAccountId(accountsDir = ACCOUNTS_DIR) {
3866
2262
  function enumerateValidAccountIds(accountsDir) {
3867
2263
  let names;
3868
2264
  try {
3869
- names = readdirSync2(accountsDir);
2265
+ names = readdirSync(accountsDir);
3870
2266
  } catch (err) {
3871
2267
  if (err.code === "ENOENT") return [];
3872
2268
  throw err;
@@ -3874,9 +2270,9 @@ function enumerateValidAccountIds(accountsDir) {
3874
2270
  const valid = [];
3875
2271
  for (const name of names) {
3876
2272
  if (!UUID_RE.test(name)) continue;
3877
- const configPath2 = resolve6(accountsDir, name, "account.json");
2273
+ const configPath2 = resolve2(accountsDir, name, "account.json");
3878
2274
  try {
3879
- JSON.parse(readFileSync5(configPath2, "utf-8"));
2275
+ JSON.parse(readFileSync2(configPath2, "utf-8"));
3880
2276
  valid.push(name);
3881
2277
  } catch (err) {
3882
2278
  const code = err.code;
@@ -3887,9 +2283,9 @@ function enumerateValidAccountIds(accountsDir) {
3887
2283
  }
3888
2284
 
3889
2285
  // app/lib/whatsapp/inbound/media.ts
3890
- import { randomUUID as randomUUID3 } from "crypto";
2286
+ import { randomUUID as randomUUID2 } from "crypto";
3891
2287
  import { writeFile, mkdir } from "fs/promises";
3892
- import { join as join4 } from "path";
2288
+ import { join as join3 } from "path";
3893
2289
  import {
3894
2290
  downloadMediaMessage,
3895
2291
  downloadContentFromMessage,
@@ -3974,8 +2370,8 @@ async function downloadInboundMedia(msg, sock, opts) {
3974
2370
  }
3975
2371
  await mkdir(MEDIA_DIR, { recursive: true });
3976
2372
  const ext = mimeToExt(mimetype ?? "application/octet-stream");
3977
- const filename = `${randomUUID3()}.${ext}`;
3978
- const filePath = join4(MEDIA_DIR, filename);
2373
+ const filename = `${randomUUID2()}.${ext}`;
2374
+ const filePath = join3(MEDIA_DIR, filename);
3979
2375
  await writeFile(filePath, buffer);
3980
2376
  const sizeKB = (buffer.length / 1024).toFixed(0);
3981
2377
  console.error(`${TAG9} media downloaded type=${mimetype ?? "unknown"} size=${sizeKB}KB path=${filePath}`);
@@ -4663,11 +3059,11 @@ async function connectWithReconnect(conn) {
4663
3059
  console.error(
4664
3060
  `${TAG13} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${decision.nextAttempts}/${maxAttempts})`
4665
3061
  );
4666
- await new Promise((resolve26) => {
4667
- const timer2 = setTimeout(resolve26, delay);
3062
+ await new Promise((resolve22) => {
3063
+ const timer2 = setTimeout(resolve22, delay);
4668
3064
  conn.abortController.signal.addEventListener("abort", () => {
4669
3065
  clearTimeout(timer2);
4670
- resolve26();
3066
+ resolve22();
4671
3067
  }, { once: true });
4672
3068
  });
4673
3069
  }
@@ -4675,16 +3071,16 @@ async function connectWithReconnect(conn) {
4675
3071
  }
4676
3072
  }
4677
3073
  function waitForDisconnectEvent(conn) {
4678
- return new Promise((resolve26) => {
3074
+ return new Promise((resolve22) => {
4679
3075
  if (!conn.sock) {
4680
- resolve26();
3076
+ resolve22();
4681
3077
  return;
4682
3078
  }
4683
3079
  const sock = conn.sock;
4684
3080
  const handler = (update) => {
4685
3081
  if (update.connection === "close") {
4686
3082
  sock.ev.off("connection.update", handler);
4687
- resolve26();
3083
+ resolve22();
4688
3084
  }
4689
3085
  };
4690
3086
  sock.ev.on("connection.update", handler);
@@ -4945,8 +3341,8 @@ async function handleInboundMessage(conn, msg) {
4945
3341
  const conversationKey = isGroup ? remoteJid : senderPhone;
4946
3342
  const debounceKey = `${conn.accountId}:${conversationKey}:${senderPhone}`;
4947
3343
  let resolvePending;
4948
- const sttPending = new Promise((resolve26) => {
4949
- resolvePending = resolve26;
3344
+ const sttPending = new Promise((resolve22) => {
3345
+ resolvePending = resolve22;
4950
3346
  });
4951
3347
  if (conn.debouncer) conn.debouncer.registerPending(debounceKey, sttPending);
4952
3348
  try {
@@ -5067,20 +3463,20 @@ async function probeApiKey() {
5067
3463
  return result.status;
5068
3464
  }
5069
3465
  function checkPort(port2, timeoutMs = 500) {
5070
- return new Promise((resolve26) => {
3466
+ return new Promise((resolve22) => {
5071
3467
  const socket = createConnection(port2, "127.0.0.1");
5072
3468
  socket.setTimeout(timeoutMs);
5073
3469
  socket.once("connect", () => {
5074
3470
  socket.destroy();
5075
- resolve26(true);
3471
+ resolve22(true);
5076
3472
  });
5077
3473
  socket.once("error", () => {
5078
3474
  socket.destroy();
5079
- resolve26(false);
3475
+ resolve22(false);
5080
3476
  });
5081
3477
  socket.once("timeout", () => {
5082
3478
  socket.destroy();
5083
- resolve26(false);
3479
+ resolve22(false);
5084
3480
  });
5085
3481
  });
5086
3482
  }
@@ -5089,8 +3485,8 @@ app.get("/", async (c) => {
5089
3485
  const browserTransport = resolveBrowserTransport(c.req.raw, c.env?.incoming?.socket?.remoteAddress);
5090
3486
  let pinConfigured = false;
5091
3487
  try {
5092
- if (existsSync6(USERS_FILE)) {
5093
- const raw = readFileSync6(USERS_FILE, "utf-8").trim();
3488
+ if (existsSync3(USERS_FILE)) {
3489
+ const raw = readFileSync3(USERS_FILE, "utf-8").trim();
5094
3490
  if (raw) {
5095
3491
  const users = JSON.parse(raw);
5096
3492
  pinConfigured = Array.isArray(users) && users.length > 0;
@@ -5109,7 +3505,7 @@ app.get("/", async (c) => {
5109
3505
  const vncRunning = await checkPort(6080);
5110
3506
  let apiKeyConfigured = false;
5111
3507
  try {
5112
- apiKeyConfigured = existsSync6(keyFilePath());
3508
+ apiKeyConfigured = existsSync3(keyFilePath());
5113
3509
  } catch {
5114
3510
  }
5115
3511
  let apiKeyStatus = "missing";
@@ -5142,7 +3538,6 @@ app.get("/", async (c) => {
5142
3538
  const step = await loadOnboardingStep(account.accountId);
5143
3539
  if (step !== null) onboardingComplete = step >= 6;
5144
3540
  }
5145
- const reviewDetector = getReviewDetectorSnapshot();
5146
3541
  return c.json({
5147
3542
  pin_configured: pinConfigured,
5148
3543
  claude_authenticated: claudeAuthenticated,
@@ -5158,31 +3553,20 @@ app.get("/", async (c) => {
5158
3553
  any_connected: whatsappAnyConnected,
5159
3554
  any_stuck: whatsappAnyStuck,
5160
3555
  accounts: whatsappAccounts
5161
- },
5162
- review_detector: {
5163
- state: reviewDetector.state,
5164
- started_at: reviewDetector.startedAt,
5165
- last_scan_at: reviewDetector.lastScanAt,
5166
- last_scan_duration_ms: reviewDetector.lastScanDurationMs,
5167
- scan_cycles: reviewDetector.scanCycles,
5168
- rules_loaded: reviewDetector.rulesLoaded,
5169
- sources_tracked: reviewDetector.sourcesTracked,
5170
- active_alerts: reviewDetector.activeAlerts,
5171
- last_error: reviewDetector.lastError
5172
3556
  }
5173
3557
  });
5174
3558
  });
5175
3559
  var health_default = app;
5176
3560
 
5177
3561
  // server/routes/session.ts
5178
- import { resolve as resolve7 } from "path";
5179
- import { existsSync as existsSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4 } from "fs";
3562
+ import { resolve as resolve3 } from "path";
3563
+ import { existsSync as existsSync4, writeFileSync as writeFileSync2, mkdirSync } from "fs";
5180
3564
  var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5181
3565
  function writeBrandingCache(accountId, agentSlug, branding) {
5182
3566
  try {
5183
- const cacheDir = resolve7(MAXY_DIR, "branding-cache", accountId);
5184
- mkdirSync4(cacheDir, { recursive: true });
5185
- writeFileSync5(resolve7(cacheDir, `${agentSlug}.json`), JSON.stringify(branding), "utf-8");
3567
+ const cacheDir = resolve3(MAXY_DIR, "branding-cache", accountId);
3568
+ mkdirSync(cacheDir, { recursive: true });
3569
+ writeFileSync2(resolve3(cacheDir, `${agentSlug}.json`), JSON.stringify(branding), "utf-8");
5186
3570
  } catch (err) {
5187
3571
  console.error(`[branding] cache write failed: ${err instanceof Error ? err.message : String(err)}`);
5188
3572
  }
@@ -5252,9 +3636,9 @@ app2.post("/", async (c) => {
5252
3636
  }
5253
3637
  let agentConfig = null;
5254
3638
  if (account) {
5255
- const agentDir = resolve7(account.accountDir, "agents", agentSlug);
5256
- const agentConfigPath = resolve7(agentDir, "config.json");
5257
- if (!existsSync7(agentDir) || !existsSync7(agentConfigPath)) {
3639
+ const agentDir = resolve3(account.accountDir, "agents", agentSlug);
3640
+ const agentConfigPath = resolve3(agentDir, "config.json");
3641
+ if (!existsSync4(agentDir) || !existsSync4(agentConfigPath)) {
5258
3642
  return c.json({ error: "Agent not found" }, 404);
5259
3643
  }
5260
3644
  agentConfig = resolveAgentConfig(account.accountDir, agentSlug);
@@ -5504,12 +3888,12 @@ ${raw}`;
5504
3888
  }
5505
3889
 
5506
3890
  // app/lib/attachments.ts
5507
- import { randomUUID as randomUUID4 } from "crypto";
3891
+ import { randomUUID as randomUUID3 } from "crypto";
5508
3892
  import { mkdir as mkdir2, readFile, stat as stat2, writeFile as writeFile2 } from "fs/promises";
5509
3893
  import { realpathSync } from "fs";
5510
- import { resolve as resolve8, extname, basename as basename3 } from "path";
5511
- var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ?? resolve8(process.cwd(), "../platform");
5512
- var ATTACHMENTS_ROOT = resolve8(PLATFORM_ROOT2, "..", "data/uploads");
3894
+ import { resolve as resolve4, extname, basename } from "path";
3895
+ var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ?? resolve4(process.cwd(), "../platform");
3896
+ var ATTACHMENTS_ROOT = resolve4(PLATFORM_ROOT2, "..", "data/uploads");
5513
3897
  var SUPPORTED_MIME_TYPES = /* @__PURE__ */ new Set([
5514
3898
  "image/jpeg",
5515
3899
  "image/png",
@@ -5535,12 +3919,12 @@ function assertSupportedMime(mimeType) {
5535
3919
  }
5536
3920
  }
5537
3921
  async function writeAttachment(scope, filename, mimeType, sizeBytes, buffer) {
5538
- const attachmentId = randomUUID4();
5539
- const dir = resolve8(ATTACHMENTS_ROOT, scope, attachmentId);
3922
+ const attachmentId = randomUUID3();
3923
+ const dir = resolve4(ATTACHMENTS_ROOT, scope, attachmentId);
5540
3924
  await mkdir2(dir, { recursive: true });
5541
3925
  const ext = extname(filename) || "";
5542
- const storagePath = resolve8(dir, `${attachmentId}${ext}`);
5543
- const metaPath = resolve8(dir, `${attachmentId}.meta.json`);
3926
+ const storagePath = resolve4(dir, `${attachmentId}${ext}`);
3927
+ const metaPath = resolve4(dir, `${attachmentId}.meta.json`);
5544
3928
  const meta = {
5545
3929
  attachmentId,
5546
3930
  scope,
@@ -5614,7 +3998,7 @@ async function storeGeneratedFile(accountId, filePath) {
5614
3998
  `File exceeds the 20 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB).`
5615
3999
  );
5616
4000
  }
5617
- const filename = basename3(filePath);
4001
+ const filename = basename(filePath);
5618
4002
  const mimeType = detectMimeType(filePath);
5619
4003
  const buffer = await readFile(filePath);
5620
4004
  return writeAttachment(accountId, filename, mimeType, fileStat.size, buffer);
@@ -5623,7 +4007,7 @@ async function storeGeneratedFile(accountId, filePath) {
5623
4007
  // app/lib/stt/voice-note.ts
5624
4008
  import { writeFile as writeFile3, mkdtemp, rm } from "fs/promises";
5625
4009
  import { tmpdir } from "os";
5626
- import { join as join5 } from "path";
4010
+ import { join as join4 } from "path";
5627
4011
  var TAG14 = "[voice]";
5628
4012
  var AUDIO_MIME_TYPES = /* @__PURE__ */ new Set([
5629
4013
  "audio/ogg",
@@ -5661,9 +4045,9 @@ async function transcribeVoiceNote(file, source) {
5661
4045
  let tempDir;
5662
4046
  let tempPath;
5663
4047
  try {
5664
- tempDir = await mkdtemp(join5(tmpdir(), "voice-"));
4048
+ tempDir = await mkdtemp(join4(tmpdir(), "voice-"));
5665
4049
  const ext = audioExtension(mimeType);
5666
- tempPath = join5(tempDir, `recording${ext}`);
4050
+ tempPath = join4(tempDir, `recording${ext}`);
5667
4051
  const buffer = Buffer.from(await file.arrayBuffer());
5668
4052
  await writeFile3(tempPath, buffer);
5669
4053
  } catch (err) {
@@ -6218,16 +4602,16 @@ var group_default = app4;
6218
4602
 
6219
4603
  // app/lib/access-gate.ts
6220
4604
  import neo4j from "neo4j-driver";
6221
- import { readFileSync as readFileSync7 } from "fs";
6222
- import { resolve as resolve9 } from "path";
6223
- import { randomUUID as randomUUID5, randomInt } from "crypto";
6224
- var PLATFORM_ROOT3 = process.env.MAXY_PLATFORM_ROOT ?? resolve9(process.cwd(), "..");
4605
+ import { readFileSync as readFileSync4 } from "fs";
4606
+ import { resolve as resolve5 } from "path";
4607
+ import { randomUUID as randomUUID4, randomInt } from "crypto";
4608
+ var PLATFORM_ROOT3 = process.env.MAXY_PLATFORM_ROOT ?? resolve5(process.cwd(), "..");
6225
4609
  var driver = null;
6226
4610
  function readPassword() {
6227
4611
  if (process.env.NEO4J_PASSWORD) return process.env.NEO4J_PASSWORD;
6228
- const passwordFile = resolve9(PLATFORM_ROOT3, "config/.neo4j-password");
4612
+ const passwordFile = resolve5(PLATFORM_ROOT3, "config/.neo4j-password");
6229
4613
  try {
6230
- return readFileSync7(passwordFile, "utf-8").trim();
4614
+ return readFileSync4(passwordFile, "utf-8").trim();
6231
4615
  } catch {
6232
4616
  throw new Error(
6233
4617
  `Neo4j password not found. Expected at ${passwordFile} or in NEO4J_PASSWORD env var.`
@@ -6476,7 +4860,7 @@ async function setGrantPassword(grantId, passwordHash) {
6476
4860
  }
6477
4861
  }
6478
4862
  async function generateNewMagicToken(grantId) {
6479
- const token = randomUUID5();
4863
+ const token = randomUUID4();
6480
4864
  const session = getSession2();
6481
4865
  try {
6482
4866
  await session.run(
@@ -6538,19 +4922,19 @@ async function findActiveGrantByContact(contactValue, agentSlug, accountId) {
6538
4922
  }
6539
4923
 
6540
4924
  // app/lib/brevo-sms.ts
6541
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync8, chmodSync } from "fs";
6542
- import { dirname as dirname4 } from "path";
6543
- import { resolve as resolve10 } from "path";
6544
- var BREVO_API_KEY_FILE = resolve10(MAXY_DIR, ".brevo-api-key");
4925
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync5, chmodSync } from "fs";
4926
+ import { dirname } from "path";
4927
+ import { resolve as resolve6 } from "path";
4928
+ var BREVO_API_KEY_FILE = resolve6(MAXY_DIR, ".brevo-api-key");
6545
4929
  var BREVO_API_URL = "https://api.brevo.com/v3/transactionalSMS/sms";
6546
4930
  var BREVO_TIMEOUT_MS = 1e4;
6547
4931
  var BREVO_SENDER = "Maxy";
6548
4932
  var platformRoot = process.env.MAXY_PLATFORM_ROOT;
6549
4933
  if (platformRoot) {
6550
4934
  try {
6551
- const brandPath = resolve10(platformRoot, "config", "brand.json");
6552
- if (existsSync8(brandPath)) {
6553
- const brand = JSON.parse(readFileSync8(brandPath, "utf-8"));
4935
+ const brandPath = resolve6(platformRoot, "config", "brand.json");
4936
+ if (existsSync5(brandPath)) {
4937
+ const brand = JSON.parse(readFileSync5(brandPath, "utf-8"));
6554
4938
  if (brand.productName) BREVO_SENDER = brand.productName;
6555
4939
  }
6556
4940
  } catch {
@@ -6558,7 +4942,7 @@ if (platformRoot) {
6558
4942
  }
6559
4943
  function readBrevoApiKey() {
6560
4944
  try {
6561
- const key = readFileSync8(BREVO_API_KEY_FILE, "utf-8").trim();
4945
+ const key = readFileSync5(BREVO_API_KEY_FILE, "utf-8").trim();
6562
4946
  if (!key) {
6563
4947
  throw new Error(`Brevo API key file is empty: ${BREVO_API_KEY_FILE}`);
6564
4948
  }
@@ -6573,7 +4957,7 @@ function readBrevoApiKey() {
6573
4957
  }
6574
4958
  }
6575
4959
  function hasBrevoApiKey() {
6576
- return existsSync8(BREVO_API_KEY_FILE);
4960
+ return existsSync5(BREVO_API_KEY_FILE);
6577
4961
  }
6578
4962
  async function sendSms(recipient, content, opts) {
6579
4963
  let apiKey;
@@ -6989,7 +5373,7 @@ app5.post("/send-otp", async (c) => {
6989
5373
  var access_default = app5;
6990
5374
 
6991
5375
  // server/routes/telegram.ts
6992
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
5376
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
6993
5377
  import { timingSafeEqual } from "crypto";
6994
5378
 
6995
5379
  // app/lib/telegram/access-control.ts
@@ -7026,8 +5410,8 @@ var TELEGRAM_API = "https://api.telegram.org";
7026
5410
  function getWebhookSecret(botType) {
7027
5411
  const filePath = botType === "admin" ? TELEGRAM_ADMIN_WEBHOOK_SECRET_FILE : TELEGRAM_WEBHOOK_SECRET_FILE;
7028
5412
  try {
7029
- if (!existsSync9(filePath)) return null;
7030
- const secret = readFileSync9(filePath, "utf-8").trim();
5413
+ if (!existsSync6(filePath)) return null;
5414
+ const secret = readFileSync6(filePath, "utf-8").trim();
7031
5415
  return secret || null;
7032
5416
  } catch {
7033
5417
  return null;
@@ -7185,12 +5569,12 @@ app6.post("/webhook", async (c) => {
7185
5569
  var telegram_default = app6;
7186
5570
 
7187
5571
  // server/routes/whatsapp.ts
7188
- import { join as join6, resolve as resolve11, basename as basename4 } from "path";
5572
+ import { join as join5, resolve as resolve7, basename as basename2 } from "path";
7189
5573
  import { readFile as readFile2, stat as stat3 } from "fs/promises";
7190
- import { realpathSync as realpathSync2, readdirSync as readdirSync3, readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
5574
+ import { realpathSync as realpathSync2, readdirSync as readdirSync2, readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
7191
5575
 
7192
5576
  // app/lib/whatsapp/login.ts
7193
- import { randomUUID as randomUUID6 } from "crypto";
5577
+ import { randomUUID as randomUUID5 } from "crypto";
7194
5578
  var TAG17 = "[whatsapp:login]";
7195
5579
  var ACTIVE_LOGIN_TTL_MS = 3 * 6e4;
7196
5580
  var activeLogins = /* @__PURE__ */ new Map();
@@ -7293,8 +5677,8 @@ async function startLogin(opts) {
7293
5677
  resetActiveLogin(accountId);
7294
5678
  let resolveQr = null;
7295
5679
  let rejectQr = null;
7296
- const qrPromise = new Promise((resolve26, reject) => {
7297
- resolveQr = resolve26;
5680
+ const qrPromise = new Promise((resolve22, reject) => {
5681
+ resolveQr = resolve22;
7298
5682
  rejectQr = reject;
7299
5683
  });
7300
5684
  const qrTimer = setTimeout(
@@ -7329,7 +5713,7 @@ async function startLogin(opts) {
7329
5713
  const login = {
7330
5714
  accountId,
7331
5715
  authDir,
7332
- id: randomUUID6(),
5716
+ id: randomUUID5(),
7333
5717
  sock,
7334
5718
  startedAt: Date.now(),
7335
5719
  connected: false
@@ -7528,7 +5912,7 @@ app7.post("/login/start", async (c) => {
7528
5912
  const body = await c.req.json().catch(() => ({}));
7529
5913
  const accountId = validateAccountId(body.accountId);
7530
5914
  const force = body.force ?? false;
7531
- const authDir = join6(MAXY_DIR, "credentials", "whatsapp", accountId);
5915
+ const authDir = join5(MAXY_DIR, "credentials", "whatsapp", accountId);
7532
5916
  const result = await startLogin({ accountId, authDir, force });
7533
5917
  console.error(`${TAG18} login/start result account=${accountId} hasQr=${!!result.qrRaw}${result.selfPhone ? ` phone=${result.selfPhone}` : ""}`);
7534
5918
  return c.json(result);
@@ -7688,17 +6072,17 @@ app7.post("/config", async (c) => {
7688
6072
  return c.json(result, result.ok ? 200 : 400);
7689
6073
  }
7690
6074
  case "list-public-agents": {
7691
- const agentsDir = resolve11(account.accountDir, "agents");
6075
+ const agentsDir = resolve7(account.accountDir, "agents");
7692
6076
  const agents = [];
7693
- if (existsSync10(agentsDir)) {
6077
+ if (existsSync7(agentsDir)) {
7694
6078
  try {
7695
- const entries = readdirSync3(agentsDir, { withFileTypes: true });
6079
+ const entries = readdirSync2(agentsDir, { withFileTypes: true });
7696
6080
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
7697
6081
  if (!entry.isDirectory() || entry.name === "admin") continue;
7698
- const configPath2 = resolve11(agentsDir, entry.name, "config.json");
7699
- if (!existsSync10(configPath2)) continue;
6082
+ const configPath2 = resolve7(agentsDir, entry.name, "config.json");
6083
+ if (!existsSync7(configPath2)) continue;
7700
6084
  try {
7701
- const config = JSON.parse(readFileSync10(configPath2, "utf-8"));
6085
+ const config = JSON.parse(readFileSync7(configPath2, "utf-8"));
7702
6086
  agents.push({ slug: entry.name, displayName: config.displayName ?? entry.name });
7703
6087
  } catch {
7704
6088
  console.error(`${TAG18} config action=list-public-agents error="failed to parse config.json for agent ${entry.name}" \u2014 skipping`);
@@ -7773,7 +6157,7 @@ app7.post("/send-document", async (c) => {
7773
6157
  if (!maxyAccountId || !PLATFORM_ROOT4) {
7774
6158
  return c.json({ error: "Cannot validate file path: missing account or platform context" }, 400);
7775
6159
  }
7776
- const accountDir = resolve11(PLATFORM_ROOT4, "..", "data/accounts", maxyAccountId);
6160
+ const accountDir = resolve7(PLATFORM_ROOT4, "..", "data/accounts", maxyAccountId);
7777
6161
  let resolvedPath;
7778
6162
  try {
7779
6163
  resolvedPath = realpathSync2(filePath);
@@ -7797,7 +6181,7 @@ app7.post("/send-document", async (c) => {
7797
6181
  return c.json({ error: `File exceeds 20 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB)` }, 400);
7798
6182
  }
7799
6183
  const buffer = Buffer.from(await readFile2(resolvedPath));
7800
- const filename = basename4(resolvedPath);
6184
+ const filename = basename2(resolvedPath);
7801
6185
  const mimetype = detectMimeType(resolvedPath);
7802
6186
  const sock = getSocket(accountId);
7803
6187
  if (!sock) {
@@ -7997,16 +6381,16 @@ var whatsapp_default = app7;
7997
6381
 
7998
6382
  // server/routes/onboarding.ts
7999
6383
  import { spawn, execFileSync } from "child_process";
8000
- import { openSync as openSync2, closeSync as closeSync2, writeFileSync as writeFileSync7, writeSync, existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync11, unlinkSync } from "fs";
8001
- import { resolve as resolve12, dirname as dirname5 } from "path";
8002
- import { createHash, randomUUID as randomUUID7 } from "crypto";
6384
+ import { openSync, closeSync, writeFileSync as writeFileSync4, writeSync, existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync } from "fs";
6385
+ import { resolve as resolve8, dirname as dirname2 } from "path";
6386
+ import { createHash, randomUUID as randomUUID6 } from "crypto";
8003
6387
  var PLATFORM_ROOT5 = process.env.MAXY_PLATFORM_ROOT || "";
8004
6388
  function hashPin(pin) {
8005
6389
  return createHash("sha256").update(pin).digest("hex");
8006
6390
  }
8007
6391
  function readUsersFile() {
8008
- if (!existsSync11(USERS_FILE)) return null;
8009
- const raw = readFileSync11(USERS_FILE, "utf-8").trim();
6392
+ if (!existsSync8(USERS_FILE)) return null;
6393
+ const raw = readFileSync8(USERS_FILE, "utf-8").trim();
8010
6394
  if (!raw) return [];
8011
6395
  return JSON.parse(raw);
8012
6396
  }
@@ -8072,11 +6456,11 @@ app8.post("/claude-auth", async (c) => {
8072
6456
  if (!vncReady) return c.json({ error: "VNC display failed to start" }, 500);
8073
6457
  }
8074
6458
  await ensureCdp(transport);
8075
- writeFileSync7(logPath("claude-auth"), "");
6459
+ writeFileSync4(logPath("claude-auth"), "");
8076
6460
  const chromiumWrapper = writeChromiumWrapper();
8077
6461
  const x11Env = buildX11Env(chromiumWrapper, transport);
8078
6462
  vncLog("claude-auth", { action: "start", transport });
8079
- const claudeAuthLogFd = openSync2(logPath("claude-auth"), "a");
6463
+ const claudeAuthLogFd = openSync(logPath("claude-auth"), "a");
8080
6464
  const claudeProc = spawn("claude", ["auth", "login"], {
8081
6465
  env: x11Env,
8082
6466
  stdio: ["ignore", "pipe", "pipe"]
@@ -8085,7 +6469,7 @@ app8.post("/claude-auth", async (c) => {
8085
6469
  const onClaudeOutput = (chunk) => writeSync(claudeAuthLogFd, chunk);
8086
6470
  claudeProc.stdout?.on("data", onClaudeOutput);
8087
6471
  claudeProc.stderr?.on("data", onClaudeOutput);
8088
- claudeProc.once("close", () => closeSync2(claudeAuthLogFd));
6472
+ claudeProc.once("close", () => closeSync(claudeAuthLogFd));
8089
6473
  await waitForAuthPage(2e4);
8090
6474
  return c.json({ started: true, transport });
8091
6475
  });
@@ -8116,22 +6500,22 @@ app8.post("/set-pin", async (c) => {
8116
6500
  const hash = hashPin(body.pin);
8117
6501
  const account = resolveAccount();
8118
6502
  const existingOwnerUserId = account?.config.admins?.find((a) => a.role === "owner")?.userId;
8119
- const userId = existingOwnerUserId ?? randomUUID7();
6503
+ const userId = existingOwnerUserId ?? randomUUID6();
8120
6504
  if (existingOwnerUserId) {
8121
6505
  console.log(`[set-pin] reusing existing owner userId=${userId.slice(0, 8)}\u2026 (change-PIN preserves identity)`);
8122
6506
  } else {
8123
6507
  console.log(`[set-pin] minted new userId=${userId.slice(0, 8)}\u2026 (first-time install)`);
8124
6508
  }
8125
- mkdirSync6(dirname5(USERS_FILE), { recursive: true });
8126
- writeFileSync7(USERS_FILE, JSON.stringify([{ userId, pin: hash }]), { mode: 384 });
6509
+ mkdirSync3(dirname2(USERS_FILE), { recursive: true });
6510
+ writeFileSync4(USERS_FILE, JSON.stringify([{ userId, pin: hash }]), { mode: 384 });
8127
6511
  console.log(`[set-pin] wrote users.json: userId=${userId.slice(0, 8)}\u2026 hash=${hash.slice(0, 8)}\u2026`);
8128
6512
  if (account) {
8129
6513
  try {
8130
- const config = JSON.parse(readFileSync11(`${account.accountDir}/account.json`, "utf-8"));
6514
+ const config = JSON.parse(readFileSync8(`${account.accountDir}/account.json`, "utf-8"));
8131
6515
  if (!config.admins) config.admins = [];
8132
6516
  if (!config.admins.some((a) => a.userId === userId)) {
8133
6517
  config.admins.push({ userId, role: "owner" });
8134
- writeFileSync7(`${account.accountDir}/account.json`, JSON.stringify(config, null, 2) + "\n");
6518
+ writeFileSync4(`${account.accountDir}/account.json`, JSON.stringify(config, null, 2) + "\n");
8135
6519
  console.log(`[set-pin] added userId=${userId.slice(0, 8)}\u2026 to account.json admins`);
8136
6520
  }
8137
6521
  } catch (err) {
@@ -8184,7 +6568,7 @@ app8.delete("/set-pin", async (c) => {
8184
6568
  unlinkSync(USERS_FILE);
8185
6569
  console.log(`[set-pin] cleared users.json (last entry removed): userId=${matchedUser.userId.slice(0, 8)}\u2026`);
8186
6570
  } else {
8187
- writeFileSync7(USERS_FILE, JSON.stringify(remaining), { mode: 384 });
6571
+ writeFileSync4(USERS_FILE, JSON.stringify(remaining), { mode: 384 });
8188
6572
  console.log(`[set-pin] removed entry from users.json: userId=${matchedUser.userId.slice(0, 8)}\u2026 remaining=${remaining.length}`);
8189
6573
  }
8190
6574
  return c.json({ ok: true });
@@ -8203,19 +6587,19 @@ app8.post("/skip", async (c) => {
8203
6587
  }
8204
6588
  const { accountId, accountDir } = account;
8205
6589
  let agentName = "Maxy";
8206
- const brandPath = PLATFORM_ROOT5 ? resolve12(PLATFORM_ROOT5, "config", "brand.json") : "";
8207
- if (brandPath && existsSync11(brandPath)) {
6590
+ const brandPath = PLATFORM_ROOT5 ? resolve8(PLATFORM_ROOT5, "config", "brand.json") : "";
6591
+ if (brandPath && existsSync8(brandPath)) {
8208
6592
  try {
8209
- const brand = JSON.parse(readFileSync11(brandPath, "utf-8"));
6593
+ const brand = JSON.parse(readFileSync8(brandPath, "utf-8"));
8210
6594
  if (brand.productName) agentName = brand.productName;
8211
6595
  } catch (err) {
8212
6596
  console.error(`[onboarding-skip] brand.json read failed: ${err instanceof Error ? err.message : String(err)}`);
8213
6597
  }
8214
6598
  }
8215
- const soulPath = resolve12(accountDir, "agents", "admin", "SOUL.md");
6599
+ const soulPath = resolve8(accountDir, "agents", "admin", "SOUL.md");
8216
6600
  try {
8217
- mkdirSync6(dirname5(soulPath), { recursive: true });
8218
- writeFileSync7(soulPath, `You are ${agentName}, an AI operations manager.
6601
+ mkdirSync3(dirname2(soulPath), { recursive: true });
6602
+ writeFileSync4(soulPath, `You are ${agentName}, an AI operations manager.
8219
6603
  `);
8220
6604
  console.log(`[onboarding-skip] wrote SOUL.md: ${soulPath}`);
8221
6605
  } catch (err) {
@@ -8259,9 +6643,9 @@ app8.post("/skip", async (c) => {
8259
6643
  var onboarding_default = app8;
8260
6644
 
8261
6645
  // server/routes/client-error.ts
8262
- import { appendFileSync as appendFileSync2, existsSync as existsSync12, renameSync as renameSync4, statSync as statSync5 } from "fs";
8263
- import { join as join7 } from "path";
8264
- var CLIENT_ERRORS_LOG = join7(LOG_DIR, "client-errors.log");
6646
+ import { appendFileSync, existsSync as existsSync9, renameSync, statSync as statSync2 } from "fs";
6647
+ import { join as join6 } from "path";
6648
+ var CLIENT_ERRORS_LOG = join6(LOG_DIR, "client-errors.log");
8265
6649
  var MAX_LOG_SIZE = 10 * 1024 * 1024;
8266
6650
  var MAX_BODY_SIZE = 8 * 1024;
8267
6651
  var MAX_STACK_LEN = 2e3;
@@ -8304,10 +6688,10 @@ function stackHead(stack) {
8304
6688
  }
8305
6689
  function rotateIfNeeded() {
8306
6690
  try {
8307
- if (!existsSync12(CLIENT_ERRORS_LOG)) return;
8308
- const stats = statSync5(CLIENT_ERRORS_LOG);
6691
+ if (!existsSync9(CLIENT_ERRORS_LOG)) return;
6692
+ const stats = statSync2(CLIENT_ERRORS_LOG);
8309
6693
  if (stats.size < MAX_LOG_SIZE) return;
8310
- renameSync4(CLIENT_ERRORS_LOG, CLIENT_ERRORS_LOG + ".1");
6694
+ renameSync(CLIENT_ERRORS_LOG, CLIENT_ERRORS_LOG + ".1");
8311
6695
  } catch (err) {
8312
6696
  console.error(`[client-error] log rotation failed: ${err instanceof Error ? err.message : String(err)}`);
8313
6697
  }
@@ -8375,7 +6759,7 @@ app9.post("/", async (c) => {
8375
6759
  const safe = typeof v === "string" ? truncate(v, 200) : typeof v === "number" || typeof v === "boolean" ? String(v) : JSON.stringify(v).slice(0, 200);
8376
6760
  return `${k}=${safe}`;
8377
6761
  }).join(" ");
8378
- const TAGGED_PREFIX_SOURCES = /* @__PURE__ */ new Set(["admin-chat-relay-poll"]);
6762
+ const TAGGED_PREFIX_SOURCES = /* @__PURE__ */ new Set([]);
8379
6763
  if (TAGGED_PREFIX_SOURCES.has(source)) {
8380
6764
  console.log(
8381
6765
  `[${source}] ts=${ts} ip=${ip} version=${version || "unknown"}${extra ? " " + extra : ""}`
@@ -8408,7 +6792,7 @@ app9.post("/", async (c) => {
8408
6792
  tag: typeof body.tag === "string" ? truncate(body.tag, 32) : void 0,
8409
6793
  status: typeof body.status === "number" ? body.status : void 0
8410
6794
  };
8411
- appendFileSync2(CLIENT_ERRORS_LOG, JSON.stringify(payload) + "\n", "utf-8");
6795
+ appendFileSync(CLIENT_ERRORS_LOG, JSON.stringify(payload) + "\n", "utf-8");
8412
6796
  } catch (err) {
8413
6797
  console.error(`[client-error] append failed: ${err instanceof Error ? err.message : String(err)}`);
8414
6798
  }
@@ -8418,15 +6802,15 @@ app9.post("/", async (c) => {
8418
6802
  var client_error_default = app9;
8419
6803
 
8420
6804
  // server/routes/admin/session.ts
8421
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync13 } from "fs";
6805
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
8422
6806
  import { createHash as createHash2 } from "crypto";
8423
6807
  var deprecationLogged = /* @__PURE__ */ new Set();
8424
6808
  function hashPin2(pin) {
8425
6809
  return createHash2("sha256").update(pin).digest("hex");
8426
6810
  }
8427
6811
  function readUsersFile2() {
8428
- if (!existsSync13(USERS_FILE)) return null;
8429
- const raw = readFileSync12(USERS_FILE, "utf-8").trim();
6812
+ if (!existsSync10(USERS_FILE)) return null;
6813
+ const raw = readFileSync9(USERS_FILE, "utf-8").trim();
8430
6814
  if (!raw) return [];
8431
6815
  return JSON.parse(raw);
8432
6816
  }
@@ -8443,7 +6827,7 @@ function stripLegacyNameField(users) {
8443
6827
  }
8444
6828
  }
8445
6829
  try {
8446
- writeFileSync8(USERS_FILE, JSON.stringify(users), { mode: 384 });
6830
+ writeFileSync5(USERS_FILE, JSON.stringify(users), { mode: 384 });
8447
6831
  } catch (err) {
8448
6832
  console.error(`[admin-identity] users-json strip failed: ${err instanceof Error ? err.message : String(err)}`);
8449
6833
  }
@@ -8597,49 +6981,16 @@ app10.post("/", async (c) => {
8597
6981
  const payload = await createAdminSession(selected.accountId, selected.config.thinkingView, userId, userName, selected.role, avatar);
8598
6982
  return c.json(payload);
8599
6983
  });
8600
- app10.post("/rebind", async (c) => {
8601
- let body;
8602
- try {
8603
- body = await c.req.json();
8604
- } catch {
8605
- return c.json({ ok: false, error: "invalid_json" }, 400);
8606
- }
8607
- const sessionKey = typeof body.session_key === "string" ? body.session_key : "";
8608
- const conversationId = typeof body.lastKnownConversationId === "string" ? body.lastKnownConversationId : "";
8609
- if (!sessionKey || !conversationId) {
8610
- return c.json({ ok: false, error: "session_key and lastKnownConversationId required" }, 400);
8611
- }
8612
- const sk8 = sessionKey.slice(0, 8);
8613
- const cid8 = conversationId.slice(0, 8);
8614
- const bridge = await tryCookieBridgeForConversation(c, sessionKey, conversationId);
8615
- if (!bridge.ok) {
8616
- console.log(`[admin/session/rebind] sessionKey=${sk8}\u2026 result=bridge-rejected reason=${bridge.reason} conversationId=${cid8}\u2026`);
8617
- const status = bridge.reason === "conversation-not-found" ? 404 : bridge.reason === "account-mismatch" ? 403 : 401;
8618
- return c.json({ ok: false, error: bridge.reason }, status);
8619
- }
8620
- const existing = getConversationIdForSession(sessionKey);
8621
- if (existing && existing !== conversationId) {
8622
- console.log(`[admin/session/rebind] sessionKey=${sk8}\u2026 result=conflict conversationId=${existing.slice(0, 8)}\u2026 requested=${cid8}\u2026`);
8623
- return c.json({ ok: false, error: "conflict", conversationId: existing }, 409);
8624
- }
8625
- const bound = setConversationIdForSession(sessionKey, conversationId);
8626
- if (!bound) {
8627
- console.error(`[admin/session/rebind] sessionKey=${sk8}\u2026 result=bind-failed conversationId=${cid8}\u2026 (post-bridge session-store missing \u2014 programmer error)`);
8628
- return c.json({ ok: false, error: "bind_failed" }, 500);
8629
- }
8630
- console.log(`[admin/session/rebind] sessionKey=${sk8}\u2026 result=ok conversationId=${cid8}\u2026`);
8631
- return c.json({ ok: true, conversationId });
8632
- });
8633
6984
  var session_default2 = app10;
8634
6985
 
8635
6986
  // server/routes/admin/chat.ts
8636
- import { resolve as resolve13 } from "path";
8637
- import { appendFileSync as appendFileSync4 } from "fs";
6987
+ import { resolve as resolve9 } from "path";
6988
+ import { appendFileSync as appendFileSync3 } from "fs";
8638
6989
 
8639
6990
  // app/lib/script-stream-tailer.ts
8640
6991
  import * as childProcess from "child_process";
8641
- import { appendFileSync as appendFileSync3, createReadStream as createReadStream2, mkdirSync as mkdirSync7, statSync as statSync6 } from "fs";
8642
- import { dirname as dirname6 } from "path";
6992
+ import { appendFileSync as appendFileSync2, createReadStream as createReadStream2, mkdirSync as mkdirSync4, statSync as statSync3 } from "fs";
6993
+ import { dirname as dirname3 } from "path";
8643
6994
  import { StringDecoder } from "string_decoder";
8644
6995
  var SCRIPT_STREAM_RE = /^\[([^\]]+)\] \[script:([a-z][a-z0-9-]*)((?::[a-z0-9:_-]+)?)\] (.*)$/;
8645
6996
  function parseLine(line) {
@@ -8657,7 +7008,7 @@ function startScriptStreamTailer(opts) {
8657
7008
  const { path: path2, onEvent, onError } = opts;
8658
7009
  let offset;
8659
7010
  try {
8660
- offset = statSync6(path2).size;
7011
+ offset = statSync3(path2).size;
8661
7012
  } catch {
8662
7013
  offset = 0;
8663
7014
  }
@@ -8676,7 +7027,7 @@ function startScriptStreamTailer(opts) {
8676
7027
  try {
8677
7028
  let size;
8678
7029
  try {
8679
- size = statSync6(path2).size;
7030
+ size = statSync3(path2).size;
8680
7031
  } catch {
8681
7032
  return;
8682
7033
  }
@@ -8748,8 +7099,8 @@ function writeRouteMilestone(streamLogPath, scope, line) {
8748
7099
  }
8749
7100
  const ts = (/* @__PURE__ */ new Date()).toISOString();
8750
7101
  try {
8751
- mkdirSync7(dirname6(streamLogPath), { recursive: true });
8752
- appendFileSync3(streamLogPath, `[${ts}] [script:${scope}] ${line}
7102
+ mkdirSync4(dirname3(streamLogPath), { recursive: true });
7103
+ appendFileSync2(streamLogPath, `[${ts}] [script:${scope}] ${line}
8753
7104
  `);
8754
7105
  } catch (err) {
8755
7106
  console.error(
@@ -8924,7 +7275,7 @@ var app11 = new Hono();
8924
7275
  app11.post("/cancel", requireAdminSession, async (c) => {
8925
7276
  const session_key = c.var.sessionKey;
8926
7277
  try {
8927
- const { interruptClient: interruptClient2 } = await import("./client-pool-BMPFHXHB.js");
7278
+ const { interruptClient: interruptClient2 } = await import("./client-pool-GBY5I2KQ.js");
8928
7279
  await interruptClient2(session_key);
8929
7280
  return c.json({ ok: true });
8930
7281
  } catch (err) {
@@ -9088,10 +7439,10 @@ app11.post("/", requireAdminSession, async (c) => {
9088
7439
  function resolveTeeStreamLogPath() {
9089
7440
  const liveConvId = getConversationIdForSession(session_key);
9090
7441
  const key = liveConvId ?? preflushStreamLogKey(session_key);
9091
- return resolve13(account.accountDir, "logs", `claude-agent-stream-${key}.log`);
7442
+ return resolve9(account.accountDir, "logs", `claude-agent-stream-${key}.log`);
9092
7443
  }
9093
7444
  try {
9094
- appendFileSync4(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [chat-route-version=task606-tee-path-resolve] sessionKey=${session_key.slice(0, 12)}\u2026
7445
+ appendFileSync3(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [chat-route-version=task606-tee-path-resolve] sessionKey=${session_key.slice(0, 12)}\u2026
9095
7446
  `);
9096
7447
  } catch {
9097
7448
  }
@@ -9100,7 +7451,7 @@ app11.post("/", requireAdminSession, async (c) => {
9100
7451
  incoming.on("close", () => {
9101
7452
  const tsClose = (/* @__PURE__ */ new Date()).toISOString();
9102
7453
  try {
9103
- appendFileSync4(resolveTeeStreamLogPath(), `[${tsClose}] [incoming-close] sessionKey=${session_key.slice(0, 12)}\u2026 complete=${incoming.complete}
7454
+ appendFileSync3(resolveTeeStreamLogPath(), `[${tsClose}] [incoming-close] sessionKey=${session_key.slice(0, 12)}\u2026 complete=${incoming.complete}
9104
7455
  `);
9105
7456
  } catch {
9106
7457
  }
@@ -9108,7 +7459,7 @@ app11.post("/", requireAdminSession, async (c) => {
9108
7459
  console.error(`[${tsClose}] [incoming-close] DISCONNECT sessionKey=${session_key.slice(0, 12)}\u2026`);
9109
7460
  interruptClient(session_key).catch((err) => {
9110
7461
  try {
9111
- appendFileSync4(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [incoming-close] interrupt-failed: ${err instanceof Error ? err.message : String(err)}
7462
+ appendFileSync3(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [incoming-close] interrupt-failed: ${err instanceof Error ? err.message : String(err)}
9112
7463
  `);
9113
7464
  } catch {
9114
7465
  }
@@ -9117,7 +7468,7 @@ app11.post("/", requireAdminSession, async (c) => {
9117
7468
  });
9118
7469
  } else {
9119
7470
  try {
9120
- appendFileSync4(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [incoming-close] UNAVAILABLE \u2014 c.env.incoming is not an EventEmitter
7471
+ appendFileSync3(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [incoming-close] UNAVAILABLE \u2014 c.env.incoming is not an EventEmitter
9121
7472
  `);
9122
7473
  } catch {
9123
7474
  }
@@ -9129,7 +7480,7 @@ app11.post("/", requireAdminSession, async (c) => {
9129
7480
  } catch {
9130
7481
  }
9131
7482
  try {
9132
- appendFileSync4(resolveTeeStreamLogPath(), line);
7483
+ appendFileSync3(resolveTeeStreamLogPath(), line);
9133
7484
  } catch {
9134
7485
  }
9135
7486
  return true;
@@ -9190,7 +7541,7 @@ app11.post("/", requireAdminSession, async (c) => {
9190
7541
  try {
9191
7542
  registerAdminSSE(sseEntry);
9192
7543
  if (sseConvId) {
9193
- const streamLogPath = resolve13(account.accountDir, "logs", `claude-agent-stream-${sseConvId}.log`);
7544
+ const streamLogPath = resolve9(account.accountDir, "logs", `claude-agent-stream-${sseConvId}.log`);
9194
7545
  tailer = startScriptStreamTailer({
9195
7546
  path: streamLogPath,
9196
7547
  onEvent: (event) => {
@@ -9319,22 +7670,22 @@ app12.post("/", requireAdminSession, async (c) => {
9319
7670
  var compact_default = app12;
9320
7671
 
9321
7672
  // server/routes/admin/logs.ts
9322
- import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as readFileSync13, statSync as statSync7 } from "fs";
9323
- import { resolve as resolve14, basename as basename5 } from "path";
7673
+ import { existsSync as existsSync12, readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync4 } from "fs";
7674
+ import { resolve as resolve10, basename as basename3 } from "path";
9324
7675
 
9325
7676
  // app/lib/logs-read-resolve.ts
9326
- import { existsSync as existsSync14 } from "fs";
9327
- import { join as join8 } from "path";
7677
+ import { existsSync as existsSync11 } from "fs";
7678
+ import { join as join7 } from "path";
9328
7679
  function resolveConversationLogPaths(fullFilename, preflushFilename, logDirs) {
9329
7680
  const tried = [fullFilename, preflushFilename];
9330
7681
  const hits = [];
9331
7682
  const stalePreflushPaths = [];
9332
7683
  for (const dir of logDirs) {
9333
- const fullPath = join8(dir, fullFilename);
9334
- if (existsSync14(fullPath)) {
7684
+ const fullPath = join7(dir, fullFilename);
7685
+ if (existsSync11(fullPath)) {
9335
7686
  hits.push({ path: fullPath, shape: "full", dir });
9336
- const preflushSibling = join8(dir, preflushFilename);
9337
- if (existsSync14(preflushSibling)) {
7687
+ const preflushSibling = join7(dir, preflushFilename);
7688
+ if (existsSync11(preflushSibling)) {
9338
7689
  stalePreflushPaths.push(preflushSibling);
9339
7690
  }
9340
7691
  }
@@ -9343,8 +7694,8 @@ function resolveConversationLogPaths(fullFilename, preflushFilename, logDirs) {
9343
7694
  return { hits, stalePreflushPaths, tried };
9344
7695
  }
9345
7696
  for (const dir of logDirs) {
9346
- const preflushPath = join8(dir, preflushFilename);
9347
- if (existsSync14(preflushPath)) {
7697
+ const preflushPath = join7(dir, preflushFilename);
7698
+ if (existsSync11(preflushPath)) {
9348
7699
  hits.push({ path: preflushPath, shape: "preflush", dir });
9349
7700
  }
9350
7701
  }
@@ -9364,19 +7715,19 @@ app13.get("/", async (c) => {
9364
7715
  const sessionKeyParam = c.req.query("sessionKey");
9365
7716
  const download = c.req.query("download") === "1";
9366
7717
  const account = resolveAccount();
9367
- const accountLogDir2 = account ? resolve14(account.accountDir, "logs") : null;
7718
+ const accountLogDir = account ? resolve10(account.accountDir, "logs") : null;
9368
7719
  const logDirs = [];
9369
- if (accountLogDir2) logDirs.push(accountLogDir2);
7720
+ if (accountLogDir) logDirs.push(accountLogDir);
9370
7721
  logDirs.push(LOG_DIR);
9371
7722
  if (fileParam) {
9372
- const safe = basename5(fileParam);
7723
+ const safe = basename3(fileParam);
9373
7724
  const searched = [];
9374
7725
  for (const dir of logDirs) {
9375
- const filePath = resolve14(dir, safe);
7726
+ const filePath = resolve10(dir, safe);
9376
7727
  searched.push(filePath);
9377
7728
  try {
9378
- const buffer = readFileSync13(filePath);
9379
- const onDiskBytes = statSync7(filePath).size;
7729
+ const buffer = readFileSync10(filePath);
7730
+ const onDiskBytes = statSync4(filePath).size;
9380
7731
  const headers = {
9381
7732
  "Content-Type": "text/plain; charset=utf-8",
9382
7733
  "Content-Length": String(buffer.byteLength)
@@ -9448,9 +7799,9 @@ app13.get("/", async (c) => {
9448
7799
  const hit = hits[0];
9449
7800
  console.info(`[admin/logs] resolved sessionKey=${sessionKeySlice} conversationId=${conversationIdSlice} shape=${hit.shape} stalePreflushCount=${stalePreflushCount}`);
9450
7801
  try {
9451
- const filename = basename5(hit.path);
7802
+ const filename = basename3(hit.path);
9452
7803
  if (stalePreflushCount > 0 && !download) {
9453
- const content = readFileSync13(hit.path, "utf-8");
7804
+ const content = readFileSync10(hit.path, "utf-8");
9454
7805
  return c.json({
9455
7806
  log: content,
9456
7807
  filename,
@@ -9458,8 +7809,8 @@ app13.get("/", async (c) => {
9458
7809
  warnings: stalePreflushPaths.map((path2) => ({ kind: "stale-preflush", path: path2 }))
9459
7810
  });
9460
7811
  }
9461
- const buffer = readFileSync13(hit.path);
9462
- const onDiskBytes = statSync7(hit.path).size;
7812
+ const buffer = readFileSync10(hit.path);
7813
+ const onDiskBytes = statSync4(hit.path).size;
9463
7814
  const headers = {
9464
7815
  "Content-Type": "text/plain; charset=utf-8",
9465
7816
  "Content-Length": String(buffer.byteLength)
@@ -9492,19 +7843,19 @@ app13.get("/", async (c) => {
9492
7843
  const seen = /* @__PURE__ */ new Set();
9493
7844
  const logs = {};
9494
7845
  for (const dir of logDirs) {
9495
- if (!existsSync15(dir)) continue;
7846
+ if (!existsSync12(dir)) continue;
9496
7847
  let files;
9497
7848
  try {
9498
- files = readdirSync4(dir).filter((f) => f.endsWith(".log"));
7849
+ files = readdirSync3(dir).filter((f) => f.endsWith(".log"));
9499
7850
  } catch (err) {
9500
7851
  const reason = err instanceof Error ? err.message : String(err);
9501
7852
  console.warn(`[admin/logs] readdir-fail dir=${dir} reason=${reason}`);
9502
7853
  continue;
9503
7854
  }
9504
- 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 }) => {
7855
+ 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 }) => {
9505
7856
  seen.add(name);
9506
7857
  try {
9507
- const content = readFileSync13(resolve14(dir, name));
7858
+ const content = readFileSync10(resolve10(dir, name));
9508
7859
  const tail = content.length > TAIL_BYTES ? content.subarray(content.length - TAIL_BYTES).toString("utf-8") : content.toString("utf-8");
9509
7860
  logs[name] = tail.trim() || "(empty)";
9510
7861
  } catch (err) {
@@ -9543,8 +7894,8 @@ var claude_info_default = app14;
9543
7894
 
9544
7895
  // server/routes/admin/attachment.ts
9545
7896
  import { readFile as readFile3, readdir } from "fs/promises";
9546
- import { existsSync as existsSync16 } from "fs";
9547
- import { resolve as resolve15 } from "path";
7897
+ import { existsSync as existsSync13 } from "fs";
7898
+ import { resolve as resolve11 } from "path";
9548
7899
  var app15 = new Hono();
9549
7900
  app15.get("/:attachmentId", requireAdminSession, async (c) => {
9550
7901
  const attachmentId = c.req.param("attachmentId");
@@ -9556,12 +7907,12 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
9556
7907
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(attachmentId)) {
9557
7908
  return new Response("Not found", { status: 404 });
9558
7909
  }
9559
- const dir = resolve15(ATTACHMENTS_ROOT, accountId, attachmentId);
9560
- if (!existsSync16(dir)) {
7910
+ const dir = resolve11(ATTACHMENTS_ROOT, accountId, attachmentId);
7911
+ if (!existsSync13(dir)) {
9561
7912
  return new Response("Not found", { status: 404 });
9562
7913
  }
9563
- const metaPath = resolve15(dir, `${attachmentId}.meta.json`);
9564
- if (!existsSync16(metaPath)) {
7914
+ const metaPath = resolve11(dir, `${attachmentId}.meta.json`);
7915
+ if (!existsSync13(metaPath)) {
9565
7916
  return new Response("Not found", { status: 404 });
9566
7917
  }
9567
7918
  let meta;
@@ -9575,7 +7926,7 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
9575
7926
  if (!dataFile) {
9576
7927
  return new Response("Not found", { status: 404 });
9577
7928
  }
9578
- const filePath = resolve15(dir, dataFile);
7929
+ const filePath = resolve11(dir, dataFile);
9579
7930
  const buffer = await readFile3(filePath);
9580
7931
  return new Response(new Uint8Array(buffer), {
9581
7932
  headers: {
@@ -9588,24 +7939,24 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
9588
7939
  var attachment_default = app15;
9589
7940
 
9590
7941
  // server/routes/admin/agents.ts
9591
- import { resolve as resolve16 } from "path";
9592
- import { readdirSync as readdirSync5, readFileSync as readFileSync14, existsSync as existsSync17, rmSync } from "fs";
7942
+ import { resolve as resolve12 } from "path";
7943
+ import { readdirSync as readdirSync4, readFileSync as readFileSync11, existsSync as existsSync14, rmSync } from "fs";
9593
7944
  var app16 = new Hono();
9594
7945
  app16.get("/", (c) => {
9595
7946
  const account = resolveAccount();
9596
7947
  if (!account) return c.json({ agents: [] });
9597
- const agentsDir = resolve16(account.accountDir, "agents");
9598
- if (!existsSync17(agentsDir)) return c.json({ agents: [] });
7948
+ const agentsDir = resolve12(account.accountDir, "agents");
7949
+ if (!existsSync14(agentsDir)) return c.json({ agents: [] });
9599
7950
  const agents = [];
9600
7951
  try {
9601
- const entries = readdirSync5(agentsDir, { withFileTypes: true });
7952
+ const entries = readdirSync4(agentsDir, { withFileTypes: true });
9602
7953
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
9603
7954
  if (!entry.isDirectory()) continue;
9604
7955
  if (entry.name === "admin") continue;
9605
- const configPath2 = resolve16(agentsDir, entry.name, "config.json");
9606
- if (!existsSync17(configPath2)) continue;
7956
+ const configPath2 = resolve12(agentsDir, entry.name, "config.json");
7957
+ if (!existsSync14(configPath2)) continue;
9607
7958
  try {
9608
- const config = JSON.parse(readFileSync14(configPath2, "utf-8"));
7959
+ const config = JSON.parse(readFileSync11(configPath2, "utf-8"));
9609
7960
  agents.push({
9610
7961
  slug: entry.name,
9611
7962
  displayName: config.displayName ?? entry.name,
@@ -9631,8 +7982,8 @@ app16.delete("/:slug", async (c) => {
9631
7982
  if (slug.includes("/") || slug.includes("..") || slug.includes("\\")) {
9632
7983
  return c.json({ error: "Invalid agent slug" }, 400);
9633
7984
  }
9634
- const agentDir = resolve16(account.accountDir, "agents", slug);
9635
- if (!existsSync17(agentDir)) {
7985
+ const agentDir = resolve12(account.accountDir, "agents", slug);
7986
+ if (!existsSync14(agentDir)) {
9636
7987
  return c.json({ error: "Agent not found" }, 404);
9637
7988
  }
9638
7989
  try {
@@ -9661,8 +8012,8 @@ app16.post("/:slug/project", async (c) => {
9661
8012
  if (slug.includes("/") || slug.includes("..") || slug.includes("\\")) {
9662
8013
  return c.json({ error: "Invalid agent slug" }, 400);
9663
8014
  }
9664
- const agentDir = resolve16(account.accountDir, "agents", slug);
9665
- if (!existsSync17(agentDir)) {
8015
+ const agentDir = resolve12(account.accountDir, "agents", slug);
8016
+ if (!existsSync14(agentDir)) {
9666
8017
  return c.json({ error: "Agent not found on disk" }, 404);
9667
8018
  }
9668
8019
  try {
@@ -9678,7 +8029,7 @@ var agents_default = app16;
9678
8029
  // server/routes/admin/sessions.ts
9679
8030
  import crypto2 from "crypto";
9680
8031
  import { resolve as resolvePath } from "path";
9681
- import { appendFileSync as appendFileSync5, existsSync as existsSync18 } from "fs";
8032
+ import { appendFileSync as appendFileSync4, existsSync as existsSync15 } from "fs";
9682
8033
  function validateAndShapeAttachments(raws, conversationAccountId, conversationId, messageId, streamLogPath) {
9683
8034
  const chips = [];
9684
8035
  let valid = 0;
@@ -9687,11 +8038,11 @@ function validateAndShapeAttachments(raws, conversationAccountId, conversationId
9687
8038
  let reason = null;
9688
8039
  if (!a.attachmentId || !a.filename || !a.mimeType || !a.storagePath) reason = "schema-fail";
9689
8040
  else if (a.accountId !== conversationAccountId) reason = "account-mismatch";
9690
- else if (!existsSync18(a.storagePath)) reason = "missing-file";
8041
+ else if (!existsSync15(a.storagePath)) reason = "missing-file";
9691
8042
  if (reason) {
9692
8043
  invalid++;
9693
8044
  try {
9694
- 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}
8045
+ 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}
9695
8046
  `);
9696
8047
  } catch {
9697
8048
  }
@@ -9754,7 +8105,7 @@ function reconstructAssistantEvents(content, components, conversationId, message
9754
8105
  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}
9755
8106
  `;
9756
8107
  try {
9757
- appendFileSync5(streamLogPath, line);
8108
+ appendFileSync4(streamLogPath, line);
9758
8109
  } catch {
9759
8110
  }
9760
8111
  continue;
@@ -9860,13 +8211,14 @@ app17.delete("/:id", requireAdminSession, async (c) => {
9860
8211
  return c.json({ error: "Failed to delete session" }, 500);
9861
8212
  }
9862
8213
  });
9863
- app17.get("/:id/messages", async (c) => {
8214
+ app17.post("/:id/resume", async (c) => {
9864
8215
  const conversationId = c.req.param("id");
9865
8216
  const sessionKey = c.req.query("session_key") ?? "";
9866
8217
  if (!sessionKey) {
9867
8218
  console.error(`[session] middleware-reject status=400 code=session-missing reason="session_key required" path=${c.req.path}`);
9868
8219
  return c.json({ error: "session_key required", code: "session-missing" }, 400);
9869
8220
  }
8221
+ let bridged = false;
9870
8222
  let result = validateSession(sessionKey, "admin");
9871
8223
  if (!result.ok) {
9872
8224
  if (result.reason === "session-not-registered") {
@@ -9879,6 +8231,7 @@ app17.get("/:id/messages", async (c) => {
9879
8231
  console.error(`[session] middleware-reject status=401 code=session-not-registered reason="cookie-bridge-rejected:${bridge.reason}" path=${c.req.path} sessionKey=${tail}\u2026`);
9880
8232
  return c.json({ error: "Invalid or expired admin session", code: "session-not-registered" }, 401);
9881
8233
  }
8234
+ bridged = true;
9882
8235
  result = validateSession(sessionKey, "admin");
9883
8236
  if (!result.ok) {
9884
8237
  const tail = sessionKey.slice(0, 8);
@@ -9893,34 +8246,12 @@ app17.get("/:id/messages", async (c) => {
9893
8246
  }
9894
8247
  }
9895
8248
  const accountId = getAccountIdForSession(sessionKey);
9896
- if (!accountId) return c.json({ error: "Account not found for session" }, 401);
9897
- const owned = await verifyConversationOwnership(conversationId, accountId);
9898
- if (!owned) return c.json({ error: "Conversation not found" }, 404);
9899
- try {
9900
- const messages = await getRecentMessages(conversationId, 50);
9901
- const sanitised = messages.map((m) => ({
9902
- ...m,
9903
- attachments: m.attachments.map((a) => ({
9904
- attachmentId: a.attachmentId,
9905
- filename: a.filename,
9906
- mimeType: a.mimeType,
9907
- sizeBytes: a.sizeBytes,
9908
- ordinal: a.ordinal
9909
- }))
9910
- }));
9911
- return c.json({ messages: sanitised });
9912
- } catch (err) {
9913
- console.error(`[sessions-messages] Failed: ${err instanceof Error ? err.message : String(err)}`);
9914
- return c.json({ error: "Failed to fetch messages" }, 500);
9915
- }
9916
- });
9917
- app17.post("/:id/resume", requireAdminSession, async (c) => {
9918
- const conversationId = c.req.param("id");
9919
- const sessionKey = c.var.sessionKey;
9920
- const accountId = getAccountIdForSession(sessionKey);
9921
8249
  const userId = getUserIdForSession(sessionKey);
9922
- if (!accountId || !userId) {
9923
- return c.json({ error: "Session missing account or user context" }, 400);
8250
+ if (!accountId) {
8251
+ return c.json({ error: "Session missing account context" }, 400);
8252
+ }
8253
+ if (!userId && !bridged) {
8254
+ return c.json({ error: "Session missing user context" }, 400);
9924
8255
  }
9925
8256
  const updatedAt = await verifyAndGetConversationUpdatedAt(conversationId, accountId);
9926
8257
  if (updatedAt === null) return c.json({ error: "Conversation not found" }, 404);
@@ -9975,21 +8306,22 @@ app17.post("/:id/resume", requireAdminSession, async (c) => {
9975
8306
  });
9976
8307
  const textRuns = rehydrated.reduce((n, m) => n + (m.events?.filter((e) => e.type === "text").length ?? 0), 0);
9977
8308
  const userMessageCount = rehydrated.filter((m) => m.role !== "assistant").length;
8309
+ const reason = bridged ? "post-restart" : "page-refresh";
9978
8310
  try {
9979
- appendFileSync5(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [admin-resume] sessionKey=${sessionKey.slice(0, 8)} conversationId=${conversationId.slice(0, 8)} ${tag} loadedMessages=${messages.length} componentCount=${totalComponents} userAttachmentCount=${totalAttachments}
8311
+ 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}
9980
8312
  `);
9981
8313
  if (totalComponents > 0) {
9982
- appendFileSync5(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [component-rehydrate] conversationId=${conversationId.slice(0, 8)} count=${totalComponents} valid=${totalValid} invalid=${totalInvalid} textRuns=${textRuns}
8314
+ appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [component-rehydrate] conversationId=${conversationId.slice(0, 8)} count=${totalComponents} valid=${totalValid} invalid=${totalInvalid} textRuns=${textRuns}
9983
8315
  `);
9984
8316
  }
9985
8317
  if (totalAttachments > 0 || totalAttachmentInvalid > 0) {
9986
- appendFileSync5(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [attachment-rehydrate] conversationId=${conversationId.slice(0, 8)} userMessages=${userMessageCount} attachments=${totalAttachments} invalid=${totalAttachmentInvalid}
8318
+ appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [attachment-rehydrate] conversationId=${conversationId.slice(0, 8)} userMessages=${userMessageCount} attachments=${totalAttachments} invalid=${totalAttachmentInvalid}
9987
8319
  `);
9988
8320
  }
9989
8321
  } catch {
9990
8322
  }
9991
8323
  const age = formatAge(updatedAt);
9992
- console.log(`[admin-resume] ${(/* @__PURE__ */ new Date()).toISOString()} conversationId=${conversationId.slice(0, 8)}\u2026 age=${age} loaded=${messages.length} messages ${tag} components=${totalComponents} attachments=${totalAttachments} sessionKey=${sessionKey.slice(0, 8)}\u2026`);
8324
+ console.log(`[admin-resume] ${(/* @__PURE__ */ new Date()).toISOString()} reason=${reason} conversationId=${conversationId.slice(0, 8)}\u2026 age=${age} loaded=${messages.length} messages ${tag} components=${totalComponents} attachments=${totalAttachments} sessionKey=${sessionKey.slice(0, 8)}\u2026`);
9993
8325
  return c.json({ conversationId, messages: rehydrated });
9994
8326
  });
9995
8327
  app17.post("/:id/label", requireAdminSession, async (c) => {
@@ -10116,13 +8448,13 @@ async function cdpNavigateNewTab(url, opts = {}) {
10116
8448
  // server/routes/admin/device-browser.ts
10117
8449
  var app19 = new Hono();
10118
8450
  app19.post("/navigate", async (c) => {
10119
- const TAG20 = "[device-url:click]";
8451
+ const TAG19 = "[device-url:click]";
10120
8452
  let body;
10121
8453
  try {
10122
8454
  body = await c.req.json();
10123
8455
  } catch (err) {
10124
8456
  const detail = err instanceof Error ? err.message : String(err);
10125
- console.error(`${TAG20} reject reason=body-not-json detail=${detail} browser=fallback navigateResult=error`);
8457
+ console.error(`${TAG19} reject reason=body-not-json detail=${detail} browser=fallback navigateResult=error`);
10126
8458
  return c.json(
10127
8459
  { ok: false, navigateResult: "error", browser: "fallback", detail: "Request body was not valid JSON" },
10128
8460
  400
@@ -10132,7 +8464,7 @@ app19.post("/navigate", async (c) => {
10132
8464
  const intent = typeof body.intent === "string" ? body.intent : "";
10133
8465
  const hostname2 = typeof body.hostname === "string" ? body.hostname : "";
10134
8466
  if (!url) {
10135
- console.error(`${TAG20} reject reason=missing-url intent=${JSON.stringify(intent)} browser=fallback navigateResult=error`);
8467
+ console.error(`${TAG19} reject reason=missing-url intent=${JSON.stringify(intent)} browser=fallback navigateResult=error`);
10136
8468
  return c.json(
10137
8469
  { ok: false, navigateResult: "error", browser: "fallback", detail: "url field is required" },
10138
8470
  400
@@ -10142,7 +8474,7 @@ app19.post("/navigate", async (c) => {
10142
8474
  try {
10143
8475
  parsed = new URL(url);
10144
8476
  } catch {
10145
- console.error(`${TAG20} reject reason=url-malformed intent=${JSON.stringify(intent)} url=${url} browser=fallback navigateResult=error`);
8477
+ console.error(`${TAG19} reject reason=url-malformed intent=${JSON.stringify(intent)} url=${url} browser=fallback navigateResult=error`);
10146
8478
  return c.json(
10147
8479
  { ok: false, navigateResult: "error", browser: "fallback", detail: "url is not a valid URL" },
10148
8480
  400
@@ -10150,7 +8482,7 @@ app19.post("/navigate", async (c) => {
10150
8482
  }
10151
8483
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
10152
8484
  console.error(
10153
- `${TAG20} reject reason=scheme-not-allowed scheme=${parsed.protocol} intent=${JSON.stringify(intent)} browser=fallback navigateResult=error`
8485
+ `${TAG19} reject reason=scheme-not-allowed scheme=${parsed.protocol} intent=${JSON.stringify(intent)} browser=fallback navigateResult=error`
10154
8486
  );
10155
8487
  return c.json(
10156
8488
  {
@@ -10166,7 +8498,7 @@ app19.post("/navigate", async (c) => {
10166
8498
  const cdpOk = await ensureCdp(transport);
10167
8499
  if (!cdpOk) {
10168
8500
  console.error(
10169
- `${TAG20} intent=${JSON.stringify(intent)} browser=fallback navigateResult=cdp-unreachable hostname=${JSON.stringify(hostname2)}`
8501
+ `${TAG19} intent=${JSON.stringify(intent)} browser=fallback navigateResult=cdp-unreachable hostname=${JSON.stringify(hostname2)}`
10170
8502
  );
10171
8503
  return c.json(
10172
8504
  {
@@ -10182,7 +8514,7 @@ app19.post("/navigate", async (c) => {
10182
8514
  const browser = outcome.result === "ok" ? "vnc" : "fallback";
10183
8515
  const detailStr = outcome.detail ? ` detail=${JSON.stringify(outcome.detail.length > 230 ? outcome.detail.slice(0, 227) + "..." : outcome.detail)}` : "";
10184
8516
  console.error(
10185
- `${TAG20} intent=${JSON.stringify(intent)} browser=${browser} navigateResult=${outcome.result} hostname=${JSON.stringify(hostname2)} targetId=${outcome.targetId ?? "none"}${detailStr}`
8517
+ `${TAG19} intent=${JSON.stringify(intent)} browser=${browser} navigateResult=${outcome.result} hostname=${JSON.stringify(hostname2)} targetId=${outcome.targetId ?? "none"}${detailStr}`
10186
8518
  );
10187
8519
  if (outcome.result !== "ok") {
10188
8520
  return c.json(
@@ -10213,18 +8545,18 @@ var ALLOWED_EVENTS = /* @__PURE__ */ new Set([
10213
8545
  ]);
10214
8546
  var app20 = new Hono();
10215
8547
  app20.post("/", async (c) => {
10216
- const TAG20 = "[admin:events]";
8548
+ const TAG19 = "[admin:events]";
10217
8549
  let body;
10218
8550
  try {
10219
8551
  body = await c.req.json();
10220
8552
  } catch (err) {
10221
8553
  const detail = err instanceof Error ? err.message : String(err);
10222
- console.error(`${TAG20} reject reason=body-not-json detail=${detail}`);
8554
+ console.error(`${TAG19} reject reason=body-not-json detail=${detail}`);
10223
8555
  return c.json({ ok: false, detail: "Request body was not valid JSON" }, 400);
10224
8556
  }
10225
8557
  const event = typeof body.event === "string" ? body.event : "";
10226
8558
  if (!ALLOWED_EVENTS.has(event)) {
10227
- console.error(`${TAG20} reject reason=event-not-allowed event=${JSON.stringify(event)}`);
8559
+ console.error(`${TAG19} reject reason=event-not-allowed event=${JSON.stringify(event)}`);
10228
8560
  return c.json({ ok: false, detail: `Event "${event}" is not allowed` }, 400);
10229
8561
  }
10230
8562
  const rawFields = body.fields && typeof body.fields === "object" ? body.fields : {};
@@ -10247,8 +8579,8 @@ var events_default = app20;
10247
8579
 
10248
8580
  // server/routes/admin/cloudflare.ts
10249
8581
  import { homedir } from "os";
10250
- import { resolve as resolve18 } from "path";
10251
- import { readFileSync as readFileSync17 } from "fs";
8582
+ import { resolve as resolve14 } from "path";
8583
+ import { readFileSync as readFileSync13 } from "fs";
10252
8584
 
10253
8585
  // app/lib/dns-label.ts
10254
8586
  var VALID_LABEL = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -10264,14 +8596,14 @@ function isValidDomain(value) {
10264
8596
  }
10265
8597
 
10266
8598
  // app/lib/alias-domains.ts
10267
- import { existsSync as existsSync19, mkdirSync as mkdirSync8, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
10268
- import { dirname as dirname7 } from "path";
10269
- import { resolve as resolve17 } from "path";
10270
- var ALIAS_DOMAINS_PATH = resolve17(MAXY_DIR, "alias-domains.json");
8599
+ import { existsSync as existsSync16, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
8600
+ import { dirname as dirname4 } from "path";
8601
+ import { resolve as resolve13 } from "path";
8602
+ var ALIAS_DOMAINS_PATH = resolve13(MAXY_DIR, "alias-domains.json");
10271
8603
  function readExisting() {
10272
- if (!existsSync19(ALIAS_DOMAINS_PATH)) return /* @__PURE__ */ new Set();
8604
+ if (!existsSync16(ALIAS_DOMAINS_PATH)) return /* @__PURE__ */ new Set();
10273
8605
  try {
10274
- const parsed = JSON.parse(readFileSync15(ALIAS_DOMAINS_PATH, "utf-8"));
8606
+ const parsed = JSON.parse(readFileSync12(ALIAS_DOMAINS_PATH, "utf-8"));
10275
8607
  if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
10276
8608
  return new Set(parsed.filter((h) => typeof h === "string"));
10277
8609
  } catch {
@@ -10282,135 +8614,18 @@ function addAliasDomain(hostname2) {
10282
8614
  const existing = readExisting();
10283
8615
  if (existing.has(hostname2)) return;
10284
8616
  existing.add(hostname2);
10285
- mkdirSync8(dirname7(ALIAS_DOMAINS_PATH), { recursive: true });
10286
- writeFileSync9(ALIAS_DOMAINS_PATH, JSON.stringify([...existing], null, 2) + "\n", "utf-8");
10287
- }
10288
-
10289
- // app/lib/action-relay-queue.ts
10290
- import {
10291
- existsSync as existsSync20,
10292
- mkdirSync as mkdirSync9,
10293
- readdirSync as readdirSync6,
10294
- readFileSync as readFileSync16,
10295
- unlinkSync as unlinkSync2,
10296
- writeFileSync as writeFileSync10
10297
- } from "fs";
10298
- import { join as join9 } from "path";
10299
- var TAG19 = "[action-relay-queue]";
10300
- var FILE_PREFIX = "action-completion-relay-";
10301
- var FILE_SUFFIX = ".json";
10302
- function queueDir(accountDir) {
10303
- return join9(accountDir, "queue");
10304
- }
10305
- function relayFilePath(accountDir, actionId) {
10306
- return join9(queueDir(accountDir), `${FILE_PREFIX}${actionId}${FILE_SUFFIX}`);
10307
- }
10308
- function enqueueActionCompletionRelay(opts) {
10309
- const dir = queueDir(opts.accountDir);
10310
- mkdirSync9(dir, { recursive: true });
10311
- const filePath = relayFilePath(opts.accountDir, opts.actionId);
10312
- const record = {
10313
- actionId: opts.actionId,
10314
- conversationId: opts.conversationId,
10315
- message: opts.message,
10316
- queuedAt: (/* @__PURE__ */ new Date()).toISOString()
10317
- };
10318
- const body = JSON.stringify(record);
10319
- try {
10320
- writeFileSync10(filePath, body, { flag: "wx", encoding: "utf-8" });
10321
- console.log(
10322
- `${TAG19} phase=enqueued actionId=${opts.actionId} conversationId=${opts.conversationId} path=${filePath} bytes=${Buffer.byteLength(body, "utf-8")}`
10323
- );
10324
- return { enqueued: true, path: filePath };
10325
- } catch (e) {
10326
- if (e.code === "EEXIST") {
10327
- console.log(
10328
- `${TAG19} phase=enqueue-skipped actionId=${opts.actionId} reason=already-queued`
10329
- );
10330
- return { enqueued: false, reason: "already-queued", path: filePath };
10331
- }
10332
- throw e;
10333
- }
10334
- }
10335
- function consumeActionCompletionRelays(accountDir) {
10336
- const dir = queueDir(accountDir);
10337
- if (!existsSync20(dir)) return [];
10338
- let entries;
10339
- try {
10340
- entries = readdirSync6(dir);
10341
- } catch (e) {
10342
- console.error(
10343
- `${TAG19} phase=readdir-failed dir=${dir} error=${e instanceof Error ? e.message : String(e)}`
10344
- );
10345
- return [];
10346
- }
10347
- const records = [];
10348
- for (const name of entries) {
10349
- if (!name.startsWith(FILE_PREFIX) || !name.endsWith(FILE_SUFFIX)) continue;
10350
- const filePath = join9(dir, name);
10351
- let raw;
10352
- try {
10353
- raw = readFileSync16(filePath, "utf-8");
10354
- } catch (e) {
10355
- console.error(
10356
- `${TAG19} phase=read-failed file=${name} error=${e instanceof Error ? e.message : String(e)}`
10357
- );
10358
- continue;
10359
- }
10360
- let parsed;
10361
- try {
10362
- parsed = JSON.parse(raw);
10363
- } catch (e) {
10364
- console.error(
10365
- `${TAG19} phase=parse-failed file=${name} error=${e instanceof Error ? e.message : String(e)}`
10366
- );
10367
- continue;
10368
- }
10369
- if (!isActionCompletionRelay(parsed)) {
10370
- console.error(
10371
- `${TAG19} phase=shape-invalid file=${name} keys=${parsed && typeof parsed === "object" ? Object.keys(parsed).join(",") : "non-object"}`
10372
- );
10373
- continue;
10374
- }
10375
- records.push({ rec: parsed, filePath });
10376
- }
10377
- records.sort((a, b) => a.rec.queuedAt.localeCompare(b.rec.queuedAt));
10378
- const out = [];
10379
- const now = Date.now();
10380
- for (const { rec, filePath } of records) {
10381
- const queuedAtMs = Date.parse(rec.queuedAt);
10382
- const ageMs = Number.isFinite(queuedAtMs) ? now - queuedAtMs : 0;
10383
- out.push({ ...rec, ageMs, filePath });
10384
- }
10385
- return out;
10386
- }
10387
- function deleteConsumedRelay(filePath) {
10388
- try {
10389
- unlinkSync2(filePath);
10390
- } catch (e) {
10391
- const code = e.code;
10392
- if (code !== "ENOENT") {
10393
- console.error(
10394
- `${TAG19} phase=unlink-failed file=${filePath} error=${e instanceof Error ? e.message : String(e)}`
10395
- );
10396
- }
10397
- }
10398
- }
10399
- function isActionCompletionRelay(value) {
10400
- if (!value || typeof value !== "object") return false;
10401
- const v = value;
10402
- return typeof v.actionId === "string" && typeof v.conversationId === "string" && typeof v.message === "string" && typeof v.queuedAt === "string";
8617
+ mkdirSync5(dirname4(ALIAS_DOMAINS_PATH), { recursive: true });
8618
+ writeFileSync6(ALIAS_DOMAINS_PATH, JSON.stringify([...existing], null, 2) + "\n", "utf-8");
10403
8619
  }
10404
8620
 
10405
8621
  // server/routes/admin/cloudflare.ts
10406
- import { existsSync as existsSyncFs, readFileSync as readFileSyncFs } from "fs";
10407
8622
  var SETUP_TIMEOUT_MS = 10 * 60 * 1e3;
10408
8623
  var DOMAINS_TIMEOUT_MS = 40 * 1e3;
10409
8624
  function loadBrandInfo() {
10410
- const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ?? resolve18(process.cwd(), "..");
10411
- const brandPath = resolve18(platformRoot2, "config", "brand.json");
8625
+ const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ?? resolve14(process.cwd(), "..");
8626
+ const brandPath = resolve14(platformRoot2, "config", "brand.json");
10412
8627
  try {
10413
- const parsed = JSON.parse(readFileSync17(brandPath, "utf-8"));
8628
+ const parsed = JSON.parse(readFileSync13(brandPath, "utf-8"));
10414
8629
  const hostname2 = typeof parsed.hostname === "string" && parsed.hostname ? parsed.hostname : "maxy";
10415
8630
  const configDir2 = typeof parsed.configDir === "string" && parsed.configDir ? parsed.configDir : ".maxy";
10416
8631
  return { hostname: hostname2, configDir: configDir2 };
@@ -10513,7 +8728,7 @@ app21.get("/domains", requireAdminSession, async (c) => {
10513
8728
  streamLogPath = streamLogPathFor(accountId, correlationId).streamLogPath;
10514
8729
  log(`phase=stream-log-resolved path=${streamLogPath}`);
10515
8730
  const brand = loadBrandInfo();
10516
- const scriptPath = resolve18(homedir(), "list-cf-domains.sh");
8731
+ const scriptPath = resolve14(homedir(), "list-cf-domains.sh");
10517
8732
  const result = await runFormSpawn({
10518
8733
  scriptPath,
10519
8734
  args: [brand.hostname],
@@ -10706,89 +8921,23 @@ actionId: ${actionId}`,
10706
8921
  };
10707
8922
  return ok(success);
10708
8923
  });
10709
- var RELAY_MAX_BODY = 8 * 1024;
10710
- var RELAY_MAX_MESSAGE = 2048;
10711
- var ACTION_ID_RE = /^[a-z0-9-]+$/;
10712
- app21.post("/relay-completion", requireAdminSession, async (c) => {
10713
- const sessionKey = c.var.sessionKey;
10714
- let raw;
10715
- try {
10716
- raw = await c.req.text();
10717
- } catch {
10718
- return c.json({ ok: false, reason: "invalid-body" }, 400);
10719
- }
10720
- if (Buffer.byteLength(raw, "utf-8") > RELAY_MAX_BODY) {
10721
- return c.json({ ok: false, reason: "body-too-large" }, 413);
10722
- }
10723
- let body;
10724
- try {
10725
- body = JSON.parse(raw);
10726
- } catch {
10727
- return c.json({ ok: false, reason: "invalid-json" }, 400);
10728
- }
10729
- if (!body || typeof body !== "object") {
10730
- return c.json({ ok: false, reason: "invalid-body" }, 400);
10731
- }
10732
- const { actionId, message } = body;
10733
- if (typeof actionId !== "string" || !ACTION_ID_RE.test(actionId) || actionId.length > 200) {
10734
- console.error(`[cloudflare-relay-completion] phase=enqueue-skipped reason=invalid-actionid sessionKey=${sessionKey.slice(-8)}`);
10735
- return c.json({ ok: false, reason: "invalid-actionid" }, 400);
10736
- }
10737
- if (typeof message !== "string" || message.length === 0 || message.length > RELAY_MAX_MESSAGE) {
10738
- return c.json({ ok: false, reason: "invalid-message" }, 400);
10739
- }
10740
- const conversationId = getConversationIdForSession(sessionKey);
10741
- if (!conversationId) {
10742
- console.error(`[cloudflare-relay-completion] phase=enqueue-skipped reason=missing-conversation-id sessionKey=${sessionKey.slice(-8)}`);
10743
- return c.json({ ok: false, reason: "missing-conversation-id" }, 400);
10744
- }
10745
- const account = resolveAccount();
10746
- if (!account) {
10747
- console.error(`[cloudflare-relay-completion] phase=enqueue-skipped reason=missing-account-dir actionId=${actionId}`);
10748
- return c.json({ ok: false, reason: "missing-account-dir" }, 500);
10749
- }
10750
- const logPath2 = actionLogPath(actionId);
10751
- let auditOutcome = "log-absent";
10752
- if (existsSyncFs(logPath2)) {
10753
- try {
10754
- const lines = readFileSyncFs(logPath2, "utf-8").split("\n");
10755
- const reconciled = reconcileCloudflareSetupFromLog(lines);
10756
- auditOutcome = reconciled ? reconciled.kind : "log-incomplete";
10757
- } catch (e) {
10758
- auditOutcome = `read-failed:${e instanceof Error ? e.message : String(e)}`;
10759
- }
10760
- }
10761
- console.log(`[cloudflare-relay-completion] phase=enqueue-attempt actionId=${actionId} auditOutcome=${auditOutcome} conversationId=${conversationId}`);
10762
- try {
10763
- enqueueActionCompletionRelay({
10764
- accountDir: account.accountDir,
10765
- actionId,
10766
- conversationId,
10767
- message
10768
- });
10769
- } catch (e) {
10770
- console.error(`[cloudflare-relay-completion] phase=enqueue-failed actionId=${actionId} error=${e instanceof Error ? e.message : String(e)}`);
10771
- return c.json({ ok: false, reason: "enqueue-failed" }, 500);
10772
- }
10773
- return c.body(null, 204);
10774
- });
10775
8924
  var cloudflare_default = app21;
10776
8925
 
10777
8926
  // server/routes/admin/files.ts
10778
8927
  import { createReadStream as createReadStream3 } from "fs";
10779
8928
  import { readdir as readdir2, readFile as readFile4, stat as stat4, mkdir as mkdir3, writeFile as writeFile4, unlink as unlink2 } from "fs/promises";
10780
8929
  import { realpathSync as realpathSync4 } from "fs";
10781
- import { basename as basename6, dirname as dirname8, join as join10, resolve as resolve20, sep as sep2 } from "path";
8930
+ import { basename as basename4, dirname as dirname5, join as join8, resolve as resolve16, sep as sep2 } from "path";
10782
8931
  import { Readable as Readable2 } from "stream";
10783
8932
 
10784
8933
  // app/lib/data-path.ts
10785
8934
  import { realpathSync as realpathSync3 } from "fs";
10786
- import { resolve as resolve19, normalize, sep, relative } from "path";
10787
- var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ?? resolve19(process.cwd(), "../platform");
10788
- var DATA_ROOT = resolve19(PLATFORM_ROOT6, "..", "data");
8935
+ import { resolve as resolve15, normalize, sep, relative } from "path";
8936
+ var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ?? resolve15(process.cwd(), "../platform");
8937
+ var DATA_ROOT = resolve15(PLATFORM_ROOT6, "..", "data");
10789
8938
  function resolveDataPath(raw) {
10790
8939
  const cleaned = normalize("/" + (raw ?? "").replace(/\\/g, "/")).replace(/^\/+/, "");
10791
- const absolute = resolve19(DATA_ROOT, cleaned);
8940
+ const absolute = resolve15(DATA_ROOT, cleaned);
10792
8941
  let dataRootReal;
10793
8942
  try {
10794
8943
  dataRootReal = realpathSync3(DATA_ROOT);
@@ -11136,7 +9285,7 @@ async function cascadeDeleteDocument(params) {
11136
9285
  var UUID_RE4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11137
9286
  async function readMeta(absDir, baseName) {
11138
9287
  try {
11139
- const raw = await readFile4(join10(absDir, `${baseName}.meta.json`), "utf8");
9288
+ const raw = await readFile4(join8(absDir, `${baseName}.meta.json`), "utf8");
11140
9289
  const parsed = JSON.parse(raw);
11141
9290
  if (typeof parsed?.filename === "string") {
11142
9291
  return { filename: parsed.filename, mimeType: typeof parsed.mimeType === "string" ? parsed.mimeType : void 0 };
@@ -11147,7 +9296,7 @@ async function readMeta(absDir, baseName) {
11147
9296
  }
11148
9297
  async function readAccountNames() {
11149
9298
  const map = /* @__PURE__ */ new Map();
11150
- const accountsDir = resolve20(DATA_ROOT, "accounts");
9299
+ const accountsDir = resolve16(DATA_ROOT, "accounts");
11151
9300
  let names;
11152
9301
  try {
11153
9302
  names = await readdir2(accountsDir);
@@ -11156,7 +9305,7 @@ async function readAccountNames() {
11156
9305
  }
11157
9306
  for (const name of names) {
11158
9307
  if (!UUID_RE4.test(name)) continue;
11159
- const configPath2 = resolve20(accountsDir, name, "account.json");
9308
+ const configPath2 = resolve16(accountsDir, name, "account.json");
11160
9309
  try {
11161
9310
  const raw = await readFile4(configPath2, "utf8");
11162
9311
  const parsed = JSON.parse(raw);
@@ -11174,7 +9323,7 @@ async function readAccountNames() {
11174
9323
  }
11175
9324
  async function enrich(absolute, entry, accountNames) {
11176
9325
  if (entry.kind === "directory" && UUID_RE4.test(entry.name)) {
11177
- const meta = await readMeta(join10(absolute, entry.name), entry.name);
9326
+ const meta = await readMeta(join8(absolute, entry.name), entry.name);
11178
9327
  if (meta?.filename) {
11179
9328
  entry.displayName = meta.filename;
11180
9329
  entry.mimeType = meta.mimeType;
@@ -11233,7 +9382,7 @@ app22.get("/", requireAdminSession, async (c) => {
11233
9382
  continue;
11234
9383
  }
11235
9384
  try {
11236
- const entryPath = join10(absolute, name);
9385
+ const entryPath = join8(absolute, name);
11237
9386
  const s = await stat4(entryPath);
11238
9387
  entries.push({
11239
9388
  name,
@@ -11288,7 +9437,7 @@ app22.get("/download", requireAdminSession, async (c) => {
11288
9437
  if (!info.isFile()) {
11289
9438
  return c.json({ error: "Path is not a file" }, 400);
11290
9439
  }
11291
- const filename = basename6(absolute);
9440
+ const filename = basename4(absolute);
11292
9441
  const mimeType = detectMimeType(absolute);
11293
9442
  const nodeStream = createReadStream3(absolute);
11294
9443
  const webStream = Readable2.toWeb(nodeStream);
@@ -11345,10 +9494,10 @@ app22.post("/upload", requireAdminSession, async (c) => {
11345
9494
  error: `Unsupported file type: "${file.type}". Supported: ${[...SUPPORTED_MIME_TYPES].join(", ")}.`
11346
9495
  }, 422);
11347
9496
  }
11348
- const safeName = basename6(file.name).replace(/[\0/\\]/g, "_");
9497
+ const safeName = basename4(file.name).replace(/[\0/\\]/g, "_");
11349
9498
  const finalName = `${Date.now()}-${safeName}`;
11350
- const destDir = resolve20(DATA_ROOT, "uploads", accountId);
11351
- const destPath = resolve20(destDir, finalName);
9499
+ const destDir = resolve16(DATA_ROOT, "uploads", accountId);
9500
+ const destPath = resolve16(destDir, finalName);
11352
9501
  try {
11353
9502
  await mkdir3(destDir, { recursive: true });
11354
9503
  const dataRootReal = realpathSync4(DATA_ROOT);
@@ -11390,7 +9539,7 @@ app22.delete("/", requireAdminSession, async (c) => {
11390
9539
  return c.json({ error: resolution.error }, resolution.status);
11391
9540
  }
11392
9541
  const { absolute, relative: relPath2 } = resolution;
11393
- const base = basename6(absolute);
9542
+ const base = basename4(absolute);
11394
9543
  const segments = relPath2.split("/").filter(Boolean);
11395
9544
  if (base === "account.json" || segments.includes(".git")) {
11396
9545
  console.error(`[data] file-delete blocked path="${relPath2}" reason="protected"`);
@@ -11406,7 +9555,7 @@ app22.delete("/", requireAdminSession, async (c) => {
11406
9555
  }
11407
9556
  const dot = base.lastIndexOf(".");
11408
9557
  const stem = dot === -1 ? base : base.slice(0, dot);
11409
- const sidecarPath = UUID_RE4.test(stem) && base !== `${stem}.meta.json` ? join10(dirname8(absolute), `${stem}.meta.json`) : null;
9558
+ const sidecarPath = UUID_RE4.test(stem) && base !== `${stem}.meta.json` ? join8(dirname5(absolute), `${stem}.meta.json`) : null;
11410
9559
  await unlink2(absolute);
11411
9560
  if (sidecarPath) {
11412
9561
  try {
@@ -11957,11 +10106,6 @@ var GRAPH_LABEL_COLOURS = {
11957
10106
  // confusion doesn't apply, but kept distinguishable for the legend)
11958
10107
  Email: "#6F7F4A",
11959
10108
  EmailAccount: "#91A063",
11960
- // Review signals (Task 626 — previously written by review-detector but
11961
- // unregistered here, producing an `unknown label` 400 whenever the
11962
- // filter popover advertised the label. Muted brick — still reads as
11963
- // alert against the cream background but doesn't shout fire-engine red.)
11964
- ReviewAlert: "#A85C5C",
11965
10109
  // Public-agent projection (Task 837) — burnished bronze sits between
11966
10110
  // people-terracotta and email-moss but shares neither hue, signalling
11967
10111
  // "operator-defined persona that traverses to KnowledgeDocuments,
@@ -12886,8 +11030,8 @@ var adherence_default = app30;
12886
11030
  // server/routes/admin/sidebar-artefacts.ts
12887
11031
  import neo4j3 from "neo4j-driver";
12888
11032
  import { readFile as readFile5, readdir as readdir3, stat as stat5 } from "fs/promises";
12889
- import { resolve as resolve21, relative as relative2, isAbsolute } from "path";
12890
- import { existsSync as existsSync21 } from "fs";
11033
+ import { resolve as resolve17, relative as relative2, isAbsolute } from "path";
11034
+ import { existsSync as existsSync17 } from "fs";
12891
11035
  var LIMIT = 50;
12892
11036
  var TEXT_MIME_PREFIXES = ["text/", "application/json", "application/markdown"];
12893
11037
  var ADMIN_AGENT_FILES = ["IDENTITY.md", "SOUL.md", "KNOWLEDGE.md"];
@@ -12903,7 +11047,7 @@ app31.get("/", requireAdminSession, async (c) => {
12903
11047
  if (docs === null) {
12904
11048
  return c.json({ error: "Failed to load artefacts" }, 500);
12905
11049
  }
12906
- const accountDir = resolve21(ACCOUNTS_DIR, accountId);
11050
+ const accountDir = resolve17(ACCOUNTS_DIR, accountId);
12907
11051
  const agents = await fetchAgentTemplateRows(accountDir);
12908
11052
  const artefacts = [...docs, ...agents].sort(
12909
11053
  (a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "")
@@ -12966,8 +11110,8 @@ async function readArtefactContent(accountId, attachmentId, mimeType, displayNam
12966
11110
  logSkip(displayName, "non-text-mime", mimeType);
12967
11111
  return { content: "", skipReason: "non-text-mime" };
12968
11112
  }
12969
- const accountDir = resolve21(ATTACHMENTS_ROOT, accountId);
12970
- const dir = resolve21(accountDir, attachmentId);
11113
+ const accountDir = resolve17(ATTACHMENTS_ROOT, accountId);
11114
+ const dir = resolve17(accountDir, attachmentId);
12971
11115
  try {
12972
11116
  validateFilePathInAccount(dir, accountDir);
12973
11117
  } catch {
@@ -12981,7 +11125,7 @@ async function readArtefactContent(accountId, attachmentId, mimeType, displayNam
12981
11125
  logSkip(displayName, "missing-on-disk", mimeType);
12982
11126
  return { content: "", skipReason: "missing-on-disk" };
12983
11127
  }
12984
- return { content: await readFile5(resolve21(dir, dataFile), "utf-8"), skipReason: null };
11128
+ return { content: await readFile5(resolve17(dir, dataFile), "utf-8"), skipReason: null };
12985
11129
  } catch (err) {
12986
11130
  const message = err instanceof Error ? err.message : String(err);
12987
11131
  console.error(`[admin/sidebar-artefacts] read-failed attachmentId=${attachmentId.slice(0, 8)} error="${message}"`);
@@ -12997,8 +11141,8 @@ function logSkip(name, reason, mimeType) {
12997
11141
  async function fetchAgentTemplateRows(accountDir) {
12998
11142
  const rows = [];
12999
11143
  for (const filename of ADMIN_AGENT_FILES) {
13000
- const overridePath = resolve21(accountDir, "agents", "admin", filename);
13001
- const bundledPath = resolve21(PLATFORM_ROOT, "templates", "agents", "admin", filename);
11144
+ const overridePath = resolve17(accountDir, "agents", "admin", filename);
11145
+ const bundledPath = resolve17(PLATFORM_ROOT, "templates", "agents", "admin", filename);
13002
11146
  const labelStem = filename.replace(/\.md$/, "");
13003
11147
  const row = await readAgentTemplateRow({
13004
11148
  id: `agent-template:admin:${filename}`,
@@ -13012,12 +11156,12 @@ async function fetchAgentTemplateRows(accountDir) {
13012
11156
  });
13013
11157
  if (row) rows.push(row);
13014
11158
  }
13015
- const overrideDir = resolve21(accountDir, "specialists", "agents");
13016
- const bundledDir = resolve21(PLATFORM_ROOT, "templates", "specialists", "agents");
11159
+ const overrideDir = resolve17(accountDir, "specialists", "agents");
11160
+ const bundledDir = resolve17(PLATFORM_ROOT, "templates", "specialists", "agents");
13017
11161
  const specialistNames = await unionSpecialistFilenames(overrideDir, bundledDir);
13018
11162
  for (const filename of specialistNames) {
13019
- const overridePath = resolve21(overrideDir, filename);
13020
- const bundledPath = resolve21(bundledDir, filename);
11163
+ const overridePath = resolve17(overrideDir, filename);
11164
+ const bundledPath = resolve17(bundledDir, filename);
13021
11165
  const row = await readAgentTemplateRow({
13022
11166
  id: `agent-template:specialist:${filename}`,
13023
11167
  displayName: filename.replace(/\.md$/, ""),
@@ -13035,7 +11179,7 @@ async function fetchAgentTemplateRows(accountDir) {
13035
11179
  async function unionSpecialistFilenames(overrideDir, bundledDir) {
13036
11180
  const names = /* @__PURE__ */ new Set();
13037
11181
  for (const dir of [overrideDir, bundledDir]) {
13038
- if (!existsSync21(dir)) continue;
11182
+ if (!existsSync17(dir)) continue;
13039
11183
  try {
13040
11184
  const entries = await readdir3(dir);
13041
11185
  for (const entry of entries) {
@@ -13050,7 +11194,7 @@ async function unionSpecialistFilenames(overrideDir, bundledDir) {
13050
11194
  }
13051
11195
  async function readAgentTemplateRow(inp) {
13052
11196
  let chosenPath = null;
13053
- if (existsSync21(inp.overridePath)) {
11197
+ if (existsSync17(inp.overridePath)) {
13054
11198
  try {
13055
11199
  validateFilePathInAccount(inp.overridePath, inp.overrideRoot);
13056
11200
  chosenPath = inp.overridePath;
@@ -13061,7 +11205,7 @@ async function readAgentTemplateRow(inp) {
13061
11205
  );
13062
11206
  return null;
13063
11207
  }
13064
- } else if (existsSync21(inp.bundledPath)) {
11208
+ } else if (existsSync17(inp.bundledPath)) {
13065
11209
  if (!isWithin(inp.bundledPath, inp.bundledRoot)) {
13066
11210
  console.error(
13067
11211
  `[admin/sidebar-artefacts] agent-template-read-failed agent=${inp.displayName} kind=${inp.logName} error="bundled path outside PLATFORM_ROOT"`
@@ -13102,8 +11246,8 @@ var sidebar_artefacts_default = app31;
13102
11246
 
13103
11247
  // server/routes/admin/sidebar-artefact-save.ts
13104
11248
  import { mkdir as mkdir4, readdir as readdir4, stat as stat6, writeFile as writeFile5 } from "fs/promises";
13105
- import { resolve as resolve22 } from "path";
13106
- import { existsSync as existsSync22 } from "fs";
11249
+ import { resolve as resolve18 } from "path";
11250
+ import { existsSync as existsSync18 } from "fs";
13107
11251
  var ADMIN_AGENT_FILES2 = /* @__PURE__ */ new Set(["IDENTITY.md", "SOUL.md", "KNOWLEDGE.md"]);
13108
11252
  var UUID_RE5 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
13109
11253
  var app32 = new Hono();
@@ -13115,7 +11259,7 @@ app32.post("/", requireAdminSession, async (c) => {
13115
11259
  if (!body || typeof body.id !== "string" || typeof body.content !== "string") {
13116
11260
  return c.json({ error: "id and content required" }, 400);
13117
11261
  }
13118
- const accountDir = resolve22(ACCOUNTS_DIR, accountId);
11262
+ const accountDir = resolve18(ACCOUNTS_DIR, accountId);
13119
11263
  const resolved = await resolveSavePath(body.id, accountId, accountDir);
13120
11264
  if (resolved.kind === "reject") {
13121
11265
  console.error(
@@ -13156,22 +11300,22 @@ async function resolveSavePath(id, accountId, accountDir) {
13156
11300
  if (role !== "admin" || !ADMIN_AGENT_FILES2.has(filename)) {
13157
11301
  return { kind: "reject", status: 400, reason: "invalid-id" };
13158
11302
  }
13159
- const parent = resolve22(accountDir, "agents", "admin");
11303
+ const parent = resolve18(accountDir, "agents", "admin");
13160
11304
  await mkdir4(parent, { recursive: true });
13161
11305
  try {
13162
11306
  validateFilePathInAccount(parent, accountDir);
13163
11307
  } catch {
13164
11308
  return { kind: "reject", status: 400, reason: "containment-rejected" };
13165
11309
  }
13166
- return { kind: "admin-template", path: resolve22(parent, filename) };
11310
+ return { kind: "admin-template", path: resolve18(parent, filename) };
13167
11311
  }
13168
11312
  if (UUID_RE5.test(id)) {
13169
- const dir = resolve22(ATTACHMENTS_ROOT, accountId, id);
13170
- if (!existsSync22(dir)) {
11313
+ const dir = resolve18(ATTACHMENTS_ROOT, accountId, id);
11314
+ if (!existsSync18(dir)) {
13171
11315
  return { kind: "reject", status: 400, reason: "not-found" };
13172
11316
  }
13173
11317
  try {
13174
- validateFilePathInAccount(dir, resolve22(ATTACHMENTS_ROOT, accountId));
11318
+ validateFilePathInAccount(dir, resolve18(ATTACHMENTS_ROOT, accountId));
13175
11319
  } catch {
13176
11320
  return { kind: "reject", status: 400, reason: "containment-rejected" };
13177
11321
  }
@@ -13180,7 +11324,7 @@ async function resolveSavePath(id, accountId, accountDir) {
13180
11324
  if (!dataFile) {
13181
11325
  return { kind: "reject", status: 400, reason: "not-found" };
13182
11326
  }
13183
- return { kind: "knowledge-doc", path: resolve22(dir, dataFile) };
11327
+ return { kind: "knowledge-doc", path: resolve18(dir, dataFile) };
13184
11328
  }
13185
11329
  return { kind: "reject", status: 400, reason: "invalid-id" };
13186
11330
  }
@@ -13191,8 +11335,8 @@ var sidebar_artefact_save_default = app32;
13191
11335
 
13192
11336
  // server/routes/admin/sidebar-artefact-content.ts
13193
11337
  import { readFile as readFile6, readdir as readdir5 } from "fs/promises";
13194
- import { existsSync as existsSync23 } from "fs";
13195
- import { resolve as resolve23 } from "path";
11338
+ import { existsSync as existsSync19 } from "fs";
11339
+ import { resolve as resolve19 } from "path";
13196
11340
  var UUID_RE6 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
13197
11341
  var app33 = new Hono();
13198
11342
  app33.get("/", requireAdminSession, async (c) => {
@@ -13204,14 +11348,14 @@ app33.get("/", requireAdminSession, async (c) => {
13204
11348
  console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
13205
11349
  return new Response("Not found", { status: 404 });
13206
11350
  }
13207
- const dir = resolve23(ATTACHMENTS_ROOT, accountId, id);
13208
- if (!existsSync23(dir)) {
11351
+ const dir = resolve19(ATTACHMENTS_ROOT, accountId, id);
11352
+ if (!existsSync19(dir)) {
13209
11353
  console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
13210
11354
  return new Response("Not found", { status: 404 });
13211
11355
  }
13212
11356
  let meta;
13213
11357
  try {
13214
- meta = JSON.parse(await readFile6(resolve23(dir, `${id}.meta.json`), "utf-8"));
11358
+ meta = JSON.parse(await readFile6(resolve19(dir, `${id}.meta.json`), "utf-8"));
13215
11359
  } catch {
13216
11360
  console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
13217
11361
  return new Response("Not found", { status: 404 });
@@ -13223,7 +11367,7 @@ app33.get("/", requireAdminSession, async (c) => {
13223
11367
  return new Response("Not found", { status: 404 });
13224
11368
  }
13225
11369
  const start = Date.now();
13226
- const buffer = await readFile6(resolve23(dir, dataFile));
11370
+ const buffer = await readFile6(resolve19(dir, dataFile));
13227
11371
  const ms = Date.now() - start;
13228
11372
  console.log(
13229
11373
  `[admin/sidebar-artefact-content] account=${accountId} id=${id.slice(0, 8)} mime=${meta.mimeType} bytes=${buffer.length} ms=${ms}`
@@ -13267,8 +11411,8 @@ app34.route("/sidebar-artefact-content", sidebar_artefact_content_default);
13267
11411
  var admin_default = app34;
13268
11412
 
13269
11413
  // server/routes/sites.ts
13270
- import { existsSync as existsSync24, readFileSync as readFileSync18, realpathSync as realpathSync5, statSync as statSync8 } from "fs";
13271
- import { resolve as resolve24 } from "path";
11414
+ import { existsSync as existsSync20, readFileSync as readFileSync14, realpathSync as realpathSync5, statSync as statSync5 } from "fs";
11415
+ import { resolve as resolve20 } from "path";
13272
11416
  var SAFE_SEG_RE = /^[a-z0-9_][a-z0-9_.-]{0,99}$/i;
13273
11417
  var MIME = {
13274
11418
  ".html": "text/html; charset=utf-8",
@@ -13326,28 +11470,28 @@ app35.get("/:rel{.*}", (c) => {
13326
11470
  }
13327
11471
  segments.push(seg);
13328
11472
  }
13329
- const rootDir = resolve24(account.accountDir, "sites");
13330
- let filePath = segments.length === 0 ? rootDir : resolve24(rootDir, ...segments);
11473
+ const rootDir = resolve20(account.accountDir, "sites");
11474
+ let filePath = segments.length === 0 ? rootDir : resolve20(rootDir, ...segments);
13331
11475
  if (filePath !== rootDir && !filePath.startsWith(rootDir + "/")) {
13332
11476
  console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
13333
11477
  return c.text("Forbidden", 403);
13334
11478
  }
13335
11479
  let stat7;
13336
11480
  try {
13337
- stat7 = existsSync24(filePath) ? statSync8(filePath) : null;
11481
+ stat7 = existsSync20(filePath) ? statSync5(filePath) : null;
13338
11482
  } catch {
13339
11483
  stat7 = null;
13340
11484
  }
13341
11485
  if (stat7?.isDirectory()) {
13342
- filePath = resolve24(filePath, "index.html");
11486
+ filePath = resolve20(filePath, "index.html");
13343
11487
  } else if (stat7 === null && isDirRequest) {
13344
- filePath = resolve24(filePath, "index.html");
11488
+ filePath = resolve20(filePath, "index.html");
13345
11489
  }
13346
11490
  if (!filePath.startsWith(rootDir + "/")) {
13347
11491
  console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
13348
11492
  return c.text("Forbidden", 403);
13349
11493
  }
13350
- if (!existsSync24(filePath)) {
11494
+ if (!existsSync20(filePath)) {
13351
11495
  console.error(`[sites] not-found path=${reqPath} status=404`);
13352
11496
  return c.text("Not found", 404);
13353
11497
  }
@@ -13366,7 +11510,7 @@ app35.get("/:rel{.*}", (c) => {
13366
11510
  }
13367
11511
  let body;
13368
11512
  try {
13369
- body = readFileSync18(realPath);
11513
+ body = readFileSync14(realPath);
13370
11514
  } catch (err) {
13371
11515
  const code = err?.code;
13372
11516
  if (code === "EISDIR") {
@@ -13498,14 +11642,14 @@ function clientFrom(c) {
13498
11642
  );
13499
11643
  }
13500
11644
  var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT || "";
13501
- var BRAND_JSON_PATH = PLATFORM_ROOT7 ? join11(PLATFORM_ROOT7, "config", "brand.json") : "";
11645
+ var BRAND_JSON_PATH = PLATFORM_ROOT7 ? join9(PLATFORM_ROOT7, "config", "brand.json") : "";
13502
11646
  var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", domain: "getmaxy.com" };
13503
- if (BRAND_JSON_PATH && !existsSync25(BRAND_JSON_PATH)) {
11647
+ if (BRAND_JSON_PATH && !existsSync21(BRAND_JSON_PATH)) {
13504
11648
  console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
13505
11649
  }
13506
- if (BRAND_JSON_PATH && existsSync25(BRAND_JSON_PATH)) {
11650
+ if (BRAND_JSON_PATH && existsSync21(BRAND_JSON_PATH)) {
13507
11651
  try {
13508
- const parsed = JSON.parse(readFileSync19(BRAND_JSON_PATH, "utf-8"));
11652
+ const parsed = JSON.parse(readFileSync15(BRAND_JSON_PATH, "utf-8"));
13509
11653
  BRAND = { ...BRAND, ...parsed };
13510
11654
  } catch (err) {
13511
11655
  console.error(`[brand] Failed to parse brand.json: ${err.message}`);
@@ -13524,11 +11668,11 @@ var brandLoginOpts = {
13524
11668
  bodyFont: BRAND.defaultFonts?.body,
13525
11669
  logoContainsName: !!BRAND.logoContainsName
13526
11670
  };
13527
- var ALIAS_DOMAINS_PATH2 = join11(homedir2(), BRAND.configDir, "alias-domains.json");
11671
+ var ALIAS_DOMAINS_PATH2 = join9(homedir2(), BRAND.configDir, "alias-domains.json");
13528
11672
  function loadAliasDomains() {
13529
11673
  try {
13530
- if (!existsSync25(ALIAS_DOMAINS_PATH2)) return null;
13531
- const parsed = JSON.parse(readFileSync19(ALIAS_DOMAINS_PATH2, "utf-8"));
11674
+ if (!existsSync21(ALIAS_DOMAINS_PATH2)) return null;
11675
+ const parsed = JSON.parse(readFileSync15(ALIAS_DOMAINS_PATH2, "utf-8"));
13532
11676
  if (!Array.isArray(parsed)) {
13533
11677
  console.error("[alias-domains] malformed alias-domains.json \u2014 expected array");
13534
11678
  return null;
@@ -13868,20 +12012,20 @@ app36.get("/agent-assets/:slug/:filename", (c) => {
13868
12012
  console.error(`[agent-assets] no-account slug=${slug} file=${filename}`);
13869
12013
  return c.text("Not found", 404);
13870
12014
  }
13871
- const filePath = resolve25(account.accountDir, "agents", slug, "assets", filename);
13872
- const expectedDir = resolve25(account.accountDir, "agents", slug, "assets");
12015
+ const filePath = resolve21(account.accountDir, "agents", slug, "assets", filename);
12016
+ const expectedDir = resolve21(account.accountDir, "agents", slug, "assets");
13873
12017
  if (!filePath.startsWith(expectedDir + "/")) {
13874
12018
  console.error(`[agent-assets] path-traversal-rejected slug=${slug} file=${filename}`);
13875
12019
  return c.text("Forbidden", 403);
13876
12020
  }
13877
- if (!existsSync25(filePath)) {
12021
+ if (!existsSync21(filePath)) {
13878
12022
  console.error(`[agent-assets] serve slug=${slug} file=${filename} status=404`);
13879
12023
  return c.text("Not found", 404);
13880
12024
  }
13881
12025
  const ext = "." + filename.split(".").pop()?.toLowerCase();
13882
12026
  const contentType = IMAGE_MIME[ext] || "application/octet-stream";
13883
12027
  console.log(`[agent-assets] serve slug=${slug} file=${filename} status=200`);
13884
- const body = readFileSync19(filePath);
12028
+ const body = readFileSync15(filePath);
13885
12029
  return c.body(body, 200, {
13886
12030
  "Content-Type": contentType,
13887
12031
  "Cache-Control": "public, max-age=3600"
@@ -13898,20 +12042,20 @@ app36.get("/generated/:filename", (c) => {
13898
12042
  console.error(`[generated] serve file=${filename} status=404`);
13899
12043
  return c.text("Not found", 404);
13900
12044
  }
13901
- const filePath = resolve25(account.accountDir, "generated", filename);
13902
- const expectedDir = resolve25(account.accountDir, "generated");
12045
+ const filePath = resolve21(account.accountDir, "generated", filename);
12046
+ const expectedDir = resolve21(account.accountDir, "generated");
13903
12047
  if (!filePath.startsWith(expectedDir + "/")) {
13904
12048
  console.error(`[generated] serve file=${filename} status=403`);
13905
12049
  return c.text("Forbidden", 403);
13906
12050
  }
13907
- if (!existsSync25(filePath)) {
12051
+ if (!existsSync21(filePath)) {
13908
12052
  console.error(`[generated] serve file=${filename} status=404`);
13909
12053
  return c.text("Not found", 404);
13910
12054
  }
13911
12055
  const ext = "." + filename.split(".").pop()?.toLowerCase();
13912
12056
  const contentType = IMAGE_MIME[ext] || "application/octet-stream";
13913
12057
  console.log(`[generated] serve file=${filename} status=200`);
13914
- const body = readFileSync19(filePath);
12058
+ const body = readFileSync15(filePath);
13915
12059
  return c.body(body, 200, {
13916
12060
  "Content-Type": contentType,
13917
12061
  "Cache-Control": "public, max-age=86400"
@@ -13921,9 +12065,9 @@ app36.route("/sites", sites_default);
13921
12065
  var htmlCache = /* @__PURE__ */ new Map();
13922
12066
  var brandLogoPath = "/brand/maxy-monochrome.png";
13923
12067
  var brandIconPath = "/brand/maxy-monochrome.png";
13924
- if (BRAND_JSON_PATH && existsSync25(BRAND_JSON_PATH)) {
12068
+ if (BRAND_JSON_PATH && existsSync21(BRAND_JSON_PATH)) {
13925
12069
  try {
13926
- const fullBrand = JSON.parse(readFileSync19(BRAND_JSON_PATH, "utf-8"));
12070
+ const fullBrand = JSON.parse(readFileSync15(BRAND_JSON_PATH, "utf-8"));
13927
12071
  if (fullBrand.assets?.logo) brandLogoPath = `/brand/${fullBrand.assets.logo}`;
13928
12072
  brandIconPath = fullBrand.assets?.icon ? `/brand/${fullBrand.assets.icon}` : brandLogoPath;
13929
12073
  } catch {
@@ -13940,9 +12084,9 @@ var brandScript = `<script>window.__BRAND__=${JSON.stringify({
13940
12084
  function readInstalledVersion() {
13941
12085
  try {
13942
12086
  if (!PLATFORM_ROOT7) return "unknown";
13943
- const versionFile = join11(PLATFORM_ROOT7, "config", `.${BRAND.hostname}-version`);
13944
- if (!existsSync25(versionFile)) return "unknown";
13945
- const content = readFileSync19(versionFile, "utf-8").trim();
12087
+ const versionFile = join9(PLATFORM_ROOT7, "config", `.${BRAND.hostname}-version`);
12088
+ if (!existsSync21(versionFile)) return "unknown";
12089
+ const content = readFileSync15(versionFile, "utf-8").trim();
13946
12090
  return content || "unknown";
13947
12091
  } catch {
13948
12092
  return "unknown";
@@ -13983,7 +12127,7 @@ var clientErrorReporterScript = `<script>
13983
12127
  function cachedHtml(file) {
13984
12128
  let html = htmlCache.get(file);
13985
12129
  if (!html) {
13986
- html = readFileSync19(resolve25(process.cwd(), "public", file), "utf-8");
12130
+ html = readFileSync15(resolve21(process.cwd(), "public", file), "utf-8");
13987
12131
  const productNameEsc = escapeHtml(BRAND.productName);
13988
12132
  html = html.replace(/<title>([^<]*)<\/title>/, (_match, inner) => `<title>${escapeHtml(inner).replace(/Maxy/g, productNameEsc)}</title>`);
13989
12133
  html = html.replace('href="/favicon.ico"', `href="${escapeHtml(brandFaviconPath)}"`);
@@ -13999,26 +12143,26 @@ ${clientErrorReporterScript}
13999
12143
  }
14000
12144
  var brandedHtmlCache = /* @__PURE__ */ new Map();
14001
12145
  function loadBrandingCache(agentSlug) {
14002
- const configDir2 = join11(homedir2(), BRAND.configDir);
12146
+ const configDir2 = join9(homedir2(), BRAND.configDir);
14003
12147
  try {
14004
- const accountJsonPath = join11(configDir2, "account.json");
14005
- if (!existsSync25(accountJsonPath)) return null;
14006
- const account = JSON.parse(readFileSync19(accountJsonPath, "utf-8"));
12148
+ const accountJsonPath = join9(configDir2, "account.json");
12149
+ if (!existsSync21(accountJsonPath)) return null;
12150
+ const account = JSON.parse(readFileSync15(accountJsonPath, "utf-8"));
14007
12151
  const accountId = account.accountId;
14008
12152
  if (!accountId) return null;
14009
- const cachePath = join11(configDir2, "branding-cache", accountId, `${agentSlug}.json`);
14010
- if (!existsSync25(cachePath)) return null;
14011
- return JSON.parse(readFileSync19(cachePath, "utf-8"));
12153
+ const cachePath = join9(configDir2, "branding-cache", accountId, `${agentSlug}.json`);
12154
+ if (!existsSync21(cachePath)) return null;
12155
+ return JSON.parse(readFileSync15(cachePath, "utf-8"));
14012
12156
  } catch {
14013
12157
  return null;
14014
12158
  }
14015
12159
  }
14016
12160
  function resolveDefaultSlug() {
14017
12161
  try {
14018
- const configDir2 = join11(homedir2(), BRAND.configDir);
14019
- const accountJsonPath = join11(configDir2, "account.json");
14020
- if (!existsSync25(accountJsonPath)) return null;
14021
- const account = JSON.parse(readFileSync19(accountJsonPath, "utf-8"));
12162
+ const configDir2 = join9(homedir2(), BRAND.configDir);
12163
+ const accountJsonPath = join9(configDir2, "account.json");
12164
+ if (!existsSync21(accountJsonPath)) return null;
12165
+ const account = JSON.parse(readFileSync15(accountJsonPath, "utf-8"));
14022
12166
  return account.defaultAgent || null;
14023
12167
  } catch {
14024
12168
  return null;
@@ -14091,7 +12235,7 @@ app36.use("/vnc-popout.html", logViewerFetch);
14091
12235
  app36.get("/vnc-popout.html", (c) => {
14092
12236
  let html = htmlCache.get("vnc-popout.html");
14093
12237
  if (!html) {
14094
- html = readFileSync19(resolve25(process.cwd(), "public", "vnc-popout.html"), "utf-8");
12238
+ html = readFileSync15(resolve21(process.cwd(), "public", "vnc-popout.html"), "utf-8");
14095
12239
  const name = escapeHtml(BRAND.productName);
14096
12240
  html = html.replace("<title>Browser \u2014 Maxy</title>", `<title>${name}</title>`);
14097
12241
  html = html.replace("</head>", ` ${brandScript}
@@ -14181,8 +12325,8 @@ try {
14181
12325
  (async () => {
14182
12326
  try {
14183
12327
  let userId = "";
14184
- if (existsSync25(USERS_FILE)) {
14185
- const users = JSON.parse(readFileSync19(USERS_FILE, "utf-8").trim() || "[]");
12328
+ if (existsSync21(USERS_FILE)) {
12329
+ const users = JSON.parse(readFileSync15(USERS_FILE, "utf-8").trim() || "[]");
14186
12330
  userId = users[0]?.userId ?? "";
14187
12331
  }
14188
12332
  await backfillNullUserIdConversations(userId);
@@ -14197,15 +12341,8 @@ try {
14197
12341
  console.error(`[migration] runBootMigrations rejected: ${err instanceof Error ? err.message : String(err)}`);
14198
12342
  }
14199
12343
  })();
14200
- (async () => {
14201
- try {
14202
- await startReviewDetector();
14203
- } catch (err) {
14204
- console.error(`[review] startReviewDetector rejected: ${err instanceof Error ? err.message : String(err)}`);
14205
- }
14206
- })();
14207
12344
  startGraphHealthTimer();
14208
- var configDirForWhatsApp = basename7(MAXY_DIR) || ".maxy";
12345
+ var configDirForWhatsApp = basename5(MAXY_DIR) || ".maxy";
14209
12346
  var bootAccount = resolveAccount();
14210
12347
  var bootAccountConfig = bootAccount?.config;
14211
12348
  var bootPublicAgent = bootAccount ? resolvePublicAgent(bootAccount.accountDir, { accountId: bootAccount.accountId })?.slug ?? null : null;
@@ -14242,48 +12379,6 @@ for (const dir of bootEnabled) {
14242
12379
  bootDelivered.push(dir);
14243
12380
  }
14244
12381
  console.error(`[plugins] readiness enabled=${bootEnabled.length} delivered=${bootDelivered.length} dist-missing=[${bootDistMissing.join(",")}] missing=[${bootMissing.join(",")}]`);
14245
- (async () => {
14246
- if (!bootAccount) {
14247
- console.log("[action-completion-relay] phase=consumed-skip reason=no-account");
14248
- return;
14249
- }
14250
- let records;
14251
- try {
14252
- records = consumeActionCompletionRelays(bootAccount.accountDir);
14253
- } catch (err) {
14254
- console.error(`[action-completion-relay] phase=consume-failed error=${err instanceof Error ? err.message : String(err)}`);
14255
- return;
14256
- }
14257
- if (records.length === 0) {
14258
- console.log(`[action-completion-relay] phase=consumed-empty accountId=${bootAccount.accountId.slice(0, 8)}\u2026`);
14259
- return;
14260
- }
14261
- console.log(`[action-completion-relay] phase=consume-batch count=${records.length} accountId=${bootAccount.accountId.slice(0, 8)}\u2026`);
14262
- for (const rec of records) {
14263
- const sessionKey = `cloudflare-relay-boot:${rec.actionId}`;
14264
- let outcome = "injected";
14265
- let dispatchError;
14266
- try {
14267
- registerSession(sessionKey, "admin", bootAccount.accountId);
14268
- setConversationIdForSession(sessionKey, rec.conversationId);
14269
- for await (const _ev of invokeAgent({ type: "admin" }, rec.message, sessionKey)) {
14270
- }
14271
- } catch (err) {
14272
- outcome = "dispatch-failed";
14273
- dispatchError = err instanceof Error ? err.message : String(err);
14274
- } finally {
14275
- unregisterSession(sessionKey);
14276
- }
14277
- if (outcome === "injected") {
14278
- deleteConsumedRelay(rec.filePath);
14279
- console.log(`[action-completion-relay] phase=consumed actionId=${rec.actionId} conversationId=${rec.conversationId} ageMs=${rec.ageMs} outcome=injected`);
14280
- } else {
14281
- console.error(`[action-completion-relay] phase=consumed actionId=${rec.actionId} conversationId=${rec.conversationId} ageMs=${rec.ageMs} outcome=${outcome} reason=${JSON.stringify(dispatchError ?? "")} fileRetained=${JSON.stringify(rec.filePath)}`);
14282
- }
14283
- }
14284
- })().catch((err) => {
14285
- console.error(`[action-completion-relay] boot-drain rejected: ${err instanceof Error ? err.message : String(err)}`);
14286
- });
14287
12382
  if (bootAccountConfig?.whatsapp) {
14288
12383
  console.error(`[whatsapp:boot] loading whatsapp config from account.json publicAgent=${bootPublicAgent ?? "none"}`);
14289
12384
  } else {
@@ -14291,7 +12386,7 @@ if (bootAccountConfig?.whatsapp) {
14291
12386
  }
14292
12387
  init({
14293
12388
  configDir: configDirForWhatsApp,
14294
- platformRoot: resolve25(process.env.MAXY_PLATFORM_ROOT ?? join11(__dirname, "..")),
12389
+ platformRoot: resolve21(process.env.MAXY_PLATFORM_ROOT ?? join9(__dirname, "..")),
14295
12390
  accountConfig: bootAccountConfig,
14296
12391
  onMessage: async (msg) => {
14297
12392
  try {
@@ -14428,11 +12523,6 @@ process.on("SIGTERM", async () => {
14428
12523
  } catch (err) {
14429
12524
  console.error(`[server] shutdown error: ${String(err)}`);
14430
12525
  }
14431
- try {
14432
- await shutdownReviewDetector();
14433
- } catch (err) {
14434
- console.error(`[server] review detector shutdown error: ${String(err)}`);
14435
- }
14436
12526
  console.error("[server] graceful shutdown complete \u2014 exiting");
14437
12527
  process.exit(0);
14438
12528
  });