@rubytech/create-realagent-code 0.1.576 → 0.1.578
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.
- package/package.json +1 -1
- package/payload/platform/lib/active-rules/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/active-rules/dist/index.js +32 -1
- package/payload/platform/lib/active-rules/dist/index.js.map +1 -1
- package/payload/platform/lib/active-rules/src/index.test.ts +52 -3
- package/payload/platform/lib/active-rules/src/index.ts +36 -2
- package/payload/platform/lib/dispatch-read/dist/allocate.d.ts +57 -0
- package/payload/platform/lib/dispatch-read/dist/allocate.d.ts.map +1 -0
- package/payload/platform/lib/dispatch-read/dist/allocate.js +33 -0
- package/payload/platform/lib/dispatch-read/dist/allocate.js.map +1 -0
- package/payload/platform/lib/dispatch-read/dist/index.d.ts +2 -0
- package/payload/platform/lib/dispatch-read/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/dispatch-read/dist/index.js +10 -1
- package/payload/platform/lib/dispatch-read/dist/index.js.map +1 -1
- package/payload/platform/lib/dispatch-read/src/__tests__/allocate.test.ts +41 -0
- package/payload/platform/lib/dispatch-read/src/allocate.ts +89 -0
- package/payload/platform/lib/dispatch-read/src/index.ts +14 -0
- package/payload/platform/lib/routine-templates/dist/index.d.ts +4 -4
- package/payload/platform/lib/routine-templates/dist/index.js +4 -4
- package/payload/platform/lib/routine-templates/src/index.ts +4 -4
- package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-compliance.test.sh +7 -2
- package/payload/platform/plugins/admin/hooks/prompt-optimiser-compliance.sh +8 -2
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +4 -2
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +17 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visit-status-write.test.ts +333 -22
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visits-scope.test.ts +262 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +126 -21
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visit-status.ts +142 -2
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visits.ts +118 -6
- package/payload/platform/plugins/dispatch/PLUGIN.md +2 -0
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.d.ts +19 -50
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.d.ts.map +1 -1
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.js +1 -28
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.js.map +1 -1
- package/payload/platform/plugins/dispatch/skills/dispatch-page/SKILL.md +45 -6
- package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.css +60 -0
- package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.html +13 -0
- package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.js +203 -5
- package/payload/platform/plugins/docs/references/internals.md +1 -1
- package/payload/platform/plugins/docs/references/platform.md +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +0 -2
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/platform/services/telegram-channel/dist/notification.d.ts +35 -3
- package/payload/platform/services/telegram-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/telegram-channel/dist/notification.js +38 -5
- package/payload/platform/services/telegram-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/telegram-channel/dist/server.js +1 -1
- package/payload/platform/services/telegram-channel/dist/server.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts +2 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +2 -2
- package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
- package/payload/server/public/activity.html +4 -4
- package/payload/server/public/agents.html +3 -3
- package/payload/server/public/assets/{AdminLoginScreens-BLGZEaKU.js → AdminLoginScreens-CAZ3DSwA.js} +1 -1
- package/payload/server/public/assets/AdminLoginScreens-CAZ3DSwA.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-CAZ3DSwA.js.gz +0 -0
- package/payload/server/public/assets/{AdminShell-C_M7YE5Z.js → AdminShell-BLs5v0qF.js} +1 -1
- package/payload/server/public/assets/AdminShell-BLs5v0qF.js.br +0 -0
- package/payload/server/public/assets/AdminShell-BLs5v0qF.js.gz +0 -0
- package/payload/server/public/assets/{activity-BL6qbB9a.js → activity-BwPck2yT.js} +1 -1
- package/payload/server/public/assets/activity-BwPck2yT.js.br +0 -0
- package/payload/server/public/assets/activity-BwPck2yT.js.gz +0 -0
- package/payload/server/public/assets/{admin-jnJ3P2u4.js → admin-DjtOoabA.js} +1 -1
- package/payload/server/public/assets/admin-DjtOoabA.js.br +0 -0
- package/payload/server/public/assets/admin-DjtOoabA.js.gz +0 -0
- package/payload/server/public/assets/{agents-BzhfKZDN.js → agents-IQYaRvoh.js} +1 -1
- package/payload/server/public/assets/agents-IQYaRvoh.js.br +0 -0
- package/payload/server/public/assets/agents-IQYaRvoh.js.gz +0 -0
- package/payload/server/public/assets/{browser-C-6io2B9.js → browser-CPG8w8rQ.js} +1 -1
- package/payload/server/public/assets/browser-CPG8w8rQ.js.br +0 -0
- package/payload/server/public/assets/browser-CPG8w8rQ.js.gz +0 -0
- package/payload/server/public/assets/{calendar-Dt3gS88s.js → calendar-CRwrgNdq.js} +1 -1
- package/payload/server/public/assets/calendar-CRwrgNdq.js.br +0 -0
- package/payload/server/public/assets/calendar-CRwrgNdq.js.gz +0 -0
- package/payload/server/public/assets/chat-B7yZ4KLl.js +1 -0
- package/payload/server/public/assets/chat-B7yZ4KLl.js.br +3 -0
- package/payload/server/public/assets/chat-B7yZ4KLl.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-BzKUQqLy.js +1 -0
- package/payload/server/public/assets/chevron-left-BzKUQqLy.js.br +0 -0
- package/payload/server/public/assets/clock-BafWFNGo.js +1 -0
- package/payload/server/public/assets/clock-BafWFNGo.js.br +0 -0
- package/payload/server/public/assets/{copy-TkPJgllS.js → copy-JyiMTV0k.js} +1 -1
- package/payload/server/public/assets/copy-JyiMTV0k.js.br +0 -0
- package/payload/server/public/assets/copy-JyiMTV0k.js.gz +0 -0
- package/payload/server/public/assets/data-DHtGCVrl.js +1 -0
- package/payload/server/public/assets/data-DHtGCVrl.js.br +0 -0
- package/payload/server/public/assets/data-DHtGCVrl.js.gz +0 -0
- package/payload/server/public/assets/{file-text-DyAL2DCh.js → file-text-cnHv9ZS6.js} +1 -1
- package/payload/server/public/assets/file-text-cnHv9ZS6.js.br +0 -0
- package/payload/server/public/assets/file-text-cnHv9ZS6.js.gz +0 -0
- package/payload/server/public/assets/{graph-PiNIcCfX.js → graph-lFwEUDSH.js} +1 -1
- package/payload/server/public/assets/graph-lFwEUDSH.js.br +0 -0
- package/payload/server/public/assets/graph-lFwEUDSH.js.gz +0 -0
- package/payload/server/public/assets/{graph-labels-8jKFrLHn.js → graph-labels-C6qUA0vO.js} +1 -1
- package/payload/server/public/assets/graph-labels-C6qUA0vO.js.br +0 -0
- package/payload/server/public/assets/graph-labels-C6qUA0vO.js.gz +0 -0
- package/payload/server/public/assets/{operator-DKGKGzrb.js → operator-BdoEy5mh.js} +1 -1
- package/payload/server/public/assets/operator-BdoEy5mh.js.br +0 -0
- package/payload/server/public/assets/operator-BdoEy5mh.js.gz +0 -0
- package/payload/server/public/assets/{page-DXz_K10G.js → page-DMrnnXNA.js} +1 -1
- package/payload/server/public/assets/page-DMrnnXNA.js.br +0 -0
- package/payload/server/public/assets/page-DMrnnXNA.js.gz +0 -0
- package/payload/server/public/assets/{page-BU5vlY9Y.js → page-DeMikGYL.js} +1 -1
- package/payload/server/public/assets/page-DeMikGYL.js.br +0 -0
- package/payload/server/public/assets/page-DeMikGYL.js.gz +0 -0
- package/payload/server/public/assets/play-D6OAQVJ_.js +1 -0
- package/payload/server/public/assets/play-D6OAQVJ_.js.br +0 -0
- package/payload/server/public/assets/play-D6OAQVJ_.js.gz +0 -0
- package/payload/server/public/assets/{public-BmA3-jLj.js → public-B-X1Cxfw.js} +1 -1
- package/payload/server/public/assets/public-B-X1Cxfw.js.br +0 -0
- package/payload/server/public/assets/public-B-X1Cxfw.js.gz +0 -0
- package/payload/server/public/assets/{rotate-ccw-fCH0LSMq.js → rotate-ccw-DVFvQAHG.js} +1 -1
- package/payload/server/public/assets/rotate-ccw-DVFvQAHG.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-DVFvQAHG.js.gz +0 -0
- package/payload/server/public/assets/{routines-BeFrmkls.js → routines-CgkkGJaS.js} +1 -1
- package/payload/server/public/assets/routines-CgkkGJaS.js.br +0 -0
- package/payload/server/public/assets/routines-CgkkGJaS.js.gz +0 -0
- package/payload/server/public/assets/{skills-mjls5HBa.js → skills-CW7gVcaG.js} +1 -1
- package/payload/server/public/assets/skills-CW7gVcaG.js.br +0 -0
- package/payload/server/public/assets/skills-CW7gVcaG.js.gz +0 -0
- package/payload/server/public/assets/{tasks-p6puHHD0.js → tasks-Ddjp0Rwh.js} +1 -1
- package/payload/server/public/assets/tasks-Ddjp0Rwh.js.br +0 -0
- package/payload/server/public/assets/tasks-Ddjp0Rwh.js.gz +0 -0
- package/payload/server/public/assets/{triangle-alert-CrZ1ewbd.js → triangle-alert-DUrUutX4.js} +1 -1
- package/payload/server/public/assets/triangle-alert-DUrUutX4.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-DUrUutX4.js.gz +0 -0
- package/payload/server/public/assets/{useCopyFeedback-zOQNrIwd.js → useCopyFeedback-ClVINDrQ.js} +1 -1
- package/payload/server/public/assets/useCopyFeedback-ClVINDrQ.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-ClVINDrQ.js.gz +0 -0
- package/payload/server/public/assets/{useMediaQuery-BmG090FR.js → useMediaQuery-BUFkZYRs.js} +1 -1
- package/payload/server/public/assets/useMediaQuery-BUFkZYRs.js.br +0 -0
- package/payload/server/public/assets/{useMediaQuery-BmG090FR.js.gz → useMediaQuery-BUFkZYRs.js.gz} +0 -0
- package/payload/server/public/assets/{useVoiceRecorder-9pf6FFJ-.js → useVoiceRecorder-DnyqQFet.js} +1 -1
- package/payload/server/public/assets/useVoiceRecorder-DnyqQFet.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-DnyqQFet.js.gz +0 -0
- package/payload/server/public/assets/{wrench-k4GYG5rc.js → wrench-BW28s_d0.js} +1 -1
- package/payload/server/public/assets/wrench-BW28s_d0.js.br +0 -0
- package/payload/server/public/assets/wrench-BW28s_d0.js.gz +0 -0
- package/payload/server/public/browser.html +3 -3
- package/payload/server/public/calendar.html +6 -6
- package/payload/server/public/chat.html +12 -12
- package/payload/server/public/data.html +10 -10
- package/payload/server/public/graph.html +8 -8
- package/payload/server/public/index.html +12 -12
- package/payload/server/public/operator.html +13 -13
- package/payload/server/public/public.html +12 -12
- package/payload/server/public/routines.html +6 -6
- package/payload/server/public/skills.html +4 -4
- package/payload/server/public/tasks.html +5 -5
- package/payload/server/server.js +1014 -375
- package/payload/platform/plugins/dispatch/mcp/dist/scripts/converge-visit-instants.d.ts +0 -6
- package/payload/platform/plugins/dispatch/mcp/dist/scripts/converge-visit-instants.d.ts.map +0 -1
- package/payload/platform/plugins/dispatch/mcp/dist/scripts/converge-visit-instants.js +0 -102
- package/payload/platform/plugins/dispatch/mcp/dist/scripts/converge-visit-instants.js.map +0 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.d.ts +0 -2
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.d.ts.map +0 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.js +0 -87
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.js.map +0 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-startdate-utc.test.d.ts +0 -2
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-startdate-utc.test.d.ts.map +0 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-startdate-utc.test.js +0 -79
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-startdate-utc.test.js.map +0 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.d.ts +0 -35
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.d.ts.map +0 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.js +0 -56
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.js.map +0 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-startdate-utc.d.ts +0 -24
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-startdate-utc.d.ts.map +0 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-startdate-utc.js +0 -87
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-startdate-utc.js.map +0 -1
- package/payload/server/public/assets/AdminLoginScreens-BLGZEaKU.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-BLGZEaKU.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-C_M7YE5Z.js.br +0 -0
- package/payload/server/public/assets/AdminShell-C_M7YE5Z.js.gz +0 -0
- package/payload/server/public/assets/activity-BL6qbB9a.js.br +0 -0
- package/payload/server/public/assets/activity-BL6qbB9a.js.gz +0 -0
- package/payload/server/public/assets/admin-jnJ3P2u4.js.br +0 -0
- package/payload/server/public/assets/admin-jnJ3P2u4.js.gz +0 -0
- package/payload/server/public/assets/agents-BzhfKZDN.js.br +0 -0
- package/payload/server/public/assets/agents-BzhfKZDN.js.gz +0 -0
- package/payload/server/public/assets/browser-C-6io2B9.js.br +0 -0
- package/payload/server/public/assets/browser-C-6io2B9.js.gz +0 -0
- package/payload/server/public/assets/calendar-Dt3gS88s.js.br +0 -0
- package/payload/server/public/assets/calendar-Dt3gS88s.js.gz +0 -0
- package/payload/server/public/assets/chat-IUOvVkL-.js +0 -1
- package/payload/server/public/assets/chat-IUOvVkL-.js.br +0 -1
- package/payload/server/public/assets/chat-IUOvVkL-.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-DSzR9zR7.js +0 -1
- package/payload/server/public/assets/chevron-left-DSzR9zR7.js.br +0 -0
- package/payload/server/public/assets/clock-C5xE5p2I.js +0 -1
- package/payload/server/public/assets/clock-C5xE5p2I.js.br +0 -0
- package/payload/server/public/assets/copy-TkPJgllS.js.br +0 -0
- package/payload/server/public/assets/copy-TkPJgllS.js.gz +0 -0
- package/payload/server/public/assets/data-Bhv0Pvfl.js +0 -1
- package/payload/server/public/assets/data-Bhv0Pvfl.js.br +0 -0
- package/payload/server/public/assets/data-Bhv0Pvfl.js.gz +0 -0
- package/payload/server/public/assets/file-text-DyAL2DCh.js.br +0 -0
- package/payload/server/public/assets/file-text-DyAL2DCh.js.gz +0 -0
- package/payload/server/public/assets/graph-PiNIcCfX.js.br +0 -0
- package/payload/server/public/assets/graph-PiNIcCfX.js.gz +0 -0
- package/payload/server/public/assets/graph-labels-8jKFrLHn.js.br +0 -0
- package/payload/server/public/assets/graph-labels-8jKFrLHn.js.gz +0 -0
- package/payload/server/public/assets/operator-DKGKGzrb.js.br +0 -0
- package/payload/server/public/assets/operator-DKGKGzrb.js.gz +0 -0
- package/payload/server/public/assets/page-BU5vlY9Y.js.br +0 -0
- package/payload/server/public/assets/page-BU5vlY9Y.js.gz +0 -0
- package/payload/server/public/assets/page-DXz_K10G.js.br +0 -0
- package/payload/server/public/assets/page-DXz_K10G.js.gz +0 -0
- package/payload/server/public/assets/play-CmSyYrB6.js +0 -1
- package/payload/server/public/assets/play-CmSyYrB6.js.br +0 -0
- package/payload/server/public/assets/play-CmSyYrB6.js.gz +0 -0
- package/payload/server/public/assets/public-BmA3-jLj.js.br +0 -0
- package/payload/server/public/assets/public-BmA3-jLj.js.gz +0 -0
- package/payload/server/public/assets/rotate-ccw-fCH0LSMq.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-fCH0LSMq.js.gz +0 -0
- package/payload/server/public/assets/routines-BeFrmkls.js.br +0 -0
- package/payload/server/public/assets/routines-BeFrmkls.js.gz +0 -0
- package/payload/server/public/assets/skills-mjls5HBa.js.br +0 -0
- package/payload/server/public/assets/skills-mjls5HBa.js.gz +0 -0
- package/payload/server/public/assets/tasks-p6puHHD0.js.br +0 -0
- package/payload/server/public/assets/tasks-p6puHHD0.js.gz +0 -0
- package/payload/server/public/assets/triangle-alert-CrZ1ewbd.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-CrZ1ewbd.js.gz +0 -0
- package/payload/server/public/assets/useCopyFeedback-zOQNrIwd.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-zOQNrIwd.js.gz +0 -0
- package/payload/server/public/assets/useMediaQuery-BmG090FR.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-9pf6FFJ-.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-9pf6FFJ-.js.gz +0 -0
- package/payload/server/public/assets/wrench-k4GYG5rc.js.br +0 -0
- package/payload/server/public/assets/wrench-k4GYG5rc.js.gz +0 -0
package/payload/server/server.js
CHANGED
|
@@ -471,6 +471,16 @@ var init_agent_errors = __esm({
|
|
|
471
471
|
});
|
|
472
472
|
|
|
473
473
|
// app/lib/reader/parse-transcript.ts
|
|
474
|
+
function emptyEnvelopeCensus() {
|
|
475
|
+
return {
|
|
476
|
+
rows: 0,
|
|
477
|
+
wrapped: 0,
|
|
478
|
+
nested: 0,
|
|
479
|
+
stripped: 0,
|
|
480
|
+
unstripped: 0,
|
|
481
|
+
sources: { whatsapp: 0, webchat: 0, telegram: 0, none: 0 }
|
|
482
|
+
};
|
|
483
|
+
}
|
|
474
484
|
function isCompactSummaryRow(row, content) {
|
|
475
485
|
return row.isCompactSummary === true || content !== null && content.startsWith(COMPACT_SUMMARY_PREFIX);
|
|
476
486
|
}
|
|
@@ -481,10 +491,11 @@ function isScheduleSource(text) {
|
|
|
481
491
|
}
|
|
482
492
|
function unwrapChannel(text) {
|
|
483
493
|
const m = text.match(CHANNEL_WRAPPER);
|
|
484
|
-
if (!m) return { text };
|
|
494
|
+
if (!m) return { text, wrapped: false, nested: false };
|
|
485
495
|
let source = SOURCE_MAP[m[1].match(SOURCE_ATTR)?.[1] ?? ""];
|
|
486
496
|
let inner = m[2];
|
|
487
497
|
const im = inner.match(CHANNEL_WRAPPER);
|
|
498
|
+
const nested = im !== null;
|
|
488
499
|
if (im) {
|
|
489
500
|
const innerSource = SOURCE_MAP[im[1].match(SOURCE_ATTR)?.[1] ?? ""];
|
|
490
501
|
if (innerSource) {
|
|
@@ -492,7 +503,7 @@ function unwrapChannel(text) {
|
|
|
492
503
|
inner = im[2];
|
|
493
504
|
}
|
|
494
505
|
}
|
|
495
|
-
return source ? { text: inner, source } : { text: inner };
|
|
506
|
+
return source ? { text: inner, source, wrapped: true, nested } : { text: inner, wrapped: true, nested };
|
|
496
507
|
}
|
|
497
508
|
function splitReplyNote(payload) {
|
|
498
509
|
const m = payload.match(REPLY_NOTE);
|
|
@@ -508,7 +519,17 @@ function uploadCountOf(text) {
|
|
|
508
519
|
const n = Number(text.match(UPLOAD_COUNT)?.[1]);
|
|
509
520
|
return Number.isInteger(n) && n > 0 ? n : void 0;
|
|
510
521
|
}
|
|
511
|
-
function
|
|
522
|
+
function noteEnvelope(census, u) {
|
|
523
|
+
if (!census) return;
|
|
524
|
+
census.rows++;
|
|
525
|
+
if (u.wrapped) census.wrapped++;
|
|
526
|
+
if (u.nested) census.nested++;
|
|
527
|
+
if (u.text.startsWith("<channel ")) census.unstripped++;
|
|
528
|
+
else if (u.wrapped) census.stripped++;
|
|
529
|
+
census.sources[u.source ?? "none"]++;
|
|
530
|
+
}
|
|
531
|
+
function operatorInboundTurn(u, ts, uuid, census) {
|
|
532
|
+
noteEnvelope(census, u);
|
|
512
533
|
const framed = rawFromComposedAdminFrame(u.text);
|
|
513
534
|
const split = framed === void 0 ? void 0 : splitReplyNote(framed);
|
|
514
535
|
const rawBody = split?.body;
|
|
@@ -635,12 +656,12 @@ function lastTurnTs(turns) {
|
|
|
635
656
|
function isModelGated(turns) {
|
|
636
657
|
return turns.some((t) => t.kind === "agent-error" && t.code === "model_unavailable");
|
|
637
658
|
}
|
|
638
|
-
function renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, uuid) {
|
|
659
|
+
function renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, uuid, census) {
|
|
639
660
|
const armed2 = queuedPendingSuppress.get(u.text) ?? 0;
|
|
640
661
|
if (armed2 > 0) {
|
|
641
662
|
queuedPendingSuppress.set(u.text, armed2 - 1);
|
|
642
663
|
} else {
|
|
643
|
-
out.push(operatorInboundTurn(u, ts, uuid));
|
|
664
|
+
out.push(operatorInboundTurn(u, ts, uuid, census));
|
|
644
665
|
}
|
|
645
666
|
}
|
|
646
667
|
function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map(), turnCtx = {}) {
|
|
@@ -678,7 +699,7 @@ function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map(
|
|
|
678
699
|
const u = unwrapChannel(content);
|
|
679
700
|
turnCtx.lastChannelSource = u.source;
|
|
680
701
|
if (!isScheduleSource(content)) {
|
|
681
|
-
out.push(operatorInboundTurn(u, ts));
|
|
702
|
+
out.push(operatorInboundTurn(u, ts, void 0, turnCtx.envelopeCensus));
|
|
682
703
|
queuedPendingSuppress.set(u.text, (queuedPendingSuppress.get(u.text) ?? 0) + 1);
|
|
683
704
|
}
|
|
684
705
|
}
|
|
@@ -691,7 +712,7 @@ function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map(
|
|
|
691
712
|
const u = unwrapChannel(att.prompt);
|
|
692
713
|
turnCtx.lastChannelSource = u.source;
|
|
693
714
|
if (!isScheduleSource(att.prompt)) {
|
|
694
|
-
renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, asString(row.uuid) ?? void 0);
|
|
715
|
+
renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, asString(row.uuid) ?? void 0, turnCtx.envelopeCensus);
|
|
695
716
|
}
|
|
696
717
|
}
|
|
697
718
|
continue;
|
|
@@ -720,7 +741,7 @@ function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map(
|
|
|
720
741
|
const u = unwrapChannel(text);
|
|
721
742
|
turnCtx.lastChannelSource = u.source;
|
|
722
743
|
if (!isScheduleSource(text)) {
|
|
723
|
-
renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, asString(row.uuid) ?? void 0);
|
|
744
|
+
renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, asString(row.uuid) ?? void 0, turnCtx.envelopeCensus);
|
|
724
745
|
}
|
|
725
746
|
} else if (row.isMeta === true) {
|
|
726
747
|
out.push({ kind: "harness-internal", text, ts });
|
|
@@ -747,7 +768,7 @@ var init_parse_transcript = __esm({
|
|
|
747
768
|
HARNESS_LIFECYCLE_PREFIXES = ["<task-notification>"];
|
|
748
769
|
CC_IMAGE_VISION_HINT = /^\[Image:[^\]]*Multiply coordinates by [\d.]+ to map to original image\.\]$/;
|
|
749
770
|
COMPACT_SUMMARY_PREFIX = "This session is being continued from a previous conversation that ran out of context.";
|
|
750
|
-
CHANNEL_WRAPPER = /^<channel\b([^>]*)>\n?([\s\S]*?)\n
|
|
771
|
+
CHANNEL_WRAPPER = /^<channel\b([^>]*)>\n?([\s\S]*?)\n?<\\?\/channel>$/;
|
|
751
772
|
SOURCE_ATTR = /\bsource="([^"]+)"/;
|
|
752
773
|
SOURCE_MAP = {
|
|
753
774
|
whatsapp: "whatsapp",
|
|
@@ -757,7 +778,7 @@ var init_parse_transcript = __esm({
|
|
|
757
778
|
webchat: "webchat",
|
|
758
779
|
"webchat-channel": "webchat"
|
|
759
780
|
};
|
|
760
|
-
COMPOSED_ADMIN_FRAME = /^(?:## Scheduled automation \(platform-verified\)\n[\s\S]*?\n\n)?(?:## Message provenance \(platform-verified\)\n[\s\S]*?\n\n)?(?:## Standing rules\n[\s\S]*?\n\n)?## Context\n([\s\S]*)\n\n## Instruction\n[\s\S]*$/;
|
|
781
|
+
COMPOSED_ADMIN_FRAME = /^(?:## Scheduled automation \(platform-verified\)\n[\s\S]*?\n\n)?(?:## Message provenance \(platform-verified\)\n[\s\S]*?\n\n)?(?:## Standing rules(?: \(account owner\))?\n[\s\S]*?\n\n)?## Context\n([\s\S]*)\n\n## Instruction\n[\s\S]*$/;
|
|
761
782
|
SCHEDULED_MARKER_PREFIX = "## Scheduled automation (platform-verified)\n";
|
|
762
783
|
UPLOAD_ANNOTATION = /(?:\n\n)?\[The operator uploaded \d+ file\(s\): [\s\S]*\]$/;
|
|
763
784
|
REPLY_NOTE = /^\[In reply to an earlier message in this conversation(?:\.|: "([\s\S]*?)")\]\n\n/;
|
|
@@ -815,7 +836,7 @@ var init_audio_mime = __esm({
|
|
|
815
836
|
// app/lib/stt/voice-note.ts
|
|
816
837
|
import { writeFile, mkdtemp, rm } from "fs/promises";
|
|
817
838
|
import { tmpdir as tmpdir2 } from "os";
|
|
818
|
-
import { join as
|
|
839
|
+
import { join as join7 } from "path";
|
|
819
840
|
async function transcribeVoiceNote(file, source) {
|
|
820
841
|
const startMs = Date.now();
|
|
821
842
|
const bytes = file.size;
|
|
@@ -826,9 +847,9 @@ async function transcribeVoiceNote(file, source) {
|
|
|
826
847
|
let tempDir;
|
|
827
848
|
let tempPath;
|
|
828
849
|
try {
|
|
829
|
-
tempDir = await mkdtemp(
|
|
850
|
+
tempDir = await mkdtemp(join7(tmpdir2(), "voice-"));
|
|
830
851
|
const ext = audioExtension(mimeType);
|
|
831
|
-
tempPath =
|
|
852
|
+
tempPath = join7(tempDir, `recording${ext}`);
|
|
832
853
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
833
854
|
await writeFile(tempPath, buffer);
|
|
834
855
|
} catch (err) {
|
|
@@ -1239,10 +1260,56 @@ function buildStoreConversationRows(accountId, summaries, chatMeta) {
|
|
|
1239
1260
|
}
|
|
1240
1261
|
return rows.sort((a, b) => (b.lastMessageAt ?? "").localeCompare(a.lastMessageAt ?? ""));
|
|
1241
1262
|
}
|
|
1263
|
+
function buildTelegramStoreConversationRows(accountId, summaries) {
|
|
1264
|
+
const rows = [];
|
|
1265
|
+
for (const [channelKey, { first, last }] of summaries) {
|
|
1266
|
+
rows.push({
|
|
1267
|
+
sessionId: "",
|
|
1268
|
+
projectDir: "",
|
|
1269
|
+
title: channelKey,
|
|
1270
|
+
senderId: last.senderId,
|
|
1271
|
+
startedAt: first.dateSent,
|
|
1272
|
+
channel: "telegram",
|
|
1273
|
+
role: last.scope === "admin" ? "admin" : "public",
|
|
1274
|
+
operatorName: null,
|
|
1275
|
+
whatsappName: null,
|
|
1276
|
+
// The name is what it is NOW, matching how `scope` is taken from `last`.
|
|
1277
|
+
// The fallback covers an outbound last record: the outbound write points
|
|
1278
|
+
// have no sender to name, so they store null.
|
|
1279
|
+
telegramName: last.senderName ?? first.senderName ?? null,
|
|
1280
|
+
lastMessageAt: last.dateSent,
|
|
1281
|
+
modelGated: false,
|
|
1282
|
+
model: null,
|
|
1283
|
+
source: "store",
|
|
1284
|
+
accountId,
|
|
1285
|
+
channelKey,
|
|
1286
|
+
scope: last.scope,
|
|
1287
|
+
preview: last.body
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
return rows.sort((a, b) => (b.lastMessageAt ?? "").localeCompare(a.lastMessageAt ?? ""));
|
|
1291
|
+
}
|
|
1292
|
+
function dropTelegramSessionDuplicates(rows, telegramStoreRows) {
|
|
1293
|
+
const coveredKeys = /* @__PURE__ */ new Set();
|
|
1294
|
+
const coveredSenders = /* @__PURE__ */ new Set();
|
|
1295
|
+
for (const r of telegramStoreRows) {
|
|
1296
|
+
const parsed = r.channelKey ? parseTelegramChannelKey(r.channelKey) : null;
|
|
1297
|
+
if (!parsed) continue;
|
|
1298
|
+
coveredKeys.add(r.channelKey);
|
|
1299
|
+
coveredSenders.add(parsed.senderId);
|
|
1300
|
+
}
|
|
1301
|
+
if (coveredKeys.size === 0) return rows;
|
|
1302
|
+
return rows.filter((r) => {
|
|
1303
|
+
if (r.source !== "session" || r.channel !== "telegram" || r.senderId == null) return true;
|
|
1304
|
+
const parsed = parseTelegramChannelKey(r.senderId);
|
|
1305
|
+
return parsed ? !coveredKeys.has(r.senderId) : !coveredSenders.has(r.senderId);
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1242
1308
|
var init_select_sessions = __esm({
|
|
1243
1309
|
"app/lib/reader/select-sessions.ts"() {
|
|
1244
1310
|
"use strict";
|
|
1245
1311
|
init_normalize();
|
|
1312
|
+
init_channel_key();
|
|
1246
1313
|
init_conversation_preview();
|
|
1247
1314
|
}
|
|
1248
1315
|
});
|
|
@@ -1911,8 +1978,9 @@ var require_dist2 = __commonJS({
|
|
|
1911
1978
|
if (rules.length === 0)
|
|
1912
1979
|
return "";
|
|
1913
1980
|
const lines = rules.map((r) => `- ${r.value}`);
|
|
1914
|
-
return `## Standing rules
|
|
1915
|
-
|
|
1981
|
+
return `## Standing rules (account owner)
|
|
1982
|
+
These standing rules belong to the account owner. Some of them may name the account owner in the third person. None of them says who sent this turn, so do not infer from them who is speaking.
|
|
1983
|
+
` + lines.join("\n");
|
|
1916
1984
|
}
|
|
1917
1985
|
async function resolveOwnerUserId3(session, accountId) {
|
|
1918
1986
|
const res = await session.run(`
|
|
@@ -3018,6 +3086,40 @@ var require_keyboard = __commonJS({
|
|
|
3018
3086
|
}
|
|
3019
3087
|
});
|
|
3020
3088
|
|
|
3089
|
+
// ../lib/dispatch-read/dist/allocate.js
|
|
3090
|
+
var require_allocate = __commonJS({
|
|
3091
|
+
"../lib/dispatch-read/dist/allocate.js"(exports) {
|
|
3092
|
+
"use strict";
|
|
3093
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3094
|
+
exports.selectCandidates = selectCandidates2;
|
|
3095
|
+
exports.overlaps = overlaps2;
|
|
3096
|
+
function selectCandidates2(params) {
|
|
3097
|
+
const { jobType, cfg, commitments } = params;
|
|
3098
|
+
const type = cfg.jobTypes.find((t) => t.name === jobType);
|
|
3099
|
+
if (!type) {
|
|
3100
|
+
return { offered: [], excluded: [], required: "" };
|
|
3101
|
+
}
|
|
3102
|
+
const offered = [];
|
|
3103
|
+
const excluded = [];
|
|
3104
|
+
for (const worker of cfg.workers) {
|
|
3105
|
+
if (!worker.qualifications.includes(type.requiredQualification)) {
|
|
3106
|
+
excluded.push({ workerId: worker.workerId, reason: "missing-qualification" });
|
|
3107
|
+
continue;
|
|
3108
|
+
}
|
|
3109
|
+
offered.push({
|
|
3110
|
+
workerId: worker.workerId,
|
|
3111
|
+
name: worker.name,
|
|
3112
|
+
conflicts: commitments.get(worker.workerId) ?? []
|
|
3113
|
+
});
|
|
3114
|
+
}
|
|
3115
|
+
return { offered, excluded, required: type.requiredQualification };
|
|
3116
|
+
}
|
|
3117
|
+
function overlaps2(a, b) {
|
|
3118
|
+
return a.start < b.end && b.start < a.end;
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
});
|
|
3122
|
+
|
|
3021
3123
|
// ../lib/dispatch-read/dist/session.js
|
|
3022
3124
|
var require_session = __commonJS({
|
|
3023
3125
|
"../lib/dispatch-read/dist/session.js"(exports) {
|
|
@@ -3217,7 +3319,7 @@ var require_read = __commonJS({
|
|
|
3217
3319
|
exports.readJobList = readJobList2;
|
|
3218
3320
|
exports.readCounts = readCounts2;
|
|
3219
3321
|
var session_js_1 = require_session();
|
|
3220
|
-
function
|
|
3322
|
+
function num5(v) {
|
|
3221
3323
|
if (typeof v === "number")
|
|
3222
3324
|
return v;
|
|
3223
3325
|
if (v && typeof v === "object" && "low" in v) {
|
|
@@ -3241,7 +3343,7 @@ var require_read = __commonJS({
|
|
|
3241
3343
|
workerId: String(a.workerId),
|
|
3242
3344
|
arrivedPressedAt: str2(a.arrivedPressedAt),
|
|
3243
3345
|
arrivedObservedAt: str2(a.arrivedObservedAt),
|
|
3244
|
-
arrivedObservedDistanceM: a.arrivedObservedDistanceM == null ? null :
|
|
3346
|
+
arrivedObservedDistanceM: a.arrivedObservedDistanceM == null ? null : num5(a.arrivedObservedDistanceM)
|
|
3245
3347
|
})).sort((x, y) => x.workerId < y.workerId ? -1 : x.workerId > y.workerId ? 1 : 0);
|
|
3246
3348
|
const sole = attendances.length === 1 ? attendances[0] : null;
|
|
3247
3349
|
return {
|
|
@@ -3334,12 +3436,12 @@ var require_read = __commonJS({
|
|
|
3334
3436
|
}
|
|
3335
3437
|
const g = r.records[0];
|
|
3336
3438
|
return {
|
|
3337
|
-
notes:
|
|
3338
|
-
media:
|
|
3339
|
-
parts:
|
|
3340
|
-
checklistDone:
|
|
3341
|
-
checklistTotal:
|
|
3342
|
-
signatures:
|
|
3439
|
+
notes: num5(g.get("notes")),
|
|
3440
|
+
media: num5(g.get("media")),
|
|
3441
|
+
parts: num5(g.get("parts")),
|
|
3442
|
+
checklistDone: num5(g.get("checklistDone")),
|
|
3443
|
+
checklistTotal: num5(g.get("checklistTotal")),
|
|
3444
|
+
signatures: num5(g.get("signatures"))
|
|
3343
3445
|
};
|
|
3344
3446
|
}
|
|
3345
3447
|
async function readVisitsForJob3(session, accountId, jobId) {
|
|
@@ -3392,7 +3494,7 @@ var require_read = __commonJS({
|
|
|
3392
3494
|
workerId,
|
|
3393
3495
|
arrivedPressedAt: str2(g.get("ownArrivedPressedAt")),
|
|
3394
3496
|
arrivedObservedAt: str2(g.get("ownArrivedObservedAt")),
|
|
3395
|
-
arrivedObservedDistanceM: dist == null ? null :
|
|
3497
|
+
arrivedObservedDistanceM: dist == null ? null : num5(dist)
|
|
3396
3498
|
};
|
|
3397
3499
|
});
|
|
3398
3500
|
}
|
|
@@ -3517,9 +3619,9 @@ var require_read = __commonJS({
|
|
|
3517
3619
|
const rad = g.get("geofenceRadiusM");
|
|
3518
3620
|
return {
|
|
3519
3621
|
siteId: String(g.get("siteId")),
|
|
3520
|
-
latitude: lat == null ? null :
|
|
3521
|
-
longitude: lon == null ? null :
|
|
3522
|
-
geofenceRadiusM: rad == null ? null :
|
|
3622
|
+
latitude: lat == null ? null : num5(lat),
|
|
3623
|
+
longitude: lon == null ? null : num5(lon),
|
|
3624
|
+
geofenceRadiusM: rad == null ? null : num5(rad)
|
|
3523
3625
|
};
|
|
3524
3626
|
}
|
|
3525
3627
|
var DRAFT_RETURN = `d.draftId AS draftId, d.receivedAt AS receivedAt, d.source AS source,
|
|
@@ -3584,7 +3686,7 @@ var require_read = __commonJS({
|
|
|
3584
3686
|
notes: g.get("notes") ?? [],
|
|
3585
3687
|
// A count, not the references. A worker at a door needs to know whether
|
|
3586
3688
|
// photographs exist before deciding to load them on a bad signal.
|
|
3587
|
-
mediaCount:
|
|
3689
|
+
mediaCount: num5(g.get("mediaCount") ?? 0),
|
|
3588
3690
|
...projectAttendances2(g)
|
|
3589
3691
|
}));
|
|
3590
3692
|
}
|
|
@@ -3709,7 +3811,7 @@ var require_read = __commonJS({
|
|
|
3709
3811
|
workerId,
|
|
3710
3812
|
arrivedPressedAt: str2(g.get("ownArrivedPressedAt")),
|
|
3711
3813
|
arrivedObservedAt: str2(g.get("ownArrivedObservedAt")),
|
|
3712
|
-
arrivedObservedDistanceM: dist == null ? null :
|
|
3814
|
+
arrivedObservedDistanceM: dist == null ? null : num5(dist)
|
|
3713
3815
|
});
|
|
3714
3816
|
});
|
|
3715
3817
|
});
|
|
@@ -3749,9 +3851,9 @@ var require_read = __commonJS({
|
|
|
3749
3851
|
sum(CASE WHEN overdue THEN 1 ELSE 0 END) AS overdue`, { accountId, nowIso, terminalJob: terminalJobStatuses2 });
|
|
3750
3852
|
const g = r.records[0];
|
|
3751
3853
|
return {
|
|
3752
|
-
total:
|
|
3753
|
-
unallocated:
|
|
3754
|
-
overdue:
|
|
3854
|
+
total: num5(g?.get("total")),
|
|
3855
|
+
unallocated: num5(g?.get("unallocated")),
|
|
3856
|
+
overdue: num5(g?.get("overdue"))
|
|
3755
3857
|
};
|
|
3756
3858
|
});
|
|
3757
3859
|
}
|
|
@@ -3792,7 +3894,7 @@ var require_dist3 = __commonJS({
|
|
|
3792
3894
|
"../lib/dispatch-read/dist/index.js"(exports) {
|
|
3793
3895
|
"use strict";
|
|
3794
3896
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3795
|
-
exports.readWorkerDay = exports.readJobList = exports.readDay = exports.readCounts = exports.nonZuluStarts = exports.geofenceVerdict = exports.distanceM = exports.readVisitsInWindow = exports.readVisitsForJob = exports.readVisitRecordState = exports.readVisit = exports.readSiteLocation = exports.readSiteHistory = exports.readPreviousVisits = exports.readOpenVisitsForWorker = exports.readNodeId = exports.readJobs = exports.readJob = exports.readFieldVisit = exports.readDrafts = exports.readDraft = exports.readCommitments = exports.projectVisit = exports.projectJob = exports.projectFieldVisit = exports.buildJobListQuery = exports.zonedRangeBounds = exports.zonedDayStartUtc = exports.ymdInZone = exports.minutesIntoDay = exports.daysBetween = exports.currentWeekYmd = exports.addDaysYmd = exports.MAX_RANGE_DAYS = exports.resolveAccountZone = exports.withSession = exports.parseDispatchCallback = exports.dispatchCallbackData = exports.buildVisitKeyboard = exports.CALLBACK_DATA_MAX_BYTES = exports.workerForTelegramUser = exports.terminalVisitStatuses = exports.terminalJobStatuses = exports.loadDispatchConfig = exports.RECORDABLE_FIELDS = void 0;
|
|
3897
|
+
exports.readWorkerDay = exports.readJobList = exports.readDay = exports.readCounts = exports.nonZuluStarts = exports.geofenceVerdict = exports.distanceM = exports.readVisitsInWindow = exports.readVisitsForJob = exports.readVisitRecordState = exports.readVisit = exports.readSiteLocation = exports.readSiteHistory = exports.readPreviousVisits = exports.readOpenVisitsForWorker = exports.readNodeId = exports.readJobs = exports.readJob = exports.readFieldVisit = exports.readDrafts = exports.readDraft = exports.readCommitments = exports.projectVisit = exports.projectJob = exports.projectFieldVisit = exports.buildJobListQuery = exports.zonedRangeBounds = exports.zonedDayStartUtc = exports.ymdInZone = exports.minutesIntoDay = exports.daysBetween = exports.currentWeekYmd = exports.addDaysYmd = exports.MAX_RANGE_DAYS = exports.resolveAccountZone = exports.withSession = exports.selectCandidates = exports.overlaps = exports.parseDispatchCallback = exports.dispatchCallbackData = exports.buildVisitKeyboard = exports.CALLBACK_DATA_MAX_BYTES = exports.workerForTelegramUser = exports.terminalVisitStatuses = exports.terminalJobStatuses = exports.loadDispatchConfig = exports.RECORDABLE_FIELDS = void 0;
|
|
3796
3898
|
var config_js_1 = require_config();
|
|
3797
3899
|
Object.defineProperty(exports, "RECORDABLE_FIELDS", { enumerable: true, get: function() {
|
|
3798
3900
|
return config_js_1.RECORDABLE_FIELDS;
|
|
@@ -3822,6 +3924,13 @@ var require_dist3 = __commonJS({
|
|
|
3822
3924
|
Object.defineProperty(exports, "parseDispatchCallback", { enumerable: true, get: function() {
|
|
3823
3925
|
return keyboard_js_1.parseDispatchCallback;
|
|
3824
3926
|
} });
|
|
3927
|
+
var allocate_js_1 = require_allocate();
|
|
3928
|
+
Object.defineProperty(exports, "overlaps", { enumerable: true, get: function() {
|
|
3929
|
+
return allocate_js_1.overlaps;
|
|
3930
|
+
} });
|
|
3931
|
+
Object.defineProperty(exports, "selectCandidates", { enumerable: true, get: function() {
|
|
3932
|
+
return allocate_js_1.selectCandidates;
|
|
3933
|
+
} });
|
|
3825
3934
|
var session_js_1 = require_session();
|
|
3826
3935
|
Object.defineProperty(exports, "withSession", { enumerable: true, get: function() {
|
|
3827
3936
|
return session_js_1.withSession;
|
|
@@ -5418,9 +5527,186 @@ import { homedir as homedir5 } from "os";
|
|
|
5418
5527
|
init_route_cache();
|
|
5419
5528
|
init_account();
|
|
5420
5529
|
init_message_store();
|
|
5421
|
-
init_src2();
|
|
5422
5530
|
import { watch } from "fs";
|
|
5423
|
-
import { resolve } from "path";
|
|
5531
|
+
import { resolve as resolve2 } from "path";
|
|
5532
|
+
|
|
5533
|
+
// app/lib/telegram/message-store.ts
|
|
5534
|
+
import { existsSync, mkdirSync, readFileSync, appendFileSync, readdirSync, statSync } from "fs";
|
|
5535
|
+
import { homedir } from "os";
|
|
5536
|
+
import { dirname, join, resolve } from "path";
|
|
5537
|
+
function configDirName() {
|
|
5538
|
+
const platformRoot5 = process.env.MAXY_PLATFORM_ROOT ?? resolve(process.cwd(), "..");
|
|
5539
|
+
const brandPath = join(platformRoot5, "config", "brand.json");
|
|
5540
|
+
if (existsSync(brandPath)) {
|
|
5541
|
+
try {
|
|
5542
|
+
return JSON.parse(readFileSync(brandPath, "utf-8")).configDir ?? ".maxy";
|
|
5543
|
+
} catch {
|
|
5544
|
+
}
|
|
5545
|
+
}
|
|
5546
|
+
return ".maxy";
|
|
5547
|
+
}
|
|
5548
|
+
var STORE_ROOT2 = () => join(process.env.MAXY_DIR_OVERRIDE ?? resolve(homedir(), configDirName()), "data", "telegram-messages");
|
|
5549
|
+
function isStorableChannelKey(channelKey) {
|
|
5550
|
+
return /^-?\d+:-?\d+$/.test(channelKey);
|
|
5551
|
+
}
|
|
5552
|
+
function channelKeyToFileStem(channelKey) {
|
|
5553
|
+
if (!isStorableChannelKey(channelKey)) {
|
|
5554
|
+
throw new Error(`telegram-message-store: refusing unstorable channelKey ${JSON.stringify(channelKey)}`);
|
|
5555
|
+
}
|
|
5556
|
+
return channelKey.replace(":", "_");
|
|
5557
|
+
}
|
|
5558
|
+
function fileStemToChannelKey(stem) {
|
|
5559
|
+
const i = stem.indexOf("_");
|
|
5560
|
+
return i <= 0 ? stem : `${stem.slice(0, i)}:${stem.slice(i + 1)}`;
|
|
5561
|
+
}
|
|
5562
|
+
function conversationFile(accountId, channelKey) {
|
|
5563
|
+
return join(STORE_ROOT2(), accountId, `${channelKeyToFileStem(channelKey)}.jsonl`);
|
|
5564
|
+
}
|
|
5565
|
+
function conversationFilePath2(accountId, channelKey) {
|
|
5566
|
+
return conversationFile(accountId, channelKey);
|
|
5567
|
+
}
|
|
5568
|
+
function isStorableStem(stem) {
|
|
5569
|
+
return isStorableChannelKey(fileStemToChannelKey(stem));
|
|
5570
|
+
}
|
|
5571
|
+
function listStoredAccountIds2() {
|
|
5572
|
+
const root = STORE_ROOT2();
|
|
5573
|
+
if (!existsSync(root)) return [];
|
|
5574
|
+
return readdirSync(root).filter((name) => {
|
|
5575
|
+
try {
|
|
5576
|
+
return statSync(join(root, name)).isDirectory();
|
|
5577
|
+
} catch {
|
|
5578
|
+
return false;
|
|
5579
|
+
}
|
|
5580
|
+
});
|
|
5581
|
+
}
|
|
5582
|
+
function parseLines(raw) {
|
|
5583
|
+
const out = [];
|
|
5584
|
+
for (const line of raw.split("\n")) {
|
|
5585
|
+
if (!line.trim()) continue;
|
|
5586
|
+
try {
|
|
5587
|
+
out.push(JSON.parse(line));
|
|
5588
|
+
} catch {
|
|
5589
|
+
console.error(`[telegram-message-store] skip reason=corrupt-line`);
|
|
5590
|
+
}
|
|
5591
|
+
}
|
|
5592
|
+
return out;
|
|
5593
|
+
}
|
|
5594
|
+
var byDateSentAsc = (a, b) => a.dateSent.localeCompare(b.dateSent);
|
|
5595
|
+
function appendMessage2(accountId, record) {
|
|
5596
|
+
if (!isStorableChannelKey(record.channelKey)) {
|
|
5597
|
+
console.error(
|
|
5598
|
+
`[telegram-message-store] FAIL op=append reason=unstorable-channel-key messageId=${record.messageId}`
|
|
5599
|
+
);
|
|
5600
|
+
return { created: false };
|
|
5601
|
+
}
|
|
5602
|
+
const file = conversationFile(accountId, record.channelKey);
|
|
5603
|
+
try {
|
|
5604
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
5605
|
+
} catch (err) {
|
|
5606
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
5607
|
+
console.error(
|
|
5608
|
+
`[telegram-message-store] FAIL op=append reason=${reason.slice(0, 100)} messageId=${record.messageId}`
|
|
5609
|
+
);
|
|
5610
|
+
return { created: false };
|
|
5611
|
+
}
|
|
5612
|
+
try {
|
|
5613
|
+
if (existsSync(file)) {
|
|
5614
|
+
const existing = parseLines(readFileSync(file, "utf8"));
|
|
5615
|
+
if (existing.some((r) => r.messageId === record.messageId)) {
|
|
5616
|
+
console.error(
|
|
5617
|
+
`[telegram-message-store] op=append channelKey=${record.channelKey} origin=${record.origin} created=false`
|
|
5618
|
+
);
|
|
5619
|
+
return { created: false };
|
|
5620
|
+
}
|
|
5621
|
+
}
|
|
5622
|
+
appendFileSync(file, `${JSON.stringify(record)}
|
|
5623
|
+
`);
|
|
5624
|
+
console.error(
|
|
5625
|
+
`[telegram-message-store] op=append channelKey=${record.channelKey} origin=${record.origin} created=true`
|
|
5626
|
+
);
|
|
5627
|
+
return { created: true };
|
|
5628
|
+
} catch (err) {
|
|
5629
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
5630
|
+
console.error(
|
|
5631
|
+
`[telegram-message-store] FAIL op=append reason=${reason.slice(0, 100)} messageId=${record.messageId}`
|
|
5632
|
+
);
|
|
5633
|
+
return { created: false };
|
|
5634
|
+
}
|
|
5635
|
+
}
|
|
5636
|
+
function readConversation2(accountId, channelKey) {
|
|
5637
|
+
if (!isStorableChannelKey(channelKey)) return [];
|
|
5638
|
+
const file = conversationFile(accountId, channelKey);
|
|
5639
|
+
try {
|
|
5640
|
+
if (!existsSync(file)) return [];
|
|
5641
|
+
return parseLines(readFileSync(file, "utf8")).sort(byDateSentAsc);
|
|
5642
|
+
} catch (err) {
|
|
5643
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
5644
|
+
console.error(
|
|
5645
|
+
`[telegram-message-store] FAIL op=read reason=${reason.slice(0, 100)} channelKey=${channelKey}`
|
|
5646
|
+
);
|
|
5647
|
+
return [];
|
|
5648
|
+
}
|
|
5649
|
+
}
|
|
5650
|
+
var summaryCache = /* @__PURE__ */ new Map();
|
|
5651
|
+
function summariseRecords(records2) {
|
|
5652
|
+
if (records2.length === 0) return null;
|
|
5653
|
+
const sorted = records2.slice().sort(byDateSentAsc);
|
|
5654
|
+
return { first: sorted[0], last: sorted[sorted.length - 1] };
|
|
5655
|
+
}
|
|
5656
|
+
function readConversationSummaries2(accountId) {
|
|
5657
|
+
const dir = join(STORE_ROOT2(), accountId);
|
|
5658
|
+
const out = /* @__PURE__ */ new Map();
|
|
5659
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5660
|
+
try {
|
|
5661
|
+
if (!existsSync(dir)) return out;
|
|
5662
|
+
for (const entry2 of readdirSync(dir)) {
|
|
5663
|
+
if (!entry2.endsWith(".jsonl")) continue;
|
|
5664
|
+
const stem = entry2.slice(0, -".jsonl".length);
|
|
5665
|
+
if (!isStorableStem(stem)) continue;
|
|
5666
|
+
const channelKey = fileStemToChannelKey(stem);
|
|
5667
|
+
const file = join(dir, entry2);
|
|
5668
|
+
seen.add(file);
|
|
5669
|
+
let size;
|
|
5670
|
+
let mtimeMs;
|
|
5671
|
+
try {
|
|
5672
|
+
const st = statSync(file);
|
|
5673
|
+
size = st.size;
|
|
5674
|
+
mtimeMs = st.mtimeMs;
|
|
5675
|
+
} catch {
|
|
5676
|
+
continue;
|
|
5677
|
+
}
|
|
5678
|
+
const cached3 = summaryCache.get(file);
|
|
5679
|
+
if (cached3 && cached3.size === size && cached3.mtimeMs === mtimeMs) {
|
|
5680
|
+
out.set(channelKey, cached3.summary);
|
|
5681
|
+
continue;
|
|
5682
|
+
}
|
|
5683
|
+
let summary;
|
|
5684
|
+
try {
|
|
5685
|
+
summary = summariseRecords(parseLines(readFileSync(file, "utf8")));
|
|
5686
|
+
} catch (err) {
|
|
5687
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
5688
|
+
console.error(
|
|
5689
|
+
`[telegram-message-store] FAIL op=summarise reason=${reason.slice(0, 100)} channelKey=${channelKey}`
|
|
5690
|
+
);
|
|
5691
|
+
continue;
|
|
5692
|
+
}
|
|
5693
|
+
if (!summary) continue;
|
|
5694
|
+
summaryCache.set(file, { size, mtimeMs, summary });
|
|
5695
|
+
out.set(channelKey, summary);
|
|
5696
|
+
}
|
|
5697
|
+
} catch (err) {
|
|
5698
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
5699
|
+
console.error(`[telegram-message-store] FAIL op=summarise-dir reason=${reason.slice(0, 100)}`);
|
|
5700
|
+
return out;
|
|
5701
|
+
}
|
|
5702
|
+
for (const key2 of [...summaryCache.keys()]) {
|
|
5703
|
+
if (key2.startsWith(dir) && !seen.has(key2)) summaryCache.delete(key2);
|
|
5704
|
+
}
|
|
5705
|
+
return out;
|
|
5706
|
+
}
|
|
5707
|
+
|
|
5708
|
+
// server/lib/route-cache-watchers.ts
|
|
5709
|
+
init_src2();
|
|
5424
5710
|
function watchDirInto(dir, resource, accountId) {
|
|
5425
5711
|
try {
|
|
5426
5712
|
const w = watch(dir, { recursive: true }, () => {
|
|
@@ -5441,9 +5727,9 @@ function watchDirInto(dir, resource, accountId) {
|
|
|
5441
5727
|
var perAccountAttached = /* @__PURE__ */ new Set();
|
|
5442
5728
|
function ensureAccountWatchers(accountId, accountDir) {
|
|
5443
5729
|
for (const [dir, resource] of [
|
|
5444
|
-
[
|
|
5445
|
-
[
|
|
5446
|
-
[
|
|
5730
|
+
[resolve2(accountDir, "plugins"), "skills"],
|
|
5731
|
+
[resolve2(accountDir, "agents", "admin"), "sidebar-artefacts"],
|
|
5732
|
+
[resolve2(accountDir, "specialists", "agents"), "sidebar-artefacts"]
|
|
5447
5733
|
]) {
|
|
5448
5734
|
if (perAccountAttached.has(dir)) continue;
|
|
5449
5735
|
const { attached: attached2 } = watchDirInto(dir, resource, accountId);
|
|
@@ -5456,11 +5742,11 @@ function attachRouteCacheWatchers() {
|
|
|
5456
5742
|
attached = true;
|
|
5457
5743
|
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
|
5458
5744
|
if (configDir) {
|
|
5459
|
-
watchDirInto(
|
|
5745
|
+
watchDirInto(resolve2(configDir, "projects"), "sidebar-sessions", null);
|
|
5460
5746
|
} else {
|
|
5461
5747
|
console.error("[admin-events] op=watch-failed resource=sidebar-sessions reason=CLAUDE_CONFIG_DIR-unset");
|
|
5462
5748
|
}
|
|
5463
|
-
watchDirInto(
|
|
5749
|
+
watchDirInto(resolve2(PLATFORM_ROOT, "templates", "specialists", "agents"), "sidebar-artefacts", null);
|
|
5464
5750
|
let houseId = null;
|
|
5465
5751
|
try {
|
|
5466
5752
|
houseId = resolveHouseOrSoleAccountId(ACCOUNTS_DIR);
|
|
@@ -5469,6 +5755,7 @@ function attachRouteCacheWatchers() {
|
|
|
5469
5755
|
}
|
|
5470
5756
|
if (houseId) {
|
|
5471
5757
|
watchDirInto(STORE_ROOT(), "conversations-all", houseId);
|
|
5758
|
+
watchDirInto(STORE_ROOT2(), "conversations-all", houseId);
|
|
5472
5759
|
} else {
|
|
5473
5760
|
console.error("[admin-events] op=watch-failed resource=conversations-all reason=house-account-unresolved");
|
|
5474
5761
|
}
|
|
@@ -5665,8 +5952,8 @@ var _baseMimes = {
|
|
|
5665
5952
|
var baseMimes = _baseMimes;
|
|
5666
5953
|
|
|
5667
5954
|
// node_modules/@hono/node-server/dist/serve-static.mjs
|
|
5668
|
-
import { createReadStream, statSync, existsSync } from "fs";
|
|
5669
|
-
import { join } from "path";
|
|
5955
|
+
import { createReadStream, statSync as statSync2, existsSync as existsSync2 } from "fs";
|
|
5956
|
+
import { join as join2 } from "path";
|
|
5670
5957
|
import { versions } from "process";
|
|
5671
5958
|
import { Readable } from "stream";
|
|
5672
5959
|
var COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i;
|
|
@@ -5706,7 +5993,7 @@ var createStreamBody = (stream2) => {
|
|
|
5706
5993
|
var getStats = (path) => {
|
|
5707
5994
|
let stats;
|
|
5708
5995
|
try {
|
|
5709
|
-
stats =
|
|
5996
|
+
stats = statSync2(path);
|
|
5710
5997
|
} catch {
|
|
5711
5998
|
}
|
|
5712
5999
|
return stats;
|
|
@@ -5728,7 +6015,7 @@ var tryDecodeURI = (str2) => tryDecode(str2, decodeURI);
|
|
|
5728
6015
|
var serveStatic = (options = { root: "" }) => {
|
|
5729
6016
|
const root = options.root || "";
|
|
5730
6017
|
const optionPath = options.path;
|
|
5731
|
-
if (root !== "" && !
|
|
6018
|
+
if (root !== "" && !existsSync2(root)) {
|
|
5732
6019
|
console.error(`serveStatic: root path '${root}' is not found, are you sure it's correct?`);
|
|
5733
6020
|
}
|
|
5734
6021
|
return async (c, next) => {
|
|
@@ -5749,14 +6036,14 @@ var serveStatic = (options = { root: "" }) => {
|
|
|
5749
6036
|
return next();
|
|
5750
6037
|
}
|
|
5751
6038
|
}
|
|
5752
|
-
let path =
|
|
6039
|
+
let path = join2(
|
|
5753
6040
|
root,
|
|
5754
6041
|
!optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename
|
|
5755
6042
|
);
|
|
5756
6043
|
let stats = getStats(path);
|
|
5757
6044
|
if (stats && stats.isDirectory()) {
|
|
5758
6045
|
const indexFile2 = options.index ?? "index.html";
|
|
5759
|
-
path =
|
|
6046
|
+
path = join2(path, indexFile2);
|
|
5760
6047
|
stats = getStats(path);
|
|
5761
6048
|
}
|
|
5762
6049
|
if (!stats) {
|
|
@@ -5814,13 +6101,13 @@ var serveStatic = (options = { root: "" }) => {
|
|
|
5814
6101
|
};
|
|
5815
6102
|
|
|
5816
6103
|
// server/lib/static-cache.ts
|
|
5817
|
-
import { statSync as
|
|
5818
|
-
import { join as
|
|
6104
|
+
import { statSync as statSync3 } from "fs";
|
|
6105
|
+
import { join as join3 } from "path";
|
|
5819
6106
|
var IMMUTABLE = "public, max-age=31536000, immutable";
|
|
5820
6107
|
var REVALIDATE = "public, no-cache";
|
|
5821
6108
|
function statOrNull(path) {
|
|
5822
6109
|
try {
|
|
5823
|
-
return
|
|
6110
|
+
return statSync3(path);
|
|
5824
6111
|
} catch {
|
|
5825
6112
|
return null;
|
|
5826
6113
|
}
|
|
@@ -5841,10 +6128,10 @@ function tryDecodeURI2(str2) {
|
|
|
5841
6128
|
function resolveStaticFile(root, reqPath) {
|
|
5842
6129
|
const filename = tryDecodeURI2(reqPath);
|
|
5843
6130
|
if (/(?:^|[/\\])\.\.(?:$|[/\\])/.test(filename)) return null;
|
|
5844
|
-
let path =
|
|
6131
|
+
let path = join3(root, filename);
|
|
5845
6132
|
let stats = statOrNull(path);
|
|
5846
6133
|
if (stats?.isDirectory()) {
|
|
5847
|
-
path =
|
|
6134
|
+
path = join3(path, "index.html");
|
|
5848
6135
|
stats = statOrNull(path);
|
|
5849
6136
|
}
|
|
5850
6137
|
if (!stats?.isFile()) return null;
|
|
@@ -5919,8 +6206,8 @@ ${clientErrorReporterBody}
|
|
|
5919
6206
|
</script>`;
|
|
5920
6207
|
|
|
5921
6208
|
// app/lib/whatsapp/ingest-audit.ts
|
|
5922
|
-
import { readdirSync, readFileSync, statSync as
|
|
5923
|
-
import { join as
|
|
6209
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync4 } from "fs";
|
|
6210
|
+
import { join as join4 } from "path";
|
|
5924
6211
|
init_message_store();
|
|
5925
6212
|
var TAG2 = "[wa-ingest-audit]";
|
|
5926
6213
|
var NOTIFY_TAG = "[wa-notify]";
|
|
@@ -5941,16 +6228,16 @@ function readAllConversationsCached(accountId) {
|
|
|
5941
6228
|
const out = /* @__PURE__ */ new Map();
|
|
5942
6229
|
let dir;
|
|
5943
6230
|
try {
|
|
5944
|
-
dir =
|
|
6231
|
+
dir = readdirSync2(join4(STORE_ROOT(), accountId));
|
|
5945
6232
|
} catch {
|
|
5946
6233
|
return out;
|
|
5947
6234
|
}
|
|
5948
6235
|
for (const file of dir) {
|
|
5949
6236
|
if (!file.endsWith(".jsonl")) continue;
|
|
5950
|
-
const path =
|
|
6237
|
+
const path = join4(STORE_ROOT(), accountId, file);
|
|
5951
6238
|
let stat11;
|
|
5952
6239
|
try {
|
|
5953
|
-
stat11 =
|
|
6240
|
+
stat11 = statSync4(path);
|
|
5954
6241
|
} catch {
|
|
5955
6242
|
continue;
|
|
5956
6243
|
}
|
|
@@ -5961,7 +6248,7 @@ function readAllConversationsCached(accountId) {
|
|
|
5961
6248
|
}
|
|
5962
6249
|
const parsed = [];
|
|
5963
6250
|
try {
|
|
5964
|
-
for (const line of
|
|
6251
|
+
for (const line of readFileSync2(path, "utf8").split("\n")) {
|
|
5965
6252
|
if (!line.trim()) continue;
|
|
5966
6253
|
try {
|
|
5967
6254
|
const row = JSON.parse(line);
|
|
@@ -6077,17 +6364,17 @@ function runNotifyReconcile(opts) {
|
|
|
6077
6364
|
init_parse_transcript();
|
|
6078
6365
|
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
6079
6366
|
import { tmpdir } from "os";
|
|
6080
|
-
import { join as
|
|
6367
|
+
import { join as join5 } from "path";
|
|
6081
6368
|
|
|
6082
6369
|
// server/lib/tail-window.ts
|
|
6083
|
-
import { openSync, readSync, closeSync, statSync as
|
|
6370
|
+
import { openSync, readSync, closeSync, statSync as statSync5 } from "fs";
|
|
6084
6371
|
var TAIL_WINDOW_BYTES = 1024 * 1024;
|
|
6085
6372
|
var ALIGN_SCAN_MAX_BYTES = 4 * 1024 * 1024;
|
|
6086
6373
|
function tailWindowStart(fileEnd, budget = TAIL_WINDOW_BYTES) {
|
|
6087
6374
|
return Math.max(0, fileEnd - budget);
|
|
6088
6375
|
}
|
|
6089
6376
|
function readRange(path, from, to) {
|
|
6090
|
-
const size =
|
|
6377
|
+
const size = statSync5(path).size;
|
|
6091
6378
|
const start = Math.max(0, from);
|
|
6092
6379
|
const end = Math.min(to, size);
|
|
6093
6380
|
if (end <= start) return Buffer.alloc(0);
|
|
@@ -6209,8 +6496,62 @@ function censusSnapshot() {
|
|
|
6209
6496
|
return { windowed: windowedOpens.size, withBackfill };
|
|
6210
6497
|
}
|
|
6211
6498
|
|
|
6499
|
+
// server/lib/frame-strip-census.ts
|
|
6500
|
+
var MAX_ACCOUNTS = 50;
|
|
6501
|
+
var MAX_UNSTRIPPED_SESSIONS = 3;
|
|
6502
|
+
var byAccount = /* @__PURE__ */ new Map();
|
|
6503
|
+
function rowFor(accountId) {
|
|
6504
|
+
const existing = byAccount.get(accountId);
|
|
6505
|
+
if (existing) return existing;
|
|
6506
|
+
if (byAccount.size >= MAX_ACCOUNTS) {
|
|
6507
|
+
const oldest = byAccount.keys().next();
|
|
6508
|
+
if (!oldest.done) byAccount.delete(oldest.value);
|
|
6509
|
+
}
|
|
6510
|
+
const fresh = {
|
|
6511
|
+
accountId,
|
|
6512
|
+
rows: 0,
|
|
6513
|
+
wrapped: 0,
|
|
6514
|
+
nested: 0,
|
|
6515
|
+
stripped: 0,
|
|
6516
|
+
unstripped: 0,
|
|
6517
|
+
sources: { whatsapp: 0, webchat: 0, telegram: 0, none: 0 },
|
|
6518
|
+
unstrippedSessions: []
|
|
6519
|
+
};
|
|
6520
|
+
byAccount.set(accountId, fresh);
|
|
6521
|
+
return fresh;
|
|
6522
|
+
}
|
|
6523
|
+
function foldFrameCensus(accountId, sessionId, acc) {
|
|
6524
|
+
if (acc.rows === 0) return;
|
|
6525
|
+
const row = rowFor(accountId);
|
|
6526
|
+
row.rows += acc.rows;
|
|
6527
|
+
row.wrapped += acc.wrapped;
|
|
6528
|
+
row.nested += acc.nested;
|
|
6529
|
+
row.stripped += acc.stripped;
|
|
6530
|
+
row.unstripped += acc.unstripped;
|
|
6531
|
+
row.sources.whatsapp += acc.sources.whatsapp;
|
|
6532
|
+
row.sources.webchat += acc.sources.webchat;
|
|
6533
|
+
row.sources.telegram += acc.sources.telegram;
|
|
6534
|
+
row.sources.none += acc.sources.none;
|
|
6535
|
+
if (acc.unstripped > 0 && row.unstrippedSessions.length < MAX_UNSTRIPPED_SESSIONS && !row.unstrippedSessions.includes(sessionId)) {
|
|
6536
|
+
row.unstrippedSessions.push(sessionId);
|
|
6537
|
+
}
|
|
6538
|
+
acc.rows = 0;
|
|
6539
|
+
acc.wrapped = 0;
|
|
6540
|
+
acc.nested = 0;
|
|
6541
|
+
acc.stripped = 0;
|
|
6542
|
+
acc.unstripped = 0;
|
|
6543
|
+
acc.sources.whatsapp = 0;
|
|
6544
|
+
acc.sources.webchat = 0;
|
|
6545
|
+
acc.sources.telegram = 0;
|
|
6546
|
+
acc.sources.none = 0;
|
|
6547
|
+
}
|
|
6548
|
+
function frameCensusSnapshot() {
|
|
6549
|
+
return [...byAccount.values()];
|
|
6550
|
+
}
|
|
6551
|
+
|
|
6212
6552
|
// server/lib/reader-window-audit.ts
|
|
6213
6553
|
var TAG3 = "[reader-audit]";
|
|
6554
|
+
var FRAME_TAG = "[chat-reader]";
|
|
6214
6555
|
var FIXTURE_ROWS = 60;
|
|
6215
6556
|
var FIXTURE_BUDGET = 2e3;
|
|
6216
6557
|
function fixtureBody() {
|
|
@@ -6244,8 +6585,25 @@ var key = (t) => JSON.stringify(t);
|
|
|
6244
6585
|
function runReaderWindowAudit() {
|
|
6245
6586
|
const census = censusSnapshot();
|
|
6246
6587
|
console.log(`${TAG3} op=backfill-reach windowed=${census.windowed} withBackfill=${census.withBackfill}`);
|
|
6247
|
-
const
|
|
6248
|
-
|
|
6588
|
+
const frames = frameCensusSnapshot();
|
|
6589
|
+
let frameRows = 0;
|
|
6590
|
+
let frameUnstripped = 0;
|
|
6591
|
+
if (frames.length === 0) {
|
|
6592
|
+
console.log(`${FRAME_TAG} op=frame-census accounts=0`);
|
|
6593
|
+
}
|
|
6594
|
+
for (const f of frames) {
|
|
6595
|
+
frameRows += f.rows;
|
|
6596
|
+
frameUnstripped += f.unstripped;
|
|
6597
|
+
console.log(
|
|
6598
|
+
`${FRAME_TAG} op=frame-census accountId=${f.accountId} rows=${f.rows} wrapped=${f.wrapped} nested=${f.nested} stripped=${f.stripped} sources=whatsapp:${f.sources.whatsapp},webchat:${f.sources.webchat},telegram:${f.sources.telegram},none:${f.sources.none}`
|
|
6599
|
+
);
|
|
6600
|
+
console.log(
|
|
6601
|
+
`${FRAME_TAG} op=frame-unstripped accountId=${f.accountId} rows=${f.unstripped} sessions=${f.unstrippedSessions.join(",")}`
|
|
6602
|
+
);
|
|
6603
|
+
}
|
|
6604
|
+
const frameSummary = `frames=${frameRows} unstripped=${frameUnstripped}`;
|
|
6605
|
+
const dir = mkdtempSync(join5(tmpdir(), "reader-audit-"));
|
|
6606
|
+
const path = join5(dir, "fixture.jsonl");
|
|
6249
6607
|
try {
|
|
6250
6608
|
const body = fixtureBody();
|
|
6251
6609
|
writeFileSync(path, body);
|
|
@@ -6290,11 +6648,11 @@ function runReaderWindowAudit() {
|
|
|
6290
6648
|
`${TAG3} op=window-kinds fixture=synthetic windowTurns=${windowTurns.length} mismatched=${mismatched}`
|
|
6291
6649
|
);
|
|
6292
6650
|
const ok2 = duplicates === 0 && gaps === 0 && ordered && walkTerminated && mismatched === 0;
|
|
6293
|
-
return `continuity=${ok2 ? "ok" : "FAILED"} full=${full.length} assembled=${assembled.length} walk=${walkTerminated ? "ok" : "STUCK"} reach=${census.withBackfill}/${census.windowed}`;
|
|
6651
|
+
return `continuity=${ok2 ? "ok" : "FAILED"} full=${full.length} assembled=${assembled.length} walk=${walkTerminated ? "ok" : "STUCK"} reach=${census.withBackfill}/${census.windowed} ${frameSummary}`;
|
|
6294
6652
|
} catch (err) {
|
|
6295
6653
|
const reason = err instanceof Error ? err.message : String(err);
|
|
6296
6654
|
console.error(`${TAG3} op=audit-failed reason=${reason.slice(0, 120)}`);
|
|
6297
|
-
return `continuity=ERROR ${reason.slice(0, 60)}`;
|
|
6655
|
+
return `continuity=ERROR ${reason.slice(0, 60)} ${frameSummary}`;
|
|
6298
6656
|
} finally {
|
|
6299
6657
|
try {
|
|
6300
6658
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -6356,12 +6714,12 @@ init_src();
|
|
|
6356
6714
|
init_pwa();
|
|
6357
6715
|
|
|
6358
6716
|
// server/webchat-descriptor.ts
|
|
6359
|
-
import { resolve as
|
|
6717
|
+
import { resolve as resolve3, join as join6 } from "path";
|
|
6360
6718
|
function webchatGatewayUrl() {
|
|
6361
6719
|
return `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`;
|
|
6362
6720
|
}
|
|
6363
6721
|
function webchatServerPath() {
|
|
6364
|
-
return process.env.MAXY_WEBCHAT_CHANNEL_SERVER_PATH ??
|
|
6722
|
+
return process.env.MAXY_WEBCHAT_CHANNEL_SERVER_PATH ?? resolve3(process.env.MAXY_PLATFORM_ROOT ?? join6(__dirname, ".."), "services/webchat-channel/dist/server.js");
|
|
6365
6723
|
}
|
|
6366
6724
|
function webchatChannelDescriptor(key2) {
|
|
6367
6725
|
return { key: key2, gatewayUrl: webchatGatewayUrl(), serverPath: webchatServerPath() };
|
|
@@ -6379,7 +6737,7 @@ init_paths();
|
|
|
6379
6737
|
// app/lib/claude-auth.ts
|
|
6380
6738
|
init_paths();
|
|
6381
6739
|
init_loop_registry();
|
|
6382
|
-
import { readFileSync as
|
|
6740
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "fs";
|
|
6383
6741
|
import { sep } from "path";
|
|
6384
6742
|
import { lock } from "proper-lockfile";
|
|
6385
6743
|
if (!CLAUDE_CREDENTIALS_FILE.startsWith(MAXY_DIR + sep)) {
|
|
@@ -6396,7 +6754,7 @@ var REFRESH_NOT_FIRING_THRESHOLD = 2;
|
|
|
6396
6754
|
function readCredentials() {
|
|
6397
6755
|
let raw;
|
|
6398
6756
|
try {
|
|
6399
|
-
raw =
|
|
6757
|
+
raw = readFileSync3(CLAUDE_CREDENTIALS_FILE, "utf-8");
|
|
6400
6758
|
} catch {
|
|
6401
6759
|
return null;
|
|
6402
6760
|
}
|
|
@@ -6424,7 +6782,7 @@ function writeOAuthCredentials(tokens) {
|
|
|
6424
6782
|
try {
|
|
6425
6783
|
let fileData = {};
|
|
6426
6784
|
try {
|
|
6427
|
-
const rawFile =
|
|
6785
|
+
const rawFile = readFileSync3(CLAUDE_CREDENTIALS_FILE, "utf-8");
|
|
6428
6786
|
fileData = JSON.parse(rawFile);
|
|
6429
6787
|
} catch {
|
|
6430
6788
|
}
|
|
@@ -6652,7 +7010,7 @@ init_helpers();
|
|
|
6652
7010
|
// server/routes/health.ts
|
|
6653
7011
|
init_dist();
|
|
6654
7012
|
init_paths();
|
|
6655
|
-
import { existsSync as
|
|
7013
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
|
|
6656
7014
|
import { createConnection as createConnection2 } from "net";
|
|
6657
7015
|
|
|
6658
7016
|
// app/lib/network.ts
|
|
@@ -6677,11 +7035,11 @@ function getLanIp() {
|
|
|
6677
7035
|
init_paths();
|
|
6678
7036
|
import { spawnSync, execFileSync } from "child_process";
|
|
6679
7037
|
import { createConnection } from "net";
|
|
6680
|
-
import { mkdirSync, readFileSync as
|
|
6681
|
-
import { resolve as
|
|
7038
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
7039
|
+
import { resolve as resolve4 } from "path";
|
|
6682
7040
|
init_remote_auth();
|
|
6683
|
-
var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ??
|
|
6684
|
-
var VNC_SCRIPT =
|
|
7041
|
+
var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ?? resolve4(process.cwd(), "..");
|
|
7042
|
+
var VNC_SCRIPT = resolve4(PLATFORM_ROOT2, "scripts/vnc.sh");
|
|
6685
7043
|
var displayMode = process.env.DISPLAY_MODE ?? "virtual";
|
|
6686
7044
|
if (displayMode === "native") {
|
|
6687
7045
|
console.log(`[vnc] DISPLAY_MODE=native \u2014 local requests use desktop display, remote requests use VNC`);
|
|
@@ -6736,7 +7094,7 @@ function discoverNativeDisplay() {
|
|
|
6736
7094
|
const leaderPid = leaderResult.stdout?.trim();
|
|
6737
7095
|
if (leaderPid) {
|
|
6738
7096
|
try {
|
|
6739
|
-
const environ =
|
|
7097
|
+
const environ = readFileSync4(`/proc/${leaderPid}/environ`, "utf8");
|
|
6740
7098
|
const match = environ.split("\0").find((e) => e.startsWith("WAYLAND_DISPLAY="));
|
|
6741
7099
|
if (match) waylandDisplay = match.split("=")[1];
|
|
6742
7100
|
} catch {
|
|
@@ -6791,10 +7149,10 @@ async function waitForPort(port2, timeoutMs = 12e3) {
|
|
|
6791
7149
|
return false;
|
|
6792
7150
|
}
|
|
6793
7151
|
function ensureLogDir() {
|
|
6794
|
-
|
|
7152
|
+
mkdirSync2(LOG_DIR, { recursive: true });
|
|
6795
7153
|
}
|
|
6796
7154
|
function logPath(name) {
|
|
6797
|
-
return
|
|
7155
|
+
return resolve4(LOG_DIR, `${name}.log`);
|
|
6798
7156
|
}
|
|
6799
7157
|
async function ensureVnc() {
|
|
6800
7158
|
if (process.platform !== "linux") {
|
|
@@ -6879,19 +7237,19 @@ async function ensureCdp(transport = "vnc") {
|
|
|
6879
7237
|
function killChromium() {
|
|
6880
7238
|
spawnSync("pkill", ["-f", "chromium"], { stdio: "pipe" });
|
|
6881
7239
|
}
|
|
6882
|
-
var AUTH_TARGET_ID_FILE =
|
|
7240
|
+
var AUTH_TARGET_ID_FILE = resolve4(LOG_DIR, "claude-auth-target.id");
|
|
6883
7241
|
function readAuthTargetId() {
|
|
6884
7242
|
try {
|
|
6885
|
-
const id =
|
|
7243
|
+
const id = readFileSync4(AUTH_TARGET_ID_FILE, "utf8").trim();
|
|
6886
7244
|
return id.length > 0 ? id : null;
|
|
6887
7245
|
} catch {
|
|
6888
7246
|
return null;
|
|
6889
7247
|
}
|
|
6890
7248
|
}
|
|
6891
7249
|
function writeChromiumWrapper() {
|
|
6892
|
-
|
|
7250
|
+
mkdirSync2(BIN_DIR, { recursive: true });
|
|
6893
7251
|
rmSync2(AUTH_TARGET_ID_FILE, { force: true });
|
|
6894
|
-
const wrapperPath =
|
|
7252
|
+
const wrapperPath = resolve4(BIN_DIR, "chromium");
|
|
6895
7253
|
writeFileSync3(wrapperPath, `#!/bin/bash
|
|
6896
7254
|
# Generated by writeChromiumWrapper (app/lib/vnc.ts) \u2014 Task 1562.
|
|
6897
7255
|
# Hands its URL argument to the brand's running Chromium over CDP.
|
|
@@ -6993,8 +7351,8 @@ app.get("/", async (c) => {
|
|
|
6993
7351
|
const browserTransport = resolveBrowserTransport(c.req.raw, c.env?.incoming?.socket?.remoteAddress);
|
|
6994
7352
|
let pinConfigured = false;
|
|
6995
7353
|
try {
|
|
6996
|
-
if (
|
|
6997
|
-
const raw =
|
|
7354
|
+
if (existsSync3(USERS_FILE)) {
|
|
7355
|
+
const raw = readFileSync5(USERS_FILE, "utf-8").trim();
|
|
6998
7356
|
if (raw) {
|
|
6999
7357
|
const users = JSON.parse(raw);
|
|
7000
7358
|
pinConfigured = Array.isArray(users) && users.length > 0;
|
|
@@ -7487,183 +7845,6 @@ init_dist();
|
|
|
7487
7845
|
init_channel_key();
|
|
7488
7846
|
import { statSync as statSync7 } from "fs";
|
|
7489
7847
|
import { join as join12 } from "path";
|
|
7490
|
-
|
|
7491
|
-
// app/lib/telegram/message-store.ts
|
|
7492
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, appendFileSync, readdirSync as readdirSync2, statSync as statSync5 } from "fs";
|
|
7493
|
-
import { homedir } from "os";
|
|
7494
|
-
import { dirname, join as join7, resolve as resolve4 } from "path";
|
|
7495
|
-
function configDirName() {
|
|
7496
|
-
const platformRoot5 = process.env.MAXY_PLATFORM_ROOT ?? resolve4(process.cwd(), "..");
|
|
7497
|
-
const brandPath = join7(platformRoot5, "config", "brand.json");
|
|
7498
|
-
if (existsSync3(brandPath)) {
|
|
7499
|
-
try {
|
|
7500
|
-
return JSON.parse(readFileSync5(brandPath, "utf-8")).configDir ?? ".maxy";
|
|
7501
|
-
} catch {
|
|
7502
|
-
}
|
|
7503
|
-
}
|
|
7504
|
-
return ".maxy";
|
|
7505
|
-
}
|
|
7506
|
-
var STORE_ROOT2 = () => join7(process.env.MAXY_DIR_OVERRIDE ?? resolve4(homedir(), configDirName()), "data", "telegram-messages");
|
|
7507
|
-
function isStorableChannelKey(channelKey) {
|
|
7508
|
-
return /^-?\d+:-?\d+$/.test(channelKey);
|
|
7509
|
-
}
|
|
7510
|
-
function channelKeyToFileStem(channelKey) {
|
|
7511
|
-
if (!isStorableChannelKey(channelKey)) {
|
|
7512
|
-
throw new Error(`telegram-message-store: refusing unstorable channelKey ${JSON.stringify(channelKey)}`);
|
|
7513
|
-
}
|
|
7514
|
-
return channelKey.replace(":", "_");
|
|
7515
|
-
}
|
|
7516
|
-
function fileStemToChannelKey(stem) {
|
|
7517
|
-
const i = stem.indexOf("_");
|
|
7518
|
-
return i <= 0 ? stem : `${stem.slice(0, i)}:${stem.slice(i + 1)}`;
|
|
7519
|
-
}
|
|
7520
|
-
function conversationFile(accountId, channelKey) {
|
|
7521
|
-
return join7(STORE_ROOT2(), accountId, `${channelKeyToFileStem(channelKey)}.jsonl`);
|
|
7522
|
-
}
|
|
7523
|
-
function conversationFilePath2(accountId, channelKey) {
|
|
7524
|
-
return conversationFile(accountId, channelKey);
|
|
7525
|
-
}
|
|
7526
|
-
function isStorableStem(stem) {
|
|
7527
|
-
return isStorableChannelKey(fileStemToChannelKey(stem));
|
|
7528
|
-
}
|
|
7529
|
-
function listStoredAccountIds2() {
|
|
7530
|
-
const root = STORE_ROOT2();
|
|
7531
|
-
if (!existsSync3(root)) return [];
|
|
7532
|
-
return readdirSync2(root).filter((name) => {
|
|
7533
|
-
try {
|
|
7534
|
-
return statSync5(join7(root, name)).isDirectory();
|
|
7535
|
-
} catch {
|
|
7536
|
-
return false;
|
|
7537
|
-
}
|
|
7538
|
-
});
|
|
7539
|
-
}
|
|
7540
|
-
function parseLines(raw) {
|
|
7541
|
-
const out = [];
|
|
7542
|
-
for (const line of raw.split("\n")) {
|
|
7543
|
-
if (!line.trim()) continue;
|
|
7544
|
-
try {
|
|
7545
|
-
out.push(JSON.parse(line));
|
|
7546
|
-
} catch {
|
|
7547
|
-
console.error(`[telegram-message-store] skip reason=corrupt-line`);
|
|
7548
|
-
}
|
|
7549
|
-
}
|
|
7550
|
-
return out;
|
|
7551
|
-
}
|
|
7552
|
-
var byDateSentAsc = (a, b) => a.dateSent.localeCompare(b.dateSent);
|
|
7553
|
-
function appendMessage2(accountId, record) {
|
|
7554
|
-
if (!isStorableChannelKey(record.channelKey)) {
|
|
7555
|
-
console.error(
|
|
7556
|
-
`[telegram-message-store] FAIL op=append reason=unstorable-channel-key messageId=${record.messageId}`
|
|
7557
|
-
);
|
|
7558
|
-
return { created: false };
|
|
7559
|
-
}
|
|
7560
|
-
const file = conversationFile(accountId, record.channelKey);
|
|
7561
|
-
try {
|
|
7562
|
-
mkdirSync2(dirname(file), { recursive: true });
|
|
7563
|
-
} catch (err) {
|
|
7564
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
7565
|
-
console.error(
|
|
7566
|
-
`[telegram-message-store] FAIL op=append reason=${reason.slice(0, 100)} messageId=${record.messageId}`
|
|
7567
|
-
);
|
|
7568
|
-
return { created: false };
|
|
7569
|
-
}
|
|
7570
|
-
try {
|
|
7571
|
-
if (existsSync3(file)) {
|
|
7572
|
-
const existing = parseLines(readFileSync5(file, "utf8"));
|
|
7573
|
-
if (existing.some((r) => r.messageId === record.messageId)) {
|
|
7574
|
-
console.error(
|
|
7575
|
-
`[telegram-message-store] op=append channelKey=${record.channelKey} origin=${record.origin} created=false`
|
|
7576
|
-
);
|
|
7577
|
-
return { created: false };
|
|
7578
|
-
}
|
|
7579
|
-
}
|
|
7580
|
-
appendFileSync(file, `${JSON.stringify(record)}
|
|
7581
|
-
`);
|
|
7582
|
-
console.error(
|
|
7583
|
-
`[telegram-message-store] op=append channelKey=${record.channelKey} origin=${record.origin} created=true`
|
|
7584
|
-
);
|
|
7585
|
-
return { created: true };
|
|
7586
|
-
} catch (err) {
|
|
7587
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
7588
|
-
console.error(
|
|
7589
|
-
`[telegram-message-store] FAIL op=append reason=${reason.slice(0, 100)} messageId=${record.messageId}`
|
|
7590
|
-
);
|
|
7591
|
-
return { created: false };
|
|
7592
|
-
}
|
|
7593
|
-
}
|
|
7594
|
-
function readConversation2(accountId, channelKey) {
|
|
7595
|
-
if (!isStorableChannelKey(channelKey)) return [];
|
|
7596
|
-
const file = conversationFile(accountId, channelKey);
|
|
7597
|
-
try {
|
|
7598
|
-
if (!existsSync3(file)) return [];
|
|
7599
|
-
return parseLines(readFileSync5(file, "utf8")).sort(byDateSentAsc);
|
|
7600
|
-
} catch (err) {
|
|
7601
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
7602
|
-
console.error(
|
|
7603
|
-
`[telegram-message-store] FAIL op=read reason=${reason.slice(0, 100)} channelKey=${channelKey}`
|
|
7604
|
-
);
|
|
7605
|
-
return [];
|
|
7606
|
-
}
|
|
7607
|
-
}
|
|
7608
|
-
var summaryCache = /* @__PURE__ */ new Map();
|
|
7609
|
-
function summariseRecords(records2) {
|
|
7610
|
-
if (records2.length === 0) return null;
|
|
7611
|
-
const sorted = records2.slice().sort(byDateSentAsc);
|
|
7612
|
-
return { first: sorted[0], last: sorted[sorted.length - 1] };
|
|
7613
|
-
}
|
|
7614
|
-
function readConversationSummaries2(accountId) {
|
|
7615
|
-
const dir = join7(STORE_ROOT2(), accountId);
|
|
7616
|
-
const out = /* @__PURE__ */ new Map();
|
|
7617
|
-
const seen = /* @__PURE__ */ new Set();
|
|
7618
|
-
try {
|
|
7619
|
-
if (!existsSync3(dir)) return out;
|
|
7620
|
-
for (const entry2 of readdirSync2(dir)) {
|
|
7621
|
-
if (!entry2.endsWith(".jsonl")) continue;
|
|
7622
|
-
const stem = entry2.slice(0, -".jsonl".length);
|
|
7623
|
-
if (!isStorableStem(stem)) continue;
|
|
7624
|
-
const channelKey = fileStemToChannelKey(stem);
|
|
7625
|
-
const file = join7(dir, entry2);
|
|
7626
|
-
seen.add(file);
|
|
7627
|
-
let size;
|
|
7628
|
-
let mtimeMs;
|
|
7629
|
-
try {
|
|
7630
|
-
const st = statSync5(file);
|
|
7631
|
-
size = st.size;
|
|
7632
|
-
mtimeMs = st.mtimeMs;
|
|
7633
|
-
} catch {
|
|
7634
|
-
continue;
|
|
7635
|
-
}
|
|
7636
|
-
const cached3 = summaryCache.get(file);
|
|
7637
|
-
if (cached3 && cached3.size === size && cached3.mtimeMs === mtimeMs) {
|
|
7638
|
-
out.set(channelKey, cached3.summary);
|
|
7639
|
-
continue;
|
|
7640
|
-
}
|
|
7641
|
-
let summary;
|
|
7642
|
-
try {
|
|
7643
|
-
summary = summariseRecords(parseLines(readFileSync5(file, "utf8")));
|
|
7644
|
-
} catch (err) {
|
|
7645
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
7646
|
-
console.error(
|
|
7647
|
-
`[telegram-message-store] FAIL op=summarise reason=${reason.slice(0, 100)} channelKey=${channelKey}`
|
|
7648
|
-
);
|
|
7649
|
-
continue;
|
|
7650
|
-
}
|
|
7651
|
-
if (!summary) continue;
|
|
7652
|
-
summaryCache.set(file, { size, mtimeMs, summary });
|
|
7653
|
-
out.set(channelKey, summary);
|
|
7654
|
-
}
|
|
7655
|
-
} catch (err) {
|
|
7656
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
7657
|
-
console.error(`[telegram-message-store] FAIL op=summarise-dir reason=${reason.slice(0, 100)}`);
|
|
7658
|
-
return out;
|
|
7659
|
-
}
|
|
7660
|
-
for (const key2 of [...summaryCache.keys()]) {
|
|
7661
|
-
if (key2.startsWith(dir) && !seen.has(key2)) summaryCache.delete(key2);
|
|
7662
|
-
}
|
|
7663
|
-
return out;
|
|
7664
|
-
}
|
|
7665
|
-
|
|
7666
|
-
// server/routes/channel/schedule-inject.ts
|
|
7667
7848
|
init_admin_session_id();
|
|
7668
7849
|
init_normalize();
|
|
7669
7850
|
|
|
@@ -8246,6 +8427,32 @@ async function sendTelegramText(botToken, chatId, text) {
|
|
|
8246
8427
|
}
|
|
8247
8428
|
}
|
|
8248
8429
|
|
|
8430
|
+
// app/lib/telegram/outbound/send-chat-action.ts
|
|
8431
|
+
async function sendTelegramChatAction(botToken, chatId, key2, site) {
|
|
8432
|
+
const report = (ok2, reason) => console.error(
|
|
8433
|
+
`[telegram-native] op=chat-action key=${key2} site=${site} ok=${ok2}${ok2 ? "" : ` reason=${reason}`}`
|
|
8434
|
+
);
|
|
8435
|
+
try {
|
|
8436
|
+
const res = await fetch(`https://api.telegram.org/bot${botToken}/sendChatAction`, {
|
|
8437
|
+
method: "POST",
|
|
8438
|
+
headers: { "Content-Type": "application/json" },
|
|
8439
|
+
body: JSON.stringify({ chat_id: chatId, action: "typing" })
|
|
8440
|
+
});
|
|
8441
|
+
const data = await res.json();
|
|
8442
|
+
if (data.ok) {
|
|
8443
|
+
report(true);
|
|
8444
|
+
return { ok: true };
|
|
8445
|
+
}
|
|
8446
|
+
const error = data.description ?? "chat action failed";
|
|
8447
|
+
report(false, error);
|
|
8448
|
+
return { ok: false, error };
|
|
8449
|
+
} catch (e) {
|
|
8450
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
8451
|
+
report(false, error);
|
|
8452
|
+
return { ok: false, error };
|
|
8453
|
+
}
|
|
8454
|
+
}
|
|
8455
|
+
|
|
8249
8456
|
// server/routes/whatsapp.ts
|
|
8250
8457
|
init_dist();
|
|
8251
8458
|
import { basename as basename3, join as join17, resolve as resolve8 } from "path";
|
|
@@ -12706,6 +12913,32 @@ function buildVisitKeyboard(p) {
|
|
|
12706
12913
|
return { rows, dropped };
|
|
12707
12914
|
}
|
|
12708
12915
|
|
|
12916
|
+
// ../lib/dispatch-read/src/allocate.ts
|
|
12917
|
+
function selectCandidates(params) {
|
|
12918
|
+
const { jobType, cfg, commitments } = params;
|
|
12919
|
+
const type = cfg.jobTypes.find((t) => t.name === jobType);
|
|
12920
|
+
if (!type) {
|
|
12921
|
+
return { offered: [], excluded: [], required: "" };
|
|
12922
|
+
}
|
|
12923
|
+
const offered = [];
|
|
12924
|
+
const excluded = [];
|
|
12925
|
+
for (const worker of cfg.workers) {
|
|
12926
|
+
if (!worker.qualifications.includes(type.requiredQualification)) {
|
|
12927
|
+
excluded.push({ workerId: worker.workerId, reason: "missing-qualification" });
|
|
12928
|
+
continue;
|
|
12929
|
+
}
|
|
12930
|
+
offered.push({
|
|
12931
|
+
workerId: worker.workerId,
|
|
12932
|
+
name: worker.name,
|
|
12933
|
+
conflicts: commitments.get(worker.workerId) ?? []
|
|
12934
|
+
});
|
|
12935
|
+
}
|
|
12936
|
+
return { offered, excluded, required: type.requiredQualification };
|
|
12937
|
+
}
|
|
12938
|
+
function overlaps(a, b) {
|
|
12939
|
+
return a.start < b.end && b.start < a.end;
|
|
12940
|
+
}
|
|
12941
|
+
|
|
12709
12942
|
// ../lib/dispatch-read/src/session.ts
|
|
12710
12943
|
import { readFileSync as readFileSync15 } from "fs";
|
|
12711
12944
|
import { resolve as resolve12 } from "path";
|
|
@@ -12989,6 +13222,30 @@ async function readOpenVisitsForWorker(session, accountId, workerId, terminalSta
|
|
|
12989
13222
|
};
|
|
12990
13223
|
});
|
|
12991
13224
|
}
|
|
13225
|
+
async function readCommitments(session, accountId, from, to) {
|
|
13226
|
+
const r = await session.run(
|
|
13227
|
+
// One row per ATTENDANCE is correct here and nowhere else: a commitment
|
|
13228
|
+
// belongs to a worker, so a two-worker visit legitimately commits both.
|
|
13229
|
+
`MATCH (v:Visit {accountId: $accountId})-[:HAS_ATTENDANCE]->(:Attendance)-[:BY_ENGINEER]->(e:Engineer)
|
|
13230
|
+
WHERE v.startDate < $to AND coalesce(v.endDate, v.startDate) > $from
|
|
13231
|
+
RETURN e.engineerId AS workerId, v.visitId AS visitId,
|
|
13232
|
+
v.startDate AS start, coalesce(v.endDate, v.startDate) AS end`,
|
|
13233
|
+
{ accountId, from, to }
|
|
13234
|
+
);
|
|
13235
|
+
const out = /* @__PURE__ */ new Map();
|
|
13236
|
+
for (const g of r.records) {
|
|
13237
|
+
const workerId = str(g.get("workerId"));
|
|
13238
|
+
if (!workerId) continue;
|
|
13239
|
+
const list = out.get(workerId) ?? [];
|
|
13240
|
+
list.push({
|
|
13241
|
+
visitId: String(g.get("visitId")),
|
|
13242
|
+
start: String(g.get("start")),
|
|
13243
|
+
end: String(g.get("end"))
|
|
13244
|
+
});
|
|
13245
|
+
out.set(workerId, list);
|
|
13246
|
+
}
|
|
13247
|
+
return out;
|
|
13248
|
+
}
|
|
12992
13249
|
async function readSiteLocation(session, accountId, siteId) {
|
|
12993
13250
|
const r = await session.run(
|
|
12994
13251
|
`MATCH (s:Site {accountId: $accountId, siteId: $siteId})
|
|
@@ -13170,6 +13427,7 @@ var ZONE_FALLBACK = "UTC";
|
|
|
13170
13427
|
var EMPTY_META = {
|
|
13171
13428
|
vocabulary: { job: "", visit: "", worker: "" },
|
|
13172
13429
|
statuses: [],
|
|
13430
|
+
transitions: [],
|
|
13173
13431
|
zone: ""
|
|
13174
13432
|
};
|
|
13175
13433
|
var PORTAL_VISITS_PUSH_INTERVAL_MS = 6e4;
|
|
@@ -13185,17 +13443,28 @@ function parseVisitRows(raw) {
|
|
|
13185
13443
|
const visitId = typeof o.visitId === "string" ? o.visitId : "";
|
|
13186
13444
|
if (!visitId) continue;
|
|
13187
13445
|
const str2 = (v) => typeof v === "string" ? v : "";
|
|
13446
|
+
const cnt = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
13188
13447
|
out.push({
|
|
13189
13448
|
visitId,
|
|
13190
13449
|
ownerId: str2(o.ownerId),
|
|
13191
13450
|
jobId: str2(o.jobId),
|
|
13192
13451
|
status: str2(o.status),
|
|
13193
13452
|
startDate: str2(o.startDate),
|
|
13194
|
-
purpose: str2(o.purpose)
|
|
13453
|
+
purpose: str2(o.purpose),
|
|
13454
|
+
notes: cnt(o.notes),
|
|
13455
|
+
media: cnt(o.media),
|
|
13456
|
+
parts: cnt(o.parts),
|
|
13457
|
+
checklistDone: cnt(o.checklistDone),
|
|
13458
|
+
checklistTotal: cnt(o.checklistTotal),
|
|
13459
|
+
signatures: cnt(o.signatures)
|
|
13195
13460
|
});
|
|
13196
13461
|
}
|
|
13197
13462
|
return out;
|
|
13198
13463
|
}
|
|
13464
|
+
function num2(v) {
|
|
13465
|
+
const n = Number(v?.toNumber?.() ?? v);
|
|
13466
|
+
return Number.isFinite(n) ? n : 0;
|
|
13467
|
+
}
|
|
13199
13468
|
async function readVisitsForAccount(accountId) {
|
|
13200
13469
|
const session = getSession();
|
|
13201
13470
|
try {
|
|
@@ -13207,6 +13476,12 @@ async function readVisitsForAccount(accountId) {
|
|
|
13207
13476
|
// the worker sees no job and has nothing to report but confusion, where a
|
|
13208
13477
|
// duplicate on the account-scope list is visible and reads as what it is.
|
|
13209
13478
|
// The duplicate is real and is Task 2551.
|
|
13479
|
+
// The record counts ride the visit row (Task 2511). One projection rather
|
|
13480
|
+
// than a `readVisitRecordState` call per visit: this query already runs
|
|
13481
|
+
// once for the whole account, and a per-visit round trip would multiply
|
|
13482
|
+
// every tick by the account's whole visit history. The six expressions
|
|
13483
|
+
// are `readVisitRecordState`'s own, so the two cannot disagree about what
|
|
13484
|
+
// a count means.
|
|
13210
13485
|
`MATCH (j:Job {accountId: $accountId})-[:HAS_VISIT]->(v:Visit {accountId: $accountId})
|
|
13211
13486
|
OPTIONAL MATCH (v)-[:HAS_ATTENDANCE]->(:Attendance)-[:BY_ENGINEER]->(e:Engineer)
|
|
13212
13487
|
RETURN toString(v.visitId) AS visitId,
|
|
@@ -13214,7 +13489,13 @@ async function readVisitsForAccount(accountId) {
|
|
|
13214
13489
|
coalesce(toString(j.jobId), '') AS jobId,
|
|
13215
13490
|
coalesce(toString(v.status), '') AS status,
|
|
13216
13491
|
coalesce(toString(v.startDate), '') AS startDate,
|
|
13217
|
-
coalesce(toString(v.purpose), '') AS purpose
|
|
13492
|
+
coalesce(toString(v.purpose), '') AS purpose,
|
|
13493
|
+
size(coalesce(v.notes, [])) AS notes,
|
|
13494
|
+
size(coalesce(v.media, [])) AS media,
|
|
13495
|
+
size(coalesce(v.parts, [])) AS parts,
|
|
13496
|
+
size(coalesce(v.checklistDone, [])) AS checklistDone,
|
|
13497
|
+
size(coalesce(v.checklist, [])) AS checklistTotal,
|
|
13498
|
+
size(coalesce(v.signatures, [])) AS signatures`,
|
|
13218
13499
|
{ accountId }
|
|
13219
13500
|
);
|
|
13220
13501
|
return parseVisitRows([
|
|
@@ -13225,7 +13506,13 @@ async function readVisitsForAccount(accountId) {
|
|
|
13225
13506
|
jobId: rec.get("jobId"),
|
|
13226
13507
|
status: rec.get("status"),
|
|
13227
13508
|
startDate: rec.get("startDate"),
|
|
13228
|
-
purpose: rec.get("purpose")
|
|
13509
|
+
purpose: rec.get("purpose"),
|
|
13510
|
+
notes: num2(rec.get("notes")),
|
|
13511
|
+
media: num2(rec.get("media")),
|
|
13512
|
+
parts: num2(rec.get("parts")),
|
|
13513
|
+
checklistDone: num2(rec.get("checklistDone")),
|
|
13514
|
+
checklistTotal: num2(rec.get("checklistTotal")),
|
|
13515
|
+
signatures: num2(rec.get("signatures"))
|
|
13229
13516
|
}))
|
|
13230
13517
|
}
|
|
13231
13518
|
]);
|
|
@@ -13256,8 +13543,72 @@ async function readRosterForAccount(accountId, workers) {
|
|
|
13256
13543
|
await session.close();
|
|
13257
13544
|
}
|
|
13258
13545
|
}
|
|
13546
|
+
async function readCandidatesForAccount(accountId, cfg) {
|
|
13547
|
+
const session = getSession();
|
|
13548
|
+
try {
|
|
13549
|
+
const open3 = await session.run(
|
|
13550
|
+
`MATCH (j:Job {accountId: $accountId})-[:HAS_VISIT]->(v:Visit {accountId: $accountId})
|
|
13551
|
+
WHERE NOT (v)-[:HAS_ATTENDANCE]->()
|
|
13552
|
+
RETURN toString(v.visitId) AS visitId,
|
|
13553
|
+
coalesce(toString(j.jobType), '') AS jobType,
|
|
13554
|
+
coalesce(toString(v.startDate), '') AS startDate,
|
|
13555
|
+
coalesce(toString(v.endDate), toString(v.startDate), '') AS endDate`,
|
|
13556
|
+
{ accountId }
|
|
13557
|
+
);
|
|
13558
|
+
if (open3.records.length === 0) return [];
|
|
13559
|
+
const windows = open3.records.map((rec) => ({
|
|
13560
|
+
visitId: String(rec.get("visitId")),
|
|
13561
|
+
jobType: String(rec.get("jobType")),
|
|
13562
|
+
start: String(rec.get("startDate")),
|
|
13563
|
+
end: String(rec.get("endDate"))
|
|
13564
|
+
}));
|
|
13565
|
+
const dated = windows.filter((w) => w.start !== "");
|
|
13566
|
+
const from = dated.reduce((lo, w) => lo === "" || w.start < lo ? w.start : lo, "");
|
|
13567
|
+
const to = dated.reduce((hi, w) => w.end > hi ? w.end : hi, "");
|
|
13568
|
+
const committed = from === "" ? /* @__PURE__ */ new Map() : await readCommitments(session, accountId, from, to);
|
|
13569
|
+
const out = [];
|
|
13570
|
+
const nameOf = new Map(cfg.workers.map((w) => [w.workerId, w.name]));
|
|
13571
|
+
for (const w of windows) {
|
|
13572
|
+
const windowed = new Map(
|
|
13573
|
+
[...committed].map(([workerId, list]) => [
|
|
13574
|
+
workerId,
|
|
13575
|
+
// A visit with no start overlaps nothing rather than everything: an
|
|
13576
|
+
// undated visit is already listed separately by the page, and
|
|
13577
|
+
// reporting the whole roster as clashing with it would be false.
|
|
13578
|
+
w.start === "" ? [] : list.filter((c) => overlaps({ start: w.start, end: w.end }, c))
|
|
13579
|
+
])
|
|
13580
|
+
);
|
|
13581
|
+
const selection = selectCandidates({ jobType: w.jobType, cfg, commitments: windowed });
|
|
13582
|
+
for (const c of selection.offered) {
|
|
13583
|
+
out.push({
|
|
13584
|
+
visitId: w.visitId,
|
|
13585
|
+
workerId: c.workerId,
|
|
13586
|
+
name: c.name,
|
|
13587
|
+
kind: "offered",
|
|
13588
|
+
reason: "",
|
|
13589
|
+
conflicts: c.conflicts
|
|
13590
|
+
});
|
|
13591
|
+
}
|
|
13592
|
+
for (const e of selection.excluded) {
|
|
13593
|
+
out.push({
|
|
13594
|
+
visitId: w.visitId,
|
|
13595
|
+
workerId: e.workerId,
|
|
13596
|
+
// The config is the authority on a worker's name, and an excluded
|
|
13597
|
+
// worker is never in `selection.offered` to carry one.
|
|
13598
|
+
name: nameOf.get(e.workerId) ?? "",
|
|
13599
|
+
kind: "excluded",
|
|
13600
|
+
reason: e.reason,
|
|
13601
|
+
conflicts: []
|
|
13602
|
+
});
|
|
13603
|
+
}
|
|
13604
|
+
}
|
|
13605
|
+
return out;
|
|
13606
|
+
} finally {
|
|
13607
|
+
await session.close();
|
|
13608
|
+
}
|
|
13609
|
+
}
|
|
13259
13610
|
async function pushVisitsForAccount(opts) {
|
|
13260
|
-
const { accountId, client, rows, roster, meta, log: log2 } = opts;
|
|
13611
|
+
const { accountId, client, rows, roster, meta, candidates, log: log2 } = opts;
|
|
13261
13612
|
const unbound = rows.filter((r) => r.ownerId === "").length;
|
|
13262
13613
|
const seen = /* @__PURE__ */ new Set();
|
|
13263
13614
|
const shared = /* @__PURE__ */ new Set();
|
|
@@ -13268,7 +13619,7 @@ async function pushVisitsForAccount(opts) {
|
|
|
13268
13619
|
const unresolved = roster.filter((w) => w.ownerId === "").length;
|
|
13269
13620
|
if (rows.length === 0 && roster.length === 0) {
|
|
13270
13621
|
log2(`${TAG19} op=skip account=${accountId} reason=no-visits-no-roster`);
|
|
13271
|
-
return { rows: 0, unbound: 0, roster: 0, unresolved: 0 };
|
|
13622
|
+
return { rows: 0, unbound: 0, roster: 0, unresolved: 0, candidates: 0 };
|
|
13272
13623
|
}
|
|
13273
13624
|
const state = await client.query(
|
|
13274
13625
|
"SELECT currentGeneration FROM visits_state WHERE accountId = ?",
|
|
@@ -13281,8 +13632,23 @@ async function pushVisitsForAccount(opts) {
|
|
|
13281
13632
|
const next = Math.max(current, Number(staged[0]?.g ?? 0)) + 1;
|
|
13282
13633
|
for (const row of rows) {
|
|
13283
13634
|
await client.query(
|
|
13284
|
-
"INSERT INTO visits (accountId, visitId, ownerId, jobId, status, startDate, purpose, generation) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
13285
|
-
[
|
|
13635
|
+
"INSERT INTO visits (accountId, visitId, ownerId, jobId, status, startDate, purpose, notes, media, parts, checklistDone, checklistTotal, signatures, generation) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
13636
|
+
[
|
|
13637
|
+
accountId,
|
|
13638
|
+
row.visitId,
|
|
13639
|
+
row.ownerId,
|
|
13640
|
+
row.jobId,
|
|
13641
|
+
row.status,
|
|
13642
|
+
row.startDate,
|
|
13643
|
+
row.purpose,
|
|
13644
|
+
row.notes,
|
|
13645
|
+
row.media,
|
|
13646
|
+
row.parts,
|
|
13647
|
+
row.checklistDone,
|
|
13648
|
+
row.checklistTotal,
|
|
13649
|
+
row.signatures,
|
|
13650
|
+
next
|
|
13651
|
+
]
|
|
13286
13652
|
);
|
|
13287
13653
|
}
|
|
13288
13654
|
for (const w of roster) {
|
|
@@ -13293,19 +13659,26 @@ async function pushVisitsForAccount(opts) {
|
|
|
13293
13659
|
}
|
|
13294
13660
|
if (roster.length > 0) {
|
|
13295
13661
|
await client.query(
|
|
13296
|
-
"INSERT INTO dispatch_meta (accountId, vocabulary, statuses, zone, publishedAt, generation) VALUES (?, ?, ?, ?, ?, ?)",
|
|
13662
|
+
"INSERT INTO dispatch_meta (accountId, vocabulary, statuses, transitions, zone, publishedAt, generation) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
13297
13663
|
[
|
|
13298
13664
|
accountId,
|
|
13299
13665
|
JSON.stringify(meta.vocabulary),
|
|
13300
13666
|
JSON.stringify(meta.statuses),
|
|
13667
|
+
JSON.stringify(meta.transitions),
|
|
13301
13668
|
meta.zone,
|
|
13302
13669
|
(/* @__PURE__ */ new Date()).toISOString(),
|
|
13303
13670
|
next
|
|
13304
13671
|
]
|
|
13305
13672
|
);
|
|
13306
13673
|
}
|
|
13674
|
+
for (const c of candidates) {
|
|
13675
|
+
await client.query(
|
|
13676
|
+
"INSERT INTO visit_candidates (accountId, visitId, workerId, name, kind, reason, conflicts, generation) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
13677
|
+
[accountId, c.visitId, c.workerId, c.name, c.kind, c.reason, JSON.stringify(c.conflicts), next]
|
|
13678
|
+
);
|
|
13679
|
+
}
|
|
13307
13680
|
log2(
|
|
13308
|
-
`${TAG19} op=stage account=${accountId} generation=${next} rows=${rows.length} visits=${seen.size} shared=${shared.size} roster=${roster.length}`
|
|
13681
|
+
`${TAG19} op=stage account=${accountId} generation=${next} rows=${rows.length} visits=${seen.size} shared=${shared.size} roster=${roster.length} candidates=${candidates.length}`
|
|
13309
13682
|
);
|
|
13310
13683
|
await client.query(
|
|
13311
13684
|
"INSERT INTO visits_state (accountId, currentGeneration) VALUES (?, ?) ON CONFLICT (accountId) DO UPDATE SET currentGeneration = excluded.currentGeneration WHERE excluded.currentGeneration > visits_state.currentGeneration",
|
|
@@ -13325,6 +13698,7 @@ async function pushVisitsForAccount(opts) {
|
|
|
13325
13698
|
unbound,
|
|
13326
13699
|
roster: roster.length,
|
|
13327
13700
|
unresolved,
|
|
13701
|
+
candidates: candidates.length,
|
|
13328
13702
|
generation: next,
|
|
13329
13703
|
superseded: true
|
|
13330
13704
|
};
|
|
@@ -13345,12 +13719,23 @@ async function pushVisitsForAccount(opts) {
|
|
|
13345
13719
|
accountId,
|
|
13346
13720
|
next
|
|
13347
13721
|
]);
|
|
13722
|
+
await client.query("DELETE FROM visit_candidates WHERE accountId = ? AND generation < ?", [
|
|
13723
|
+
accountId,
|
|
13724
|
+
next
|
|
13725
|
+
]);
|
|
13348
13726
|
} catch (err) {
|
|
13349
13727
|
log2(
|
|
13350
13728
|
`${TAG19} op=sweep account=${accountId} generation=${next} result=failed err="${err instanceof Error ? err.message : String(err)}"`
|
|
13351
13729
|
);
|
|
13352
13730
|
}
|
|
13353
|
-
return {
|
|
13731
|
+
return {
|
|
13732
|
+
rows: rows.length,
|
|
13733
|
+
unbound,
|
|
13734
|
+
roster: roster.length,
|
|
13735
|
+
unresolved,
|
|
13736
|
+
candidates: candidates.length,
|
|
13737
|
+
generation: next
|
|
13738
|
+
};
|
|
13354
13739
|
}
|
|
13355
13740
|
function visitsStateFilePath(platformRoot5) {
|
|
13356
13741
|
return resolve13(platformRoot5, "..", "data", "portal-visits-state.json");
|
|
@@ -13358,17 +13743,40 @@ function visitsStateFilePath(platformRoot5) {
|
|
|
13358
13743
|
function visitsDigest(rows) {
|
|
13359
13744
|
const h = createHash3("sha256");
|
|
13360
13745
|
for (const r of [...rows].sort((a, b) => a.visitId.localeCompare(b.visitId))) {
|
|
13361
|
-
h.update(
|
|
13746
|
+
h.update(
|
|
13747
|
+
JSON.stringify([
|
|
13748
|
+
r.visitId,
|
|
13749
|
+
r.ownerId,
|
|
13750
|
+
r.jobId,
|
|
13751
|
+
r.status,
|
|
13752
|
+
r.startDate,
|
|
13753
|
+
r.purpose,
|
|
13754
|
+
// Task 2511's counts are fields of a visit, so they belong in the
|
|
13755
|
+
// visits half. Listed explicitly rather than spread, because the array
|
|
13756
|
+
// literal is what fixes field order here rather than key insertion.
|
|
13757
|
+
r.notes,
|
|
13758
|
+
r.media,
|
|
13759
|
+
r.parts,
|
|
13760
|
+
r.checklistDone,
|
|
13761
|
+
r.checklistTotal,
|
|
13762
|
+
r.signatures
|
|
13763
|
+
])
|
|
13764
|
+
);
|
|
13362
13765
|
}
|
|
13363
13766
|
return h.digest("hex");
|
|
13364
13767
|
}
|
|
13365
|
-
function payloadDigest(rows, roster, meta) {
|
|
13768
|
+
function payloadDigest(rows, roster, meta, candidates) {
|
|
13366
13769
|
const h = createHash3("sha256");
|
|
13367
13770
|
h.update(visitsDigest(rows));
|
|
13368
13771
|
for (const w of [...roster].sort((a, b) => a.workerId.localeCompare(b.workerId))) {
|
|
13369
13772
|
h.update(JSON.stringify([w.workerId, w.ownerId, w.name, w.workingHours]));
|
|
13370
13773
|
}
|
|
13371
|
-
h.update(JSON.stringify([meta.vocabulary, meta.statuses, meta.zone]));
|
|
13774
|
+
h.update(JSON.stringify([meta.vocabulary, meta.statuses, meta.transitions, meta.zone]));
|
|
13775
|
+
for (const c of [...candidates].sort(
|
|
13776
|
+
(a, b) => a.visitId.localeCompare(b.visitId) || a.workerId.localeCompare(b.workerId)
|
|
13777
|
+
)) {
|
|
13778
|
+
h.update(JSON.stringify([c.visitId, c.workerId, c.name, c.kind, c.reason, c.conflicts]));
|
|
13779
|
+
}
|
|
13372
13780
|
return h.digest("hex");
|
|
13373
13781
|
}
|
|
13374
13782
|
function readVisitsPushState(platformRoot5) {
|
|
@@ -13416,9 +13824,11 @@ async function runPortalVisitsPush(root) {
|
|
|
13416
13824
|
const meta = cfg.present ? {
|
|
13417
13825
|
vocabulary: cfg.value.vocabulary,
|
|
13418
13826
|
statuses: cfg.value.statuses.visit,
|
|
13827
|
+
transitions: cfg.value.statuses.visitTransitions,
|
|
13419
13828
|
zone: await resolveAccountZone(portal.accountId) ?? ZONE_FALLBACK
|
|
13420
13829
|
} : EMPTY_META;
|
|
13421
|
-
const
|
|
13830
|
+
const candidates = cfg.present ? await readCandidatesForAccount(portal.accountId, cfg.value) : [];
|
|
13831
|
+
const digest = payloadDigest(rows, roster, meta, candidates);
|
|
13422
13832
|
const unresolved = roster.filter((w) => w.ownerId === "").length;
|
|
13423
13833
|
if (state[portal.accountId]?.digest === digest && (rows.length > 0 || roster.length > 0)) {
|
|
13424
13834
|
recordVisitsPush(root, portal.accountId, {
|
|
@@ -13443,10 +13853,11 @@ async function runPortalVisitsPush(root) {
|
|
|
13443
13853
|
rows,
|
|
13444
13854
|
roster,
|
|
13445
13855
|
meta,
|
|
13856
|
+
candidates,
|
|
13446
13857
|
log: (l) => console.error(l)
|
|
13447
13858
|
});
|
|
13448
13859
|
console.error(
|
|
13449
|
-
`${TAG19} op=push account=${portal.accountId} visits=${rows.length} pushed=${r.rows} roster=${r.roster} workers-unresolved=${r.unresolved} ms=${Date.now() - startedAt}`
|
|
13860
|
+
`${TAG19} op=push account=${portal.accountId} visits=${rows.length} pushed=${r.rows} roster=${r.roster} workers-unresolved=${r.unresolved} transitions=${meta.transitions.length} candidates=${r.candidates} ms=${Date.now() - startedAt}`
|
|
13450
13861
|
);
|
|
13451
13862
|
if (r.generation !== void 0) {
|
|
13452
13863
|
recordVisitsPush(root, portal.accountId, {
|
|
@@ -13491,12 +13902,15 @@ function parsePendingStatusRows(raw) {
|
|
|
13491
13902
|
statusAt: str2(o.statusAt),
|
|
13492
13903
|
dayHundredths: typeof o.dayHundredths === "number" ? o.dayHundredths : 0,
|
|
13493
13904
|
workDate: str2(o.workDate),
|
|
13494
|
-
workNote: str2(o.workNote)
|
|
13905
|
+
workNote: str2(o.workNote),
|
|
13906
|
+
assignedOwnerId: str2(o.assignedOwnerId),
|
|
13907
|
+
startDate: str2(o.startDate)
|
|
13495
13908
|
});
|
|
13496
13909
|
}
|
|
13497
13910
|
return out;
|
|
13498
13911
|
}
|
|
13499
13912
|
var WORK_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
13913
|
+
var INSTANT_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
13500
13914
|
function buildVisitStatusWrite(accountId, row) {
|
|
13501
13915
|
const base = {
|
|
13502
13916
|
accountId,
|
|
@@ -13504,6 +13918,72 @@ function buildVisitStatusWrite(accountId, row) {
|
|
|
13504
13918
|
status: row.status,
|
|
13505
13919
|
statusAt: row.statusAt
|
|
13506
13920
|
};
|
|
13921
|
+
if (row.assignedOwnerId !== "" || row.startDate !== "") {
|
|
13922
|
+
if (row.startDate !== "" && !INSTANT_RE.test(row.startDate)) return { kind: "bad-instant" };
|
|
13923
|
+
if (row.dayHundredths > 0) return { kind: "move-with-labour" };
|
|
13924
|
+
return {
|
|
13925
|
+
kind: "move",
|
|
13926
|
+
// EXACTLY ONE ATTENDANCE to replace, and EXACTLY ONE ENGINEER to replace
|
|
13927
|
+
// it with, or nothing runs at all.
|
|
13928
|
+
//
|
|
13929
|
+
// A visit two workers share cannot be re-assigned by replacing "the"
|
|
13930
|
+
// attendance: whichever went, the other worker would silently lose the
|
|
13931
|
+
// job and the arrival times they had already pressed. A per-worker move
|
|
13932
|
+
// is Task 2551. The engineer gate is the mirror: the `portalOwnerId`
|
|
13933
|
+
// binding is a property and is not unique by construction, so an id
|
|
13934
|
+
// naming several must not resolve to whichever came back first. Both turn
|
|
13935
|
+
// into the zero count this function already returns for a visit missing
|
|
13936
|
+
// its job, so the row stays unclaimed and counted.
|
|
13937
|
+
//
|
|
13938
|
+
// NO `CALL` SUBQUERY, deliberately. A returning subquery replaces each
|
|
13939
|
+
// outer row with the rows it yields, so one that yields nothing on a
|
|
13940
|
+
// status-only or re-time-only edit would eliminate the visit before the
|
|
13941
|
+
// SET ran: the re-timing would write nothing, return 0, and the row would
|
|
13942
|
+
// never drain. A unit subquery does not filter, but then an unresolvable
|
|
13943
|
+
// ownerId would leave the SET running and the row CLAIMED with nobody
|
|
13944
|
+
// moved. Both gates therefore sit in the main query's WHERE, where they
|
|
13945
|
+
// fail the whole statement, and the writes ride FOREACH over lists that
|
|
13946
|
+
// are empty when this edit re-assigns nobody.
|
|
13947
|
+
//
|
|
13948
|
+
// The re-assignment REPLACES rather than adds, because a drag onto
|
|
13949
|
+
// another worker's row means "move this", not "add a second person". The
|
|
13950
|
+
// attendance key is `engineerId + '-' + visitId`, the key
|
|
13951
|
+
// `dispatch-allocate` merges on, so allocating that worker to that visit
|
|
13952
|
+
// afterwards is a no-op rather than a second attendance — and the delete
|
|
13953
|
+
// skips an attendance already held by the incoming engineer, so
|
|
13954
|
+
// re-assigning somebody to the visit they are already on keeps the
|
|
13955
|
+
// arrival times they pressed instead of resetting them.
|
|
13956
|
+
cypher: `MATCH (v:Visit {accountId: $accountId, visitId: $visitId})
|
|
13957
|
+
OPTIONAL MATCH (v)-[:HAS_ATTENDANCE]->(a:Attendance)
|
|
13958
|
+
WITH v, collect(a) AS atts
|
|
13959
|
+
OPTIONAL MATCH (e:Engineer {accountId: $accountId})
|
|
13960
|
+
WHERE $assignedOwnerId <> ''
|
|
13961
|
+
AND e.portalOwnerId = $assignedOwnerId
|
|
13962
|
+
AND e.engineerId IS NOT NULL
|
|
13963
|
+
WITH v, atts, collect(e) AS es
|
|
13964
|
+
WHERE $assignedOwnerId = '' OR (size(atts) = 1 AND size(es) = 1)
|
|
13965
|
+
SET v.status = $status, v.statusAt = $statusAt
|
|
13966
|
+
FOREACH (_ IN CASE WHEN $startDate <> '' THEN [1] ELSE [] END |
|
|
13967
|
+
SET v.startDate = $startDate)
|
|
13968
|
+
FOREACH (old IN [x IN atts
|
|
13969
|
+
WHERE NOT x.engineerId IN [eng IN es | eng.engineerId]] |
|
|
13970
|
+
DETACH DELETE old)
|
|
13971
|
+
FOREACH (eng IN es |
|
|
13972
|
+
MERGE (a2:Attendance {
|
|
13973
|
+
accountId: $accountId,
|
|
13974
|
+
attendanceId: eng.engineerId + '-' + $visitId
|
|
13975
|
+
})
|
|
13976
|
+
ON CREATE SET a2.engineerId = eng.engineerId, a2.createdAt = $statusAt
|
|
13977
|
+
MERGE (v)-[:HAS_ATTENDANCE]->(a2)
|
|
13978
|
+
MERGE (a2)-[:BY_ENGINEER]->(eng))
|
|
13979
|
+
RETURN count(v) AS n`,
|
|
13980
|
+
params: {
|
|
13981
|
+
...base,
|
|
13982
|
+
assignedOwnerId: row.assignedOwnerId,
|
|
13983
|
+
startDate: row.startDate
|
|
13984
|
+
}
|
|
13985
|
+
};
|
|
13986
|
+
}
|
|
13507
13987
|
if (row.dayHundredths <= 0) {
|
|
13508
13988
|
return {
|
|
13509
13989
|
kind: "status",
|
|
@@ -13562,6 +14042,14 @@ async function pullVisitStatusForAccount(opts) {
|
|
|
13562
14042
|
await claim(row.id);
|
|
13563
14043
|
merged += 1;
|
|
13564
14044
|
log2(`${TAG20} op=merged account=${accountId} id=${row.id} labour=${row.dayHundredths > 0}`);
|
|
14045
|
+
const fields = ["status"];
|
|
14046
|
+
if (row.workDate !== "") fields.push("workDate");
|
|
14047
|
+
if (row.workNote !== "") fields.push("workNote");
|
|
14048
|
+
if (row.assignedOwnerId !== "") fields.push("assignedOwnerId");
|
|
14049
|
+
if (row.startDate !== "") fields.push("startDate");
|
|
14050
|
+
for (const field2 of fields) {
|
|
14051
|
+
log2(`${TAG20} op=applied visitId=${row.visitId} field=${field2}`);
|
|
14052
|
+
}
|
|
13565
14053
|
} catch (err) {
|
|
13566
14054
|
failed2 += 1;
|
|
13567
14055
|
const detail = err.message.split("\n")[0].replaceAll('"', "'").slice(0, 160);
|
|
@@ -13589,7 +14077,7 @@ async function runPortalVisitsPull(root) {
|
|
|
13589
14077
|
const pending = parsePendingStatusRows(
|
|
13590
14078
|
await cf.d1Exec(
|
|
13591
14079
|
portal.portalDbName,
|
|
13592
|
-
"SELECT id, visitId, ownerId, status, statusAt, dayHundredths, workDate, workNote FROM visit_status WHERE accountId = ? AND claimed = 0",
|
|
14080
|
+
"SELECT id, visitId, ownerId, status, statusAt, dayHundredths, workDate, workNote, assignedOwnerId, startDate FROM visit_status WHERE accountId = ? AND claimed = 0",
|
|
13593
14081
|
[portal.accountId]
|
|
13594
14082
|
)
|
|
13595
14083
|
);
|
|
@@ -13598,7 +14086,9 @@ async function runPortalVisitsPull(root) {
|
|
|
13598
14086
|
pending,
|
|
13599
14087
|
applyToGraph: async (accountId, row) => {
|
|
13600
14088
|
const write = buildVisitStatusWrite(accountId, row);
|
|
13601
|
-
if (write.kind === "undated")
|
|
14089
|
+
if (write.kind === "undated" || write.kind === "bad-instant" || write.kind === "move-with-labour") {
|
|
14090
|
+
return false;
|
|
14091
|
+
}
|
|
13602
14092
|
const session = getSession();
|
|
13603
14093
|
try {
|
|
13604
14094
|
const res = await session.run(write.cypher, write.params);
|
|
@@ -13642,7 +14132,7 @@ function buildVisitsAuditLine(input) {
|
|
|
13642
14132
|
input.d1Error !== null ? `d1:${auditErrorText(input.d1Error)}` : null,
|
|
13643
14133
|
input.neo4jError !== null ? `neo4j:${auditErrorText(input.neo4jError)}` : null
|
|
13644
14134
|
].filter(Boolean).join(" | ");
|
|
13645
|
-
return `${TAG21} account=${input.accountId} pushedRows=${na(input.pushedRows)} graphVisits=${na(input.graphVisits)} unboundVisits=${na(input.unboundVisits)} pendingStatusRows=${na(input.pendingStatusRows)} oldestPendingHrs=${na(input.oldestPendingHrs, 1)} unresolvedVisitIds=${na(input.unresolvedVisitIds)} labourClaims=${na(input.labourClaims)} labourCosts=${na(input.labourCosts)} undatedClaims=${na(input.undatedClaims)} lastPushAgeSec=${na(input.lastPushAgeSec)}` + (degradation ? ` degraded=${degradation} err="${reasons}"` : "");
|
|
14135
|
+
return `${TAG21} account=${input.accountId} pushedRows=${na(input.pushedRows)} graphVisits=${na(input.graphVisits)} unboundVisits=${na(input.unboundVisits)} pendingStatusRows=${na(input.pendingStatusRows)} oldestPendingHrs=${na(input.oldestPendingHrs, 1)} unresolvedVisitIds=${na(input.unresolvedVisitIds)} labourClaims=${na(input.labourClaims)} labourCosts=${na(input.labourCosts)} undatedClaims=${na(input.undatedClaims)} stalePendingRows=${na(input.stalePendingRows)} lastPushAgeSec=${na(input.lastPushAgeSec)}` + (degradation ? ` degraded=${degradation} err="${reasons}"` : "");
|
|
13646
14136
|
}
|
|
13647
14137
|
function readLastVisitsPushAgeSec(platformRoot5, accountId, nowMs) {
|
|
13648
14138
|
try {
|
|
@@ -13655,7 +14145,7 @@ function readLastVisitsPushAgeSec(platformRoot5, accountId, nowMs) {
|
|
|
13655
14145
|
return null;
|
|
13656
14146
|
}
|
|
13657
14147
|
}
|
|
13658
|
-
var
|
|
14148
|
+
var num3 = (raw, field2) => {
|
|
13659
14149
|
const envelope = Array.isArray(raw) ? raw[0] : null;
|
|
13660
14150
|
const results = envelope?.results;
|
|
13661
14151
|
if (!Array.isArray(results)) {
|
|
@@ -13681,12 +14171,13 @@ async function runPortalVisitsAudit(root) {
|
|
|
13681
14171
|
let unresolvedVisitIds = null;
|
|
13682
14172
|
let labourClaims = null;
|
|
13683
14173
|
let undatedClaims = null;
|
|
14174
|
+
let stalePendingRows = null;
|
|
13684
14175
|
let d1Error = null;
|
|
13685
14176
|
try {
|
|
13686
14177
|
if (!SAFE_ACCOUNT_ID3.test(portal.accountId)) {
|
|
13687
14178
|
throw new Error(`refusing to query for unsafe accountId "${portal.accountId}"`);
|
|
13688
14179
|
}
|
|
13689
|
-
pushedRows =
|
|
14180
|
+
pushedRows = num3(
|
|
13690
14181
|
await cf.d1Exec(
|
|
13691
14182
|
portal.portalDbName,
|
|
13692
14183
|
"SELECT COUNT(*) AS n FROM visits v JOIN visits_state s ON s.accountId = v.accountId AND s.currentGeneration = v.generation WHERE v.accountId = ?",
|
|
@@ -13694,7 +14185,7 @@ async function runPortalVisitsAudit(root) {
|
|
|
13694
14185
|
),
|
|
13695
14186
|
"n"
|
|
13696
14187
|
);
|
|
13697
|
-
unboundVisits =
|
|
14188
|
+
unboundVisits = num3(
|
|
13698
14189
|
await cf.d1Exec(
|
|
13699
14190
|
portal.portalDbName,
|
|
13700
14191
|
"SELECT COUNT(*) AS n FROM visits v JOIN visits_state s ON s.accountId = v.accountId AND s.currentGeneration = v.generation WHERE v.accountId = ? AND v.ownerId = ''",
|
|
@@ -13707,12 +14198,12 @@ async function runPortalVisitsAudit(root) {
|
|
|
13707
14198
|
"SELECT COUNT(*) AS n, MIN(statusAt) AS oldest FROM visit_status WHERE accountId = ? AND claimed = 0",
|
|
13708
14199
|
[portal.accountId]
|
|
13709
14200
|
);
|
|
13710
|
-
pendingStatusRows =
|
|
14201
|
+
pendingStatusRows = num3(pending, "n");
|
|
13711
14202
|
const envelope = Array.isArray(pending) ? pending[0] : null;
|
|
13712
14203
|
const oldest = envelope?.results?.[0]?.oldest;
|
|
13713
14204
|
const oldestMs = typeof oldest === "string" ? Date.parse(oldest) : NaN;
|
|
13714
14205
|
oldestPendingHrs = Number.isNaN(oldestMs) ? pendingStatusRows === 0 ? 0 : null : Math.max(0, (nowMs - oldestMs) / 36e5);
|
|
13715
|
-
unresolvedVisitIds =
|
|
14206
|
+
unresolvedVisitIds = num3(
|
|
13716
14207
|
await cf.d1Exec(
|
|
13717
14208
|
portal.portalDbName,
|
|
13718
14209
|
"SELECT COUNT(*) AS n FROM visit_status vs WHERE vs.accountId = ? AND vs.claimed = 0 AND NOT EXISTS (SELECT 1 FROM visits v JOIN visits_state s ON s.accountId = v.accountId AND s.currentGeneration = v.generation WHERE v.accountId = vs.accountId AND v.visitId = vs.visitId)",
|
|
@@ -13720,7 +14211,7 @@ async function runPortalVisitsAudit(root) {
|
|
|
13720
14211
|
),
|
|
13721
14212
|
"n"
|
|
13722
14213
|
);
|
|
13723
|
-
labourClaims =
|
|
14214
|
+
labourClaims = num3(
|
|
13724
14215
|
await cf.d1Exec(
|
|
13725
14216
|
portal.portalDbName,
|
|
13726
14217
|
"SELECT COUNT(*) AS n FROM visit_status WHERE accountId = ? AND claimed = 0 AND dayHundredths > 0",
|
|
@@ -13728,7 +14219,7 @@ async function runPortalVisitsAudit(root) {
|
|
|
13728
14219
|
),
|
|
13729
14220
|
"n"
|
|
13730
14221
|
);
|
|
13731
|
-
undatedClaims =
|
|
14222
|
+
undatedClaims = num3(
|
|
13732
14223
|
await cf.d1Exec(
|
|
13733
14224
|
portal.portalDbName,
|
|
13734
14225
|
"SELECT COUNT(*) AS n FROM visit_status WHERE accountId = ? AND claimed = 0 AND dayHundredths > 0 AND workDate NOT GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]'",
|
|
@@ -13736,6 +14227,15 @@ async function runPortalVisitsAudit(root) {
|
|
|
13736
14227
|
),
|
|
13737
14228
|
"n"
|
|
13738
14229
|
);
|
|
14230
|
+
const staleCutoff = new Date(nowMs - 2 * PORTAL_VISITS_PULL_INTERVAL_MS).toISOString();
|
|
14231
|
+
stalePendingRows = num3(
|
|
14232
|
+
await cf.d1Exec(
|
|
14233
|
+
portal.portalDbName,
|
|
14234
|
+
"SELECT COUNT(*) AS n FROM visit_status WHERE accountId = ? AND claimed = 0 AND statusAt < ?",
|
|
14235
|
+
[portal.accountId, staleCutoff]
|
|
14236
|
+
),
|
|
14237
|
+
"n"
|
|
14238
|
+
);
|
|
13739
14239
|
} catch (err) {
|
|
13740
14240
|
d1Error = err.message;
|
|
13741
14241
|
}
|
|
@@ -13774,6 +14274,7 @@ async function runPortalVisitsAudit(root) {
|
|
|
13774
14274
|
labourClaims,
|
|
13775
14275
|
labourCosts,
|
|
13776
14276
|
undatedClaims,
|
|
14277
|
+
stalePendingRows,
|
|
13777
14278
|
lastPushAgeSec: readLastVisitsPushAgeSec(root, portal.accountId, nowMs),
|
|
13778
14279
|
d1Error,
|
|
13779
14280
|
neo4jError
|
|
@@ -15213,6 +15714,7 @@ function pickActivity(entries2, grown, nowMs, concurrencyWindowMs) {
|
|
|
15213
15714
|
}
|
|
15214
15715
|
|
|
15215
15716
|
// server/routes/session-reader.ts
|
|
15717
|
+
init_session_store();
|
|
15216
15718
|
init_dist();
|
|
15217
15719
|
var app7 = new Hono();
|
|
15218
15720
|
function probeDataRef(path) {
|
|
@@ -15388,6 +15890,7 @@ data: ${JSON.stringify(turn)}
|
|
|
15388
15890
|
|
|
15389
15891
|
`));
|
|
15390
15892
|
const connId = `${sessionId.slice(0, 8)}-${process.hrtime.bigint().toString(36).slice(-6)}`;
|
|
15893
|
+
const censusAccountId = getAccountIdForSession(c.get("cacheKey") ?? "") ?? "unresolved";
|
|
15391
15894
|
const startedAt = Date.now();
|
|
15392
15895
|
const lastEventId = c.req.header("Last-Event-ID");
|
|
15393
15896
|
let teardown = () => {
|
|
@@ -15423,7 +15926,8 @@ data: ${JSON.stringify(obj)}
|
|
|
15423
15926
|
log: (line) => console.log(line)
|
|
15424
15927
|
});
|
|
15425
15928
|
const queuedPendingSuppress = /* @__PURE__ */ new Map();
|
|
15426
|
-
const
|
|
15929
|
+
const envelopeCensus = emptyEnvelopeCensus();
|
|
15930
|
+
const turnCtx = { envelopeCensus };
|
|
15427
15931
|
const drain = () => {
|
|
15428
15932
|
try {
|
|
15429
15933
|
const { buf, end } = readFrom(jsonlPath, offset);
|
|
@@ -15440,6 +15944,7 @@ data: ${JSON.stringify(obj)}
|
|
|
15440
15944
|
n++;
|
|
15441
15945
|
}
|
|
15442
15946
|
if (n > 0) {
|
|
15947
|
+
foldFrameCensus(censusAccountId, sessionId, envelopeCensus);
|
|
15443
15948
|
actx.parentGrewThisTick = true;
|
|
15444
15949
|
console.log(`[wa-stream] op=drain conn=${connId} turns=${n}`);
|
|
15445
15950
|
}
|
|
@@ -15486,6 +15991,7 @@ data: ${JSON.stringify({ from: alignedFrom, fileEnd: fileEnd0 })}
|
|
|
15486
15991
|
}
|
|
15487
15992
|
n++;
|
|
15488
15993
|
}
|
|
15994
|
+
foldFrameCensus(censusAccountId, sessionId, envelopeCensus);
|
|
15489
15995
|
console.log(
|
|
15490
15996
|
`[wa-stream] op=backlog conn=${connId} turns=${n} bytes=${end - alignedFrom} from=${alignedFrom}`
|
|
15491
15997
|
);
|
|
@@ -15669,7 +16175,7 @@ import { basename as basename8, dirname as dirname9, join as join26 } from "path
|
|
|
15669
16175
|
// app/lib/reader/operator-conversations.ts
|
|
15670
16176
|
function operatorConvName(row) {
|
|
15671
16177
|
if (row.isGroup === true) return row.chatName ?? row.title;
|
|
15672
|
-
const name = row.operatorName ?? row.chatName ?? row.whatsappName;
|
|
16178
|
+
const name = row.operatorName ?? row.chatName ?? row.whatsappName ?? row.telegramName;
|
|
15673
16179
|
const senderId = row.senderId != null && row.channel === "whatsapp" ? formatSenderPhone(row.senderId) : row.senderId;
|
|
15674
16180
|
if (name) return senderId ? `${name} ${senderId}` : name;
|
|
15675
16181
|
return senderId ?? row.title;
|
|
@@ -15679,6 +16185,7 @@ function operatorConvNameSource(row) {
|
|
|
15679
16185
|
if (row.operatorName) return "operator";
|
|
15680
16186
|
if (row.chatName) return "chatmeta";
|
|
15681
16187
|
if (row.whatsappName) return "inbound-pushname";
|
|
16188
|
+
if (row.telegramName) return "telegram-sender-name";
|
|
15682
16189
|
if (row.senderId != null) return "counterparty-number";
|
|
15683
16190
|
return "jid";
|
|
15684
16191
|
}
|
|
@@ -15898,6 +16405,14 @@ app8.get("/conversations", requireAdminSession, async (c) => {
|
|
|
15898
16405
|
);
|
|
15899
16406
|
const adminScope = allStoreRows.filter((r) => r.scope === "admin").length;
|
|
15900
16407
|
const publicScope = allStoreRows.length - adminScope;
|
|
16408
|
+
const telegramStoreRows = account ? buildTelegramStoreConversationRows(account.accountId, readConversationSummaries2(account.accountId)) : [];
|
|
16409
|
+
const dedupedSessionRows = dropTelegramSessionDuplicates(sessionConversations, telegramStoreRows);
|
|
16410
|
+
const telegramRows = [
|
|
16411
|
+
...dedupedSessionRows.filter((r) => r.channel === "telegram"),
|
|
16412
|
+
...telegramStoreRows
|
|
16413
|
+
].sort((a, b) => (b.lastMessageAt ?? "").localeCompare(a.lastMessageAt ?? ""));
|
|
16414
|
+
const otherSessionRows = dedupedSessionRows.filter((r) => r.channel !== "telegram");
|
|
16415
|
+
const conversations = [...otherSessionRows, ...allStoreRows, ...telegramRows];
|
|
15901
16416
|
const census = buildLabelCensus(allStoreRows, selfPushNames(summaries));
|
|
15902
16417
|
for (const f of fallbackLabelSources(allStoreRows)) {
|
|
15903
16418
|
console.log(`[wa-label] op=row-label jid=${f.jid} source=${f.source} route=conversations`);
|
|
@@ -15905,11 +16420,11 @@ app8.get("/conversations", requireAdminSession, async (c) => {
|
|
|
15905
16420
|
console.log(
|
|
15906
16421
|
`[wa-label-census] route=conversations chats=${census.chats} withMeta=${census.withMeta} groups=${census.groups} groupsWithSubject=${census.groupsWithSubject} labelledByMember=${census.labelledByMember} dms=${census.dms} namedDms=${census.namedDms} labelledBySelfPushName=${census.labelledBySelfPushName}`
|
|
15907
16422
|
);
|
|
15908
|
-
const sessionRowCount =
|
|
16423
|
+
const sessionRowCount = dedupedSessionRows.length;
|
|
15909
16424
|
console.log(
|
|
15910
|
-
`[wa-reader] op=list pinnedAccount=${scopeAccountId?.slice(0, 8) ?? "null"} resolvedAccount=${account?.accountId.slice(0, 8) ?? "null"} multiAccount=${multiAccount} conversations=${
|
|
16425
|
+
`[wa-reader] op=list pinnedAccount=${scopeAccountId?.slice(0, 8) ?? "null"} resolvedAccount=${account?.accountId.slice(0, 8) ?? "null"} multiAccount=${multiAccount} conversations=${conversations.length} sessionRows=${sessionRowCount} storeRows=${allStoreRows.length} storeRowsByChannel=whatsapp:${allStoreRows.length},telegram:${telegramStoreRows.length} adminScope=${adminScope} publicScope=${publicScope} sessionRowsExcludedUntagged=${sessionRowsExcludedUntagged} houseResolved=${houseId ? houseId.slice(0, 8) : "null"} managerPhonesBound=${managerPhonesBound} managedStoreRows=${managedRows.length}`
|
|
15911
16426
|
);
|
|
15912
|
-
return c.json({ conversations
|
|
16427
|
+
return c.json({ conversations });
|
|
15913
16428
|
});
|
|
15914
16429
|
function tagAndMergeInstallConversations(perAccount) {
|
|
15915
16430
|
const all = [];
|
|
@@ -15920,7 +16435,7 @@ function tagAndMergeInstallConversations(perAccount) {
|
|
|
15920
16435
|
}
|
|
15921
16436
|
function dropHouseRowsOwnedBySubAccount(rows, subManagerPhones) {
|
|
15922
16437
|
return rows.filter((r) => {
|
|
15923
|
-
if (r.isHouse !== true || r.source !== "store" || r.senderId == null) return true;
|
|
16438
|
+
if (r.channel !== "whatsapp" || r.isHouse !== true || r.source !== "store" || r.senderId == null) return true;
|
|
15924
16439
|
let owner = null;
|
|
15925
16440
|
for (const [subId, phones] of subManagerPhones) {
|
|
15926
16441
|
if (counterpartyBoundToSubAccount(phones, r.senderId)) {
|
|
@@ -16060,7 +16575,14 @@ app8.get("/conversations-all", requireAdminSession, async (c) => {
|
|
|
16060
16575
|
readConversationSummaries(account.accountId),
|
|
16061
16576
|
readChatMetadata(account.accountId)
|
|
16062
16577
|
);
|
|
16063
|
-
|
|
16578
|
+
const telegramStoreRows = buildTelegramStoreConversationRows(
|
|
16579
|
+
account.accountId,
|
|
16580
|
+
readConversationSummaries2(account.accountId)
|
|
16581
|
+
);
|
|
16582
|
+
perAccount.push({
|
|
16583
|
+
isHouse,
|
|
16584
|
+
rows: [...dropTelegramSessionDuplicates(named, telegramStoreRows), ...storeRows, ...telegramStoreRows]
|
|
16585
|
+
});
|
|
16064
16586
|
}
|
|
16065
16587
|
const houseAccount = validAccounts.find((a) => a.accountId === houseId) ?? null;
|
|
16066
16588
|
const subManagerPhones = /* @__PURE__ */ new Map();
|
|
@@ -16078,16 +16600,19 @@ app8.get("/conversations-all", requireAdminSession, async (c) => {
|
|
|
16078
16600
|
for (const account of validAccounts) {
|
|
16079
16601
|
for (const [jid, s] of readConversationSummaries(account.accountId)) allSummaries.set(`${account.accountId}:${jid}`, s);
|
|
16080
16602
|
}
|
|
16081
|
-
const
|
|
16082
|
-
|
|
16603
|
+
const whatsappRows = conversations.filter((r) => r.channel === "whatsapp");
|
|
16604
|
+
const census = buildLabelCensus(whatsappRows, selfPushNames(allSummaries));
|
|
16605
|
+
for (const f of fallbackLabelSources(whatsappRows)) {
|
|
16083
16606
|
console.log(`[wa-label] op=row-label jid=${f.jid} source=${f.source} route=conversations-all`);
|
|
16084
16607
|
}
|
|
16085
16608
|
console.log(
|
|
16086
16609
|
`[wa-label-census] route=conversations-all chats=${census.chats} withMeta=${census.withMeta} groups=${census.groups} groupsWithSubject=${census.groupsWithSubject} labelledByMember=${census.labelledByMember} dms=${census.dms} namedDms=${census.namedDms} labelledBySelfPushName=${census.labelledBySelfPushName}`
|
|
16087
16610
|
);
|
|
16088
16611
|
const houseRows = conversations.filter((r) => r.isHouse).length;
|
|
16612
|
+
const storeByChannel = conversations.filter((r) => r.source === "store");
|
|
16613
|
+
const waStoreRows = storeByChannel.filter((r) => r.channel === "whatsapp").length;
|
|
16089
16614
|
console.log(
|
|
16090
|
-
`[wa-reader] op=conversations-all accounts=${validAccounts.length} rows=${conversations.length} house=${houseRows} sub=${conversations.length - houseRows} deduped=${merged.length - conversations.length}`
|
|
16615
|
+
`[wa-reader] op=conversations-all accounts=${validAccounts.length} rows=${conversations.length} house=${houseRows} sub=${conversations.length - houseRows} deduped=${merged.length - conversations.length} storeRowsByChannel=whatsapp:${waStoreRows},telegram:${storeByChannel.length - waStoreRows}`
|
|
16091
16616
|
);
|
|
16092
16617
|
return { conversations };
|
|
16093
16618
|
});
|
|
@@ -30168,7 +30693,7 @@ app55.get("/", requireAdminSession, async (c) => {
|
|
|
30168
30693
|
}
|
|
30169
30694
|
});
|
|
30170
30695
|
function mapRun(props) {
|
|
30171
|
-
const
|
|
30696
|
+
const num5 = (v) => {
|
|
30172
30697
|
if (v === null || v === void 0) return null;
|
|
30173
30698
|
if (typeof v === "number") return v;
|
|
30174
30699
|
if (typeof v === "object" && typeof v.toNumber === "function") {
|
|
@@ -30188,11 +30713,11 @@ function mapRun(props) {
|
|
|
30188
30713
|
startedAt: props.startedAt,
|
|
30189
30714
|
endedAt: props.endedAt ?? null,
|
|
30190
30715
|
finishedAt: props.finishedAt ?? null,
|
|
30191
|
-
ranMs:
|
|
30192
|
-
httpStatus:
|
|
30716
|
+
ranMs: num5(props.ranMs),
|
|
30717
|
+
httpStatus: num5(props.httpStatus),
|
|
30193
30718
|
dispatchResult: props.dispatchResult ?? null,
|
|
30194
30719
|
sessionKey: props.sessionKey ?? null,
|
|
30195
|
-
transcriptOffset:
|
|
30720
|
+
transcriptOffset: num5(props.transcriptOffset),
|
|
30196
30721
|
// Strict identity: an absent or null `bounded` means the boundary was never
|
|
30197
30722
|
// established, which is the false case.
|
|
30198
30723
|
bounded: props.bounded === true,
|
|
@@ -33931,7 +34456,7 @@ function formatConfigAuditLine(a, accountId) {
|
|
|
33931
34456
|
function formatRecordCensus(f, accountId) {
|
|
33932
34457
|
return `${RECORD_CENSUS_TAG}${accountId ? ` account=${accountId}` : ""} closedVisits=${f.closedVisits} withNote=${f.withNote} withMedia=${f.withMedia} medianRecordLagS=${f.medianRecordLagS} p90RecordLagS=${f.p90RecordLagS}`;
|
|
33933
34458
|
}
|
|
33934
|
-
function
|
|
34459
|
+
function num4(v) {
|
|
33935
34460
|
if (typeof v === "number") return v;
|
|
33936
34461
|
if (v && typeof v === "object" && "low" in v) {
|
|
33937
34462
|
return Number(v.low);
|
|
@@ -34027,15 +34552,15 @@ async function readCensusCounts(session, accountId, terminal, now) {
|
|
|
34027
34552
|
{ accountId, canonical: `^${CANONICAL_INSTANT_PATTERN}$` }
|
|
34028
34553
|
);
|
|
34029
34554
|
return {
|
|
34030
|
-
jobs:
|
|
34031
|
-
unallocated:
|
|
34555
|
+
jobs: num4(g?.get("jobs")),
|
|
34556
|
+
unallocated: num4(g?.get("unallocated")),
|
|
34032
34557
|
oldestUnallocatedAgeH,
|
|
34033
|
-
overdue:
|
|
34034
|
-
visitsOpen:
|
|
34035
|
-
visitsOpenOver24h:
|
|
34036
|
-
intakeUnreviewed:
|
|
34558
|
+
overdue: num4(g?.get("overdue")),
|
|
34559
|
+
visitsOpen: num4(vg?.get("visitsOpen")),
|
|
34560
|
+
visitsOpenOver24h: num4(vg?.get("visitsOpenOver24h")),
|
|
34561
|
+
intakeUnreviewed: num4(dg?.get("intakeUnreviewed")),
|
|
34037
34562
|
oldestIntakeAgeH,
|
|
34038
|
-
nonCanonicalInstants:
|
|
34563
|
+
nonCanonicalInstants: num4(nc.records[0]?.get("nonCanonicalInstants"))
|
|
34039
34564
|
};
|
|
34040
34565
|
}
|
|
34041
34566
|
async function readRecordCensus(session, accountId, terminal, now) {
|
|
@@ -34053,8 +34578,8 @@ async function readRecordCensus(session, accountId, terminal, now) {
|
|
|
34053
34578
|
let withMedia = 0;
|
|
34054
34579
|
const lags = [];
|
|
34055
34580
|
for (const g of r.records) {
|
|
34056
|
-
if (
|
|
34057
|
-
if (
|
|
34581
|
+
if (num4(g.get("notes")) > 0) withNote++;
|
|
34582
|
+
if (num4(g.get("media")) > 0) withMedia++;
|
|
34058
34583
|
const closedAt = g.get("closedAt");
|
|
34059
34584
|
const dueToLeaveAt = g.get("dueToLeaveAt");
|
|
34060
34585
|
if (typeof closedAt === "string" && typeof dueToLeaveAt === "string") {
|
|
@@ -35489,15 +36014,15 @@ function createWaChannelRoutes(deps) {
|
|
|
35489
36014
|
}
|
|
35490
36015
|
const interactive = body?.interactive === true;
|
|
35491
36016
|
const finalText = typeof body?.finalText === "string" ? body.finalText : "";
|
|
35492
|
-
const
|
|
35493
|
-
const finalBytes =
|
|
35494
|
-
const queueDepth =
|
|
35495
|
-
const pendingTurns =
|
|
35496
|
-
const consumed2 =
|
|
36017
|
+
const num5 = (v) => typeof v === "number" && Number.isFinite(v) ? v : -1;
|
|
36018
|
+
const finalBytes = num5(body?.finalBytes);
|
|
36019
|
+
const queueDepth = num5(body?.queueDepth);
|
|
36020
|
+
const pendingTurns = num5(body?.pendingTurns);
|
|
36021
|
+
const consumed2 = num5(body?.consumed);
|
|
35497
36022
|
const source = typeof body?.source === "string" ? body.source : null;
|
|
35498
36023
|
const sid = sessionId.slice(0, 8);
|
|
35499
36024
|
console.error(
|
|
35500
|
-
`[whatsapp-native] op=turn-context sessionId=${sid} cacheReadTokens=${
|
|
36025
|
+
`[whatsapp-native] op=turn-context sessionId=${sid} cacheReadTokens=${num5(body?.cacheReadTokens)} inputTokens=${num5(body?.inputTokens)} outputTokens=${num5(body?.outputTokens)} thinkingBlocks=${num5(body?.thinkingBlocks)} thinkingChars=${num5(body?.thinkingChars)}`
|
|
35501
36026
|
);
|
|
35502
36027
|
const role = body?.role;
|
|
35503
36028
|
if (role !== "admin" && role !== "public") {
|
|
@@ -36459,12 +36984,12 @@ function createWebchatChannelRoutes(deps) {
|
|
|
36459
36984
|
const finalBytes = typeof body?.finalBytes === "number" && Number.isFinite(body.finalBytes) ? body.finalBytes : 0;
|
|
36460
36985
|
const k = key2.slice(0, 8);
|
|
36461
36986
|
const sid = sessionId.slice(0, 8);
|
|
36462
|
-
const
|
|
36987
|
+
const num5 = (v) => typeof v === "number" && Number.isFinite(v) ? v : -1;
|
|
36463
36988
|
console.error(
|
|
36464
|
-
`[webchat-native] op=turn-context sessionId=${sid} cacheReadTokens=${
|
|
36989
|
+
`[webchat-native] op=turn-context sessionId=${sid} cacheReadTokens=${num5(body?.cacheReadTokens)} inputTokens=${num5(body?.inputTokens)} outputTokens=${num5(body?.outputTokens)} thinkingBlocks=${num5(body?.thinkingBlocks)} thinkingChars=${num5(body?.thinkingChars)}`
|
|
36465
36990
|
);
|
|
36466
|
-
const segmentStart =
|
|
36467
|
-
const segmentEnd =
|
|
36991
|
+
const segmentStart = num5(body?.segmentStart);
|
|
36992
|
+
const segmentEnd = num5(body?.segmentEnd);
|
|
36468
36993
|
const resolved = deps.onTurnEnd?.(key2, role, { sessionId, segmentStart, segmentEnd }) ?? "none";
|
|
36469
36994
|
const resolvedField = role === "public" ? ` resolved=${resolved}` : "";
|
|
36470
36995
|
console.error(`[webchat-native] op=turn-end channel=webchat key=${k} role=${role} sessionId=${sid} trailing=${finalBytes}${resolvedField}`);
|
|
@@ -37793,21 +38318,22 @@ function startNativeFileFollower(input) {
|
|
|
37793
38318
|
}
|
|
37794
38319
|
|
|
37795
38320
|
// app/lib/channel-delivery/follower-registry.ts
|
|
37796
|
-
function createFollowerRegistry(start) {
|
|
38321
|
+
function createFollowerRegistry(start, keyOf2 = (p) => p.senderId) {
|
|
37797
38322
|
const controllers = /* @__PURE__ */ new Map();
|
|
37798
38323
|
const params = /* @__PURE__ */ new Map();
|
|
37799
38324
|
return {
|
|
37800
38325
|
attach(p) {
|
|
37801
|
-
|
|
37802
|
-
|
|
38326
|
+
const doorId = keyOf2(p);
|
|
38327
|
+
controllers.get(doorId)?.abort();
|
|
38328
|
+
params.set(doorId, p);
|
|
37803
38329
|
let ac;
|
|
37804
38330
|
ac = start(p, () => {
|
|
37805
|
-
if (controllers.get(
|
|
38331
|
+
if (controllers.get(doorId) === ac) controllers.delete(doorId);
|
|
37806
38332
|
});
|
|
37807
|
-
controllers.set(
|
|
38333
|
+
controllers.set(doorId, ac);
|
|
37808
38334
|
},
|
|
37809
38335
|
live: () => [...controllers.keys()],
|
|
37810
|
-
paramsFor: (
|
|
38336
|
+
paramsFor: (doorId) => params.get(doorId) ?? null
|
|
37811
38337
|
};
|
|
37812
38338
|
}
|
|
37813
38339
|
|
|
@@ -37820,39 +38346,49 @@ var id82 = (s) => s.slice(0, 8);
|
|
|
37820
38346
|
function followerCensusTick(deps, state = makeCensusState()) {
|
|
37821
38347
|
const bound = deps.bound();
|
|
37822
38348
|
const live = new Set(deps.live());
|
|
37823
|
-
|
|
37824
|
-
const
|
|
38349
|
+
const label = deps.label ?? id82;
|
|
38350
|
+
for (const doorId of live) state.attempts.delete(doorId);
|
|
38351
|
+
const missing = bound.filter((doorId) => !live.has(doorId));
|
|
37825
38352
|
console.error(
|
|
37826
|
-
`[channel-follower] op=follower-census bound=${bound.length} attached=${live.size} missing=${missing.length} missingSenders=${missing.map(
|
|
38353
|
+
`[channel-follower] op=follower-census channel=${deps.channel} bound=${bound.length} attached=${live.size} missing=${missing.length} missingSenders=${missing.map(label).join(",") || "none"}`
|
|
37827
38354
|
);
|
|
37828
|
-
for (const
|
|
37829
|
-
const used = state.attempts.get(
|
|
38355
|
+
for (const doorId of missing) {
|
|
38356
|
+
const used = state.attempts.get(doorId) ?? 0;
|
|
37830
38357
|
if (used >= CENSUS_MAX_REATTEMPTS) {
|
|
37831
38358
|
console.error(
|
|
37832
|
-
`[channel-follower] op=follower-reattach sessionId=${id82(deps.paramsFor(
|
|
38359
|
+
`[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=${id82(deps.paramsFor(doorId)?.sessionId ?? "none")} senderId=${label(doorId)} outcome=skipped reason=reattach-exhausted attempts=${used}`
|
|
37833
38360
|
);
|
|
37834
38361
|
continue;
|
|
37835
38362
|
}
|
|
37836
|
-
const p = deps.paramsFor(
|
|
38363
|
+
const p = deps.paramsFor(doorId);
|
|
37837
38364
|
if (!p) {
|
|
37838
38365
|
console.error(
|
|
37839
|
-
`[channel-follower] op=follower-reattach sessionId=none senderId=${
|
|
38366
|
+
`[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=none senderId=${label(doorId)} outcome=failed reason=no-attach-params`
|
|
37840
38367
|
);
|
|
37841
38368
|
continue;
|
|
37842
38369
|
}
|
|
37843
38370
|
try {
|
|
37844
|
-
state.attempts.set(
|
|
38371
|
+
state.attempts.set(doorId, used + 1);
|
|
37845
38372
|
deps.attach(p);
|
|
37846
38373
|
console.error(
|
|
37847
|
-
`[channel-follower] op=follower-reattach sessionId=${id82(p.sessionId)} senderId=${
|
|
38374
|
+
`[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=${id82(p.sessionId)} senderId=${label(doorId)} outcome=attached reason=none`
|
|
37848
38375
|
);
|
|
37849
38376
|
} catch (err) {
|
|
37850
38377
|
console.error(
|
|
37851
|
-
`[channel-follower] op=follower-reattach sessionId=${id82(p.sessionId)} senderId=${
|
|
38378
|
+
`[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=${id82(p.sessionId)} senderId=${label(doorId)} outcome=failed reason=${err instanceof Error ? err.message : String(err)}`
|
|
37852
38379
|
);
|
|
37853
38380
|
}
|
|
37854
38381
|
}
|
|
37855
|
-
return `${bound.length} door(s), ${missing.length} without a reader`;
|
|
38382
|
+
return `${deps.channel}: ${bound.length} door(s), ${missing.length} without a reader`;
|
|
38383
|
+
}
|
|
38384
|
+
|
|
38385
|
+
// app/lib/channel-delivery/whatsapp-door.ts
|
|
38386
|
+
function whatsappDoorId(p) {
|
|
38387
|
+
return hubKey(p.effectiveAccountId, p.senderId);
|
|
38388
|
+
}
|
|
38389
|
+
function whatsappDoorLabel(doorId) {
|
|
38390
|
+
const [decoded] = decodeHubKeys([doorId]);
|
|
38391
|
+
return decoded ? `${id82(decoded.accountId)}:${id82(decoded.senderId)}` : id82(doorId);
|
|
37856
38392
|
}
|
|
37857
38393
|
|
|
37858
38394
|
// app/lib/telegram/gateway/inbound-hub.ts
|
|
@@ -38008,7 +38544,8 @@ function createTelegramChannelRoutes(deps) {
|
|
|
38008
38544
|
if (typeof key2 !== "string" || typeof messageId !== "string") {
|
|
38009
38545
|
return c.json({ error: "key and messageId required" }, 400);
|
|
38010
38546
|
}
|
|
38011
|
-
|
|
38547
|
+
const provenance = typeof body?.provenance === "boolean" ? body.provenance : void 0;
|
|
38548
|
+
deps.onReceived?.(key2, messageId, provenance);
|
|
38012
38549
|
return c.json({ ok: true });
|
|
38013
38550
|
});
|
|
38014
38551
|
app81.post("/tg-channel/turn-end", async (c) => {
|
|
@@ -38022,9 +38559,9 @@ function createTelegramChannelRoutes(deps) {
|
|
|
38022
38559
|
const interactive = body?.interactive === true;
|
|
38023
38560
|
const finalText = typeof body?.finalText === "string" ? body.finalText : "";
|
|
38024
38561
|
const sid = sessionId.slice(0, 8);
|
|
38025
|
-
const
|
|
38562
|
+
const num5 = (v) => typeof v === "number" && Number.isFinite(v) ? v : -1;
|
|
38026
38563
|
console.error(
|
|
38027
|
-
`[telegram-native] op=turn-context sessionId=${sid} cacheReadTokens=${
|
|
38564
|
+
`[telegram-native] op=turn-context sessionId=${sid} cacheReadTokens=${num5(body?.cacheReadTokens)} inputTokens=${num5(body?.inputTokens)} outputTokens=${num5(body?.outputTokens)} thinkingBlocks=${num5(body?.thinkingBlocks)} thinkingChars=${num5(body?.thinkingChars)}`
|
|
38028
38565
|
);
|
|
38029
38566
|
const role = body?.role;
|
|
38030
38567
|
if (role !== "admin" && role !== "public") {
|
|
@@ -38142,6 +38679,16 @@ var TelegramGateway = class {
|
|
|
38142
38679
|
}
|
|
38143
38680
|
hub = new InboundHub3();
|
|
38144
38681
|
replies = /* @__PURE__ */ new Map();
|
|
38682
|
+
/** Task 2559 — the keys whose last inbound was `role:'admin'`. The follower
|
|
38683
|
+
* census's bound set is the doors that SHOULD have a reader, and a public
|
|
38684
|
+
* Telegram session is given none by design: the public branch of
|
|
38685
|
+
* `buildTelegramEnsureSession` returns before the attach block, because the
|
|
38686
|
+
* file follower is an admin surface. Including a public key would report it
|
|
38687
|
+
* missing on every tick for ever and stop `missing=` ever reaching zero,
|
|
38688
|
+
* which is the one signal the census exists to give. Removed on a public
|
|
38689
|
+
* inbound, because the role is read from account config on every inbound and
|
|
38690
|
+
* a bot can be flipped between the two. */
|
|
38691
|
+
adminKeys = /* @__PURE__ */ new Set();
|
|
38145
38692
|
spawning = /* @__PURE__ */ new Set();
|
|
38146
38693
|
/** Task 2428 — keys whose channel server signalled ready while their spawn was
|
|
38147
38694
|
* still in flight. The harness discards a channel notification that arrives
|
|
@@ -38169,7 +38716,7 @@ var TelegramGateway = class {
|
|
|
38169
38716
|
}
|
|
38170
38717
|
this.drain(key2);
|
|
38171
38718
|
},
|
|
38172
|
-
onReceived: (key2, messageId) => {
|
|
38719
|
+
onReceived: (key2, messageId, provenance) => {
|
|
38173
38720
|
const acked = this.hub.ack(key2, messageId);
|
|
38174
38721
|
if (acked !== null && (acked.source ?? "user") === "user") {
|
|
38175
38722
|
noteReceived(key2, parseTelegramChannelKey(key2)?.botId ?? "unknown", messageId, Date.now());
|
|
@@ -38177,6 +38724,10 @@ var TelegramGateway = class {
|
|
|
38177
38724
|
console.error(
|
|
38178
38725
|
`[telegram-native] op=channel-received key=${key2} messageId=${messageId}${acked !== null ? "" : " note=unknown"}`
|
|
38179
38726
|
);
|
|
38727
|
+
const prov = provenance === void 0 ? "unknown" : provenance ? "yes" : "no";
|
|
38728
|
+
console.error(
|
|
38729
|
+
`[telegram:reframe] op=compose key=${key2} provenance=${prov} source=${acked?.source ?? "unknown"}`
|
|
38730
|
+
);
|
|
38180
38731
|
}
|
|
38181
38732
|
});
|
|
38182
38733
|
}
|
|
@@ -38249,6 +38800,8 @@ var TelegramGateway = class {
|
|
|
38249
38800
|
const willDeliverNow = this.hub.isReady(key2);
|
|
38250
38801
|
const source = input.source ?? "user";
|
|
38251
38802
|
this.replies.set(key2, input.reply);
|
|
38803
|
+
if (input.role === "admin") this.adminKeys.add(key2);
|
|
38804
|
+
else this.adminKeys.delete(key2);
|
|
38252
38805
|
let standingRules;
|
|
38253
38806
|
if (input.role === "admin" && this.deps.fetchStandingRules) {
|
|
38254
38807
|
const res = await this.deps.fetchStandingRules(input.accountId);
|
|
@@ -38260,13 +38813,36 @@ var TelegramGateway = class {
|
|
|
38260
38813
|
);
|
|
38261
38814
|
}
|
|
38262
38815
|
this.hub.deliver(
|
|
38263
|
-
{
|
|
38816
|
+
{
|
|
38817
|
+
key: key2,
|
|
38818
|
+
text: input.text,
|
|
38819
|
+
media: input.media,
|
|
38820
|
+
messageId: `tg-${++this.seq}`,
|
|
38821
|
+
source,
|
|
38822
|
+
// Task 2561 — passed explicitly rather than parsed out of `key`:
|
|
38823
|
+
// accountId is not in the key, and the channel service imports nothing
|
|
38824
|
+
// from app/lib. Without them the composer cannot name who is speaking,
|
|
38825
|
+
// and the turn opens straight into rules written about the owner.
|
|
38826
|
+
senderId: input.senderId,
|
|
38827
|
+
accountId: input.accountId,
|
|
38828
|
+
standingRules,
|
|
38829
|
+
scheduleProvenance: input.scheduleProvenance
|
|
38830
|
+
},
|
|
38264
38831
|
Date.now()
|
|
38265
38832
|
);
|
|
38266
38833
|
const resolvedSessionId = this.sessionIdFor(input);
|
|
38267
38834
|
console.error(
|
|
38268
38835
|
`[telegram-native] op=inbound key=${key2} botId=${input.botId} accountId=${input.accountId} role=${input.role} agent=${input.agentSlug} bytes=${bytes} source=${source} delivery=${willDeliverNow ? "immediate" : "queued"} sessionId=${resolvedSessionId}`
|
|
38269
38836
|
);
|
|
38837
|
+
try {
|
|
38838
|
+
if (source === "user") {
|
|
38839
|
+
this.deps.sendChatAction?.({ botToken: input.botToken, chatId: input.chatId, key: key2 });
|
|
38840
|
+
}
|
|
38841
|
+
} catch (err) {
|
|
38842
|
+
console.error(
|
|
38843
|
+
`[telegram-native] op=chat-action key=${key2} site=inbound ok=false reason=${err instanceof Error ? err.message : String(err)}`
|
|
38844
|
+
);
|
|
38845
|
+
}
|
|
38270
38846
|
if (!hadSubscriber && !this.spawning.has(key2)) {
|
|
38271
38847
|
this.spawning.add(key2);
|
|
38272
38848
|
console.error(`[telegram-native] op=spawn-trigger key=${key2} botId=${input.botId} role=${input.role}`);
|
|
@@ -38300,6 +38876,18 @@ var TelegramGateway = class {
|
|
|
38300
38876
|
liveReplyTarget(key2) {
|
|
38301
38877
|
return this.replies.get(key2) ?? null;
|
|
38302
38878
|
}
|
|
38879
|
+
/** Task 2559 — every admin door this gateway currently holds a live delivery
|
|
38880
|
+
* closure for, as the `<botId>:<senderId>` channel key the follower registry
|
|
38881
|
+
* keys on. The twin of `WaGateway.boundTargets()`: the census reconciles
|
|
38882
|
+
* against exactly this set, so "should have a reader" and "can be sent to"
|
|
38883
|
+
* are one fact and cannot drift. A key `parseTelegramChannelKey` rejects is
|
|
38884
|
+
* the pre-2368 bare-sender shape, skipped rather than halved at an arbitrary
|
|
38885
|
+
* offset, which would yield a door id nothing keys on. */
|
|
38886
|
+
boundTargets() {
|
|
38887
|
+
return [...this.replies.keys()].filter(
|
|
38888
|
+
(key2) => this.adminKeys.has(key2) && parseTelegramChannelKey(key2) !== null
|
|
38889
|
+
);
|
|
38890
|
+
}
|
|
38303
38891
|
async sendOutbound(key2, text) {
|
|
38304
38892
|
const reply = this.replies.get(key2);
|
|
38305
38893
|
if (!reply) throw new Error(`telegram-gateway: no reply route for key ${key2}`);
|
|
@@ -38394,24 +38982,13 @@ function buildTelegramEnsureSession(deps) {
|
|
|
38394
38982
|
`${TAG62} op=spawn botId=${input.botId} sessionId=${req.sessionId} targetAccountId=${input.accountId} role=admin`
|
|
38395
38983
|
);
|
|
38396
38984
|
noteSpawnOk2(input.botId);
|
|
38397
|
-
deps.followers.
|
|
38398
|
-
let ac;
|
|
38399
|
-
ac = deps.startFileFollower({
|
|
38985
|
+
deps.followers.attach({
|
|
38400
38986
|
sessionId: req.sessionId,
|
|
38401
38987
|
senderId: req.channelKey,
|
|
38402
38988
|
accountId: input.accountId,
|
|
38403
38989
|
chatId: input.chatId,
|
|
38404
|
-
botToken: input.botToken
|
|
38405
|
-
// Task 2521 — resolved per answer, not captured once, so a door that goes
|
|
38406
|
-
// live after this session started still receives.
|
|
38407
|
-
fanTargets: () => deps.fanTargets(req.channelKey),
|
|
38408
|
-
// Identity-guarded eviction: only delete if this controller is still the
|
|
38409
|
-
// current one, so a stale self-close never removes a live follower.
|
|
38410
|
-
onClose: () => {
|
|
38411
|
-
if (deps.followers.get(req.channelKey) === ac) deps.followers.delete(req.channelKey);
|
|
38412
|
-
}
|
|
38990
|
+
botToken: input.botToken
|
|
38413
38991
|
});
|
|
38414
|
-
deps.followers.set(req.channelKey, ac);
|
|
38415
38992
|
};
|
|
38416
38993
|
}
|
|
38417
38994
|
|
|
@@ -38530,6 +39107,7 @@ function makeTelegramFileDelivery(entry2, botToken) {
|
|
|
38530
39107
|
}
|
|
38531
39108
|
|
|
38532
39109
|
// app/lib/telegram/gateway/native-file-follower.ts
|
|
39110
|
+
var CHAT_ACTION_REFRESH_MS = 4e3;
|
|
38533
39111
|
function startTelegramNativeFileFollower(input) {
|
|
38534
39112
|
const entry2 = {
|
|
38535
39113
|
sessionId: input.sessionId,
|
|
@@ -38549,6 +39127,16 @@ function startTelegramNativeFileFollower(input) {
|
|
|
38549
39127
|
// The chat id the document is addressed to (set per spawn from the inbound).
|
|
38550
39128
|
replyTarget: input.chatId
|
|
38551
39129
|
};
|
|
39130
|
+
let lastChatActionAt = Number.NEGATIVE_INFINITY;
|
|
39131
|
+
const onActivity = () => {
|
|
39132
|
+
const now = Date.now();
|
|
39133
|
+
if (now - lastChatActionAt < CHAT_ACTION_REFRESH_MS) return;
|
|
39134
|
+
lastChatActionAt = now;
|
|
39135
|
+
void sendTelegramChatAction(input.botToken, input.chatId, input.senderId, "activity");
|
|
39136
|
+
};
|
|
39137
|
+
const onTurnComplete = () => {
|
|
39138
|
+
lastChatActionAt = Number.NEGATIVE_INFINITY;
|
|
39139
|
+
};
|
|
38552
39140
|
return startEmitter({
|
|
38553
39141
|
entry: entry2,
|
|
38554
39142
|
tag: "[telegram-adaptor]",
|
|
@@ -38562,6 +39150,8 @@ function startTelegramNativeFileFollower(input) {
|
|
|
38562
39150
|
// A resumed session's JSONL already holds prior SendUserFile tool_uses;
|
|
38563
39151
|
// suppress replay so historical files are not re-sent on attach.
|
|
38564
39152
|
suppressResumeReplay: true,
|
|
39153
|
+
onActivity,
|
|
39154
|
+
onTurnComplete,
|
|
38565
39155
|
// Task 2521 — the answer reaches every bound door through the one fan-out.
|
|
38566
39156
|
// Ungated by design: whatever woke the turn, the reader and the chats show
|
|
38567
39157
|
// the same thing.
|
|
@@ -38995,6 +39585,15 @@ function* walkTelegramSidecars() {
|
|
|
38995
39585
|
}
|
|
38996
39586
|
}
|
|
38997
39587
|
|
|
39588
|
+
// app/lib/telegram/gateway/origin-target.ts
|
|
39589
|
+
function answeringOriginTarget(send, key2, noteAnswered3) {
|
|
39590
|
+
if (!send) return null;
|
|
39591
|
+
return async (text) => {
|
|
39592
|
+
await send(text);
|
|
39593
|
+
noteAnswered3(key2);
|
|
39594
|
+
};
|
|
39595
|
+
}
|
|
39596
|
+
|
|
38998
39597
|
// server/telegram-migrate.ts
|
|
38999
39598
|
import { readFileSync as readFileSync53, writeFileSync as writeFileSync19, existsSync as existsSync53, renameSync as renameSync11, mkdirSync as mkdirSync11 } from "fs";
|
|
39000
39599
|
import { dirname as dirname18, join as join63 } from "path";
|
|
@@ -39495,7 +40094,12 @@ var nativeFileFollowers = createFollowerRegistry(
|
|
|
39495
40094
|
// delivered session 8e5bc4e8's answer to nothing.
|
|
39496
40095
|
fanTargets: () => whatsappFanTargets(p.effectiveAccountId, p.senderId),
|
|
39497
40096
|
onClose
|
|
39498
|
-
})
|
|
40097
|
+
}),
|
|
40098
|
+
// Task 2569 — the door is (effective account, sender), the same pair the
|
|
40099
|
+
// reply map and the session id key on. Keyed on the sender alone, a second
|
|
40100
|
+
// account's spawn for the same phone aborted the first account's live reader
|
|
40101
|
+
// and overwrote its recorded parameters, and the census called that healthy.
|
|
40102
|
+
whatsappDoorId
|
|
39499
40103
|
);
|
|
39500
40104
|
var webchatFileFollowers = /* @__PURE__ */ new Map();
|
|
39501
40105
|
async function fetchAccountStandingRules(accountId) {
|
|
@@ -39736,20 +40340,6 @@ var waGateway = new WaGateway({
|
|
|
39736
40340
|
});
|
|
39737
40341
|
}
|
|
39738
40342
|
});
|
|
39739
|
-
var followerCensusState = makeCensusState();
|
|
39740
|
-
registerLoop({
|
|
39741
|
-
name: "channel-follower",
|
|
39742
|
-
intervalMs: 6e4,
|
|
39743
|
-
run: () => followerCensusTick(
|
|
39744
|
-
{
|
|
39745
|
-
bound: () => waGateway.boundTargets(),
|
|
39746
|
-
live: () => nativeFileFollowers.live(),
|
|
39747
|
-
paramsFor: (senderId) => nativeFileFollowers.paramsFor(senderId),
|
|
39748
|
-
attach: (p) => nativeFileFollowers.attach(p)
|
|
39749
|
-
},
|
|
39750
|
-
followerCensusState
|
|
39751
|
-
)
|
|
39752
|
-
});
|
|
39753
40343
|
app80.route("/", waGateway.routes());
|
|
39754
40344
|
waGateway.startSweeper();
|
|
39755
40345
|
registerLoop({
|
|
@@ -39917,7 +40507,6 @@ setWebchatGateway(webchatGateway);
|
|
|
39917
40507
|
app80.route("/", webchatGateway.routes());
|
|
39918
40508
|
webchatGateway.startSweeper();
|
|
39919
40509
|
webchatGateway.startPublicReaper();
|
|
39920
|
-
var telegramFileFollowers = /* @__PURE__ */ new Map();
|
|
39921
40510
|
var telegramFanTargets = (key2) => {
|
|
39922
40511
|
const account = listValidAccounts().find(
|
|
39923
40512
|
(a) => listBotEntries(a.config.telegram).some((e) => e.id === key2.slice(0, key2.indexOf(":")))
|
|
@@ -39934,6 +40523,19 @@ var telegramFanTargets = (key2) => {
|
|
|
39934
40523
|
deps: adminFanDeps("telegram", account.accountId)
|
|
39935
40524
|
});
|
|
39936
40525
|
};
|
|
40526
|
+
var telegramFileFollowers = createFollowerRegistry(
|
|
40527
|
+
(p, onClose) => startTelegramNativeFileFollower({
|
|
40528
|
+
sessionId: p.sessionId,
|
|
40529
|
+
senderId: p.senderId,
|
|
40530
|
+
accountId: p.accountId,
|
|
40531
|
+
chatId: p.chatId,
|
|
40532
|
+
botToken: p.botToken,
|
|
40533
|
+
// Task 2521 — resolved per answer, not captured once, so a door that goes
|
|
40534
|
+
// live after this session started still receives.
|
|
40535
|
+
fanTargets: () => telegramFanTargets(p.senderId),
|
|
40536
|
+
onClose
|
|
40537
|
+
})
|
|
40538
|
+
);
|
|
39937
40539
|
var telegramGateway = new TelegramGateway({
|
|
39938
40540
|
fetchStandingRules: fetchAccountStandingRules,
|
|
39939
40541
|
// Task 2353 — no turn-instruction resolver on this door. See the dep comment in
|
|
@@ -39945,6 +40547,12 @@ var telegramGateway = new TelegramGateway({
|
|
|
39945
40547
|
resolveSessionOverride: (accountId, channelKey) => resolveChannelOverride(accountId, "telegram", channelKey),
|
|
39946
40548
|
// Task 2521 — hoisted above; see `telegramFanTargets`.
|
|
39947
40549
|
fanTargetsFor: telegramFanTargets,
|
|
40550
|
+
// Task 2576 — the typing indicator. Injected here because the gateway is
|
|
40551
|
+
// contracted to hold no Bot API import; see the dep comment in
|
|
40552
|
+
// telegram-gateway.ts. Not awaited: an indicator never paces a turn.
|
|
40553
|
+
sendChatAction: ({ botToken, chatId, key: key2 }) => {
|
|
40554
|
+
void sendTelegramChatAction(botToken, chatId, key2, "inbound");
|
|
40555
|
+
},
|
|
39948
40556
|
gatewayUrl: telegramGatewayUrl(),
|
|
39949
40557
|
serverPath: telegramServerPath(),
|
|
39950
40558
|
// Task 2368 — the role branch lives in its own module so it has a test that
|
|
@@ -39954,14 +40562,45 @@ var telegramGateway = new TelegramGateway({
|
|
|
39954
40562
|
managerRcSpawn,
|
|
39955
40563
|
managerSpawn,
|
|
39956
40564
|
resolveSessionOverride: (accountId, channelKey) => resolveChannelOverride(accountId, "telegram", channelKey),
|
|
39957
|
-
startFileFollower: startTelegramNativeFileFollower,
|
|
39958
|
-
fanTargets: telegramFanTargets,
|
|
39959
40565
|
followers: telegramFileFollowers
|
|
39960
40566
|
})
|
|
39961
40567
|
});
|
|
39962
40568
|
setTelegramGateway(telegramGateway);
|
|
39963
40569
|
app80.route("/", telegramGateway.routes());
|
|
39964
40570
|
telegramGateway.startSweeper();
|
|
40571
|
+
var followerCensusState = { whatsapp: makeCensusState(), telegram: makeCensusState() };
|
|
40572
|
+
registerLoop({
|
|
40573
|
+
name: "channel-follower",
|
|
40574
|
+
intervalMs: 6e4,
|
|
40575
|
+
run: () => [
|
|
40576
|
+
followerCensusTick(
|
|
40577
|
+
{
|
|
40578
|
+
channel: "whatsapp",
|
|
40579
|
+
// Task 2569 — the whole hub key, not its sender half. `boundTargets()`
|
|
40580
|
+
// decodes the reply-map keys and this re-encodes them through the same
|
|
40581
|
+
// helper pair, so the census and the gateway compare one fact. Halving
|
|
40582
|
+
// it here is what let one phone's two accounts collapse to one door.
|
|
40583
|
+
bound: () => waGateway.boundTargets().map((t) => hubKey(t.accountId, t.senderId)),
|
|
40584
|
+
live: () => nativeFileFollowers.live(),
|
|
40585
|
+
paramsFor: (doorId) => nativeFileFollowers.paramsFor(doorId),
|
|
40586
|
+
attach: (p) => nativeFileFollowers.attach(p),
|
|
40587
|
+
// The hub key carries a NUL, which must never reach server.log.
|
|
40588
|
+
label: whatsappDoorLabel
|
|
40589
|
+
},
|
|
40590
|
+
followerCensusState.whatsapp
|
|
40591
|
+
),
|
|
40592
|
+
followerCensusTick(
|
|
40593
|
+
{
|
|
40594
|
+
channel: "telegram",
|
|
40595
|
+
bound: () => telegramGateway.boundTargets(),
|
|
40596
|
+
live: () => telegramFileFollowers.live(),
|
|
40597
|
+
paramsFor: (doorId) => telegramFileFollowers.paramsFor(doorId),
|
|
40598
|
+
attach: (p) => telegramFileFollowers.attach(p)
|
|
40599
|
+
},
|
|
40600
|
+
followerCensusState.telegram
|
|
40601
|
+
)
|
|
40602
|
+
].join("; ")
|
|
40603
|
+
});
|
|
39965
40604
|
var chatRoutes = createChatRoutes({
|
|
39966
40605
|
handleInbound: (input) => webchatGateway.handleInbound(input),
|
|
39967
40606
|
awaitReply: (key2, timeoutMs) => webchatGateway.awaitReply(key2, timeoutMs)
|
|
@@ -41588,7 +42227,7 @@ function whatsappSender(accountId, key2) {
|
|
|
41588
42227
|
};
|
|
41589
42228
|
}
|
|
41590
42229
|
function telegramOriginTarget(key2) {
|
|
41591
|
-
return telegramGateway.liveReplyTarget(key2);
|
|
42230
|
+
return answeringOriginTarget(telegramGateway.liveReplyTarget(key2), key2, noteAnswered);
|
|
41592
42231
|
}
|
|
41593
42232
|
var configDirForWhatsApp = basename17(MAXY_DIR) || ".maxy";
|
|
41594
42233
|
var bootAccount = resolveAccount();
|