@rubytech/create-maxy-code 0.1.108 → 0.1.110

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 (124) hide show
  1. package/dist/index.js +11 -125
  2. package/dist/snap-chromium.js +1 -2
  3. package/dist/uninstall.js +10 -9
  4. package/package.json +1 -1
  5. package/payload/platform/plugins/admin/PLUGIN.md +1 -0
  6. package/payload/platform/plugins/admin/mcp/dist/index.js +1 -1
  7. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  8. package/payload/platform/plugins/admin/skills/upgrade/SKILL.md +34 -0
  9. package/payload/platform/plugins/cloudflare/.claude-plugin/plugin.json +1 -1
  10. package/payload/platform/plugins/cloudflare/PLUGIN.md +9 -16
  11. package/payload/platform/plugins/cloudflare/mcp/dist/index.js +7 -12
  12. package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -1
  13. package/payload/platform/plugins/cloudflare/references/dashboard-guide.md +3 -3
  14. package/payload/platform/plugins/cloudflare/references/manual-setup.md +16 -51
  15. package/payload/platform/plugins/cloudflare/references/reset-guide.md +24 -25
  16. package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +29 -144
  17. package/payload/platform/plugins/docs/references/admin-session.md +1 -1
  18. package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
  19. package/payload/platform/plugins/docs/references/cloudflare.md +20 -29
  20. package/payload/platform/plugins/docs/references/platform.md +4 -22
  21. package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
  22. package/payload/platform/plugins/docs/references/troubleshooting.md +4 -316
  23. package/payload/platform/plugins/venture-studio/PLUGIN.md +35 -4
  24. package/payload/platform/plugins/venture-studio/bin/scaffold.sh +104 -0
  25. package/payload/platform/plugins/venture-studio/skills/investor-data-room/SKILL.md +3 -1
  26. package/payload/platform/plugins/work/mcp/dist/cli/project-create-cli.d.ts +2 -0
  27. package/payload/platform/plugins/work/mcp/dist/cli/project-create-cli.d.ts.map +1 -0
  28. package/payload/platform/plugins/work/mcp/dist/cli/project-create-cli.js +78 -0
  29. package/payload/platform/plugins/work/mcp/dist/cli/project-create-cli.js.map +1 -0
  30. package/payload/platform/scripts/check-no-task-id-leaks.mjs +1 -1
  31. package/payload/platform/scripts/vnc.sh +4 -3
  32. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  33. package/payload/premium-plugins/venture-studio/PLUGIN.md +35 -4
  34. package/payload/premium-plugins/venture-studio/bin/scaffold.sh +104 -0
  35. package/payload/premium-plugins/venture-studio/skills/investor-data-room/SKILL.md +3 -1
  36. package/payload/server/{chunk-AGFS3TVN.js → chunk-FSPPVVWM.js} +1303 -496
  37. package/payload/server/maxy-edge.js +21 -259
  38. package/payload/server/public/assets/{ChatInput-DJsqm_Gf.js → ChatInput-CsnIedhS.js} +1 -5
  39. package/payload/server/public/assets/{Checkbox-DGZG9BKc.js → Checkbox-CWugFyFT.js} +1 -1
  40. package/payload/server/public/assets/admin-D3gZuyUn.js +217 -0
  41. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Dw16BhiX.js → architectureDiagram-Q4EWVU46-CeTRKWDb.js} +1 -1
  42. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-DIOpmf5Y.js → blockDiagram-DXYQGD6D-DeeIX5U3.js} +1 -1
  43. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-Tdb_HZeX.js → c4Diagram-AHTNJAMY-CFsqZuil.js} +1 -1
  44. package/payload/server/public/assets/channel-CrSx5mnG.js +1 -0
  45. package/payload/server/public/assets/{chunk-336JU56O-CebpwDDe.js → chunk-336JU56O-DpIXuFM0.js} +2 -2
  46. package/payload/server/public/assets/{chunk-426QAEUC-BtRCmfDU.js → chunk-426QAEUC-Qk8qqrvA.js} +1 -1
  47. package/payload/server/public/assets/{chunk-4TB4RGXK-BZ3GEWs3.js → chunk-4TB4RGXK-DtM8-CUn.js} +1 -1
  48. package/payload/server/public/assets/{chunk-5FUZZQ4R-iDI6Xu0U.js → chunk-5FUZZQ4R-CrSQ4ySU.js} +1 -1
  49. package/payload/server/public/assets/{chunk-5PVQY5BW-SQD_EpYa.js → chunk-5PVQY5BW-Bn2nQwdj.js} +1 -1
  50. package/payload/server/public/assets/{chunk-EDXVE4YY-CtCcA7_e.js → chunk-EDXVE4YY-CzCPnR0P.js} +1 -1
  51. package/payload/server/public/assets/{chunk-ENJZ2VHE-pXVGVCbb.js → chunk-ENJZ2VHE-CgZj9RoG.js} +1 -1
  52. package/payload/server/public/assets/{chunk-ICPOFSXX-Dkzg9o2N.js → chunk-ICPOFSXX-wy-eNjwW.js} +1 -1
  53. package/payload/server/public/assets/{chunk-OYMX7WX6-1ZZWzf9F.js → chunk-OYMX7WX6-CMmJtL8S.js} +1 -1
  54. package/payload/server/public/assets/{chunk-U2HBQHQK-CpQ3kzO0.js → chunk-U2HBQHQK-CFCW7OaT.js} +1 -1
  55. package/payload/server/public/assets/{chunk-X2U36JSP-C2LkxroC.js → chunk-X2U36JSP-Bgh-CJSN.js} +1 -1
  56. package/payload/server/public/assets/{chunk-YZCP3GAM-Bl5jBOt5.js → chunk-YZCP3GAM-BXKwZ4vN.js} +1 -1
  57. package/payload/server/public/assets/{chunk-ZZ45TVLE-CHtnptPS.js → chunk-ZZ45TVLE-BiOuK5NP.js} +1 -1
  58. package/payload/server/public/assets/classDiagram-6PBFFD2Q-C3IDJsqN.js +1 -0
  59. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BOIrJ5Zb.js +1 -0
  60. package/payload/server/public/assets/clone-P8Fkz7JD.js +1 -0
  61. package/payload/server/public/assets/{dagre-BziN0Nkh.js → dagre-DyCxa9Q2.js} +1 -1
  62. package/payload/server/public/assets/{dagre-KV5264BT-B7OG1g1Y.js → dagre-KV5264BT-Cffo_hmf.js} +1 -1
  63. package/payload/server/public/assets/data-DRfwJPja.js +1 -0
  64. package/payload/server/public/assets/{diagram-5BDNPKRD-Bf31nIDs.js → diagram-5BDNPKRD-Bt93Du3V.js} +1 -1
  65. package/payload/server/public/assets/{diagram-G4DWMVQ6-DQu85hhH.js → diagram-G4DWMVQ6-v4r-tBsC.js} +1 -1
  66. package/payload/server/public/assets/{diagram-MMDJMWI5-5tstbs4y.js → diagram-MMDJMWI5-DKrVPOP-.js} +1 -1
  67. package/payload/server/public/assets/{diagram-TYMM5635--MOV1U4o.js → diagram-TYMM5635-B1PzWQb9.js} +1 -1
  68. package/payload/server/public/assets/{erDiagram-SMLLAGMA-BtAnOJmd.js → erDiagram-SMLLAGMA-ClQzsQAs.js} +1 -1
  69. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CybiCIih.js → flowDiagram-DWJPFMVM-h3IbpN_7.js} +1 -1
  70. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-B44jN7Cy.js → ganttDiagram-T4ZO3ILL-ClaxOJi8.js} +1 -1
  71. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-CT7iYcwg.js → gitGraphDiagram-UUTBAWPF-pM-U4ClD.js} +1 -1
  72. package/payload/server/public/assets/graph-B4FFYwus.js +1 -0
  73. package/payload/server/public/assets/graph-labels-Cpk9Ktt0.js +1 -0
  74. package/payload/server/public/assets/{graphlib-DP4o0pYL.js → graphlib-CwimOv_M.js} +1 -1
  75. package/payload/server/public/assets/{infoDiagram-42DDH7IO-CvxFpFHN.js → infoDiagram-42DDH7IO-DcO3Giwe.js} +1 -1
  76. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CUwWLPeR.js → ishikawaDiagram-UXIWVN3A-BcdSL5VS.js} +1 -1
  77. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-y-OnGgLi.js → journeyDiagram-VCZTEJTY-CYahHYzf.js} +1 -1
  78. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-BhrSe8R4.js → kanban-definition-6JOO6SKY-Cl5KhoS4.js} +1 -1
  79. package/payload/server/public/assets/lib--yuBd0Xi.js +33 -0
  80. package/payload/server/public/assets/{line-C9uS7z4J.js → line-d_2oTxLp.js} +1 -1
  81. package/payload/server/public/assets/{mermaid-parser.core-Cg4ZdKp-.js → mermaid-parser.core-CMMDGv3x.js} +1 -1
  82. package/payload/server/public/assets/{mermaid.core-BHdKOsex.js → mermaid.core-DK9ENGbr.js} +3 -3
  83. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-NBYiXHo7.js → mindmap-definition-QFDTVHPH-mA3x3MFG.js} +1 -1
  84. package/payload/server/public/assets/page-CgDmg_fX.js +1 -0
  85. package/payload/server/public/assets/{page-D9YpwhIu.js → page-D9lluVl7.js} +2 -2
  86. package/payload/server/public/assets/{pieDiagram-DEJITSTG-CbojC64C.js → pieDiagram-DEJITSTG-CW1nGFlY.js} +1 -1
  87. package/payload/server/public/assets/public-DdLMkNdS.js +7 -0
  88. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CnnoZXcL.js → quadrantDiagram-34T5L4WZ-DI5igJhR.js} +1 -1
  89. package/payload/server/public/assets/{requirementDiagram-MS252O5E-DFRFRtyJ.js → requirementDiagram-MS252O5E-DyVeI4e_.js} +1 -1
  90. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-DU5gcpzO.js → sankeyDiagram-XADWPNL6-CCTTBYKY.js} +1 -1
  91. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-BN7HZ6Hq.js → sequenceDiagram-FGHM5R23-hVVf35ly.js} +1 -1
  92. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DG6cCjvg.js → stateDiagram-FHFEXIEX-CSLuQDUX.js} +1 -1
  93. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-C5JejhQU.js +1 -0
  94. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-qmV-8SEV.js → timeline-definition-GMOUNBTQ-sxC3W8Q4.js} +1 -1
  95. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-BMXQ1s3n.js → vennDiagram-DHZGUBPP-Ro-ton6Z.js} +1 -1
  96. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-CoAdP1Gw.js → wardleyDiagram-NUSXRM2D-T-dKD0Zx.js} +1 -1
  97. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-CZlfGTEn.js → xychartDiagram-5P7HB3ND-CfProPts.js} +1 -1
  98. package/payload/server/public/data.html +4 -4
  99. package/payload/server/public/graph.html +5 -5
  100. package/payload/server/public/index.html +7 -7
  101. package/payload/server/public/public.html +4 -4
  102. package/payload/server/server.js +299 -1392
  103. package/payload/platform/plugins/cloudflare/scripts/__tests__/tunnel-ingress.test.ts +0 -241
  104. package/payload/platform/plugins/cloudflare/scripts/_stream-log.sh +0 -154
  105. package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.sh +0 -98
  106. package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +0 -749
  107. package/payload/platform/plugins/cloudflare/scripts/reset-tunnel.sh +0 -107
  108. package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +0 -854
  109. package/payload/platform/plugins/cloudflare/scripts/tunnel-ingress.ts +0 -291
  110. package/payload/server/chunk-BDFOTLPW.js +0 -759
  111. package/payload/server/chunk-JRBCOVA4.js +0 -1305
  112. package/payload/server/cloudflare-task-tracker-M5ONAGUT.js +0 -22
  113. package/payload/server/public/assets/admin-BvzMvMGo.js +0 -217
  114. package/payload/server/public/assets/channel-DThrH4QF.js +0 -1
  115. package/payload/server/public/assets/classDiagram-6PBFFD2Q-BC6oGTNX.js +0 -1
  116. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-COwC5Umh.js +0 -1
  117. package/payload/server/public/assets/clone-Chp7hvnA.js +0 -1
  118. package/payload/server/public/assets/data-BAgaPs4j.js +0 -1
  119. package/payload/server/public/assets/graph-Cma7EArf.js +0 -1
  120. package/payload/server/public/assets/graph-labels-DyKk6Sxf.js +0 -1
  121. package/payload/server/public/assets/lib-CpkYtEDz.js +0 -29
  122. package/payload/server/public/assets/page-B4IWl3aZ.js +0 -1
  123. package/payload/server/public/assets/public-BtOXjy3A.js +0 -8
  124. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CvVd1Q_q.js +0 -1
@@ -1,7 +1,3 @@
1
- import {
2
- getConversationOwner
3
- } from "./chunk-JRBCOVA4.js";
4
-
5
1
  // node_modules/hono/dist/compose.js
6
2
  var compose = (middleware, onError, onNotFound) => {
7
3
  return (context, next) => {
@@ -2754,9 +2750,6 @@ function httpLog(fields) {
2754
2750
  function websockifyLog(event, fields = {}) {
2755
2751
  console.error(`[websockify] ${formatKv({ event, ...fields })}`);
2756
2752
  }
2757
- function browserViewerLog(event, fields = {}) {
2758
- console.error(`[browser-viewer] ${formatKv({ event, ...fields })}`);
2759
- }
2760
2753
 
2761
2754
  // app/lib/password-strength.ts
2762
2755
  function validatePasswordStrength(password) {
@@ -3876,6 +3869,1285 @@ function requirePortEnv(envName, options = {}) {
3876
3869
  return parsed;
3877
3870
  }
3878
3871
 
3872
+ // app/lib/neo4j-store.ts
3873
+ import neo4j from "neo4j-driver";
3874
+ import { randomUUID } from "crypto";
3875
+ import { readFileSync as readFileSync6, readdirSync as readdirSync3, existsSync as existsSync6, openSync, readSync, closeSync, statSync as statSync3 } from "fs";
3876
+ import { resolve as resolve5 } from "path";
3877
+ var PLATFORM_ROOT3 = process.env.MAXY_PLATFORM_ROOT ?? resolve5(process.cwd(), "..");
3878
+ var driver = null;
3879
+ function readPassword() {
3880
+ if (process.env.NEO4J_PASSWORD) return process.env.NEO4J_PASSWORD;
3881
+ const passwordFile = resolve5(PLATFORM_ROOT3, "config/.neo4j-password");
3882
+ try {
3883
+ return readFileSync6(passwordFile, "utf-8").trim();
3884
+ } catch {
3885
+ throw new Error(
3886
+ `Neo4j password not found. Expected at ${passwordFile} or in NEO4J_PASSWORD env var.`
3887
+ );
3888
+ }
3889
+ }
3890
+ function getDriver() {
3891
+ if (!driver) {
3892
+ const uri = process.env.NEO4J_URI;
3893
+ if (!uri) {
3894
+ throw new Error(
3895
+ "[ui/neo4j-store] NEO4J_URI unset \u2014 refusing to default to bolt://localhost:7687"
3896
+ );
3897
+ }
3898
+ const user = process.env.NEO4J_USER ?? "neo4j";
3899
+ const password = readPassword();
3900
+ console.error(`[ui/neo4j-store] resolved neo4j_uri=${uri}`);
3901
+ driver = neo4j.driver(uri, neo4j.auth.basic(user, password), {
3902
+ maxConnectionPoolSize: 5
3903
+ });
3904
+ }
3905
+ return driver;
3906
+ }
3907
+ function getSession() {
3908
+ return getDriver().session();
3909
+ }
3910
+ async function runAdminUserSelfHeal(args) {
3911
+ const { selfHealAdminUser } = await import("./adminuser-self-heal-QAWOZ3JV.js");
3912
+ return selfHealAdminUser({ driver: getDriver(), ...args });
3913
+ }
3914
+ process.on("SIGINT", async () => {
3915
+ if (driver) {
3916
+ await driver.close();
3917
+ driver = null;
3918
+ }
3919
+ });
3920
+ var OLLAMA_URL = process.env.OLLAMA_URL ?? "http://localhost:11434";
3921
+ var EMBED_MODEL = process.env.EMBED_MODEL ?? "nomic-embed-text";
3922
+ async function embed(text) {
3923
+ const res = await fetch(`${OLLAMA_URL}/api/embed`, {
3924
+ method: "POST",
3925
+ headers: { "Content-Type": "application/json" },
3926
+ body: JSON.stringify({ model: EMBED_MODEL, input: text }),
3927
+ signal: AbortSignal.timeout(5e3)
3928
+ });
3929
+ if (!res.ok) {
3930
+ const body = await res.text();
3931
+ throw new Error(`Ollama embedding failed (${res.status}): ${body}`);
3932
+ }
3933
+ const data = await res.json();
3934
+ return data.embeddings[0];
3935
+ }
3936
+ var sessionStoreRef = null;
3937
+ function getCachedConversationId(cacheKey2) {
3938
+ return sessionStoreRef?.get(cacheKey2)?.conversationId;
3939
+ }
3940
+ function cacheConversationId(cacheKey2, conversationId) {
3941
+ const session = sessionStoreRef?.get(cacheKey2);
3942
+ if (session) {
3943
+ session.conversationId = conversationId;
3944
+ }
3945
+ }
3946
+ var GREETING_DIRECTIVE = "[New session. Greet the visitor.]";
3947
+ async function ensureConversation(accountId, agentType, cacheKey2, visitorId, agentSlug, userId, channelOverride, channelAddressOverride) {
3948
+ const cached = getCachedConversationId(cacheKey2);
3949
+ if (cached) return { conversationId: cached, created: false };
3950
+ const conversationId = randomUUID();
3951
+ const channel = channelOverride ?? (cacheKey2.startsWith("whatsapp:") ? "whatsapp" : cacheKey2.startsWith("telegram:") ? "telegram" : "webchat");
3952
+ const channelAddress = channelAddressOverride ?? (cacheKey2.startsWith("whatsapp:") ? cacheKey2.slice("whatsapp:".length) : cacheKey2.startsWith("telegram:") ? cacheKey2.slice("telegram:".length) : conversationId);
3953
+ const conversationSublabel = agentType === "admin" ? "AdminConversation" : "PublicConversation";
3954
+ const handlerSlug = agentSlug ?? (agentType === "admin" ? "admin" : null);
3955
+ const wantsHandledByEdge = !!handlerSlug;
3956
+ const handledByClause = wantsHandledByEdge ? `WITH c, created
3957
+ OPTIONAL MATCH (a:Agent {accountId: $accountId, slug: $handlerSlug})
3958
+ FOREACH (_ IN CASE WHEN a IS NULL THEN [] ELSE [1] END | MERGE (c)-[:HANDLED_BY]->(a))
3959
+ RETURN c.conversationId AS conversationId, created AS created, a IS NOT NULL AS handledBy` : `RETURN c.conversationId AS conversationId, created AS created, false AS handledBy`;
3960
+ const session = getSession();
3961
+ try {
3962
+ const result = await session.run(
3963
+ `OPTIONAL MATCH (prior:Conversation {accountId: $accountId, channel: $channel, channelAddress: $channelAddress})
3964
+ WITH prior, coalesce(prior.conversationId, $conversationId) AS finalConvId, prior IS NULL AS created
3965
+ MERGE (c:Conversation {conversationId: finalConvId})
3966
+ ON CREATE SET
3967
+ c:${conversationSublabel},
3968
+ c.accountId = $accountId,
3969
+ c.agentType = $agentType,
3970
+ c.channel = $channel,
3971
+ c.channelAddress = $channelAddress,
3972
+ ${visitorId ? "c.visitorId = $visitorId," : ""}
3973
+ ${agentSlug ? "c.agentSlug = $agentSlug," : ""}
3974
+ ${userId ? "c.userId = $userId," : ""}
3975
+ c.createdAt = datetime(),
3976
+ c.updatedAt = datetime()
3977
+ ON MATCH SET
3978
+ c.updatedAt = datetime()
3979
+ ${handledByClause}`,
3980
+ {
3981
+ conversationId,
3982
+ accountId,
3983
+ agentType,
3984
+ channel,
3985
+ channelAddress,
3986
+ ...visitorId ? { visitorId } : {},
3987
+ ...agentSlug ? { agentSlug } : {},
3988
+ ...handlerSlug ? { handlerSlug } : {},
3989
+ ...userId ? { userId } : {}
3990
+ }
3991
+ );
3992
+ const id = result.records[0]?.get("conversationId");
3993
+ const created = result.records[0]?.get("created") === true;
3994
+ if (id) {
3995
+ cacheConversationId(cacheKey2, id);
3996
+ console.error(`[session] ${(/* @__PURE__ */ new Date()).toISOString()} conversation attributed: conversationId=${id.slice(0, 8)}\u2026 userId=${userId ?? "none"} ${agentType}/${accountId.slice(0, 8)}\u2026 sublabel=${conversationSublabel}`);
3997
+ if (wantsHandledByEdge) {
3998
+ const handled = result.records[0]?.get("handledBy");
3999
+ const id8 = id.slice(0, 8);
4000
+ if (handled === true) {
4001
+ console.error(`[agent-graph] handled-by-write conversationId=${id8} slug=${handlerSlug}`);
4002
+ } else {
4003
+ console.error(`[agent-graph] handled-by-skip reason=no-agent-node conversationId=${id8} slug=${handlerSlug}`);
4004
+ }
4005
+ }
4006
+ }
4007
+ return { conversationId: id ?? null, created };
4008
+ } catch (err) {
4009
+ console.error(`[persist] ensureConversation failed: ${err instanceof Error ? err.message : String(err)}`);
4010
+ return { conversationId: null, created: false };
4011
+ } finally {
4012
+ await session.close();
4013
+ }
4014
+ }
4015
+ var HEX_COLOR_RE = /^#[0-9a-fA-F]{3,8}$/;
4016
+ async function fetchBranding(accountId) {
4017
+ const session = getSession();
4018
+ try {
4019
+ const result = await session.run(
4020
+ `MATCH (b:LocalBusiness {accountId: $accountId})
4021
+ OPTIONAL MATCH (b)-[:HAS_BRAND_ASSET]->(logo:ImageObject {purpose: "logo"})
4022
+ OPTIONAL MATCH (b)-[:HAS_BRAND_ASSET]->(icon:ImageObject {purpose: "icon"})
4023
+ RETURN b.name AS name,
4024
+ b.primaryColor AS primaryColor,
4025
+ b.accentColor AS accentColor,
4026
+ b.backgroundColor AS backgroundColor,
4027
+ b.tagline AS tagline,
4028
+ logo.contentUrl AS logoUrl,
4029
+ icon.contentUrl AS faviconUrl`,
4030
+ { accountId },
4031
+ { timeout: 2e3 }
4032
+ );
4033
+ const record = result.records[0];
4034
+ if (!record) return null;
4035
+ const name = record.get("name");
4036
+ if (!name) return null;
4037
+ const primaryColor = record.get("primaryColor");
4038
+ const accentColor = record.get("accentColor");
4039
+ const backgroundColor = record.get("backgroundColor");
4040
+ const tagline = record.get("tagline");
4041
+ const logoUrl = record.get("logoUrl");
4042
+ const faviconUrl = record.get("faviconUrl");
4043
+ const hasBranding = primaryColor || accentColor || backgroundColor || tagline || logoUrl || faviconUrl;
4044
+ if (!hasBranding) return null;
4045
+ const branding = { name };
4046
+ if (primaryColor && HEX_COLOR_RE.test(primaryColor)) branding.primaryColor = primaryColor;
4047
+ if (accentColor && HEX_COLOR_RE.test(accentColor)) branding.accentColor = accentColor;
4048
+ if (backgroundColor && HEX_COLOR_RE.test(backgroundColor)) branding.backgroundColor = backgroundColor;
4049
+ if (tagline) branding.tagline = tagline;
4050
+ if (logoUrl) branding.logoUrl = logoUrl;
4051
+ if (faviconUrl) branding.faviconUrl = faviconUrl;
4052
+ console.error(`[branding] resolved for accountId=${accountId.slice(0, 8)}\u2026: primary=${branding.primaryColor ?? "\u2013"} logo=${branding.logoUrl ? "yes" : "no"}`);
4053
+ return branding;
4054
+ } catch (err) {
4055
+ console.error(`[branding] fetchBranding failed for accountId=${accountId.slice(0, 8)}\u2026: ${err instanceof Error ? err.message : String(err)}`);
4056
+ return null;
4057
+ } finally {
4058
+ await session.close();
4059
+ }
4060
+ }
4061
+ var SUMMARY_MAX_LEN = 200;
4062
+ var persistMessageLocks = /* @__PURE__ */ new Map();
4063
+ async function persistMessage(conversationId, role, content, accountId, tokens, createdAt, sender, components, attachments) {
4064
+ if (!content) return null;
4065
+ const messageId = randomUUID();
4066
+ const summary = role === "user" ? content.slice(0, SUMMARY_MAX_LEN).trim() : "";
4067
+ let embedding = null;
4068
+ try {
4069
+ embedding = await embed(content);
4070
+ } catch (err) {
4071
+ console.error(`[persist] Embedding failed, storing without: ${err instanceof Error ? err.message : String(err)}`);
4072
+ }
4073
+ const prev = persistMessageLocks.get(conversationId);
4074
+ const waited = prev !== void 0;
4075
+ let release;
4076
+ const mine = new Promise((resolve6) => {
4077
+ release = resolve6;
4078
+ });
4079
+ const chained = (prev ?? Promise.resolve()).then(() => mine);
4080
+ persistMessageLocks.set(conversationId, chained);
4081
+ await prev;
4082
+ const session = getSession();
4083
+ try {
4084
+ const result = await session.run(
4085
+ `MATCH (c:Conversation {conversationId: $conversationId})
4086
+ OPTIONAL MATCH (tail:Message)-[:PART_OF]->(c)
4087
+ WHERE NOT (tail)-[:NEXT]->(:Message)
4088
+ AND (tail:UserMessage OR tail:AssistantMessage)
4089
+ // sublabel-scope the tail to the agent-path's own sublabels.
4090
+ // Without this, a Task-857 :WhatsAppMessage row (no outgoing :NEXT)
4091
+ // would be picked as tail and the agent-path would chain
4092
+ // :WhatsAppMessage\u2192:UserMessage, crossing the live-channel chain.
4093
+ // The live writer's tail predicate is sublabel-scoped to
4094
+ // :WhatsAppMessage; this clause is the matching defence on the
4095
+ // agent side.
4096
+ // Capture whether THIS write's guard will fire. Read c.summary
4097
+ // before the SET so the boolean reflects the pre-state, not the
4098
+ // post-state \u2014 a false positive otherwise when a new user message
4099
+ // happens to equal an already-set summary (verbatim retry, short
4100
+ // catchphrase) and fooled a post-state equality check.
4101
+ WITH c, tail, (c.summary IS NULL AND $role = 'user' AND $summary <> '') AS summarySetThisWrite
4102
+ CREATE (m:Message:${role === "user" ? "UserMessage" : "AssistantMessage"} {
4103
+ messageId: $messageId,
4104
+ conversationId: $conversationId,
4105
+ accountId: $accountId,
4106
+ role: $role,
4107
+ content: $content,
4108
+ createdAt: ${createdAt ? "datetime($createdAt)" : "datetime()"}
4109
+ ${embedding ? ", embedding: $embedding" : ""}
4110
+ ${sender ? ", senderVisitorId: $senderVisitorId, senderName: $senderName" : ""}
4111
+ ${tokens?.inputTokens != null ? ", inputTokens: $inputTokens" : ""}
4112
+ ${tokens?.outputTokens != null ? ", outputTokens: $outputTokens" : ""}
4113
+ ${tokens?.cacheReadTokens != null ? ", cacheReadTokens: $cacheReadTokens" : ""}
4114
+ ${tokens?.cacheCreationTokens != null ? ", cacheCreationTokens: $cacheCreationTokens" : ""}
4115
+ })
4116
+ SET c.updatedAt = datetime()
4117
+ CREATE (m)-[:PART_OF]->(c)
4118
+ FOREACH (prev IN CASE WHEN tail IS NULL THEN [] ELSE [tail] END |
4119
+ CREATE (prev)-[:NEXT]->(m)
4120
+ )
4121
+ FOREACH (_ IN CASE WHEN summarySetThisWrite THEN [1] ELSE [] END |
4122
+ SET c.summary = $summary
4123
+ )
4124
+ FOREACH (comp IN $components |
4125
+ CREATE (m)-[:HAS_COMPONENT]->(:Component {
4126
+ componentId: comp.componentId,
4127
+ conversationId: $conversationId,
4128
+ accountId: $accountId,
4129
+ messageId: $messageId,
4130
+ name: comp.name,
4131
+ data: comp.data,
4132
+ ordinal: comp.ordinal,
4133
+ textOffset: comp.textOffset,
4134
+ submitted: false,
4135
+ createdAt: datetime(),
4136
+ // store the artefact attachmentId on the:Component
4137
+ // itself when this is a PERSISTENT_COMPONENTS write whose disk
4138
+ // write succeeded. Field is null for non-persistent / mime-skip
4139
+ // / disk-fail components. Lets the audit + backfill scripts
4140
+ // read the attachmentId directly off the :Component row instead
4141
+ // of re-deriving (the live + heal paths use different sources \u2014
4142
+ // sha256(block.id) live, componentId-fallback backfill \u2014 and
4143
+ // the audit must match either).
4144
+ attachmentId: comp.artefactAttachmentId
4145
+ })
4146
+ )
4147
+ // for PERSISTENT_COMPONENTS whose disk write succeeded
4148
+ // (artefactAttachmentId is non-null), MERGE the sibling
4149
+ // :KnowledgeDocument projection AND a :HAS_KNOWLEDGE_DOCUMENT edge
4150
+ // from the parent :Message in the same Cypher tx. ON CREATE stamps
4151
+ // name+encodingFormat from the live render; ON MATCH only bumps
4152
+ // updatedAt so a heal-on-resume re-run cannot clobber an operator's
4153
+ // subsequent rename via the knowledge-documents API (eng-review \xA72).
4154
+ // The edge makes the projection graph-discoverable from the
4155
+ // conversation timeline so file-delete-cascade and conversation
4156
+ // cleanup can reach it. FOREACH on the filtered list collapses to a
4157
+ // no-op when no component has an attachmentId, preserving the
4158
+ // text-only-turn fast path.
4159
+ FOREACH (kd IN $knowledgeDocs |
4160
+ MERGE (k:KnowledgeDocument {accountId: $accountId, attachmentId: kd.attachmentId})
4161
+ ON CREATE SET k.name = kd.title,
4162
+ k.encodingFormat = kd.mimeType,
4163
+ k.createdAt = datetime(),
4164
+ k.updatedAt = datetime()
4165
+ ON MATCH SET k.updatedAt = datetime()
4166
+ MERGE (m)-[:HAS_KNOWLEDGE_DOCUMENT]->(k)
4167
+ )
4168
+ FOREACH (att IN $attachments |
4169
+ CREATE (m)-[:HAS_ATTACHMENT]->(:Attachment {
4170
+ attachmentId: att.attachmentId,
4171
+ conversationId: $conversationId,
4172
+ accountId: $accountId,
4173
+ messageId: $messageId,
4174
+ filename: att.filename,
4175
+ mimeType: att.mimeType,
4176
+ sizeBytes: att.sizeBytes,
4177
+ storagePath: att.storagePath,
4178
+ ordinal: att.ordinal,
4179
+ createdAt: datetime()
4180
+ })
4181
+ )
4182
+ RETURN tail.messageId AS prevMessageId,
4183
+ summarySetThisWrite,
4184
+ size([(m2:Message)-[:PART_OF]->(c) | m2]) AS chainLen`,
4185
+ {
4186
+ messageId,
4187
+ conversationId,
4188
+ accountId,
4189
+ role,
4190
+ content,
4191
+ summary,
4192
+ ...createdAt ? { createdAt } : {},
4193
+ ...embedding ? { embedding } : {},
4194
+ ...sender ? { senderVisitorId: sender.visitorId, senderName: sender.displayName } : {},
4195
+ ...tokens?.inputTokens != null ? { inputTokens: neo4j.int(tokens.inputTokens) } : {},
4196
+ ...tokens?.outputTokens != null ? { outputTokens: neo4j.int(tokens.outputTokens) } : {},
4197
+ ...tokens?.cacheReadTokens != null ? { cacheReadTokens: neo4j.int(tokens.cacheReadTokens) } : {},
4198
+ ...tokens?.cacheCreationTokens != null ? { cacheCreationTokens: neo4j.int(tokens.cacheCreationTokens) } : {},
4199
+ components: (components ?? []).map((comp) => ({
4200
+ componentId: comp.componentId,
4201
+ name: comp.name,
4202
+ data: comp.data,
4203
+ ordinal: neo4j.int(comp.ordinal),
4204
+ textOffset: neo4j.int(comp.textOffset),
4205
+ // null for non-persistent / mime-skip / disk-fail
4206
+ // components; the FOREACH stamps this directly onto the
4207
+ // :Component row so audit + backfill read attachmentId without
4208
+ // re-deriving from componentId.
4209
+ artefactAttachmentId: comp.artefactAttachmentId ?? null
4210
+ })),
4211
+ //:KnowledgeDocument MERGE list. One row per
4212
+ // PERSISTENT_COMPONENTS component whose disk write succeeded;
4213
+ // text-only / mime-skip / disk-fail components are absent so
4214
+ // the FOREACH no-ops on them. attachmentId here matches the
4215
+ // value the stream-parser stamped after writeAttachment.
4216
+ knowledgeDocs: (components ?? []).filter((comp) => typeof comp.artefactAttachmentId === "string").map((comp) => ({
4217
+ attachmentId: comp.artefactAttachmentId,
4218
+ title: comp.artefactTitle ?? "",
4219
+ mimeType: comp.artefactMimeType ?? ""
4220
+ })),
4221
+ attachments: (attachments ?? []).map((att) => ({
4222
+ attachmentId: att.attachmentId,
4223
+ filename: att.filename,
4224
+ mimeType: att.mimeType,
4225
+ sizeBytes: neo4j.int(att.sizeBytes),
4226
+ storagePath: att.storagePath,
4227
+ ordinal: neo4j.int(att.ordinal)
4228
+ }))
4229
+ }
4230
+ );
4231
+ if (result.records.length === 0) {
4232
+ console.error(`[persist] Neo4j write skipped \u2014 conversation not found: ${conversationId.slice(0, 8)}\u2026`);
4233
+ return null;
4234
+ }
4235
+ const record = result.records[0];
4236
+ const prevMessageId = record.get("prevMessageId") ?? null;
4237
+ const summarySetThisWrite = record.get("summarySetThisWrite") === true;
4238
+ const chainLenRaw = record.get("chainLen");
4239
+ const chainLen = typeof chainLenRaw === "bigint" ? Number(chainLenRaw) : typeof chainLenRaw?.toNumber === "function" ? chainLenRaw.toNumber() : Number(chainLenRaw ?? 0);
4240
+ const messageSublabel = role === "user" ? "UserMessage" : "AssistantMessage";
4241
+ console.error(`[neo4j-store] append-message conversationId=${conversationId.slice(0, 8)}\u2026 messageId=${messageId.slice(0, 8)}\u2026 prev=${prevMessageId ? prevMessageId.slice(0, 8) + "\u2026" : "null"} chainLen=${chainLen} waited=${waited} sublabel=${messageSublabel}`);
4242
+ if (summarySetThisWrite) {
4243
+ console.error(`[neo4j-store] conversation-summary-set conversationId=${conversationId.slice(0, 8)}\u2026 len=${summary.length}`);
4244
+ }
4245
+ const componentList = components ?? [];
4246
+ if (componentList.length > 0) {
4247
+ const relsCreated = result.summary.counters.updates().relationshipsCreated;
4248
+ const expectedComponentEdges = componentList.length;
4249
+ const baseEdges = relsCreated - expectedComponentEdges;
4250
+ if (baseEdges < 1) {
4251
+ console.error(`[neo4j-store] persist-component WARN conversationId=${conversationId.slice(0, 8)}\u2026 messageId=${messageId.slice(0, 8)}\u2026 relsCreated=${relsCreated} expected\u2265${1 + expectedComponentEdges} \u2014 component edges may not have been created`);
4252
+ }
4253
+ for (const comp of componentList) {
4254
+ console.error(`[neo4j-store] persist-component conversationId=${conversationId.slice(0, 8)}\u2026 messageId=${messageId.slice(0, 8)}\u2026 componentId=${comp.componentId.slice(0, 8)}\u2026 name=${comp.name} dataLen=${comp.data.length} ordinal=${comp.ordinal} textOffset=${comp.textOffset}`);
4255
+ }
4256
+ }
4257
+ const attachmentList = attachments ?? [];
4258
+ if (attachmentList.length > 0) {
4259
+ const relsCreated = result.summary.counters.updates().relationshipsCreated;
4260
+ const expectedAttachmentEdges = attachmentList.length;
4261
+ const expectedComponentEdges = (components ?? []).length;
4262
+ const baseEdges = relsCreated - expectedComponentEdges - expectedAttachmentEdges;
4263
+ if (baseEdges < 1) {
4264
+ console.error(`[neo4j-store] persist-attachment WARN conversationId=${conversationId.slice(0, 8)}\u2026 messageId=${messageId.slice(0, 8)}\u2026 relsCreated=${relsCreated} expected\u2265${1 + expectedComponentEdges + expectedAttachmentEdges} \u2014 attachment edges may not have been created`);
4265
+ }
4266
+ for (const att of attachmentList) {
4267
+ console.error(`[neo4j-store] persist-attachment conversationId=${conversationId.slice(0, 8)}\u2026 messageId=${messageId.slice(0, 8)}\u2026 attachmentId=${att.attachmentId.slice(0, 8)}\u2026 filename=${att.filename} mimeType=${att.mimeType} sizeBytes=${att.sizeBytes} ordinal=${att.ordinal}`);
4268
+ }
4269
+ }
4270
+ console.error(`[persist] ${(/* @__PURE__ */ new Date()).toISOString()} conversationId=${conversationId.slice(0, 8)}\u2026 role=${role} len=${content.length}${sender ? ` sender=${sender.displayName}` : ""}`);
4271
+ return messageId;
4272
+ } catch (err) {
4273
+ console.error(`[persist] Neo4j write failed: ${err instanceof Error ? err.message : String(err)}`);
4274
+ return null;
4275
+ } finally {
4276
+ release();
4277
+ if (persistMessageLocks.get(conversationId) === chained) {
4278
+ persistMessageLocks.delete(conversationId);
4279
+ }
4280
+ await session.close();
4281
+ }
4282
+ }
4283
+ async function getAgentSessionIdForConversation(conversationId) {
4284
+ const session = getSession();
4285
+ try {
4286
+ const result = await session.run(
4287
+ `MATCH (c:Conversation {conversationId: $conversationId})
4288
+ RETURN c.agentSessionId AS agentSessionId`,
4289
+ { conversationId }
4290
+ );
4291
+ const value = result.records[0]?.get("agentSessionId");
4292
+ return typeof value === "string" && value.length > 0 ? value : null;
4293
+ } catch (err) {
4294
+ console.error(`[persist] agent-session-id read failed convId=${conversationId.slice(0, 8)}\u2026 error=${err instanceof Error ? err.message : String(err)}`);
4295
+ return null;
4296
+ } finally {
4297
+ await session.close();
4298
+ }
4299
+ }
4300
+ async function getRecentMessages(conversationId, limit = 50) {
4301
+ const session = getSession();
4302
+ try {
4303
+ const result = await session.run(
4304
+ `MATCH (tail:Message {conversationId: $conversationId})
4305
+ WHERE NOT (tail)-[:NEXT]->(:Message)
4306
+ WITH tail ORDER BY tail.createdAt DESC LIMIT 1
4307
+ MATCH path = (m:Message)-[:NEXT*0..]->(tail)
4308
+ WHERE m.conversationId = $conversationId
4309
+ AND length(path) < $limit
4310
+ WITH m, length(path) AS depthFromTail
4311
+ OPTIONAL MATCH (m)-[:HAS_COMPONENT]->(c:Component)
4312
+ WITH m, depthFromTail, c ORDER BY c.ordinal ASC
4313
+ WITH m, depthFromTail,
4314
+ [comp IN collect(c) WHERE comp IS NOT NULL | comp {.*}] AS components
4315
+ OPTIONAL MATCH (m)-[:HAS_ATTACHMENT]->(a:Attachment)
4316
+ WITH m, depthFromTail, components, a ORDER BY a.ordinal ASC
4317
+ WITH m, depthFromTail, components,
4318
+ [att IN collect(a) WHERE att IS NOT NULL | att {.*}] AS attachments
4319
+ RETURN m.messageId AS messageId, m.role AS role, m.content AS content,
4320
+ m.createdAt AS createdAt, components, attachments
4321
+ ORDER BY depthFromTail DESC`,
4322
+ { conversationId, limit: neo4j.int(limit) }
4323
+ );
4324
+ return result.records.map((r) => {
4325
+ const rawComponents = r.get("components") ?? [];
4326
+ const components = rawComponents.map((c) => {
4327
+ const rawAttachmentId = c.attachmentId;
4328
+ const attachmentId = typeof rawAttachmentId === "string" && rawAttachmentId.length > 0 ? rawAttachmentId : void 0;
4329
+ return {
4330
+ componentId: String(c.componentId ?? ""),
4331
+ name: String(c.name ?? ""),
4332
+ data: String(c.data ?? ""),
4333
+ ordinal: typeof c.ordinal?.toNumber === "function" ? c.ordinal.toNumber() : Number(c.ordinal ?? 0),
4334
+ textOffset: typeof c.textOffset?.toNumber === "function" ? c.textOffset.toNumber() : Number(c.textOffset ?? 0),
4335
+ submitted: c.submitted === true,
4336
+ ...attachmentId ? { attachmentId } : {}
4337
+ };
4338
+ });
4339
+ const rawAttachments = r.get("attachments") ?? [];
4340
+ const attachments = rawAttachments.map((a) => ({
4341
+ attachmentId: String(a.attachmentId ?? ""),
4342
+ filename: String(a.filename ?? ""),
4343
+ mimeType: String(a.mimeType ?? ""),
4344
+ sizeBytes: typeof a.sizeBytes?.toNumber === "function" ? a.sizeBytes.toNumber() : Number(a.sizeBytes ?? 0),
4345
+ storagePath: String(a.storagePath ?? ""),
4346
+ ordinal: typeof a.ordinal?.toNumber === "function" ? a.ordinal.toNumber() : Number(a.ordinal ?? 0),
4347
+ accountId: String(a.accountId ?? "")
4348
+ }));
4349
+ return {
4350
+ messageId: r.get("messageId"),
4351
+ role: r.get("role"),
4352
+ content: r.get("content"),
4353
+ createdAt: String(r.get("createdAt")),
4354
+ components,
4355
+ attachments
4356
+ };
4357
+ });
4358
+ } catch (err) {
4359
+ console.error(`[persist] getRecentMessages failed: ${err instanceof Error ? err.message : String(err)}`);
4360
+ return [];
4361
+ } finally {
4362
+ await session.close();
4363
+ }
4364
+ }
4365
+ async function verifyConversationOwnership(conversationId, accountId) {
4366
+ const session = getSession();
4367
+ try {
4368
+ const result = await session.run(
4369
+ `MATCH (c:Conversation {conversationId: $conversationId, accountId: $accountId})
4370
+ RETURN c.conversationId AS id LIMIT 1`,
4371
+ { conversationId, accountId }
4372
+ );
4373
+ return result.records.length > 0;
4374
+ } catch (err) {
4375
+ console.error(`[persist] verifyConversationOwnership failed: ${err instanceof Error ? err.message : String(err)}`);
4376
+ return false;
4377
+ } finally {
4378
+ await session.close();
4379
+ }
4380
+ }
4381
+ async function getConversationOwner(conversationId) {
4382
+ const session = getSession();
4383
+ try {
4384
+ const result = await session.run(
4385
+ `MATCH (c:Conversation {conversationId: $conversationId})
4386
+ RETURN c.accountId AS accountId, c.userId AS userId LIMIT 1`,
4387
+ { conversationId }
4388
+ );
4389
+ const record = result.records[0];
4390
+ if (!record) return null;
4391
+ const accountId = record.get("accountId");
4392
+ const userId = record.get("userId");
4393
+ if (!accountId) return null;
4394
+ return { accountId, userId: userId ?? null };
4395
+ } catch (err) {
4396
+ console.error(`[persist] getConversationOwner failed: ${err instanceof Error ? err.message : String(err)}`);
4397
+ return null;
4398
+ } finally {
4399
+ await session.close();
4400
+ }
4401
+ }
4402
+ async function verifyAndGetConversationUpdatedAt(conversationId, accountId) {
4403
+ const session = getSession();
4404
+ try {
4405
+ const result = await session.run(
4406
+ `MATCH (c:Conversation {conversationId: $conversationId, accountId: $accountId})
4407
+ RETURN toString(c.updatedAt) AS updatedAt LIMIT 1`,
4408
+ { conversationId, accountId }
4409
+ );
4410
+ const record = result.records[0];
4411
+ return record ? record.get("updatedAt") : null;
4412
+ } catch (err) {
4413
+ console.error(`[persist] verifyAndGetConversationUpdatedAt failed: ${err instanceof Error ? err.message : String(err)}`);
4414
+ return null;
4415
+ } finally {
4416
+ await session.close();
4417
+ }
4418
+ }
4419
+ async function listAdminSessions(accountId, userId, limit = 20) {
4420
+ const session = getSession();
4421
+ try {
4422
+ const result = await session.run(
4423
+ `MATCH (c:Conversation {accountId: $accountId, agentType: 'admin', userId: $userId})
4424
+ WHERE NOT c:Trashed
4425
+ RETURN c.conversationId AS conversationId,
4426
+ c.name AS name,
4427
+ c.updatedAt AS updatedAt,
4428
+ c.channel AS channel
4429
+ ORDER BY c.updatedAt DESC
4430
+ LIMIT $limit`,
4431
+ { accountId, userId, limit: neo4j.int(limit) }
4432
+ );
4433
+ return result.records.map((r) => ({
4434
+ conversationId: r.get("conversationId"),
4435
+ name: r.get("name"),
4436
+ updatedAt: String(r.get("updatedAt")),
4437
+ channel: r.get("channel")
4438
+ }));
4439
+ } catch (err) {
4440
+ console.error(`[persist] listAdminSessions failed: ${err instanceof Error ? err.message : String(err)}`);
4441
+ return [];
4442
+ } finally {
4443
+ await session.close();
4444
+ }
4445
+ }
4446
+ async function deleteConversation(conversationId) {
4447
+ const session = getSession();
4448
+ try {
4449
+ const result = await session.run(
4450
+ `MATCH (c:Conversation {conversationId: $conversationId})
4451
+ OPTIONAL MATCH (m:Message)-[:PART_OF]->(c)
4452
+ OPTIONAL MATCH (m)-[:HAS_COMPONENT]->(comp:Component)
4453
+ OPTIONAL MATCH (m)-[:HAS_ATTACHMENT]->(att:Attachment)
4454
+ OPTIONAL MATCH (c)-[:HAS_TRANSCRIPT]->(t:ClaudeSession)
4455
+ DETACH DELETE att, comp, m, t, c
4456
+ RETURN count(c) AS deleted`,
4457
+ { conversationId }
4458
+ );
4459
+ const deleted = result.records[0]?.get("deleted");
4460
+ const count = typeof deleted === "object" && deleted !== null ? Number(deleted) : Number(deleted ?? 0);
4461
+ const found = count > 0;
4462
+ console.error(`[persist] deleteConversation ${conversationId.slice(0, 8)}\u2026: ${found ? "deleted" : "not found"}`);
4463
+ return { found };
4464
+ } finally {
4465
+ await session.close();
4466
+ }
4467
+ }
4468
+ async function linkConversationTranscript(conversationId, claudeSessionId, accountId) {
4469
+ if (!conversationId || !claudeSessionId || !accountId) {
4470
+ console.error(`[transcript-link] schema-violation conversationId=${conversationId ? "set" : "EMPTY"} claudeSessionId=${claudeSessionId ? "set" : "EMPTY"} accountId=${accountId ? "set" : "EMPTY"}`);
4471
+ return { linked: false };
4472
+ }
4473
+ const session = getSession();
4474
+ try {
4475
+ const result = await session.run(
4476
+ `MATCH (c:Conversation {conversationId: $conversationId, accountId: $accountId})
4477
+ MERGE (c)-[:HAS_TRANSCRIPT]->(t:ClaudeSession {claudeSessionId: $claudeSessionId})
4478
+ ON CREATE SET
4479
+ t.accountId = $accountId,
4480
+ t.createdAt = datetime()
4481
+ RETURN count(t) AS linked`,
4482
+ { conversationId, claudeSessionId, accountId }
4483
+ );
4484
+ const linked = Number(result.records[0]?.get("linked") ?? 0) > 0;
4485
+ if (!linked) {
4486
+ console.error(`[transcript-link] convId=${conversationId.slice(0, 8)}\u2026 claudeSessionId=${claudeSessionId.slice(0, 8)}\u2026 status=skipped reason=conversation-not-found`);
4487
+ }
4488
+ return { linked };
4489
+ } catch (err) {
4490
+ console.error(`[transcript-link] convId=${conversationId.slice(0, 8)}\u2026 claudeSessionId=${claudeSessionId.slice(0, 8)}\u2026 status=failed error=${err instanceof Error ? err.message : String(err)}`);
4491
+ return { linked: false };
4492
+ } finally {
4493
+ await session.close();
4494
+ }
4495
+ }
4496
+ async function getConversationClaudeSessionIds(conversationId, accountId) {
4497
+ const session = getSession();
4498
+ try {
4499
+ const result = await session.run(
4500
+ `MATCH (c:Conversation {conversationId: $conversationId, accountId: $accountId})-[:HAS_TRANSCRIPT]->(t:ClaudeSession)
4501
+ RETURN t.claudeSessionId AS claudeSessionId
4502
+ ORDER BY t.createdAt ASC`,
4503
+ { conversationId, accountId }
4504
+ );
4505
+ return result.records.map((r) => r.get("claudeSessionId")).filter((v) => typeof v === "string" && v.length > 0);
4506
+ } finally {
4507
+ await session.close();
4508
+ }
4509
+ }
4510
+ async function getConversationIdByClaudeSessionId(claudeSessionId, accountId) {
4511
+ const session = getSession();
4512
+ try {
4513
+ const result = await session.run(
4514
+ `MATCH (c:Conversation {accountId: $accountId})-[:HAS_TRANSCRIPT]->(t:ClaudeSession {claudeSessionId: $claudeSessionId})
4515
+ RETURN c.conversationId AS conversationId
4516
+ LIMIT 1`,
4517
+ { claudeSessionId, accountId }
4518
+ );
4519
+ const value = result.records[0]?.get("conversationId");
4520
+ return typeof value === "string" && value.length > 0 ? value : null;
4521
+ } finally {
4522
+ await session.close();
4523
+ }
4524
+ }
4525
+ async function renameConversation(conversationId, label) {
4526
+ let embedding = null;
4527
+ try {
4528
+ embedding = await embed(label);
4529
+ } catch (err) {
4530
+ console.error(`[persist] manual-label: embedding failed, persisting without: ${err instanceof Error ? err.message : String(err)}`);
4531
+ }
4532
+ const session = getSession();
4533
+ try {
4534
+ await session.run(
4535
+ `MATCH (c:Conversation {conversationId: $conversationId})
4536
+ SET c.name = $label, c.updatedAt = datetime()
4537
+ ${embedding ? ", c.embedding = $embedding" : ""}`,
4538
+ { conversationId, label, ...embedding ? { embedding } : {} }
4539
+ );
4540
+ console.error(`[persist] manual-label: renamed ${conversationId} to "${label}"${embedding ? " (embedded)" : ""}`);
4541
+ } finally {
4542
+ await session.close();
4543
+ }
4544
+ }
4545
+ var DECAY_THRESHOLD_DAYS = 14;
4546
+ var DECAY_RATE_PER_DAY = 0.05;
4547
+ var INJECTION_THRESHOLD = 0.4;
4548
+ var MAX_SUMMARY_PREFERENCES = 30;
4549
+ async function getUserTimezone(accountId, userId) {
4550
+ const session = getSession();
4551
+ try {
4552
+ const query = userId ? `MATCH (up:UserProfile {accountId: $accountId, userId: $userId})
4553
+ RETURN up.timezone AS timezone` : `MATCH (up:UserProfile {accountId: $accountId})
4554
+ RETURN up.timezone AS timezone ORDER BY up.createdAt LIMIT 1`;
4555
+ const result = await session.run(query, { accountId, userId: userId ?? "" });
4556
+ if (result.records.length === 0) return null;
4557
+ const tz = result.records[0].get("timezone");
4558
+ return tz && tz.trim().length > 0 ? tz : null;
4559
+ } catch (err) {
4560
+ console.error(`[datetime] getUserTimezone failed: ${err instanceof Error ? err.message : String(err)}`);
4561
+ return null;
4562
+ } finally {
4563
+ await session.close();
4564
+ }
4565
+ }
4566
+ async function loadAdminUserName(accountId, userId) {
4567
+ const session = getSession();
4568
+ try {
4569
+ const result = await session.run(
4570
+ `MATCH (au:AdminUser {userId: $userId})-[:OWNS]->(p:Person {accountId: $accountId})
4571
+ RETURN p.givenName AS givenName, p.familyName AS familyName, p.avatar AS avatar
4572
+ LIMIT 1`,
4573
+ { accountId, userId }
4574
+ );
4575
+ if (result.records.length === 0) {
4576
+ return { source: "fallback", reason: "no-person-node" };
4577
+ }
4578
+ const givenName = result.records[0].get("givenName");
4579
+ const familyName = result.records[0].get("familyName");
4580
+ const avatar = result.records[0].get("avatar");
4581
+ if (!givenName || givenName.trim().length === 0) {
4582
+ return { source: "fallback", reason: "no-givenName" };
4583
+ }
4584
+ const trimmedFamily = familyName && familyName.trim().length > 0 ? familyName.trim() : null;
4585
+ const trimmedAvatar = avatar && avatar.trim().length > 0 ? avatar.trim() : null;
4586
+ const joined = trimmedFamily ? `${givenName.trim()} ${trimmedFamily}` : givenName.trim();
4587
+ return { source: "neo4j", joined, givenName: givenName.trim(), familyName: trimmedFamily, avatar: trimmedAvatar };
4588
+ } catch (err) {
4589
+ console.error(`[admin-identity] loadAdminUserName failed: ${err instanceof Error ? err.message : String(err)}`);
4590
+ return { source: "fallback", reason: "neo4j-unreachable" };
4591
+ } finally {
4592
+ await session.close();
4593
+ }
4594
+ }
4595
+ async function writeAdminUserAndPerson(params) {
4596
+ const { userId, fullName, accountId } = params;
4597
+ const trimmed = fullName.trim();
4598
+ if (!trimmed) {
4599
+ throw new Error("writeAdminUserAndPerson: fullName cannot be empty");
4600
+ }
4601
+ const firstSpace = trimmed.search(/\s/);
4602
+ const givenName = firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace).trim();
4603
+ const familyName = firstSpace === -1 ? null : trimmed.slice(firstSpace + 1).trim() || null;
4604
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4605
+ const session = getSession();
4606
+ try {
4607
+ const result = await session.run(
4608
+ `MERGE (au:AdminUser {userId: $userId})
4609
+ ON CREATE SET au.name = $fullName, au.createdAt = $now
4610
+ ON MATCH SET au.name = $fullName, au.updatedAt = $now
4611
+ WITH au
4612
+
4613
+ // Case-insensitive Person reuse: same accountId, same givenName, same familyName
4614
+ // (null/empty familyName collapsed to '' so 'Joel' does NOT match 'Joel Smalley').
4615
+ OPTIONAL MATCH (existingPerson:Person {accountId: $accountId})
4616
+ WHERE toLower(existingPerson.givenName) = toLower($givenName)
4617
+ AND coalesce(toLower(existingPerson.familyName), '') = coalesce(toLower($familyName), '')
4618
+ WITH au, existingPerson
4619
+
4620
+ CALL {
4621
+ WITH au, existingPerson
4622
+ WITH au, existingPerson WHERE existingPerson IS NOT NULL
4623
+ MERGE (au)-[:OWNS]->(existingPerson)
4624
+ RETURN existingPerson AS p, true AS reused
4625
+ UNION
4626
+ WITH au, existingPerson
4627
+ WITH au WHERE existingPerson IS NULL
4628
+ CREATE (newPerson:Person {
4629
+ accountId: $accountId,
4630
+ givenName: $givenName,
4631
+ familyName: $familyName,
4632
+ role: 'admin-personal',
4633
+ scope: 'admin',
4634
+ createdAt: $now
4635
+ })
4636
+ MERGE (au)-[:OWNS]->(newPerson)
4637
+ RETURN newPerson AS p, false AS reused
4638
+ }
4639
+ RETURN reused, elementId(p) AS personElementId,
4640
+ p.givenName AS givenName, p.familyName AS familyName`,
4641
+ { userId, fullName: trimmed, accountId, givenName, familyName, now }
4642
+ );
4643
+ if (result.records.length === 0) {
4644
+ throw new Error("writeAdminUserAndPerson: no record returned");
4645
+ }
4646
+ const record = result.records[0];
4647
+ return {
4648
+ personReused: record.get("reused"),
4649
+ personElementId: record.get("personElementId"),
4650
+ givenName: record.get("givenName"),
4651
+ familyName: record.get("familyName")
4652
+ };
4653
+ } finally {
4654
+ await session.close();
4655
+ }
4656
+ }
4657
+ var PROFILE_CATEGORIES = [
4658
+ "communication",
4659
+ "scheduling",
4660
+ "decision",
4661
+ "workflow",
4662
+ "content",
4663
+ "interaction"
4664
+ ];
4665
+ var PROFILE_FIELD_TEMPLATES = {
4666
+ communication: [
4667
+ "preferredChannel",
4668
+ "quietHours",
4669
+ "responseLatencyExpectation",
4670
+ "formalityLevel",
4671
+ "escalationPath"
4672
+ ],
4673
+ scheduling: [
4674
+ "workdayStartTime",
4675
+ "workdayEndTime",
4676
+ "weekendAvailability",
4677
+ "meetingBlockPreference",
4678
+ "focusBlockTiming"
4679
+ ],
4680
+ decision: [
4681
+ "riskTolerance",
4682
+ "autonomyBoundary",
4683
+ "evidenceThreshold",
4684
+ "reversibilityPreference",
4685
+ "stakeholderConsultation"
4686
+ ],
4687
+ workflow: [
4688
+ "batchingPreference",
4689
+ "taskGranularity",
4690
+ "deepWorkBlocks",
4691
+ "statusUpdateCadence",
4692
+ "toolingChoices"
4693
+ ],
4694
+ content: [
4695
+ "outputFormat",
4696
+ "lengthPreference",
4697
+ "citationStyle",
4698
+ "tonality"
4699
+ ],
4700
+ interaction: [
4701
+ "addressForm",
4702
+ "humourTolerance",
4703
+ "directnessLevel",
4704
+ "emotionalRegisterPreference"
4705
+ ]
4706
+ };
4707
+ var TOTAL_PROFILE_FIELDS = Object.values(PROFILE_FIELD_TEMPLATES).reduce(
4708
+ (n, fs) => n + fs.length,
4709
+ 0
4710
+ );
4711
+ function computeAbsentFields(preferences, extraTemplates = {}) {
4712
+ const covered = /* @__PURE__ */ new Set();
4713
+ for (const pref of preferences) {
4714
+ const isCovered = pref.notApplicable === true || pref.confidence >= INJECTION_THRESHOLD;
4715
+ if (isCovered) covered.add(`${pref.category}.${pref.key}`);
4716
+ }
4717
+ const absent = [];
4718
+ for (const [category, fields] of Object.entries(PROFILE_FIELD_TEMPLATES)) {
4719
+ for (const field of fields) {
4720
+ const dotted = `${category}.${field}`;
4721
+ if (!covered.has(dotted)) absent.push(dotted);
4722
+ }
4723
+ }
4724
+ for (const [category, fields] of Object.entries(extraTemplates)) {
4725
+ if (category in PROFILE_FIELD_TEMPLATES) continue;
4726
+ for (const field of fields) {
4727
+ const dotted = `${category}.${field}`;
4728
+ if (!covered.has(dotted)) absent.push(dotted);
4729
+ }
4730
+ }
4731
+ return absent;
4732
+ }
4733
+ function parsePluginProfileKeys(manifest) {
4734
+ const fm = manifest.match(/^---\n([\s\S]*?)\n---/);
4735
+ if (!fm) return [];
4736
+ const out = [];
4737
+ let inBlock = false;
4738
+ for (const line of fm[1].split("\n")) {
4739
+ if (/^profileKeys:/.test(line)) {
4740
+ inBlock = true;
4741
+ continue;
4742
+ }
4743
+ if (inBlock) {
4744
+ const m = line.match(/^\s+-\s+(.+?)\s*$/);
4745
+ if (m) out.push(m[1]);
4746
+ else break;
4747
+ }
4748
+ }
4749
+ return out;
4750
+ }
4751
+ function loadEnabledPluginProfileKeys(accountId) {
4752
+ const accountFile = resolve5(PLATFORM_ROOT3, "..", "data/accounts", accountId, "account.json");
4753
+ if (!existsSync6(accountFile)) return {};
4754
+ let enabled;
4755
+ try {
4756
+ const raw2 = readFileSync6(accountFile, "utf-8");
4757
+ const parsed = JSON.parse(raw2);
4758
+ enabled = Array.isArray(parsed.enabledPlugins) ? parsed.enabledPlugins.filter((p) => typeof p === "string") : [];
4759
+ } catch (err) {
4760
+ console.error(
4761
+ `[profile-keys] account.json read failed for ${accountId.slice(0, 8)}\u2026: ${err instanceof Error ? err.message : String(err)}`
4762
+ );
4763
+ return {};
4764
+ }
4765
+ const merged = {};
4766
+ for (const pluginName of enabled) {
4767
+ const manifestPath = resolve5(PLATFORM_ROOT3, "plugins", pluginName, "PLUGIN.md");
4768
+ if (!existsSync6(manifestPath)) continue;
4769
+ let manifest;
4770
+ try {
4771
+ manifest = readFileSync6(manifestPath, "utf-8");
4772
+ } catch {
4773
+ continue;
4774
+ }
4775
+ for (const dotted of parsePluginProfileKeys(manifest)) {
4776
+ const dot = dotted.indexOf(".");
4777
+ if (dot <= 0 || dot === dotted.length - 1) {
4778
+ console.error(`[profile-keys] malformed dotted key in ${pluginName}: ${dotted}`);
4779
+ continue;
4780
+ }
4781
+ const category = dotted.slice(0, dot);
4782
+ const key = dotted.slice(dot + 1);
4783
+ if (!merged[category]) merged[category] = /* @__PURE__ */ new Set();
4784
+ merged[category].add(key);
4785
+ }
4786
+ }
4787
+ const out = {};
4788
+ for (const [cat, set] of Object.entries(merged)) out[cat] = [...set];
4789
+ return out;
4790
+ }
4791
+ var sparseStateLogged = /* @__PURE__ */ new Set();
4792
+ async function loadUserProfile(accountId, userId, conversationId) {
4793
+ console.error(
4794
+ `[profile] loadUserProfile entry accountId=${accountId.slice(0, 8)}\u2026 userId=${userId} conversationId=${conversationId ? `${conversationId.slice(0, 8)}\u2026` : "<none>"}`
4795
+ );
4796
+ const session = getSession();
4797
+ try {
4798
+ const serverTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
4799
+ const mergeResult = await session.run(
4800
+ `MATCH (au:AdminUser {userId: $userId})
4801
+ MERGE (au)-[:HAS_PROFILE]->(up:UserProfile {accountId: $accountId, userId: $userId})
4802
+ ON CREATE SET
4803
+ up.createdAt = $now,
4804
+ up.updatedAt = $now,
4805
+ up.profileVersion = 0,
4806
+ up.scope = 'admin',
4807
+ up.timezone = $serverTimezone
4808
+ ON MATCH SET
4809
+ up.timezone = CASE WHEN up.timezone IS NULL OR trim(up.timezone) = '' THEN $serverTimezone ELSE up.timezone END
4810
+ RETURN up.createdAt = $now AS isNew`,
4811
+ { accountId, userId, now: (/* @__PURE__ */ new Date()).toISOString(), serverTimezone }
4812
+ );
4813
+ if (mergeResult.records.length === 0) {
4814
+ console.error(`[profile] loadUserProfile: AdminUser not found for userId=${userId} accountId=${accountId.slice(0, 8)}\u2026 \u2014 profile not created, invariant violated`);
4815
+ return null;
4816
+ }
4817
+ const isNew = mergeResult.records[0].get("isNew");
4818
+ if (isNew) {
4819
+ console.error(`[profile] created new profile: userId=${userId} accountId=${accountId.slice(0, 8)}\u2026 timezone=${serverTimezone}`);
4820
+ }
4821
+ const nowMs = Date.now();
4822
+ const thresholdMs = DECAY_THRESHOLD_DAYS * 24 * 60 * 60 * 1e3;
4823
+ const staleResult = await session.run(
4824
+ `MATCH (up:UserProfile {accountId: $accountId, userId: $userId})-[:HAS_PREFERENCE]->(pref:Preference)
4825
+ WHERE pref.observedAt IS NOT NULL
4826
+ RETURN pref.preferenceId AS preferenceId,
4827
+ pref.observedAt AS observedAt,
4828
+ pref.confidence AS confidence`,
4829
+ { accountId, userId }
4830
+ );
4831
+ let decayCount = 0;
4832
+ for (const record of staleResult.records) {
4833
+ const observedAt = record.get("observedAt");
4834
+ const confidence = record.get("confidence");
4835
+ const observedMs = new Date(observedAt).getTime();
4836
+ const ageMs = nowMs - observedMs;
4837
+ if (ageMs > thresholdMs && confidence > 0) {
4838
+ const daysPastThreshold = (ageMs - thresholdMs) / (24 * 60 * 60 * 1e3);
4839
+ const decayedConfidence = Math.max(0, confidence - DECAY_RATE_PER_DAY * daysPastThreshold);
4840
+ if (decayedConfidence !== confidence) {
4841
+ await session.run(
4842
+ `MATCH (pref:Preference {preferenceId: $preferenceId, accountId: $accountId})
4843
+ SET pref.confidence = $confidence`,
4844
+ {
4845
+ preferenceId: record.get("preferenceId"),
4846
+ accountId,
4847
+ confidence: decayedConfidence
4848
+ }
4849
+ );
4850
+ decayCount++;
4851
+ }
4852
+ }
4853
+ }
4854
+ const result = await session.run(
4855
+ `MATCH (up:UserProfile {accountId: $accountId, userId: $userId})
4856
+ OPTIONAL MATCH (au:AdminUser {userId: $userId})-[:OWNS]->(p:Person {accountId: $accountId})
4857
+ OPTIONAL MATCH (up)-[:HAS_PREFERENCE]->(pref:Preference)
4858
+ WHERE pref.confidence >= $threshold OR pref.notApplicable = true
4859
+ WITH up, p, pref
4860
+ ORDER BY pref.confidence DESC
4861
+ WITH up, p, collect(pref) AS allPrefs
4862
+ WITH up, p, allPrefs[0..$limit] AS prefs
4863
+ RETURN up, p AS person, prefs`,
4864
+ { accountId, userId, threshold: INJECTION_THRESHOLD, limit: MAX_SUMMARY_PREFERENCES }
4865
+ );
4866
+ if (result.records.length === 0 || !result.records[0].get("up")) {
4867
+ return null;
4868
+ }
4869
+ const upNode = result.records[0].get("up");
4870
+ const profileProps = { ...upNode.properties };
4871
+ const personNode = result.records[0].get("person");
4872
+ if (personNode?.properties) {
4873
+ if (personNode.properties.givenName !== void 0) profileProps.givenName = personNode.properties.givenName;
4874
+ if (personNode.properties.familyName !== void 0) profileProps.familyName = personNode.properties.familyName;
4875
+ if (personNode.properties.email !== void 0) profileProps.email = personNode.properties.email;
4876
+ if (personNode.properties.telephone !== void 0) profileProps.telephone = personNode.properties.telephone;
4877
+ }
4878
+ const prefNodes = result.records[0].get("prefs");
4879
+ const preferences = prefNodes.map((p) => ({
4880
+ preferenceId: p.properties.preferenceId,
4881
+ category: p.properties.category,
4882
+ key: p.properties.key,
4883
+ value: p.properties.value,
4884
+ confidence: p.properties.confidence,
4885
+ source: p.properties.source,
4886
+ observedAt: p.properties.observedAt,
4887
+ notApplicable: p.properties.notApplicable === true
4888
+ }));
4889
+ const extraTemplates = loadEnabledPluginProfileKeys(accountId);
4890
+ const extraFieldCount = Object.values(extraTemplates).reduce((n, fs) => n + fs.length, 0);
4891
+ const summary = formatProfileSummary(profileProps, preferences, extraTemplates);
4892
+ const absentFields = computeAbsentFields(preferences, extraTemplates);
4893
+ const totalFields = TOTAL_PROFILE_FIELDS + extraFieldCount;
4894
+ const fieldCoverageCount = totalFields - absentFields.length;
4895
+ const presentCategories = new Set(
4896
+ preferences.filter((p) => p.notApplicable === true || p.confidence >= INJECTION_THRESHOLD).map((p) => p.category)
4897
+ );
4898
+ const absentCategories = PROFILE_CATEGORIES.filter((c) => !presentCategories.has(c));
4899
+ const categoryCoverage = PROFILE_CATEGORIES.length - absentCategories.length;
4900
+ console.error(
4901
+ `[profile] loaded for userId=${userId} accountId=${accountId.slice(0, 8)}\u2026 preferences=${preferences.length} fieldCoverage=${fieldCoverageCount}/${totalFields} categoryCoverage=${categoryCoverage}/${PROFILE_CATEGORIES.length} (decay: ${decayCount} updated)`
4902
+ );
4903
+ const identityKeys = [
4904
+ { prop: "email", token: "email" },
4905
+ // Schema-canonical name. `phone` is the synonym (rejected at tool
4906
+ // surface); the log uses `telephone` to keep grep aligned with
4907
+ // schema-base.md doctrine the brief enforces.
4908
+ { prop: "telephone", token: "telephone" },
4909
+ { prop: "givenName", token: "givenName" },
4910
+ { prop: "familyName", token: "familyName" },
4911
+ { prop: "role", token: "role" }
4912
+ ];
4913
+ const absentIdentity = identityKeys.filter((k) => {
4914
+ const v = profileProps[k.prop];
4915
+ return typeof v !== "string" || v.trim().length === 0;
4916
+ });
4917
+ const sparseFired = absentCategories.length > 0 || absentFields.length > 0 || absentIdentity.length > 0;
4918
+ if (conversationId && sparseFired) {
4919
+ const dedupeKey = `${accountId}:${userId}:${conversationId}`;
4920
+ if (!sparseStateLogged.has(dedupeKey)) {
4921
+ sparseStateLogged.add(dedupeKey);
4922
+ const FIELDS_LOG_CAP = 10;
4923
+ const absentFieldsTruncated = absentFields.length <= FIELDS_LOG_CAP ? absentFields.join(",") : `${absentFields.slice(0, FIELDS_LOG_CAP).join(",")}+${absentFields.length - FIELDS_LOG_CAP} more`;
4924
+ console.error(
4925
+ `[profile] sparse-state injected accountId=${accountId.slice(0, 8)}\u2026 userId=${userId} conversationId=${conversationId.slice(0, 8)}\u2026 absentCategories=${absentCategories.join(",")} absentFields=${absentFieldsTruncated || "none"} absentIdentity=${absentIdentity.map((k) => k.token).join(",") || "none"}`
4926
+ );
4927
+ }
4928
+ }
4929
+ return summary;
4930
+ } catch (err) {
4931
+ console.error(`[profile] loadUserProfile failed: ${err instanceof Error ? err.message : String(err)}`);
4932
+ return null;
4933
+ } finally {
4934
+ await session.close();
4935
+ }
4936
+ }
4937
+ function formatProfileSummary(profile, preferences, extraTemplates = {}) {
4938
+ const summary = composeOperatorSummary(profile, preferences);
4939
+ return [
4940
+ `This is what you know about the operator: ${summary}.`,
4941
+ `You must seek to MAXIMISE what you know about the operator to do your job effectively.`
4942
+ ].join("\n");
4943
+ }
4944
+ function composeOperatorSummary(profile, preferences) {
4945
+ const facts = [];
4946
+ const givenName = typeof profile.givenName === "string" ? profile.givenName.trim() : "";
4947
+ const familyName = typeof profile.familyName === "string" ? profile.familyName.trim() : "";
4948
+ const fullName = [givenName, familyName].filter((s) => s.length > 0).join(" ");
4949
+ if (fullName) facts.push(`name ${fullName}`);
4950
+ if (typeof profile.role === "string" && profile.role.trim()) {
4951
+ facts.push(`role ${profile.role.trim()}`);
4952
+ }
4953
+ if (typeof profile.timezone === "string" && profile.timezone.trim()) {
4954
+ facts.push(`timezone ${profile.timezone.trim()}`);
4955
+ }
4956
+ if (typeof profile.locale === "string" && profile.locale.trim()) {
4957
+ facts.push(`locale ${profile.locale.trim()}`);
4958
+ }
4959
+ if (typeof profile.email === "string" && profile.email.trim()) {
4960
+ facts.push(`email ${profile.email.trim()}`);
4961
+ }
4962
+ if (typeof profile.telephone === "string" && profile.telephone.trim()) {
4963
+ facts.push(`telephone ${profile.telephone.trim()}`);
4964
+ }
4965
+ if (profile.expertise) {
4966
+ const expVal = typeof profile.expertise === "string" ? profile.expertise : JSON.stringify(profile.expertise);
4967
+ if (expVal.trim()) facts.push(`expertise ${expVal.trim()}`);
4968
+ }
4969
+ const byCategory = {};
4970
+ for (const pref of preferences) {
4971
+ if (pref.notApplicable === true) continue;
4972
+ if (typeof pref.value !== "string" || pref.value.trim().length === 0) continue;
4973
+ if (!byCategory[pref.category]) byCategory[pref.category] = [];
4974
+ byCategory[pref.category].push(pref.value.trim());
4975
+ }
4976
+ for (const [cat, vals] of Object.entries(byCategory)) {
4977
+ facts.push(`${cat} ${vals.join(", ")}`);
4978
+ }
4979
+ return facts.length > 0 ? facts.join("; ") : "NOTHING";
4980
+ }
4981
+ var RECENT_FAILURES_TAIL_BYTES = 10 * 1024;
4982
+ var AGENT_FILE_ROLES = [
4983
+ { filename: "IDENTITY.md", role: "identity" },
4984
+ { filename: "SOUL.md", role: "soul" },
4985
+ { filename: "KNOWLEDGE.md", role: "knowledge" },
4986
+ { filename: "KNOWLEDGE-SUMMARY.md", role: "knowledge-summary" }
4987
+ ];
4988
+ var ROLE_TO_EDGE = {
4989
+ identity: "HAS_IDENTITY",
4990
+ soul: "HAS_SOUL",
4991
+ knowledge: "HAS_KNOWLEDGE",
4992
+ "knowledge-summary": "HAS_KNOWLEDGE"
4993
+ };
4994
+ function assertSafeAgentSlug(slug) {
4995
+ if (!slug || slug.includes("/") || slug.includes("\\") || slug.includes("..")) {
4996
+ throw new Error(`[agent-graph] refusing unsafe slug=${JSON.stringify(slug)}`);
4997
+ }
4998
+ }
4999
+ function agentAttachmentId(accountId, slug, role) {
5000
+ return `agent:${accountId}:${slug}:${role}`;
5001
+ }
5002
+ async function projectAgent(accountId, accountDir, slug) {
5003
+ const start = Date.now();
5004
+ const account8 = accountId.slice(0, 8);
5005
+ try {
5006
+ assertSafeAgentSlug(slug);
5007
+ } catch (err) {
5008
+ const msg = err instanceof Error ? err.message : String(err);
5009
+ console.error(
5010
+ `[agent-graph] project FAILED slug=${slug} account=${account8} error="${msg}"`
5011
+ );
5012
+ return;
5013
+ }
5014
+ const agentDir = resolve5(accountDir, "agents", slug);
5015
+ const configPath = resolve5(agentDir, "config.json");
5016
+ let config;
5017
+ let presentRoles = [];
5018
+ try {
5019
+ if (!existsSync6(configPath)) {
5020
+ console.error(
5021
+ `[agent-graph] project FAILED slug=${slug} account=${account8} error="config.json missing at ${configPath}"`
5022
+ );
5023
+ return;
5024
+ }
5025
+ config = JSON.parse(readFileSync6(configPath, "utf-8"));
5026
+ for (const { filename, role } of AGENT_FILE_ROLES) {
5027
+ const filePath = resolve5(agentDir, filename);
5028
+ if (!existsSync6(filePath)) continue;
5029
+ const body = readFileSync6(filePath, "utf-8");
5030
+ presentRoles.push({ role, body, edge: ROLE_TO_EDGE[role] });
5031
+ }
5032
+ } catch (err) {
5033
+ const msg = err instanceof Error ? err.message : String(err);
5034
+ console.error(
5035
+ `[agent-graph] project FAILED slug=${slug} account=${account8} error="${msg}"`
5036
+ );
5037
+ return;
5038
+ }
5039
+ const session = getSession();
5040
+ try {
5041
+ await session.run(
5042
+ `MERGE (a:Agent {accountId: $accountId, slug: $slug})
5043
+ ON CREATE SET a.createdAt = datetime()
5044
+ SET a.displayName = $displayName,
5045
+ a.status = $status,
5046
+ a.model = $model,
5047
+ a.liveMemory = $liveMemory,
5048
+ a.knowledgeKeywords = $knowledgeKeywords,
5049
+ a.role = 'agent',
5050
+ a.updatedAt = datetime()
5051
+ RETURN a.slug AS slug`,
5052
+ {
5053
+ accountId,
5054
+ slug,
5055
+ displayName: config.displayName ?? slug,
5056
+ status: config.status ?? "unknown",
5057
+ model: config.model ?? "",
5058
+ liveMemory: config.liveMemory ?? false,
5059
+ knowledgeKeywords: config.knowledgeKeywords ?? []
5060
+ }
5061
+ );
5062
+ for (const { role, body, edge } of presentRoles) {
5063
+ const attachmentId = agentAttachmentId(accountId, slug, role);
5064
+ await session.run(
5065
+ `MATCH (a:Agent {accountId: $accountId, slug: $slug})
5066
+ MERGE (k:KnowledgeDocument {attachmentId: $attachmentId})
5067
+ ON CREATE SET k.createdAt = datetime()
5068
+ SET k.accountId = $accountId,
5069
+ k.role = $role,
5070
+ k.name = $name,
5071
+ k.text = $text,
5072
+ k.scope = 'admin',
5073
+ k.updatedAt = datetime()
5074
+ MERGE (a)-[:${edge}]->(k)
5075
+ RETURN k.attachmentId AS attachmentId`,
5076
+ {
5077
+ accountId,
5078
+ slug,
5079
+ attachmentId,
5080
+ role,
5081
+ name: `${slug} ${role}`,
5082
+ text: body
5083
+ }
5084
+ );
5085
+ }
5086
+ const usesResult = await session.run(
5087
+ `MATCH (a:Agent {accountId: $accountId, slug: $slug})
5088
+ MATCH (k:KnowledgeDocument {accountId: $accountId})
5089
+ WHERE $slug IN coalesce(k.agents, [])
5090
+ AND NOT k.attachmentId STARTS WITH $namespacePrefix
5091
+ MERGE (a)-[:USES_KNOWLEDGE]->(k)
5092
+ RETURN count(k) AS uses`,
5093
+ {
5094
+ accountId,
5095
+ slug,
5096
+ namespacePrefix: `agent:${accountId}:${slug}:`
5097
+ }
5098
+ );
5099
+ const uses = usesResult.records[0]?.get("uses");
5100
+ const usesCount = typeof uses === "number" ? uses : uses && typeof uses.toNumber === "function" ? uses.toNumber() : 0;
5101
+ const ms = Date.now() - start;
5102
+ console.error(
5103
+ `[agent-graph] project slug=${slug} account=${account8} docs=${presentRoles.length} uses=${usesCount} ms=${ms}`
5104
+ );
5105
+ } catch (err) {
5106
+ const msg = err instanceof Error ? err.message : String(err);
5107
+ console.error(
5108
+ `[agent-graph] project FAILED slug=${slug} account=${account8} error="${msg}"`
5109
+ );
5110
+ throw err;
5111
+ } finally {
5112
+ await session.close();
5113
+ }
5114
+ }
5115
+ async function deleteAgentProjection(accountId, slug) {
5116
+ const start = Date.now();
5117
+ const account8 = accountId.slice(0, 8);
5118
+ assertSafeAgentSlug(slug);
5119
+ const session = getSession();
5120
+ try {
5121
+ await session.run(
5122
+ `MATCH (a:Agent {accountId: $accountId, slug: $slug})
5123
+ DETACH DELETE a
5124
+ WITH 1 AS _
5125
+ UNWIND $attachmentIds AS aid
5126
+ OPTIONAL MATCH (k:KnowledgeDocument {accountId: $accountId, attachmentId: aid})
5127
+ WHERE k.attachmentId STARTS WITH $namespacePrefix
5128
+ DETACH DELETE k`,
5129
+ {
5130
+ accountId,
5131
+ slug,
5132
+ namespacePrefix: `agent:${accountId}:${slug}:`,
5133
+ attachmentIds: ["identity", "soul", "knowledge", "knowledge-summary"].map((role) => agentAttachmentId(accountId, slug, role))
5134
+ }
5135
+ );
5136
+ const ms = Date.now() - start;
5137
+ console.error(
5138
+ `[agent-graph] delete slug=${slug} account=${account8} ms=${ms}`
5139
+ );
5140
+ } catch (err) {
5141
+ const msg = err instanceof Error ? err.message : String(err);
5142
+ console.error(
5143
+ `[agent-graph] delete FAILED slug=${slug} account=${account8} error="${msg}"`
5144
+ );
5145
+ throw err;
5146
+ } finally {
5147
+ await session.close();
5148
+ }
5149
+ }
5150
+
3879
5151
  // server/routes/_helpers.ts
3880
5152
  async function safeJson(c) {
3881
5153
  try {
@@ -3999,486 +5271,6 @@ var clientIpMiddleware = async (c, next) => {
3999
5271
  await next();
4000
5272
  };
4001
5273
 
4002
- // server/lib/action-runner.ts
4003
- import { spawn } from "child_process";
4004
- import { createReadStream, existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as readdirSync3, statSync as statSync3, unlinkSync, watch } from "fs";
4005
- import { homedir as homedir2 } from "os";
4006
- import { join as join4, resolve as resolve5 } from "path";
4007
- import { readFileSync as readFileSync6 } from "fs";
4008
- import { setTimeout as delay } from "timers/promises";
4009
- function loadBrandInfo() {
4010
- const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ?? resolve5(process.cwd(), "..");
4011
- const brandPath = resolve5(platformRoot2, "config", "brand.json");
4012
- try {
4013
- const parsed = JSON.parse(readFileSync6(brandPath, "utf-8"));
4014
- const hostname = typeof parsed.hostname === "string" && parsed.hostname ? parsed.hostname : "maxy";
4015
- const configDir = typeof parsed.configDir === "string" && parsed.configDir ? parsed.configDir : ".maxy";
4016
- return { hostname, configDir };
4017
- } catch {
4018
- return { hostname: "maxy", configDir: ".maxy" };
4019
- }
4020
- }
4021
- function actionLogDir() {
4022
- const { configDir } = loadBrandInfo();
4023
- return join4(homedir2(), configDir, "logs", "actions");
4024
- }
4025
- function actionLogPath(actionId) {
4026
- return join4(actionLogDir(), `${actionId}.log`);
4027
- }
4028
- var WHITELIST = {
4029
- upgrade: {
4030
- build: ({ sudoPassword }) => {
4031
- const { hostname } = loadBrandInfo();
4032
- const pkg = `@rubytech/create-${hostname}`;
4033
- const cmd = [
4034
- // prime: read pw from stdin, silently cache
4035
- 'printf %s "$SUDO_PASSWORD"',
4036
- "|",
4037
- "sudo -S -v 2>/dev/null",
4038
- "&&",
4039
- // run installer — it will re-prompt only if the cache expires
4040
- `npx -y ${pkg}@latest`
4041
- ].join(" ");
4042
- return {
4043
- argv: ["bash", "-c", cmd],
4044
- env: sudoPassword ? { SUDO_PASSWORD: sudoPassword } : void 0
4045
- };
4046
- }
4047
- },
4048
- "cloudflare-setup": {
4049
- build: ({ positional, streamLogPath, accountDir, tunnelId, tunnelName }) => {
4050
- if (!positional || positional.length < 3) {
4051
- throw new Error("cloudflare-setup requires [brand, port, ...hostnames]");
4052
- }
4053
- if (!accountDir) {
4054
- throw new Error("cloudflare-setup requires accountDir");
4055
- }
4056
- const idSet = typeof tunnelId === "string" && tunnelId.length > 0;
4057
- const nameSet = typeof tunnelName === "string" && tunnelName.length > 0;
4058
- if (idSet && nameSet) {
4059
- throw new Error("cloudflare-setup: tunnelId and tunnelName are mutually exclusive \u2014 pass exactly one");
4060
- }
4061
- if (!idSet && !nameSet) {
4062
- throw new Error("cloudflare-setup: requires tunnelId (operator-selected) OR tunnelName (operator-created)");
4063
- }
4064
- const scriptPath = join4(homedir2(), "setup-tunnel.sh");
4065
- if (!existsSync6(scriptPath)) {
4066
- throw new Error(
4067
- `cloudflare-setup: ~/setup-tunnel.sh not found. Re-run the installer to repair the symlink.`
4068
- );
4069
- }
4070
- const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ?? resolve5(process.cwd(), "..");
4071
- return {
4072
- argv: [scriptPath, ...positional],
4073
- env: {
4074
- MAXY_PLATFORM_ROOT: platformRoot2,
4075
- ...streamLogPath ? { STREAM_LOG_PATH: streamLogPath } : {},
4076
- ACCOUNT_DIR: accountDir,
4077
- ...idSet ? { TUNNEL_ID: tunnelId } : {},
4078
- ...nameSet ? { TUNNEL_NAME: tunnelName } : {}
4079
- }
4080
- };
4081
- },
4082
- reconcileFromLog: reconcileCloudflareSetupFromLog
4083
- }
4084
- };
4085
- var ARMING_LINE_RE = /\bstep=service-restart-armed\b[^\n]*\bexit=0\b/;
4086
- var DONE_LINE_RE = /\bstep=done\b/;
4087
- var ERROR_LINE_RE = /\bresult=error\b/;
4088
- function reconcileCloudflareSetupFromLog(lines) {
4089
- let hasArming = false;
4090
- let hasDone = false;
4091
- let hasError = false;
4092
- for (const line of lines) {
4093
- if (!hasArming && ARMING_LINE_RE.test(line)) hasArming = true;
4094
- if (!hasDone && DONE_LINE_RE.test(line)) hasDone = true;
4095
- if (!hasError && ERROR_LINE_RE.test(line)) hasError = true;
4096
- }
4097
- if (hasDone && hasArming) return { kind: "succeeded", code: 0 };
4098
- if (hasError) return { kind: "failed", code: 1 };
4099
- if (hasArming && !hasDone) return { kind: "restart-in-progress", code: null };
4100
- return null;
4101
- }
4102
- function isKnownAction(name) {
4103
- return Object.prototype.hasOwnProperty.call(WHITELIST, name);
4104
- }
4105
- function generateActionId(name) {
4106
- const ts = Date.now().toString(36);
4107
- const nonce = Math.floor(Math.random() * 4294967295).toString(16).padStart(8, "0");
4108
- return `${name}-${ts}-${nonce}`;
4109
- }
4110
- function actionNameFromId(actionId) {
4111
- for (const name of Object.keys(WHITELIST)) {
4112
- if (actionId.startsWith(`${name}-`)) return name;
4113
- }
4114
- return "unknown";
4115
- }
4116
- function cleanupStaleLogs() {
4117
- const dir = actionLogDir();
4118
- if (!existsSync6(dir)) return;
4119
- const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
4120
- for (const entry of readdirSync3(dir)) {
4121
- const full = join4(dir, entry);
4122
- try {
4123
- const st = statSync3(full);
4124
- if (st.isFile() && st.mtimeMs < cutoff) unlinkSync(full);
4125
- } catch {
4126
- }
4127
- }
4128
- }
4129
- async function isActionActive(actionName) {
4130
- return new Promise((resolveP) => {
4131
- const proc = spawn(
4132
- "systemctl",
4133
- ["--user", "list-units", "--no-legend", "--state=active", `maxy-action-${actionName}-*`],
4134
- { stdio: ["ignore", "pipe", "pipe"] }
4135
- );
4136
- let out = "";
4137
- proc.stdout.on("data", (chunk) => {
4138
- out += chunk.toString();
4139
- });
4140
- proc.on("close", () => resolveP(out.trim().length > 0));
4141
- proc.on("error", () => resolveP(false));
4142
- });
4143
- }
4144
- function buildSystemdArgs(name, args, actionId, logPath) {
4145
- const unit = `maxy-action-${actionId}`;
4146
- const built = WHITELIST[name].build(args);
4147
- const systemdArgs = [
4148
- "--user",
4149
- `--unit=${unit}`,
4150
- "--collect",
4151
- "--property=RemainAfterExit=false",
4152
- `--property=StandardOutput=append:${logPath}`,
4153
- `--property=StandardError=append:${logPath}`,
4154
- `--description=Maxy action ${name} (${actionId})`
4155
- ];
4156
- if (built.env) {
4157
- for (const [k, v] of Object.entries(built.env)) {
4158
- systemdArgs.push(`--setenv=${k}=${v}`);
4159
- }
4160
- }
4161
- systemdArgs.push("--", ...built.argv);
4162
- return systemdArgs;
4163
- }
4164
- async function launchAction(name, args) {
4165
- const actionId = generateActionId(name);
4166
- const dir = actionLogDir();
4167
- mkdirSync3(dir, { recursive: true });
4168
- cleanupStaleLogs();
4169
- const logPath = actionLogPath(actionId);
4170
- const unit = `maxy-action-${actionId}`;
4171
- const systemdArgs = buildSystemdArgs(name, args, actionId, logPath);
4172
- await new Promise((resolveP, rejectP) => {
4173
- const proc = spawn("systemd-run", systemdArgs, { stdio: ["ignore", "pipe", "pipe"] });
4174
- let stderr = "";
4175
- proc.stderr.on("data", (chunk) => {
4176
- stderr += chunk.toString();
4177
- });
4178
- proc.on("close", (code) => {
4179
- if (code === 0) resolveP();
4180
- else rejectP(new Error(`systemd-run exited ${code}: ${stderr.trim().slice(0, 400)}`));
4181
- });
4182
- proc.on("error", rejectP);
4183
- });
4184
- console.log(`[action-runner] launched action=${name} id=${actionId} unit=${unit} log=${logPath}`);
4185
- return { actionId, unit, logPath };
4186
- }
4187
- function parseUnitShow(out) {
4188
- const kv = {};
4189
- for (const line of out.split("\n")) {
4190
- const eq = line.indexOf("=");
4191
- if (eq > 0) kv[line.slice(0, eq)] = line.slice(eq + 1);
4192
- }
4193
- const loadState = kv.LoadState ?? "";
4194
- if (loadState !== "loaded" || !kv.ActiveState) {
4195
- return { status: null, loadState };
4196
- }
4197
- return {
4198
- status: {
4199
- activeState: kv.ActiveState,
4200
- subState: kv.SubState ?? "",
4201
- mainPid: kv.MainPID ? Number(kv.MainPID) || null : null,
4202
- execMainStatus: kv.ExecMainStatus ? Number(kv.ExecMainStatus) : null,
4203
- execMainSignal: null
4204
- // systemd reports signal in SIGNAL (SIGTERM etc.), deferred — code is enough for our contract
4205
- },
4206
- loadState
4207
- };
4208
- }
4209
- async function showUnit(unit) {
4210
- return new Promise((resolveP) => {
4211
- const proc = spawn(
4212
- "systemctl",
4213
- ["--user", "show", unit, "--property=ActiveState,SubState,MainPID,ExecMainStatus,ExecMainCode,LoadState"],
4214
- { stdio: ["ignore", "pipe", "pipe"] }
4215
- );
4216
- let out = "";
4217
- proc.stdout.on("data", (chunk) => {
4218
- out += chunk.toString();
4219
- });
4220
- proc.on("close", () => resolveP(parseUnitShow(out)));
4221
- proc.on("error", () => resolveP({ status: null, loadState: "error" }));
4222
- });
4223
- }
4224
- async function getUnitStatus(unit) {
4225
- return (await showUnit(unit)).status;
4226
- }
4227
- var HEARTBEAT_INTERVAL_MS = 5e3;
4228
- var exitEmittedFor = /* @__PURE__ */ new Set();
4229
- var PHASE_BRACKETED = /\[(\d+)\/(\d+)\]/;
4230
- var PHASE_STREAM_LOG = /step=([a-z0-9-]+)/;
4231
- function extractPhase(text) {
4232
- const m1 = text.match(PHASE_BRACKETED);
4233
- if (m1) return m1[0];
4234
- const m2 = text.match(PHASE_STREAM_LOG);
4235
- if (m2) return m2[1];
4236
- return null;
4237
- }
4238
- function readLogTailLines(path, tailLines) {
4239
- if (!existsSync6(path)) return [];
4240
- let raw2;
4241
- try {
4242
- raw2 = readFileSync6(path, "utf-8");
4243
- } catch {
4244
- return [];
4245
- }
4246
- const all = raw2.split("\n");
4247
- if (all.length <= tailLines) return all;
4248
- return all.slice(all.length - tailLines);
4249
- }
4250
- async function* streamActionEvents(opts) {
4251
- const { actionId, unit, signal } = opts;
4252
- const logPath = actionLogPath(actionId);
4253
- const startedAt = Date.now();
4254
- let lastLineAt = startedAt;
4255
- let lastPhase = null;
4256
- let byteOffset = opts.fromByteOffset ?? 0;
4257
- const actionName = actionNameFromId(actionId);
4258
- const reconcileFromLog = isKnownAction(actionName) ? WHITELIST[actionName].reconcileFromLog : void 0;
4259
- if (existsSync6(logPath)) {
4260
- const snapshot = statSync3(logPath).size;
4261
- if (byteOffset < snapshot) {
4262
- for await (const ev of readLogLines(logPath, byteOffset, snapshot)) {
4263
- const phase = extractPhase(ev.text);
4264
- if (phase) lastPhase = phase;
4265
- lastLineAt = ev.ts;
4266
- byteOffset = ev.byteOffset;
4267
- yield ev;
4268
- if (signal.aborted) return;
4269
- }
4270
- }
4271
- }
4272
- if (exitEmittedFor.has(actionId)) {
4273
- return;
4274
- }
4275
- let watching = true;
4276
- let hbTimer = null;
4277
- const queue = [];
4278
- let resolveNext = null;
4279
- function push(ev) {
4280
- queue.push(ev);
4281
- if (resolveNext) {
4282
- const r = resolveNext;
4283
- resolveNext = null;
4284
- r();
4285
- }
4286
- }
4287
- const watcher = existsSync6(logPath) ? watch(logPath, async () => {
4288
- if (!watching) return;
4289
- try {
4290
- const size = statSync3(logPath).size;
4291
- if (size <= byteOffset) return;
4292
- for await (const ev of readLogLines(logPath, byteOffset, size)) {
4293
- const phase = extractPhase(ev.text);
4294
- if (phase) lastPhase = phase;
4295
- lastLineAt = ev.ts;
4296
- byteOffset = ev.byteOffset;
4297
- push(ev);
4298
- }
4299
- } catch (err) {
4300
- console.error(`[action-runner] watcher error action=${actionId}: ${err}`);
4301
- }
4302
- }) : null;
4303
- hbTimer = setInterval(async () => {
4304
- const probe = await showUnit(unit);
4305
- const status = probe.status;
4306
- const now = Date.now();
4307
- push({
4308
- type: "heartbeat",
4309
- ts: now,
4310
- elapsed_since_last_line_ms: now - lastLineAt,
4311
- systemd_state: status ? status.activeState : "unknown",
4312
- pid: status ? status.mainPid : null,
4313
- last_phase: lastPhase
4314
- });
4315
- if (!status) {
4316
- if (exitEmittedFor.has(actionId)) {
4317
- watching = false;
4318
- return;
4319
- }
4320
- exitEmittedFor.add(actionId);
4321
- console.log(`[action-runner] unit-not-loaded action=${actionNameFromId(actionId)} id=${actionId} loadState=${probe.loadState || "empty"}`);
4322
- const reconciled = reconcileFromLog ? reconcileFromLog(readLogTailLines(logPath, 200)) : null;
4323
- const reconcileMs = Date.now() - startedAt;
4324
- if (reconciled) {
4325
- console.log(
4326
- `[action-runner] reconcile actionId=${actionId} result=${reconciled.kind} source=persisted-log ms=${reconcileMs}`
4327
- );
4328
- push({
4329
- type: "exit",
4330
- ts: Date.now(),
4331
- code: reconciled.code,
4332
- signal: null,
4333
- duration_ms: reconcileMs,
4334
- source: "persisted-log",
4335
- ...reconciled.kind === "restart-in-progress" ? { kind: "restart-in-progress" } : {}
4336
- });
4337
- } else {
4338
- console.log(
4339
- `[action-runner] reconcile actionId=${actionId} result=unresolved source=persisted-log ms=${reconcileMs}`
4340
- );
4341
- push({
4342
- type: "exit",
4343
- ts: Date.now(),
4344
- code: null,
4345
- signal: null,
4346
- duration_ms: reconcileMs,
4347
- source: "persisted-log"
4348
- });
4349
- }
4350
- watching = false;
4351
- return;
4352
- }
4353
- if (status.activeState !== "active" && status.activeState !== "activating") {
4354
- if (exitEmittedFor.has(actionId)) {
4355
- watching = false;
4356
- return;
4357
- }
4358
- exitEmittedFor.add(actionId);
4359
- const finalStatus = await getUnitStatus(unit);
4360
- if (finalStatus?.execMainStatus == null && reconcileFromLog) {
4361
- const reconciled = reconcileFromLog(readLogTailLines(logPath, 200));
4362
- const reconcileMs = Date.now() - startedAt;
4363
- if (reconciled) {
4364
- console.log(
4365
- `[action-runner] reconcile actionId=${actionId} result=${reconciled.kind} source=persisted-log ms=${reconcileMs}`
4366
- );
4367
- push({
4368
- type: "exit",
4369
- ts: Date.now(),
4370
- code: reconciled.code,
4371
- signal: null,
4372
- duration_ms: reconcileMs,
4373
- source: "persisted-log",
4374
- ...reconciled.kind === "restart-in-progress" ? { kind: "restart-in-progress" } : {}
4375
- });
4376
- watching = false;
4377
- return;
4378
- }
4379
- }
4380
- const ms = Date.now() - startedAt;
4381
- const source = finalStatus?.execMainStatus != null ? "systemd" : "persisted-log";
4382
- const result = finalStatus?.execMainStatus === 0 ? "succeeded" : finalStatus?.execMainStatus != null ? "failed" : "unresolved";
4383
- console.log(
4384
- `[action-runner] reconcile actionId=${actionId} result=${result} source=${source} ms=${ms}`
4385
- );
4386
- push({
4387
- type: "exit",
4388
- ts: Date.now(),
4389
- code: finalStatus?.execMainStatus ?? null,
4390
- signal: null,
4391
- duration_ms: ms,
4392
- source
4393
- });
4394
- watching = false;
4395
- }
4396
- }, HEARTBEAT_INTERVAL_MS);
4397
- try {
4398
- while (watching && !signal.aborted) {
4399
- if (queue.length === 0) {
4400
- await new Promise((r) => {
4401
- resolveNext = r;
4402
- });
4403
- }
4404
- while (queue.length > 0) {
4405
- const ev = queue.shift();
4406
- yield ev;
4407
- if (ev.type === "exit") {
4408
- watching = false;
4409
- break;
4410
- }
4411
- }
4412
- }
4413
- } finally {
4414
- if (hbTimer) clearInterval(hbTimer);
4415
- watcher?.close();
4416
- }
4417
- }
4418
- async function* readLogLines(path, fromOffset, toOffset) {
4419
- const stream = createReadStream(path, { start: fromOffset, end: toOffset - 1, encoding: "utf-8" });
4420
- let buffer = "";
4421
- let offset = fromOffset;
4422
- for await (const chunk of stream) {
4423
- buffer += chunk;
4424
- let nl;
4425
- while ((nl = buffer.indexOf("\n")) !== -1) {
4426
- const text = buffer.slice(0, nl);
4427
- offset += Buffer.byteLength(text, "utf-8") + 1;
4428
- buffer = buffer.slice(nl + 1);
4429
- yield { type: "line", ts: Date.now(), stream: "stdout", text, byteOffset: offset };
4430
- }
4431
- }
4432
- }
4433
- var RATE_LIMIT_WINDOW_MS = 6e4;
4434
- var RATE_LIMIT_MAX_ATTEMPTS = 5;
4435
- var rateLimitBuckets = /* @__PURE__ */ new Map();
4436
- function rateLimitCheck(ip) {
4437
- const now = Date.now();
4438
- const bucket = rateLimitBuckets.get(ip);
4439
- if (!bucket || now - bucket.windowStart > RATE_LIMIT_WINDOW_MS) {
4440
- rateLimitBuckets.set(ip, { count: 1, windowStart: now });
4441
- return "ok";
4442
- }
4443
- bucket.count += 1;
4444
- if (bucket.count > RATE_LIMIT_MAX_ATTEMPTS) return "limited";
4445
- return "ok";
4446
- }
4447
- async function primeSudo(password, clientIp) {
4448
- if (!password) return "invalid";
4449
- if (rateLimitCheck(clientIp) === "limited") return "limited";
4450
- return new Promise((resolveP) => {
4451
- const proc = spawn("sudo", ["-S", "-v"], { stdio: ["pipe", "ignore", "pipe"] });
4452
- let stderr = "";
4453
- proc.stderr.on("data", (chunk) => {
4454
- stderr += chunk.toString();
4455
- });
4456
- proc.on("close", (code) => {
4457
- if (code === 0) return resolveP("ok");
4458
- if (/incorrect password|authentication failure/i.test(stderr)) return resolveP("invalid");
4459
- console.error(`[action-runner] sudo-prime failed code=${code} stderr=${stderr.slice(0, 200)}`);
4460
- resolveP("error");
4461
- });
4462
- proc.on("error", (err) => {
4463
- console.error(`[action-runner] sudo-prime spawn error: ${err}`);
4464
- resolveP("error");
4465
- });
4466
- proc.stdin.write(`${password}
4467
- `);
4468
- proc.stdin.end();
4469
- });
4470
- }
4471
- async function waitForExit(unit, timeoutMs = 10 * 60 * 1e3) {
4472
- const deadline = Date.now() + timeoutMs;
4473
- while (Date.now() < deadline) {
4474
- const status = await getUnitStatus(unit);
4475
- if (!status) return null;
4476
- if (status.activeState !== "active" && status.activeState !== "activating") return status;
4477
- await delay(500);
4478
- }
4479
- return null;
4480
- }
4481
-
4482
5274
  export {
4483
5275
  Hono2 as Hono,
4484
5276
  getRequestListener,
@@ -4500,7 +5292,6 @@ export {
4500
5292
  sanitizeClientCorrId,
4501
5293
  httpLog,
4502
5294
  websockifyLog,
4503
- browserViewerLog,
4504
5295
  validatePasswordStrength,
4505
5296
  isPasswordValid,
4506
5297
  isRemoteAuthConfigured,
@@ -4540,16 +5331,32 @@ export {
4540
5331
  emitMissingOnResolve,
4541
5332
  stripAttachmentMetaSuffix,
4542
5333
  requirePortEnv,
5334
+ getSession,
5335
+ runAdminUserSelfHeal,
5336
+ embed,
5337
+ GREETING_DIRECTIVE,
5338
+ ensureConversation,
5339
+ fetchBranding,
5340
+ persistMessage,
5341
+ getAgentSessionIdForConversation,
5342
+ getRecentMessages,
5343
+ verifyConversationOwnership,
5344
+ verifyAndGetConversationUpdatedAt,
5345
+ listAdminSessions,
5346
+ deleteConversation,
5347
+ linkConversationTranscript,
5348
+ getConversationClaudeSessionIds,
5349
+ getConversationIdByClaudeSessionId,
5350
+ renameConversation,
5351
+ getUserTimezone,
5352
+ loadAdminUserName,
5353
+ writeAdminUserAndPerson,
5354
+ loadUserProfile,
5355
+ projectAgent,
5356
+ deleteAgentProjection,
4543
5357
  safeJson,
4544
5358
  requireAdminSession,
4545
5359
  tryCookieBridgeForConversation,
4546
5360
  requireAdminCookie,
4547
- clientIpMiddleware,
4548
- actionLogPath,
4549
- isKnownAction,
4550
- isActionActive,
4551
- launchAction,
4552
- streamActionEvents,
4553
- primeSudo,
4554
- waitForExit
5361
+ clientIpMiddleware
4555
5362
  };