@rubytech/create-maxy-code 0.1.575 → 0.1.577
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/dist/__tests__/preserve-house-cloudflare.test.js +2 -72
- package/dist/index.js +8 -59
- package/dist/preserve-house-cloudflare.js +2 -49
- 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/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 +2 -2
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -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/telegram/mcp/dist/__tests__/webhook-path.test.d.ts +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.js +19 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.d.ts +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.js +110 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.d.ts +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.js +22 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/index.js +19 -163
- package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts +9 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js +11 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts +30 -3
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js +29 -4
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.d.ts +33 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.js +145 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.js.map +1 -0
- package/payload/platform/plugins/telegram/skills/configure/SKILL.md +4 -2
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +5 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +29 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.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/server.js +670 -155
- package/dist/__tests__/converge-client-admins.test.js +0 -50
package/payload/server/server.js
CHANGED
|
@@ -757,7 +757,7 @@ var init_parse_transcript = __esm({
|
|
|
757
757
|
webchat: "webchat",
|
|
758
758
|
"webchat-channel": "webchat"
|
|
759
759
|
};
|
|
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]*$/;
|
|
760
|
+
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
761
|
SCHEDULED_MARKER_PREFIX = "## Scheduled automation (platform-verified)\n";
|
|
762
762
|
UPLOAD_ANNOTATION = /(?:\n\n)?\[The operator uploaded \d+ file\(s\): [\s\S]*\]$/;
|
|
763
763
|
REPLY_NOTE = /^\[In reply to an earlier message in this conversation(?:\.|: "([\s\S]*?)")\]\n\n/;
|
|
@@ -1911,8 +1911,9 @@ var require_dist2 = __commonJS({
|
|
|
1911
1911
|
if (rules.length === 0)
|
|
1912
1912
|
return "";
|
|
1913
1913
|
const lines = rules.map((r) => `- ${r.value}`);
|
|
1914
|
-
return `## Standing rules
|
|
1915
|
-
|
|
1914
|
+
return `## Standing rules (account owner)
|
|
1915
|
+
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.
|
|
1916
|
+
` + lines.join("\n");
|
|
1916
1917
|
}
|
|
1917
1918
|
async function resolveOwnerUserId3(session, accountId) {
|
|
1918
1919
|
const res = await session.run(`
|
|
@@ -3018,6 +3019,40 @@ var require_keyboard = __commonJS({
|
|
|
3018
3019
|
}
|
|
3019
3020
|
});
|
|
3020
3021
|
|
|
3022
|
+
// ../lib/dispatch-read/dist/allocate.js
|
|
3023
|
+
var require_allocate = __commonJS({
|
|
3024
|
+
"../lib/dispatch-read/dist/allocate.js"(exports) {
|
|
3025
|
+
"use strict";
|
|
3026
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3027
|
+
exports.selectCandidates = selectCandidates2;
|
|
3028
|
+
exports.overlaps = overlaps2;
|
|
3029
|
+
function selectCandidates2(params) {
|
|
3030
|
+
const { jobType, cfg, commitments } = params;
|
|
3031
|
+
const type = cfg.jobTypes.find((t) => t.name === jobType);
|
|
3032
|
+
if (!type) {
|
|
3033
|
+
return { offered: [], excluded: [], required: "" };
|
|
3034
|
+
}
|
|
3035
|
+
const offered = [];
|
|
3036
|
+
const excluded = [];
|
|
3037
|
+
for (const worker of cfg.workers) {
|
|
3038
|
+
if (!worker.qualifications.includes(type.requiredQualification)) {
|
|
3039
|
+
excluded.push({ workerId: worker.workerId, reason: "missing-qualification" });
|
|
3040
|
+
continue;
|
|
3041
|
+
}
|
|
3042
|
+
offered.push({
|
|
3043
|
+
workerId: worker.workerId,
|
|
3044
|
+
name: worker.name,
|
|
3045
|
+
conflicts: commitments.get(worker.workerId) ?? []
|
|
3046
|
+
});
|
|
3047
|
+
}
|
|
3048
|
+
return { offered, excluded, required: type.requiredQualification };
|
|
3049
|
+
}
|
|
3050
|
+
function overlaps2(a, b) {
|
|
3051
|
+
return a.start < b.end && b.start < a.end;
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
});
|
|
3055
|
+
|
|
3021
3056
|
// ../lib/dispatch-read/dist/session.js
|
|
3022
3057
|
var require_session = __commonJS({
|
|
3023
3058
|
"../lib/dispatch-read/dist/session.js"(exports) {
|
|
@@ -3217,7 +3252,7 @@ var require_read = __commonJS({
|
|
|
3217
3252
|
exports.readJobList = readJobList2;
|
|
3218
3253
|
exports.readCounts = readCounts2;
|
|
3219
3254
|
var session_js_1 = require_session();
|
|
3220
|
-
function
|
|
3255
|
+
function num5(v) {
|
|
3221
3256
|
if (typeof v === "number")
|
|
3222
3257
|
return v;
|
|
3223
3258
|
if (v && typeof v === "object" && "low" in v) {
|
|
@@ -3241,7 +3276,7 @@ var require_read = __commonJS({
|
|
|
3241
3276
|
workerId: String(a.workerId),
|
|
3242
3277
|
arrivedPressedAt: str2(a.arrivedPressedAt),
|
|
3243
3278
|
arrivedObservedAt: str2(a.arrivedObservedAt),
|
|
3244
|
-
arrivedObservedDistanceM: a.arrivedObservedDistanceM == null ? null :
|
|
3279
|
+
arrivedObservedDistanceM: a.arrivedObservedDistanceM == null ? null : num5(a.arrivedObservedDistanceM)
|
|
3245
3280
|
})).sort((x, y) => x.workerId < y.workerId ? -1 : x.workerId > y.workerId ? 1 : 0);
|
|
3246
3281
|
const sole = attendances.length === 1 ? attendances[0] : null;
|
|
3247
3282
|
return {
|
|
@@ -3334,12 +3369,12 @@ var require_read = __commonJS({
|
|
|
3334
3369
|
}
|
|
3335
3370
|
const g = r.records[0];
|
|
3336
3371
|
return {
|
|
3337
|
-
notes:
|
|
3338
|
-
media:
|
|
3339
|
-
parts:
|
|
3340
|
-
checklistDone:
|
|
3341
|
-
checklistTotal:
|
|
3342
|
-
signatures:
|
|
3372
|
+
notes: num5(g.get("notes")),
|
|
3373
|
+
media: num5(g.get("media")),
|
|
3374
|
+
parts: num5(g.get("parts")),
|
|
3375
|
+
checklistDone: num5(g.get("checklistDone")),
|
|
3376
|
+
checklistTotal: num5(g.get("checklistTotal")),
|
|
3377
|
+
signatures: num5(g.get("signatures"))
|
|
3343
3378
|
};
|
|
3344
3379
|
}
|
|
3345
3380
|
async function readVisitsForJob3(session, accountId, jobId) {
|
|
@@ -3392,7 +3427,7 @@ var require_read = __commonJS({
|
|
|
3392
3427
|
workerId,
|
|
3393
3428
|
arrivedPressedAt: str2(g.get("ownArrivedPressedAt")),
|
|
3394
3429
|
arrivedObservedAt: str2(g.get("ownArrivedObservedAt")),
|
|
3395
|
-
arrivedObservedDistanceM: dist == null ? null :
|
|
3430
|
+
arrivedObservedDistanceM: dist == null ? null : num5(dist)
|
|
3396
3431
|
};
|
|
3397
3432
|
});
|
|
3398
3433
|
}
|
|
@@ -3517,9 +3552,9 @@ var require_read = __commonJS({
|
|
|
3517
3552
|
const rad = g.get("geofenceRadiusM");
|
|
3518
3553
|
return {
|
|
3519
3554
|
siteId: String(g.get("siteId")),
|
|
3520
|
-
latitude: lat == null ? null :
|
|
3521
|
-
longitude: lon == null ? null :
|
|
3522
|
-
geofenceRadiusM: rad == null ? null :
|
|
3555
|
+
latitude: lat == null ? null : num5(lat),
|
|
3556
|
+
longitude: lon == null ? null : num5(lon),
|
|
3557
|
+
geofenceRadiusM: rad == null ? null : num5(rad)
|
|
3523
3558
|
};
|
|
3524
3559
|
}
|
|
3525
3560
|
var DRAFT_RETURN = `d.draftId AS draftId, d.receivedAt AS receivedAt, d.source AS source,
|
|
@@ -3584,7 +3619,7 @@ var require_read = __commonJS({
|
|
|
3584
3619
|
notes: g.get("notes") ?? [],
|
|
3585
3620
|
// A count, not the references. A worker at a door needs to know whether
|
|
3586
3621
|
// photographs exist before deciding to load them on a bad signal.
|
|
3587
|
-
mediaCount:
|
|
3622
|
+
mediaCount: num5(g.get("mediaCount") ?? 0),
|
|
3588
3623
|
...projectAttendances2(g)
|
|
3589
3624
|
}));
|
|
3590
3625
|
}
|
|
@@ -3709,7 +3744,7 @@ var require_read = __commonJS({
|
|
|
3709
3744
|
workerId,
|
|
3710
3745
|
arrivedPressedAt: str2(g.get("ownArrivedPressedAt")),
|
|
3711
3746
|
arrivedObservedAt: str2(g.get("ownArrivedObservedAt")),
|
|
3712
|
-
arrivedObservedDistanceM: dist == null ? null :
|
|
3747
|
+
arrivedObservedDistanceM: dist == null ? null : num5(dist)
|
|
3713
3748
|
});
|
|
3714
3749
|
});
|
|
3715
3750
|
});
|
|
@@ -3749,9 +3784,9 @@ var require_read = __commonJS({
|
|
|
3749
3784
|
sum(CASE WHEN overdue THEN 1 ELSE 0 END) AS overdue`, { accountId, nowIso, terminalJob: terminalJobStatuses2 });
|
|
3750
3785
|
const g = r.records[0];
|
|
3751
3786
|
return {
|
|
3752
|
-
total:
|
|
3753
|
-
unallocated:
|
|
3754
|
-
overdue:
|
|
3787
|
+
total: num5(g?.get("total")),
|
|
3788
|
+
unallocated: num5(g?.get("unallocated")),
|
|
3789
|
+
overdue: num5(g?.get("overdue"))
|
|
3755
3790
|
};
|
|
3756
3791
|
});
|
|
3757
3792
|
}
|
|
@@ -3792,7 +3827,7 @@ var require_dist3 = __commonJS({
|
|
|
3792
3827
|
"../lib/dispatch-read/dist/index.js"(exports) {
|
|
3793
3828
|
"use strict";
|
|
3794
3829
|
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;
|
|
3830
|
+
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
3831
|
var config_js_1 = require_config();
|
|
3797
3832
|
Object.defineProperty(exports, "RECORDABLE_FIELDS", { enumerable: true, get: function() {
|
|
3798
3833
|
return config_js_1.RECORDABLE_FIELDS;
|
|
@@ -3822,6 +3857,13 @@ var require_dist3 = __commonJS({
|
|
|
3822
3857
|
Object.defineProperty(exports, "parseDispatchCallback", { enumerable: true, get: function() {
|
|
3823
3858
|
return keyboard_js_1.parseDispatchCallback;
|
|
3824
3859
|
} });
|
|
3860
|
+
var allocate_js_1 = require_allocate();
|
|
3861
|
+
Object.defineProperty(exports, "overlaps", { enumerable: true, get: function() {
|
|
3862
|
+
return allocate_js_1.overlaps;
|
|
3863
|
+
} });
|
|
3864
|
+
Object.defineProperty(exports, "selectCandidates", { enumerable: true, get: function() {
|
|
3865
|
+
return allocate_js_1.selectCandidates;
|
|
3866
|
+
} });
|
|
3825
3867
|
var session_js_1 = require_session();
|
|
3826
3868
|
Object.defineProperty(exports, "withSession", { enumerable: true, get: function() {
|
|
3827
3869
|
return session_js_1.withSession;
|
|
@@ -4259,9 +4301,9 @@ var require_dist5 = __commonJS({
|
|
|
4259
4301
|
try {
|
|
4260
4302
|
await onEmpty(c);
|
|
4261
4303
|
} catch (err) {
|
|
4262
|
-
const
|
|
4304
|
+
const id83 = c.elementId.slice(0, 8);
|
|
4263
4305
|
const message = err instanceof Error ? err.message : String(err);
|
|
4264
|
-
process.stderr.write(`[trash:empty-run] reason=onEmpty-failed elementId=${
|
|
4306
|
+
process.stderr.write(`[trash:empty-run] reason=onEmpty-failed elementId=${id83} labels=${c.labels.join(",")} message=${message}
|
|
4265
4307
|
`);
|
|
4266
4308
|
continue;
|
|
4267
4309
|
}
|
|
@@ -12706,6 +12748,32 @@ function buildVisitKeyboard(p) {
|
|
|
12706
12748
|
return { rows, dropped };
|
|
12707
12749
|
}
|
|
12708
12750
|
|
|
12751
|
+
// ../lib/dispatch-read/src/allocate.ts
|
|
12752
|
+
function selectCandidates(params) {
|
|
12753
|
+
const { jobType, cfg, commitments } = params;
|
|
12754
|
+
const type = cfg.jobTypes.find((t) => t.name === jobType);
|
|
12755
|
+
if (!type) {
|
|
12756
|
+
return { offered: [], excluded: [], required: "" };
|
|
12757
|
+
}
|
|
12758
|
+
const offered = [];
|
|
12759
|
+
const excluded = [];
|
|
12760
|
+
for (const worker of cfg.workers) {
|
|
12761
|
+
if (!worker.qualifications.includes(type.requiredQualification)) {
|
|
12762
|
+
excluded.push({ workerId: worker.workerId, reason: "missing-qualification" });
|
|
12763
|
+
continue;
|
|
12764
|
+
}
|
|
12765
|
+
offered.push({
|
|
12766
|
+
workerId: worker.workerId,
|
|
12767
|
+
name: worker.name,
|
|
12768
|
+
conflicts: commitments.get(worker.workerId) ?? []
|
|
12769
|
+
});
|
|
12770
|
+
}
|
|
12771
|
+
return { offered, excluded, required: type.requiredQualification };
|
|
12772
|
+
}
|
|
12773
|
+
function overlaps(a, b) {
|
|
12774
|
+
return a.start < b.end && b.start < a.end;
|
|
12775
|
+
}
|
|
12776
|
+
|
|
12709
12777
|
// ../lib/dispatch-read/src/session.ts
|
|
12710
12778
|
import { readFileSync as readFileSync15 } from "fs";
|
|
12711
12779
|
import { resolve as resolve12 } from "path";
|
|
@@ -12989,6 +13057,30 @@ async function readOpenVisitsForWorker(session, accountId, workerId, terminalSta
|
|
|
12989
13057
|
};
|
|
12990
13058
|
});
|
|
12991
13059
|
}
|
|
13060
|
+
async function readCommitments(session, accountId, from, to) {
|
|
13061
|
+
const r = await session.run(
|
|
13062
|
+
// One row per ATTENDANCE is correct here and nowhere else: a commitment
|
|
13063
|
+
// belongs to a worker, so a two-worker visit legitimately commits both.
|
|
13064
|
+
`MATCH (v:Visit {accountId: $accountId})-[:HAS_ATTENDANCE]->(:Attendance)-[:BY_ENGINEER]->(e:Engineer)
|
|
13065
|
+
WHERE v.startDate < $to AND coalesce(v.endDate, v.startDate) > $from
|
|
13066
|
+
RETURN e.engineerId AS workerId, v.visitId AS visitId,
|
|
13067
|
+
v.startDate AS start, coalesce(v.endDate, v.startDate) AS end`,
|
|
13068
|
+
{ accountId, from, to }
|
|
13069
|
+
);
|
|
13070
|
+
const out = /* @__PURE__ */ new Map();
|
|
13071
|
+
for (const g of r.records) {
|
|
13072
|
+
const workerId = str(g.get("workerId"));
|
|
13073
|
+
if (!workerId) continue;
|
|
13074
|
+
const list = out.get(workerId) ?? [];
|
|
13075
|
+
list.push({
|
|
13076
|
+
visitId: String(g.get("visitId")),
|
|
13077
|
+
start: String(g.get("start")),
|
|
13078
|
+
end: String(g.get("end"))
|
|
13079
|
+
});
|
|
13080
|
+
out.set(workerId, list);
|
|
13081
|
+
}
|
|
13082
|
+
return out;
|
|
13083
|
+
}
|
|
12992
13084
|
async function readSiteLocation(session, accountId, siteId) {
|
|
12993
13085
|
const r = await session.run(
|
|
12994
13086
|
`MATCH (s:Site {accountId: $accountId, siteId: $siteId})
|
|
@@ -13170,6 +13262,7 @@ var ZONE_FALLBACK = "UTC";
|
|
|
13170
13262
|
var EMPTY_META = {
|
|
13171
13263
|
vocabulary: { job: "", visit: "", worker: "" },
|
|
13172
13264
|
statuses: [],
|
|
13265
|
+
transitions: [],
|
|
13173
13266
|
zone: ""
|
|
13174
13267
|
};
|
|
13175
13268
|
var PORTAL_VISITS_PUSH_INTERVAL_MS = 6e4;
|
|
@@ -13185,17 +13278,28 @@ function parseVisitRows(raw) {
|
|
|
13185
13278
|
const visitId = typeof o.visitId === "string" ? o.visitId : "";
|
|
13186
13279
|
if (!visitId) continue;
|
|
13187
13280
|
const str2 = (v) => typeof v === "string" ? v : "";
|
|
13281
|
+
const cnt = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
13188
13282
|
out.push({
|
|
13189
13283
|
visitId,
|
|
13190
13284
|
ownerId: str2(o.ownerId),
|
|
13191
13285
|
jobId: str2(o.jobId),
|
|
13192
13286
|
status: str2(o.status),
|
|
13193
13287
|
startDate: str2(o.startDate),
|
|
13194
|
-
purpose: str2(o.purpose)
|
|
13288
|
+
purpose: str2(o.purpose),
|
|
13289
|
+
notes: cnt(o.notes),
|
|
13290
|
+
media: cnt(o.media),
|
|
13291
|
+
parts: cnt(o.parts),
|
|
13292
|
+
checklistDone: cnt(o.checklistDone),
|
|
13293
|
+
checklistTotal: cnt(o.checklistTotal),
|
|
13294
|
+
signatures: cnt(o.signatures)
|
|
13195
13295
|
});
|
|
13196
13296
|
}
|
|
13197
13297
|
return out;
|
|
13198
13298
|
}
|
|
13299
|
+
function num2(v) {
|
|
13300
|
+
const n = Number(v?.toNumber?.() ?? v);
|
|
13301
|
+
return Number.isFinite(n) ? n : 0;
|
|
13302
|
+
}
|
|
13199
13303
|
async function readVisitsForAccount(accountId) {
|
|
13200
13304
|
const session = getSession();
|
|
13201
13305
|
try {
|
|
@@ -13207,6 +13311,12 @@ async function readVisitsForAccount(accountId) {
|
|
|
13207
13311
|
// the worker sees no job and has nothing to report but confusion, where a
|
|
13208
13312
|
// duplicate on the account-scope list is visible and reads as what it is.
|
|
13209
13313
|
// The duplicate is real and is Task 2551.
|
|
13314
|
+
// The record counts ride the visit row (Task 2511). One projection rather
|
|
13315
|
+
// than a `readVisitRecordState` call per visit: this query already runs
|
|
13316
|
+
// once for the whole account, and a per-visit round trip would multiply
|
|
13317
|
+
// every tick by the account's whole visit history. The six expressions
|
|
13318
|
+
// are `readVisitRecordState`'s own, so the two cannot disagree about what
|
|
13319
|
+
// a count means.
|
|
13210
13320
|
`MATCH (j:Job {accountId: $accountId})-[:HAS_VISIT]->(v:Visit {accountId: $accountId})
|
|
13211
13321
|
OPTIONAL MATCH (v)-[:HAS_ATTENDANCE]->(:Attendance)-[:BY_ENGINEER]->(e:Engineer)
|
|
13212
13322
|
RETURN toString(v.visitId) AS visitId,
|
|
@@ -13214,7 +13324,13 @@ async function readVisitsForAccount(accountId) {
|
|
|
13214
13324
|
coalesce(toString(j.jobId), '') AS jobId,
|
|
13215
13325
|
coalesce(toString(v.status), '') AS status,
|
|
13216
13326
|
coalesce(toString(v.startDate), '') AS startDate,
|
|
13217
|
-
coalesce(toString(v.purpose), '') AS purpose
|
|
13327
|
+
coalesce(toString(v.purpose), '') AS purpose,
|
|
13328
|
+
size(coalesce(v.notes, [])) AS notes,
|
|
13329
|
+
size(coalesce(v.media, [])) AS media,
|
|
13330
|
+
size(coalesce(v.parts, [])) AS parts,
|
|
13331
|
+
size(coalesce(v.checklistDone, [])) AS checklistDone,
|
|
13332
|
+
size(coalesce(v.checklist, [])) AS checklistTotal,
|
|
13333
|
+
size(coalesce(v.signatures, [])) AS signatures`,
|
|
13218
13334
|
{ accountId }
|
|
13219
13335
|
);
|
|
13220
13336
|
return parseVisitRows([
|
|
@@ -13225,7 +13341,13 @@ async function readVisitsForAccount(accountId) {
|
|
|
13225
13341
|
jobId: rec.get("jobId"),
|
|
13226
13342
|
status: rec.get("status"),
|
|
13227
13343
|
startDate: rec.get("startDate"),
|
|
13228
|
-
purpose: rec.get("purpose")
|
|
13344
|
+
purpose: rec.get("purpose"),
|
|
13345
|
+
notes: num2(rec.get("notes")),
|
|
13346
|
+
media: num2(rec.get("media")),
|
|
13347
|
+
parts: num2(rec.get("parts")),
|
|
13348
|
+
checklistDone: num2(rec.get("checklistDone")),
|
|
13349
|
+
checklistTotal: num2(rec.get("checklistTotal")),
|
|
13350
|
+
signatures: num2(rec.get("signatures"))
|
|
13229
13351
|
}))
|
|
13230
13352
|
}
|
|
13231
13353
|
]);
|
|
@@ -13256,8 +13378,72 @@ async function readRosterForAccount(accountId, workers) {
|
|
|
13256
13378
|
await session.close();
|
|
13257
13379
|
}
|
|
13258
13380
|
}
|
|
13381
|
+
async function readCandidatesForAccount(accountId, cfg) {
|
|
13382
|
+
const session = getSession();
|
|
13383
|
+
try {
|
|
13384
|
+
const open3 = await session.run(
|
|
13385
|
+
`MATCH (j:Job {accountId: $accountId})-[:HAS_VISIT]->(v:Visit {accountId: $accountId})
|
|
13386
|
+
WHERE NOT (v)-[:HAS_ATTENDANCE]->()
|
|
13387
|
+
RETURN toString(v.visitId) AS visitId,
|
|
13388
|
+
coalesce(toString(j.jobType), '') AS jobType,
|
|
13389
|
+
coalesce(toString(v.startDate), '') AS startDate,
|
|
13390
|
+
coalesce(toString(v.endDate), toString(v.startDate), '') AS endDate`,
|
|
13391
|
+
{ accountId }
|
|
13392
|
+
);
|
|
13393
|
+
if (open3.records.length === 0) return [];
|
|
13394
|
+
const windows = open3.records.map((rec) => ({
|
|
13395
|
+
visitId: String(rec.get("visitId")),
|
|
13396
|
+
jobType: String(rec.get("jobType")),
|
|
13397
|
+
start: String(rec.get("startDate")),
|
|
13398
|
+
end: String(rec.get("endDate"))
|
|
13399
|
+
}));
|
|
13400
|
+
const dated = windows.filter((w) => w.start !== "");
|
|
13401
|
+
const from = dated.reduce((lo, w) => lo === "" || w.start < lo ? w.start : lo, "");
|
|
13402
|
+
const to = dated.reduce((hi, w) => w.end > hi ? w.end : hi, "");
|
|
13403
|
+
const committed = from === "" ? /* @__PURE__ */ new Map() : await readCommitments(session, accountId, from, to);
|
|
13404
|
+
const out = [];
|
|
13405
|
+
const nameOf = new Map(cfg.workers.map((w) => [w.workerId, w.name]));
|
|
13406
|
+
for (const w of windows) {
|
|
13407
|
+
const windowed = new Map(
|
|
13408
|
+
[...committed].map(([workerId, list]) => [
|
|
13409
|
+
workerId,
|
|
13410
|
+
// A visit with no start overlaps nothing rather than everything: an
|
|
13411
|
+
// undated visit is already listed separately by the page, and
|
|
13412
|
+
// reporting the whole roster as clashing with it would be false.
|
|
13413
|
+
w.start === "" ? [] : list.filter((c) => overlaps({ start: w.start, end: w.end }, c))
|
|
13414
|
+
])
|
|
13415
|
+
);
|
|
13416
|
+
const selection = selectCandidates({ jobType: w.jobType, cfg, commitments: windowed });
|
|
13417
|
+
for (const c of selection.offered) {
|
|
13418
|
+
out.push({
|
|
13419
|
+
visitId: w.visitId,
|
|
13420
|
+
workerId: c.workerId,
|
|
13421
|
+
name: c.name,
|
|
13422
|
+
kind: "offered",
|
|
13423
|
+
reason: "",
|
|
13424
|
+
conflicts: c.conflicts
|
|
13425
|
+
});
|
|
13426
|
+
}
|
|
13427
|
+
for (const e of selection.excluded) {
|
|
13428
|
+
out.push({
|
|
13429
|
+
visitId: w.visitId,
|
|
13430
|
+
workerId: e.workerId,
|
|
13431
|
+
// The config is the authority on a worker's name, and an excluded
|
|
13432
|
+
// worker is never in `selection.offered` to carry one.
|
|
13433
|
+
name: nameOf.get(e.workerId) ?? "",
|
|
13434
|
+
kind: "excluded",
|
|
13435
|
+
reason: e.reason,
|
|
13436
|
+
conflicts: []
|
|
13437
|
+
});
|
|
13438
|
+
}
|
|
13439
|
+
}
|
|
13440
|
+
return out;
|
|
13441
|
+
} finally {
|
|
13442
|
+
await session.close();
|
|
13443
|
+
}
|
|
13444
|
+
}
|
|
13259
13445
|
async function pushVisitsForAccount(opts) {
|
|
13260
|
-
const { accountId, client, rows, roster, meta, log: log2 } = opts;
|
|
13446
|
+
const { accountId, client, rows, roster, meta, candidates, log: log2 } = opts;
|
|
13261
13447
|
const unbound = rows.filter((r) => r.ownerId === "").length;
|
|
13262
13448
|
const seen = /* @__PURE__ */ new Set();
|
|
13263
13449
|
const shared = /* @__PURE__ */ new Set();
|
|
@@ -13268,7 +13454,7 @@ async function pushVisitsForAccount(opts) {
|
|
|
13268
13454
|
const unresolved = roster.filter((w) => w.ownerId === "").length;
|
|
13269
13455
|
if (rows.length === 0 && roster.length === 0) {
|
|
13270
13456
|
log2(`${TAG19} op=skip account=${accountId} reason=no-visits-no-roster`);
|
|
13271
|
-
return { rows: 0, unbound: 0, roster: 0, unresolved: 0 };
|
|
13457
|
+
return { rows: 0, unbound: 0, roster: 0, unresolved: 0, candidates: 0 };
|
|
13272
13458
|
}
|
|
13273
13459
|
const state = await client.query(
|
|
13274
13460
|
"SELECT currentGeneration FROM visits_state WHERE accountId = ?",
|
|
@@ -13281,8 +13467,23 @@ async function pushVisitsForAccount(opts) {
|
|
|
13281
13467
|
const next = Math.max(current, Number(staged[0]?.g ?? 0)) + 1;
|
|
13282
13468
|
for (const row of rows) {
|
|
13283
13469
|
await client.query(
|
|
13284
|
-
"INSERT INTO visits (accountId, visitId, ownerId, jobId, status, startDate, purpose, generation) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
13285
|
-
[
|
|
13470
|
+
"INSERT INTO visits (accountId, visitId, ownerId, jobId, status, startDate, purpose, notes, media, parts, checklistDone, checklistTotal, signatures, generation) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
13471
|
+
[
|
|
13472
|
+
accountId,
|
|
13473
|
+
row.visitId,
|
|
13474
|
+
row.ownerId,
|
|
13475
|
+
row.jobId,
|
|
13476
|
+
row.status,
|
|
13477
|
+
row.startDate,
|
|
13478
|
+
row.purpose,
|
|
13479
|
+
row.notes,
|
|
13480
|
+
row.media,
|
|
13481
|
+
row.parts,
|
|
13482
|
+
row.checklistDone,
|
|
13483
|
+
row.checklistTotal,
|
|
13484
|
+
row.signatures,
|
|
13485
|
+
next
|
|
13486
|
+
]
|
|
13286
13487
|
);
|
|
13287
13488
|
}
|
|
13288
13489
|
for (const w of roster) {
|
|
@@ -13293,19 +13494,26 @@ async function pushVisitsForAccount(opts) {
|
|
|
13293
13494
|
}
|
|
13294
13495
|
if (roster.length > 0) {
|
|
13295
13496
|
await client.query(
|
|
13296
|
-
"INSERT INTO dispatch_meta (accountId, vocabulary, statuses, zone, publishedAt, generation) VALUES (?, ?, ?, ?, ?, ?)",
|
|
13497
|
+
"INSERT INTO dispatch_meta (accountId, vocabulary, statuses, transitions, zone, publishedAt, generation) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
13297
13498
|
[
|
|
13298
13499
|
accountId,
|
|
13299
13500
|
JSON.stringify(meta.vocabulary),
|
|
13300
13501
|
JSON.stringify(meta.statuses),
|
|
13502
|
+
JSON.stringify(meta.transitions),
|
|
13301
13503
|
meta.zone,
|
|
13302
13504
|
(/* @__PURE__ */ new Date()).toISOString(),
|
|
13303
13505
|
next
|
|
13304
13506
|
]
|
|
13305
13507
|
);
|
|
13306
13508
|
}
|
|
13509
|
+
for (const c of candidates) {
|
|
13510
|
+
await client.query(
|
|
13511
|
+
"INSERT INTO visit_candidates (accountId, visitId, workerId, name, kind, reason, conflicts, generation) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
13512
|
+
[accountId, c.visitId, c.workerId, c.name, c.kind, c.reason, JSON.stringify(c.conflicts), next]
|
|
13513
|
+
);
|
|
13514
|
+
}
|
|
13307
13515
|
log2(
|
|
13308
|
-
`${TAG19} op=stage account=${accountId} generation=${next} rows=${rows.length} visits=${seen.size} shared=${shared.size} roster=${roster.length}`
|
|
13516
|
+
`${TAG19} op=stage account=${accountId} generation=${next} rows=${rows.length} visits=${seen.size} shared=${shared.size} roster=${roster.length} candidates=${candidates.length}`
|
|
13309
13517
|
);
|
|
13310
13518
|
await client.query(
|
|
13311
13519
|
"INSERT INTO visits_state (accountId, currentGeneration) VALUES (?, ?) ON CONFLICT (accountId) DO UPDATE SET currentGeneration = excluded.currentGeneration WHERE excluded.currentGeneration > visits_state.currentGeneration",
|
|
@@ -13325,6 +13533,7 @@ async function pushVisitsForAccount(opts) {
|
|
|
13325
13533
|
unbound,
|
|
13326
13534
|
roster: roster.length,
|
|
13327
13535
|
unresolved,
|
|
13536
|
+
candidates: candidates.length,
|
|
13328
13537
|
generation: next,
|
|
13329
13538
|
superseded: true
|
|
13330
13539
|
};
|
|
@@ -13345,12 +13554,23 @@ async function pushVisitsForAccount(opts) {
|
|
|
13345
13554
|
accountId,
|
|
13346
13555
|
next
|
|
13347
13556
|
]);
|
|
13557
|
+
await client.query("DELETE FROM visit_candidates WHERE accountId = ? AND generation < ?", [
|
|
13558
|
+
accountId,
|
|
13559
|
+
next
|
|
13560
|
+
]);
|
|
13348
13561
|
} catch (err) {
|
|
13349
13562
|
log2(
|
|
13350
13563
|
`${TAG19} op=sweep account=${accountId} generation=${next} result=failed err="${err instanceof Error ? err.message : String(err)}"`
|
|
13351
13564
|
);
|
|
13352
13565
|
}
|
|
13353
|
-
return {
|
|
13566
|
+
return {
|
|
13567
|
+
rows: rows.length,
|
|
13568
|
+
unbound,
|
|
13569
|
+
roster: roster.length,
|
|
13570
|
+
unresolved,
|
|
13571
|
+
candidates: candidates.length,
|
|
13572
|
+
generation: next
|
|
13573
|
+
};
|
|
13354
13574
|
}
|
|
13355
13575
|
function visitsStateFilePath(platformRoot5) {
|
|
13356
13576
|
return resolve13(platformRoot5, "..", "data", "portal-visits-state.json");
|
|
@@ -13358,17 +13578,40 @@ function visitsStateFilePath(platformRoot5) {
|
|
|
13358
13578
|
function visitsDigest(rows) {
|
|
13359
13579
|
const h = createHash3("sha256");
|
|
13360
13580
|
for (const r of [...rows].sort((a, b) => a.visitId.localeCompare(b.visitId))) {
|
|
13361
|
-
h.update(
|
|
13581
|
+
h.update(
|
|
13582
|
+
JSON.stringify([
|
|
13583
|
+
r.visitId,
|
|
13584
|
+
r.ownerId,
|
|
13585
|
+
r.jobId,
|
|
13586
|
+
r.status,
|
|
13587
|
+
r.startDate,
|
|
13588
|
+
r.purpose,
|
|
13589
|
+
// Task 2511's counts are fields of a visit, so they belong in the
|
|
13590
|
+
// visits half. Listed explicitly rather than spread, because the array
|
|
13591
|
+
// literal is what fixes field order here rather than key insertion.
|
|
13592
|
+
r.notes,
|
|
13593
|
+
r.media,
|
|
13594
|
+
r.parts,
|
|
13595
|
+
r.checklistDone,
|
|
13596
|
+
r.checklistTotal,
|
|
13597
|
+
r.signatures
|
|
13598
|
+
])
|
|
13599
|
+
);
|
|
13362
13600
|
}
|
|
13363
13601
|
return h.digest("hex");
|
|
13364
13602
|
}
|
|
13365
|
-
function payloadDigest(rows, roster, meta) {
|
|
13603
|
+
function payloadDigest(rows, roster, meta, candidates) {
|
|
13366
13604
|
const h = createHash3("sha256");
|
|
13367
13605
|
h.update(visitsDigest(rows));
|
|
13368
13606
|
for (const w of [...roster].sort((a, b) => a.workerId.localeCompare(b.workerId))) {
|
|
13369
13607
|
h.update(JSON.stringify([w.workerId, w.ownerId, w.name, w.workingHours]));
|
|
13370
13608
|
}
|
|
13371
|
-
h.update(JSON.stringify([meta.vocabulary, meta.statuses, meta.zone]));
|
|
13609
|
+
h.update(JSON.stringify([meta.vocabulary, meta.statuses, meta.transitions, meta.zone]));
|
|
13610
|
+
for (const c of [...candidates].sort(
|
|
13611
|
+
(a, b) => a.visitId.localeCompare(b.visitId) || a.workerId.localeCompare(b.workerId)
|
|
13612
|
+
)) {
|
|
13613
|
+
h.update(JSON.stringify([c.visitId, c.workerId, c.name, c.kind, c.reason, c.conflicts]));
|
|
13614
|
+
}
|
|
13372
13615
|
return h.digest("hex");
|
|
13373
13616
|
}
|
|
13374
13617
|
function readVisitsPushState(platformRoot5) {
|
|
@@ -13416,9 +13659,11 @@ async function runPortalVisitsPush(root) {
|
|
|
13416
13659
|
const meta = cfg.present ? {
|
|
13417
13660
|
vocabulary: cfg.value.vocabulary,
|
|
13418
13661
|
statuses: cfg.value.statuses.visit,
|
|
13662
|
+
transitions: cfg.value.statuses.visitTransitions,
|
|
13419
13663
|
zone: await resolveAccountZone(portal.accountId) ?? ZONE_FALLBACK
|
|
13420
13664
|
} : EMPTY_META;
|
|
13421
|
-
const
|
|
13665
|
+
const candidates = cfg.present ? await readCandidatesForAccount(portal.accountId, cfg.value) : [];
|
|
13666
|
+
const digest = payloadDigest(rows, roster, meta, candidates);
|
|
13422
13667
|
const unresolved = roster.filter((w) => w.ownerId === "").length;
|
|
13423
13668
|
if (state[portal.accountId]?.digest === digest && (rows.length > 0 || roster.length > 0)) {
|
|
13424
13669
|
recordVisitsPush(root, portal.accountId, {
|
|
@@ -13443,10 +13688,11 @@ async function runPortalVisitsPush(root) {
|
|
|
13443
13688
|
rows,
|
|
13444
13689
|
roster,
|
|
13445
13690
|
meta,
|
|
13691
|
+
candidates,
|
|
13446
13692
|
log: (l) => console.error(l)
|
|
13447
13693
|
});
|
|
13448
13694
|
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}`
|
|
13695
|
+
`${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
13696
|
);
|
|
13451
13697
|
if (r.generation !== void 0) {
|
|
13452
13698
|
recordVisitsPush(root, portal.accountId, {
|
|
@@ -13491,12 +13737,15 @@ function parsePendingStatusRows(raw) {
|
|
|
13491
13737
|
statusAt: str2(o.statusAt),
|
|
13492
13738
|
dayHundredths: typeof o.dayHundredths === "number" ? o.dayHundredths : 0,
|
|
13493
13739
|
workDate: str2(o.workDate),
|
|
13494
|
-
workNote: str2(o.workNote)
|
|
13740
|
+
workNote: str2(o.workNote),
|
|
13741
|
+
assignedOwnerId: str2(o.assignedOwnerId),
|
|
13742
|
+
startDate: str2(o.startDate)
|
|
13495
13743
|
});
|
|
13496
13744
|
}
|
|
13497
13745
|
return out;
|
|
13498
13746
|
}
|
|
13499
13747
|
var WORK_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
13748
|
+
var INSTANT_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
13500
13749
|
function buildVisitStatusWrite(accountId, row) {
|
|
13501
13750
|
const base = {
|
|
13502
13751
|
accountId,
|
|
@@ -13504,6 +13753,72 @@ function buildVisitStatusWrite(accountId, row) {
|
|
|
13504
13753
|
status: row.status,
|
|
13505
13754
|
statusAt: row.statusAt
|
|
13506
13755
|
};
|
|
13756
|
+
if (row.assignedOwnerId !== "" || row.startDate !== "") {
|
|
13757
|
+
if (row.startDate !== "" && !INSTANT_RE.test(row.startDate)) return { kind: "bad-instant" };
|
|
13758
|
+
if (row.dayHundredths > 0) return { kind: "move-with-labour" };
|
|
13759
|
+
return {
|
|
13760
|
+
kind: "move",
|
|
13761
|
+
// EXACTLY ONE ATTENDANCE to replace, and EXACTLY ONE ENGINEER to replace
|
|
13762
|
+
// it with, or nothing runs at all.
|
|
13763
|
+
//
|
|
13764
|
+
// A visit two workers share cannot be re-assigned by replacing "the"
|
|
13765
|
+
// attendance: whichever went, the other worker would silently lose the
|
|
13766
|
+
// job and the arrival times they had already pressed. A per-worker move
|
|
13767
|
+
// is Task 2551. The engineer gate is the mirror: the `portalOwnerId`
|
|
13768
|
+
// binding is a property and is not unique by construction, so an id
|
|
13769
|
+
// naming several must not resolve to whichever came back first. Both turn
|
|
13770
|
+
// into the zero count this function already returns for a visit missing
|
|
13771
|
+
// its job, so the row stays unclaimed and counted.
|
|
13772
|
+
//
|
|
13773
|
+
// NO `CALL` SUBQUERY, deliberately. A returning subquery replaces each
|
|
13774
|
+
// outer row with the rows it yields, so one that yields nothing on a
|
|
13775
|
+
// status-only or re-time-only edit would eliminate the visit before the
|
|
13776
|
+
// SET ran: the re-timing would write nothing, return 0, and the row would
|
|
13777
|
+
// never drain. A unit subquery does not filter, but then an unresolvable
|
|
13778
|
+
// ownerId would leave the SET running and the row CLAIMED with nobody
|
|
13779
|
+
// moved. Both gates therefore sit in the main query's WHERE, where they
|
|
13780
|
+
// fail the whole statement, and the writes ride FOREACH over lists that
|
|
13781
|
+
// are empty when this edit re-assigns nobody.
|
|
13782
|
+
//
|
|
13783
|
+
// The re-assignment REPLACES rather than adds, because a drag onto
|
|
13784
|
+
// another worker's row means "move this", not "add a second person". The
|
|
13785
|
+
// attendance key is `engineerId + '-' + visitId`, the key
|
|
13786
|
+
// `dispatch-allocate` merges on, so allocating that worker to that visit
|
|
13787
|
+
// afterwards is a no-op rather than a second attendance — and the delete
|
|
13788
|
+
// skips an attendance already held by the incoming engineer, so
|
|
13789
|
+
// re-assigning somebody to the visit they are already on keeps the
|
|
13790
|
+
// arrival times they pressed instead of resetting them.
|
|
13791
|
+
cypher: `MATCH (v:Visit {accountId: $accountId, visitId: $visitId})
|
|
13792
|
+
OPTIONAL MATCH (v)-[:HAS_ATTENDANCE]->(a:Attendance)
|
|
13793
|
+
WITH v, collect(a) AS atts
|
|
13794
|
+
OPTIONAL MATCH (e:Engineer {accountId: $accountId})
|
|
13795
|
+
WHERE $assignedOwnerId <> ''
|
|
13796
|
+
AND e.portalOwnerId = $assignedOwnerId
|
|
13797
|
+
AND e.engineerId IS NOT NULL
|
|
13798
|
+
WITH v, atts, collect(e) AS es
|
|
13799
|
+
WHERE $assignedOwnerId = '' OR (size(atts) = 1 AND size(es) = 1)
|
|
13800
|
+
SET v.status = $status, v.statusAt = $statusAt
|
|
13801
|
+
FOREACH (_ IN CASE WHEN $startDate <> '' THEN [1] ELSE [] END |
|
|
13802
|
+
SET v.startDate = $startDate)
|
|
13803
|
+
FOREACH (old IN [x IN atts
|
|
13804
|
+
WHERE NOT x.engineerId IN [eng IN es | eng.engineerId]] |
|
|
13805
|
+
DETACH DELETE old)
|
|
13806
|
+
FOREACH (eng IN es |
|
|
13807
|
+
MERGE (a2:Attendance {
|
|
13808
|
+
accountId: $accountId,
|
|
13809
|
+
attendanceId: eng.engineerId + '-' + $visitId
|
|
13810
|
+
})
|
|
13811
|
+
ON CREATE SET a2.engineerId = eng.engineerId, a2.createdAt = $statusAt
|
|
13812
|
+
MERGE (v)-[:HAS_ATTENDANCE]->(a2)
|
|
13813
|
+
MERGE (a2)-[:BY_ENGINEER]->(eng))
|
|
13814
|
+
RETURN count(v) AS n`,
|
|
13815
|
+
params: {
|
|
13816
|
+
...base,
|
|
13817
|
+
assignedOwnerId: row.assignedOwnerId,
|
|
13818
|
+
startDate: row.startDate
|
|
13819
|
+
}
|
|
13820
|
+
};
|
|
13821
|
+
}
|
|
13507
13822
|
if (row.dayHundredths <= 0) {
|
|
13508
13823
|
return {
|
|
13509
13824
|
kind: "status",
|
|
@@ -13562,6 +13877,14 @@ async function pullVisitStatusForAccount(opts) {
|
|
|
13562
13877
|
await claim(row.id);
|
|
13563
13878
|
merged += 1;
|
|
13564
13879
|
log2(`${TAG20} op=merged account=${accountId} id=${row.id} labour=${row.dayHundredths > 0}`);
|
|
13880
|
+
const fields = ["status"];
|
|
13881
|
+
if (row.workDate !== "") fields.push("workDate");
|
|
13882
|
+
if (row.workNote !== "") fields.push("workNote");
|
|
13883
|
+
if (row.assignedOwnerId !== "") fields.push("assignedOwnerId");
|
|
13884
|
+
if (row.startDate !== "") fields.push("startDate");
|
|
13885
|
+
for (const field2 of fields) {
|
|
13886
|
+
log2(`${TAG20} op=applied visitId=${row.visitId} field=${field2}`);
|
|
13887
|
+
}
|
|
13565
13888
|
} catch (err) {
|
|
13566
13889
|
failed2 += 1;
|
|
13567
13890
|
const detail = err.message.split("\n")[0].replaceAll('"', "'").slice(0, 160);
|
|
@@ -13589,7 +13912,7 @@ async function runPortalVisitsPull(root) {
|
|
|
13589
13912
|
const pending = parsePendingStatusRows(
|
|
13590
13913
|
await cf.d1Exec(
|
|
13591
13914
|
portal.portalDbName,
|
|
13592
|
-
"SELECT id, visitId, ownerId, status, statusAt, dayHundredths, workDate, workNote FROM visit_status WHERE accountId = ? AND claimed = 0",
|
|
13915
|
+
"SELECT id, visitId, ownerId, status, statusAt, dayHundredths, workDate, workNote, assignedOwnerId, startDate FROM visit_status WHERE accountId = ? AND claimed = 0",
|
|
13593
13916
|
[portal.accountId]
|
|
13594
13917
|
)
|
|
13595
13918
|
);
|
|
@@ -13598,7 +13921,9 @@ async function runPortalVisitsPull(root) {
|
|
|
13598
13921
|
pending,
|
|
13599
13922
|
applyToGraph: async (accountId, row) => {
|
|
13600
13923
|
const write = buildVisitStatusWrite(accountId, row);
|
|
13601
|
-
if (write.kind === "undated")
|
|
13924
|
+
if (write.kind === "undated" || write.kind === "bad-instant" || write.kind === "move-with-labour") {
|
|
13925
|
+
return false;
|
|
13926
|
+
}
|
|
13602
13927
|
const session = getSession();
|
|
13603
13928
|
try {
|
|
13604
13929
|
const res = await session.run(write.cypher, write.params);
|
|
@@ -13642,7 +13967,7 @@ function buildVisitsAuditLine(input) {
|
|
|
13642
13967
|
input.d1Error !== null ? `d1:${auditErrorText(input.d1Error)}` : null,
|
|
13643
13968
|
input.neo4jError !== null ? `neo4j:${auditErrorText(input.neo4jError)}` : null
|
|
13644
13969
|
].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}"` : "");
|
|
13970
|
+
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
13971
|
}
|
|
13647
13972
|
function readLastVisitsPushAgeSec(platformRoot5, accountId, nowMs) {
|
|
13648
13973
|
try {
|
|
@@ -13655,7 +13980,7 @@ function readLastVisitsPushAgeSec(platformRoot5, accountId, nowMs) {
|
|
|
13655
13980
|
return null;
|
|
13656
13981
|
}
|
|
13657
13982
|
}
|
|
13658
|
-
var
|
|
13983
|
+
var num3 = (raw, field2) => {
|
|
13659
13984
|
const envelope = Array.isArray(raw) ? raw[0] : null;
|
|
13660
13985
|
const results = envelope?.results;
|
|
13661
13986
|
if (!Array.isArray(results)) {
|
|
@@ -13681,12 +14006,13 @@ async function runPortalVisitsAudit(root) {
|
|
|
13681
14006
|
let unresolvedVisitIds = null;
|
|
13682
14007
|
let labourClaims = null;
|
|
13683
14008
|
let undatedClaims = null;
|
|
14009
|
+
let stalePendingRows = null;
|
|
13684
14010
|
let d1Error = null;
|
|
13685
14011
|
try {
|
|
13686
14012
|
if (!SAFE_ACCOUNT_ID3.test(portal.accountId)) {
|
|
13687
14013
|
throw new Error(`refusing to query for unsafe accountId "${portal.accountId}"`);
|
|
13688
14014
|
}
|
|
13689
|
-
pushedRows =
|
|
14015
|
+
pushedRows = num3(
|
|
13690
14016
|
await cf.d1Exec(
|
|
13691
14017
|
portal.portalDbName,
|
|
13692
14018
|
"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 +14020,7 @@ async function runPortalVisitsAudit(root) {
|
|
|
13694
14020
|
),
|
|
13695
14021
|
"n"
|
|
13696
14022
|
);
|
|
13697
|
-
unboundVisits =
|
|
14023
|
+
unboundVisits = num3(
|
|
13698
14024
|
await cf.d1Exec(
|
|
13699
14025
|
portal.portalDbName,
|
|
13700
14026
|
"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 +14033,12 @@ async function runPortalVisitsAudit(root) {
|
|
|
13707
14033
|
"SELECT COUNT(*) AS n, MIN(statusAt) AS oldest FROM visit_status WHERE accountId = ? AND claimed = 0",
|
|
13708
14034
|
[portal.accountId]
|
|
13709
14035
|
);
|
|
13710
|
-
pendingStatusRows =
|
|
14036
|
+
pendingStatusRows = num3(pending, "n");
|
|
13711
14037
|
const envelope = Array.isArray(pending) ? pending[0] : null;
|
|
13712
14038
|
const oldest = envelope?.results?.[0]?.oldest;
|
|
13713
14039
|
const oldestMs = typeof oldest === "string" ? Date.parse(oldest) : NaN;
|
|
13714
14040
|
oldestPendingHrs = Number.isNaN(oldestMs) ? pendingStatusRows === 0 ? 0 : null : Math.max(0, (nowMs - oldestMs) / 36e5);
|
|
13715
|
-
unresolvedVisitIds =
|
|
14041
|
+
unresolvedVisitIds = num3(
|
|
13716
14042
|
await cf.d1Exec(
|
|
13717
14043
|
portal.portalDbName,
|
|
13718
14044
|
"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 +14046,7 @@ async function runPortalVisitsAudit(root) {
|
|
|
13720
14046
|
),
|
|
13721
14047
|
"n"
|
|
13722
14048
|
);
|
|
13723
|
-
labourClaims =
|
|
14049
|
+
labourClaims = num3(
|
|
13724
14050
|
await cf.d1Exec(
|
|
13725
14051
|
portal.portalDbName,
|
|
13726
14052
|
"SELECT COUNT(*) AS n FROM visit_status WHERE accountId = ? AND claimed = 0 AND dayHundredths > 0",
|
|
@@ -13728,7 +14054,7 @@ async function runPortalVisitsAudit(root) {
|
|
|
13728
14054
|
),
|
|
13729
14055
|
"n"
|
|
13730
14056
|
);
|
|
13731
|
-
undatedClaims =
|
|
14057
|
+
undatedClaims = num3(
|
|
13732
14058
|
await cf.d1Exec(
|
|
13733
14059
|
portal.portalDbName,
|
|
13734
14060
|
"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 +14062,15 @@ async function runPortalVisitsAudit(root) {
|
|
|
13736
14062
|
),
|
|
13737
14063
|
"n"
|
|
13738
14064
|
);
|
|
14065
|
+
const staleCutoff = new Date(nowMs - 2 * PORTAL_VISITS_PULL_INTERVAL_MS).toISOString();
|
|
14066
|
+
stalePendingRows = num3(
|
|
14067
|
+
await cf.d1Exec(
|
|
14068
|
+
portal.portalDbName,
|
|
14069
|
+
"SELECT COUNT(*) AS n FROM visit_status WHERE accountId = ? AND claimed = 0 AND statusAt < ?",
|
|
14070
|
+
[portal.accountId, staleCutoff]
|
|
14071
|
+
),
|
|
14072
|
+
"n"
|
|
14073
|
+
);
|
|
13739
14074
|
} catch (err) {
|
|
13740
14075
|
d1Error = err.message;
|
|
13741
14076
|
}
|
|
@@ -13774,6 +14109,7 @@ async function runPortalVisitsAudit(root) {
|
|
|
13774
14109
|
labourClaims,
|
|
13775
14110
|
labourCosts,
|
|
13776
14111
|
undatedClaims,
|
|
14112
|
+
stalePendingRows,
|
|
13777
14113
|
lastPushAgeSec: readLastVisitsPushAgeSec(root, portal.accountId, nowMs),
|
|
13778
14114
|
d1Error,
|
|
13779
14115
|
neo4jError
|
|
@@ -21386,7 +21722,7 @@ init_neo4j_store();
|
|
|
21386
21722
|
async function purgeAdminConversationJsonls(args) {
|
|
21387
21723
|
const fetchImpl = args.fetchImpl ?? fetch;
|
|
21388
21724
|
const log2 = args.logger ?? ((line) => console.log(line));
|
|
21389
|
-
const
|
|
21725
|
+
const id83 = args.sessionId.slice(0, 8);
|
|
21390
21726
|
const purged = [];
|
|
21391
21727
|
for (const sessionId of args.claudeSessionIds) {
|
|
21392
21728
|
const url = `${args.managerBase}/${encodeURIComponent(sessionId)}`;
|
|
@@ -21396,7 +21732,7 @@ async function purgeAdminConversationJsonls(args) {
|
|
|
21396
21732
|
} catch (err) {
|
|
21397
21733
|
const message2 = err instanceof Error ? err.message : String(err);
|
|
21398
21734
|
log2(
|
|
21399
|
-
`[admin-conversation-delete] sessionId=${
|
|
21735
|
+
`[admin-conversation-delete] sessionId=${id83} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message2}`
|
|
21400
21736
|
);
|
|
21401
21737
|
return {
|
|
21402
21738
|
ok: false,
|
|
@@ -21413,7 +21749,7 @@ async function purgeAdminConversationJsonls(args) {
|
|
|
21413
21749
|
}
|
|
21414
21750
|
if (res.status === 409) {
|
|
21415
21751
|
log2(
|
|
21416
|
-
`[admin-conversation-delete] sessionId=${
|
|
21752
|
+
`[admin-conversation-delete] sessionId=${id83} reason=pty-still-alive claudeSessionId=${sessionId.slice(0, 8)}`
|
|
21417
21753
|
);
|
|
21418
21754
|
return {
|
|
21419
21755
|
ok: false,
|
|
@@ -21430,7 +21766,7 @@ async function purgeAdminConversationJsonls(args) {
|
|
|
21430
21766
|
} catch {
|
|
21431
21767
|
}
|
|
21432
21768
|
log2(
|
|
21433
|
-
`[admin-conversation-delete] sessionId=${
|
|
21769
|
+
`[admin-conversation-delete] sessionId=${id83} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message}`
|
|
21434
21770
|
);
|
|
21435
21771
|
return {
|
|
21436
21772
|
ok: false,
|
|
@@ -21449,13 +21785,13 @@ async function cascadeAdminConversationDelete(args) {
|
|
|
21449
21785
|
const started2 = Date.now();
|
|
21450
21786
|
const fetchImpl = args.fetchImpl ?? fetch;
|
|
21451
21787
|
const log2 = args.logger ?? ((line) => console.log(line));
|
|
21452
|
-
const
|
|
21788
|
+
const id83 = args.sessionId.slice(0, 8);
|
|
21453
21789
|
let claudeSessionIds;
|
|
21454
21790
|
try {
|
|
21455
21791
|
claudeSessionIds = args.claudeSessionIds ?? await getConversationClaudeSessionIds(args.sessionId, args.accountId);
|
|
21456
21792
|
} catch (err) {
|
|
21457
21793
|
const message = err instanceof Error ? err.message : String(err);
|
|
21458
|
-
log2(`[admin-conversation-delete] sessionId=${
|
|
21794
|
+
log2(`[admin-conversation-delete] sessionId=${id83} reason=graph-delete-failed claudeSessionIds-purged= message=transcript-resolve:${message}`);
|
|
21459
21795
|
return { ok: false, reason: "graph-delete-failed", claudeSessionIdsPurged: [], message };
|
|
21460
21796
|
}
|
|
21461
21797
|
const purgeOutcome = await purgeAdminConversationJsonls({
|
|
@@ -21486,7 +21822,7 @@ async function cascadeAdminConversationDelete(args) {
|
|
|
21486
21822
|
} catch (err) {
|
|
21487
21823
|
const message = err instanceof Error ? err.message : String(err);
|
|
21488
21824
|
const csv3 = purged.map((s) => s.slice(0, 8)).join(",");
|
|
21489
|
-
log2(`[admin-conversation-delete] sessionId=${
|
|
21825
|
+
log2(`[admin-conversation-delete] sessionId=${id83} reason=graph-delete-failed claudeSessionIds-purged=${csv3} message=${message}`);
|
|
21490
21826
|
return {
|
|
21491
21827
|
ok: false,
|
|
21492
21828
|
reason: "graph-delete-failed",
|
|
@@ -21499,7 +21835,7 @@ async function cascadeAdminConversationDelete(args) {
|
|
|
21499
21835
|
}
|
|
21500
21836
|
const ms = Date.now() - started2;
|
|
21501
21837
|
const csv2 = purged.map((s) => s.slice(0, 8)).join(",");
|
|
21502
|
-
log2(`[admin-conversation-delete] sessionId=${
|
|
21838
|
+
log2(`[admin-conversation-delete] sessionId=${id83} claudeSessionIds=${csv2} ms=${ms}`);
|
|
21503
21839
|
return { ok: true, claudeSessionIds: purged, ms };
|
|
21504
21840
|
}
|
|
21505
21841
|
|
|
@@ -29716,21 +30052,21 @@ app52.get("/booking-link", requireAdminSession, (c) => {
|
|
|
29716
30052
|
console.error("[calendar] op=booking-link auth-rejected reason=no-account-for-session accountId=none");
|
|
29717
30053
|
return c.json({ error: "no account for session", reason: "no-account-for-session" }, 401);
|
|
29718
30054
|
}
|
|
29719
|
-
const
|
|
30055
|
+
const id83 = account.accountId.slice(0, 8);
|
|
29720
30056
|
try {
|
|
29721
30057
|
if (!existsSync41(join50(account.accountDir, AVAILABILITY_FILENAME))) {
|
|
29722
|
-
console.log(`[calendar] op=booking-link accountId=${
|
|
30058
|
+
console.log(`[calendar] op=booking-link accountId=${id83} bookingDomain=none source=availability-config`);
|
|
29723
30059
|
return c.json({ bookingDomain: null });
|
|
29724
30060
|
}
|
|
29725
30061
|
const config = readAvailabilityConfig(account.accountDir);
|
|
29726
30062
|
const bookingDomain = typeof config.bookingDomain === "string" && config.bookingDomain.trim() ? config.bookingDomain.trim() : null;
|
|
29727
30063
|
console.log(
|
|
29728
|
-
`[calendar] op=booking-link accountId=${
|
|
30064
|
+
`[calendar] op=booking-link accountId=${id83} bookingDomain=${bookingDomain ?? "none"} source=availability-config`
|
|
29729
30065
|
);
|
|
29730
30066
|
return c.json({ bookingDomain });
|
|
29731
30067
|
} catch (err) {
|
|
29732
30068
|
const message = err instanceof Error ? err.message : String(err);
|
|
29733
|
-
console.error(`[calendar] op=booking-link-fail accountId=${
|
|
30069
|
+
console.error(`[calendar] op=booking-link-fail accountId=${id83} err="${message}"`);
|
|
29734
30070
|
return c.json({ bookingDomain: null });
|
|
29735
30071
|
}
|
|
29736
30072
|
});
|
|
@@ -29750,20 +30086,20 @@ app53.get("/portal-link", requireAdminSession, (c) => {
|
|
|
29750
30086
|
console.error("[data-portal] op=portal-link auth-rejected reason=no-account-for-session accountId=none");
|
|
29751
30087
|
return c.json({ error: "no account for session", reason: "no-account-for-session" }, 401);
|
|
29752
30088
|
}
|
|
29753
|
-
const
|
|
30089
|
+
const id83 = account.accountId.slice(0, 8);
|
|
29754
30090
|
try {
|
|
29755
30091
|
const path = join51(account.accountDir, PORTAL_FILENAME);
|
|
29756
30092
|
if (!existsSync42(path)) {
|
|
29757
|
-
console.log(`[data-portal] op=portal-link accountId=${
|
|
30093
|
+
console.log(`[data-portal] op=portal-link accountId=${id83} portalDomain=none source=data-portal-config`);
|
|
29758
30094
|
return c.json({ portalDomain: null });
|
|
29759
30095
|
}
|
|
29760
30096
|
const cfg = JSON.parse(readFileSync42(path, "utf-8"));
|
|
29761
30097
|
const portalDomain = typeof cfg.portalDomain === "string" && cfg.portalDomain.trim() ? cfg.portalDomain.trim() : null;
|
|
29762
|
-
console.log(`[data-portal] op=portal-link accountId=${
|
|
30098
|
+
console.log(`[data-portal] op=portal-link accountId=${id83} portalDomain=${portalDomain ?? "none"} source=data-portal-config`);
|
|
29763
30099
|
return c.json({ portalDomain });
|
|
29764
30100
|
} catch (err) {
|
|
29765
30101
|
const message = err instanceof Error ? err.message : String(err);
|
|
29766
|
-
console.error(`[data-portal] op=portal-link-fail accountId=${
|
|
30102
|
+
console.error(`[data-portal] op=portal-link-fail accountId=${id83} err="${message}"`);
|
|
29767
30103
|
return c.json({ portalDomain: null });
|
|
29768
30104
|
}
|
|
29769
30105
|
});
|
|
@@ -29785,13 +30121,13 @@ app54.get("/planner-link", requireAdminSession, (c) => {
|
|
|
29785
30121
|
);
|
|
29786
30122
|
return c.json({ error: "no account for session", reason: "no-account-for-session" }, 401);
|
|
29787
30123
|
}
|
|
29788
|
-
const
|
|
30124
|
+
const id83 = account.accountId.slice(0, 8);
|
|
29789
30125
|
try {
|
|
29790
30126
|
const portalPath = join52(account.accountDir, PORTAL_FILENAME2);
|
|
29791
30127
|
const dispatchConfigured = existsSync43(join52(account.accountDir, "dispatch", "config.json"));
|
|
29792
30128
|
if (!existsSync43(portalPath) || !dispatchConfigured) {
|
|
29793
30129
|
console.log(
|
|
29794
|
-
`[dispatch-planner] op=planner-link accountId=${
|
|
30130
|
+
`[dispatch-planner] op=planner-link accountId=${id83} plannerUrl=none portal=${existsSync43(portalPath)} dispatch=${dispatchConfigured}`
|
|
29795
30131
|
);
|
|
29796
30132
|
return c.json({ plannerUrl: null });
|
|
29797
30133
|
}
|
|
@@ -29799,12 +30135,12 @@ app54.get("/planner-link", requireAdminSession, (c) => {
|
|
|
29799
30135
|
const domain = typeof cfg.portalDomain === "string" && cfg.portalDomain.trim() ? cfg.portalDomain.trim() : null;
|
|
29800
30136
|
const plannerUrl = domain ? `https://${domain}/planner` : null;
|
|
29801
30137
|
console.log(
|
|
29802
|
-
`[dispatch-planner] op=planner-link accountId=${
|
|
30138
|
+
`[dispatch-planner] op=planner-link accountId=${id83} plannerUrl=${plannerUrl ?? "none"}`
|
|
29803
30139
|
);
|
|
29804
30140
|
return c.json({ plannerUrl });
|
|
29805
30141
|
} catch (err) {
|
|
29806
30142
|
const message = err instanceof Error ? err.message : String(err);
|
|
29807
|
-
console.error(`[dispatch-planner] op=planner-link-fail accountId=${
|
|
30143
|
+
console.error(`[dispatch-planner] op=planner-link-fail accountId=${id83} err="${message}"`);
|
|
29808
30144
|
return c.json({ plannerUrl: null });
|
|
29809
30145
|
}
|
|
29810
30146
|
});
|
|
@@ -30168,7 +30504,7 @@ app55.get("/", requireAdminSession, async (c) => {
|
|
|
30168
30504
|
}
|
|
30169
30505
|
});
|
|
30170
30506
|
function mapRun(props) {
|
|
30171
|
-
const
|
|
30507
|
+
const num5 = (v) => {
|
|
30172
30508
|
if (v === null || v === void 0) return null;
|
|
30173
30509
|
if (typeof v === "number") return v;
|
|
30174
30510
|
if (typeof v === "object" && typeof v.toNumber === "function") {
|
|
@@ -30188,11 +30524,11 @@ function mapRun(props) {
|
|
|
30188
30524
|
startedAt: props.startedAt,
|
|
30189
30525
|
endedAt: props.endedAt ?? null,
|
|
30190
30526
|
finishedAt: props.finishedAt ?? null,
|
|
30191
|
-
ranMs:
|
|
30192
|
-
httpStatus:
|
|
30527
|
+
ranMs: num5(props.ranMs),
|
|
30528
|
+
httpStatus: num5(props.httpStatus),
|
|
30193
30529
|
dispatchResult: props.dispatchResult ?? null,
|
|
30194
30530
|
sessionKey: props.sessionKey ?? null,
|
|
30195
|
-
transcriptOffset:
|
|
30531
|
+
transcriptOffset: num5(props.transcriptOffset),
|
|
30196
30532
|
// Strict identity: an absent or null `bounded` means the boundary was never
|
|
30197
30533
|
// established, which is the false case.
|
|
30198
30534
|
bounded: props.bounded === true,
|
|
@@ -30385,7 +30721,7 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30385
30721
|
if (!accountId) return c.json({ error: "Account not found for session" }, 401);
|
|
30386
30722
|
const eventId = c.req.param("eventId");
|
|
30387
30723
|
const body = await c.req.json().catch(() => ({}));
|
|
30388
|
-
const
|
|
30724
|
+
const id83 = eventId.slice(0, 8);
|
|
30389
30725
|
const setClauses = [];
|
|
30390
30726
|
const params = { eventId, accountId };
|
|
30391
30727
|
const fields = [];
|
|
@@ -30397,11 +30733,11 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30397
30733
|
const clearing = bindingTouched && body.agentChannel === null && body.agentDestination === null;
|
|
30398
30734
|
const binding = bindingTouched && typeof body.agentChannel === "string" && body.agentChannel.length > 0 && typeof body.agentDestination === "string" && body.agentDestination.length > 0;
|
|
30399
30735
|
if (bindingTouched && !clearing && !binding) {
|
|
30400
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30736
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=agentChannel+agentDestination outcome=rejected reason=partial-binding`);
|
|
30401
30737
|
return c.json({ error: "A dispatch binding needs both a channel and a destination, or both cleared." }, 400);
|
|
30402
30738
|
}
|
|
30403
30739
|
if (binding && !DISPATCH_CHANNELS2.has(body.agentChannel)) {
|
|
30404
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30740
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=agentChannel+agentDestination outcome=rejected reason=invalid-channel channel=${body.agentChannel}`);
|
|
30405
30741
|
return c.json({ error: `Unknown dispatch channel "${body.agentChannel}".` }, 400);
|
|
30406
30742
|
}
|
|
30407
30743
|
const describing = "description" in body;
|
|
@@ -30430,7 +30766,7 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30430
30766
|
storedDescription = pre.records[0]?.get("description") ?? null;
|
|
30431
30767
|
} catch (err) {
|
|
30432
30768
|
const message = err instanceof Error ? err.message : String(err);
|
|
30433
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30769
|
+
console.error(`[admin:routines] op=patch id=${id83} outcome=rejected reason=tz-read-fail err="${message}"`);
|
|
30434
30770
|
return c.json({ error: `Routine unavailable: ${message}` }, 503);
|
|
30435
30771
|
} finally {
|
|
30436
30772
|
await preSession.close();
|
|
@@ -30438,7 +30774,7 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30438
30774
|
}
|
|
30439
30775
|
if (binding) {
|
|
30440
30776
|
if (storedActionPlugin !== null) {
|
|
30441
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30777
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=agentChannel+agentDestination outcome=rejected reason=action-routine`);
|
|
30442
30778
|
return c.json(
|
|
30443
30779
|
{ error: "This routine runs a tool. Binding a dispatch would stop the tool running, so it is refused; make a separate routine for the dispatch." },
|
|
30444
30780
|
400
|
|
@@ -30446,7 +30782,7 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30446
30782
|
}
|
|
30447
30783
|
const effectivePrompt = typeof body.agentPrompt === "string" ? body.agentPrompt : storedPrompt ?? "";
|
|
30448
30784
|
if (effectivePrompt.trim().length === 0) {
|
|
30449
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30785
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=agentChannel+agentDestination outcome=rejected reason=no-prompt`);
|
|
30450
30786
|
return c.json({ error: "A bound routine needs an instruction, or it will fire and do nothing." }, 400);
|
|
30451
30787
|
}
|
|
30452
30788
|
const channel = body.agentChannel;
|
|
@@ -30456,12 +30792,12 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30456
30792
|
houseAccountId = channel === "whatsapp" ? resolveHouseOrSoleAccountId(ACCOUNTS_DIR) : accountId;
|
|
30457
30793
|
} catch (err) {
|
|
30458
30794
|
const message = err instanceof Error ? err.message : String(err);
|
|
30459
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30795
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=agentChannel+agentDestination outcome=rejected reason=house-unresolved err="${message}"`);
|
|
30460
30796
|
return c.json({ error: `WhatsApp destinations cannot be validated on this install: ${message}` }, 503);
|
|
30461
30797
|
}
|
|
30462
30798
|
const check = validateAgentDestination(ACCOUNTS_DIR, houseAccountId, accountId, channel, destination);
|
|
30463
30799
|
if (!check.valid) {
|
|
30464
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30800
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=agentChannel+agentDestination outcome=rejected reason=${check.reason}`);
|
|
30465
30801
|
return c.json(
|
|
30466
30802
|
{
|
|
30467
30803
|
error: check.reason === "cross-account" ? `That destination belongs to a different account on ${channel}.` : `That destination is not a registered admin or account manager on ${channel}.`
|
|
@@ -30478,7 +30814,7 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30478
30814
|
if (clearing) {
|
|
30479
30815
|
const keepsRow = typeof body.recurrence === "string" || storedRecurrence !== null || storedActionPlugin !== null;
|
|
30480
30816
|
if (!keepsRow) {
|
|
30481
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30817
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=agentChannel+agentDestination outcome=rejected reason=clear-would-orphan`);
|
|
30482
30818
|
return c.json(
|
|
30483
30819
|
{ error: "Clearing the dispatch on a one-time routine would leave nothing to show. Give it a schedule first, or delete it." },
|
|
30484
30820
|
400
|
|
@@ -30498,12 +30834,12 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30498
30834
|
let nextName = null;
|
|
30499
30835
|
if (naming) {
|
|
30500
30836
|
if (typeof body.name !== "string" || body.name.trim().length === 0) {
|
|
30501
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30837
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=name outcome=rejected reason=empty-name`);
|
|
30502
30838
|
return c.json({ error: "A routine needs a name." }, 400);
|
|
30503
30839
|
}
|
|
30504
30840
|
nextName = body.name.trim();
|
|
30505
30841
|
if (nextName.length > 200) {
|
|
30506
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30842
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=name outcome=rejected reason=too-long len=${nextName.length}`);
|
|
30507
30843
|
return c.json({ error: "A routine name is at most 200 characters." }, 400);
|
|
30508
30844
|
}
|
|
30509
30845
|
setClauses.push("e.name = $name");
|
|
@@ -30516,7 +30852,7 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30516
30852
|
if (describing) {
|
|
30517
30853
|
const raw = body.description;
|
|
30518
30854
|
if (typeof raw !== "string" && raw !== null) {
|
|
30519
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30855
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=description outcome=rejected reason=not-a-string`);
|
|
30520
30856
|
return c.json({ error: "A description must be text, or null to clear it." }, 400);
|
|
30521
30857
|
}
|
|
30522
30858
|
nextDescription = typeof raw === "string" && raw.trim().length > 0 ? raw : null;
|
|
@@ -30537,7 +30873,7 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30537
30873
|
params.embedding = embedding;
|
|
30538
30874
|
} else {
|
|
30539
30875
|
embedLog = " embed=failed";
|
|
30540
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30876
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=${fields.join("+")} embed=failed`);
|
|
30541
30877
|
}
|
|
30542
30878
|
}
|
|
30543
30879
|
if (typeof body.recurrence === "string") {
|
|
@@ -30551,7 +30887,7 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30551
30887
|
nextRunLog = nextRun;
|
|
30552
30888
|
} catch (err) {
|
|
30553
30889
|
const reason = err instanceof Error ? err.message : String(err);
|
|
30554
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30890
|
+
console.error(`[admin:routines] op=patch id=${id83} outcome=rejected reason=invalid-cron cron=${body.recurrence}`);
|
|
30555
30891
|
return c.json({ error: `Invalid recurrence: ${reason}` }, 400);
|
|
30556
30892
|
}
|
|
30557
30893
|
}
|
|
@@ -30569,15 +30905,15 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30569
30905
|
params
|
|
30570
30906
|
);
|
|
30571
30907
|
if (res.records.length === 0) {
|
|
30572
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30908
|
+
console.error(`[admin:routines] op=patch id=${id83} outcome=rejected reason=not-found`);
|
|
30573
30909
|
return c.json({ error: "Routine not found" }, 404);
|
|
30574
30910
|
}
|
|
30575
30911
|
const nameLog = nextName === null ? "" : ` name=len:${nextName.length} operatorEdited=stamped`;
|
|
30576
|
-
console.log(`[admin:routines] op=patch id=${
|
|
30912
|
+
console.log(`[admin:routines] op=patch id=${id83} fields=${fields.join("+")} outcome=ok nextRun=${nextRunLog} cron=${params.recurrence ?? "unchanged"}${bindingLog}${nameLog}${embedLog}`);
|
|
30577
30913
|
return c.json({ routine: mapRoutine(res.records[0]) });
|
|
30578
30914
|
} catch (err) {
|
|
30579
30915
|
const message = err instanceof Error ? err.message : String(err);
|
|
30580
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30916
|
+
console.error(`[admin:routines] op=patch id=${id83} outcome=rejected reason=${message}`);
|
|
30581
30917
|
return c.json({ error: `Update failed: ${message}` }, 503);
|
|
30582
30918
|
} finally {
|
|
30583
30919
|
await session.close();
|
|
@@ -33931,7 +34267,7 @@ function formatConfigAuditLine(a, accountId) {
|
|
|
33931
34267
|
function formatRecordCensus(f, accountId) {
|
|
33932
34268
|
return `${RECORD_CENSUS_TAG}${accountId ? ` account=${accountId}` : ""} closedVisits=${f.closedVisits} withNote=${f.withNote} withMedia=${f.withMedia} medianRecordLagS=${f.medianRecordLagS} p90RecordLagS=${f.p90RecordLagS}`;
|
|
33933
34269
|
}
|
|
33934
|
-
function
|
|
34270
|
+
function num4(v) {
|
|
33935
34271
|
if (typeof v === "number") return v;
|
|
33936
34272
|
if (v && typeof v === "object" && "low" in v) {
|
|
33937
34273
|
return Number(v.low);
|
|
@@ -34027,15 +34363,15 @@ async function readCensusCounts(session, accountId, terminal, now) {
|
|
|
34027
34363
|
{ accountId, canonical: `^${CANONICAL_INSTANT_PATTERN}$` }
|
|
34028
34364
|
);
|
|
34029
34365
|
return {
|
|
34030
|
-
jobs:
|
|
34031
|
-
unallocated:
|
|
34366
|
+
jobs: num4(g?.get("jobs")),
|
|
34367
|
+
unallocated: num4(g?.get("unallocated")),
|
|
34032
34368
|
oldestUnallocatedAgeH,
|
|
34033
|
-
overdue:
|
|
34034
|
-
visitsOpen:
|
|
34035
|
-
visitsOpenOver24h:
|
|
34036
|
-
intakeUnreviewed:
|
|
34369
|
+
overdue: num4(g?.get("overdue")),
|
|
34370
|
+
visitsOpen: num4(vg?.get("visitsOpen")),
|
|
34371
|
+
visitsOpenOver24h: num4(vg?.get("visitsOpenOver24h")),
|
|
34372
|
+
intakeUnreviewed: num4(dg?.get("intakeUnreviewed")),
|
|
34037
34373
|
oldestIntakeAgeH,
|
|
34038
|
-
nonCanonicalInstants:
|
|
34374
|
+
nonCanonicalInstants: num4(nc.records[0]?.get("nonCanonicalInstants"))
|
|
34039
34375
|
};
|
|
34040
34376
|
}
|
|
34041
34377
|
async function readRecordCensus(session, accountId, terminal, now) {
|
|
@@ -34053,8 +34389,8 @@ async function readRecordCensus(session, accountId, terminal, now) {
|
|
|
34053
34389
|
let withMedia = 0;
|
|
34054
34390
|
const lags = [];
|
|
34055
34391
|
for (const g of r.records) {
|
|
34056
|
-
if (
|
|
34057
|
-
if (
|
|
34392
|
+
if (num4(g.get("notes")) > 0) withNote++;
|
|
34393
|
+
if (num4(g.get("media")) > 0) withMedia++;
|
|
34058
34394
|
const closedAt = g.get("closedAt");
|
|
34059
34395
|
const dueToLeaveAt = g.get("dueToLeaveAt");
|
|
34060
34396
|
if (typeof closedAt === "string" && typeof dueToLeaveAt === "string") {
|
|
@@ -34975,8 +35311,18 @@ async function migrateSidecarAccountIds(opts = {}) {
|
|
|
34975
35311
|
init_src2();
|
|
34976
35312
|
|
|
34977
35313
|
// app/lib/whatsapp/gateway/inbound-hub.ts
|
|
35314
|
+
var HUB_SEPARATOR = "\0";
|
|
34978
35315
|
function hubKey(accountId, senderId) {
|
|
34979
|
-
return `${accountId}
|
|
35316
|
+
return `${accountId}${HUB_SEPARATOR}${senderId}`;
|
|
35317
|
+
}
|
|
35318
|
+
function decodeHubKeys(keys) {
|
|
35319
|
+
const out = [];
|
|
35320
|
+
for (const key2 of keys) {
|
|
35321
|
+
const i = key2.indexOf(HUB_SEPARATOR);
|
|
35322
|
+
if (i < 0) continue;
|
|
35323
|
+
out.push({ accountId: key2.slice(0, i), senderId: key2.slice(i + HUB_SEPARATOR.length) });
|
|
35324
|
+
}
|
|
35325
|
+
return out;
|
|
34980
35326
|
}
|
|
34981
35327
|
var InboundHub = class {
|
|
34982
35328
|
senders = /* @__PURE__ */ new Map();
|
|
@@ -35479,15 +35825,15 @@ function createWaChannelRoutes(deps) {
|
|
|
35479
35825
|
}
|
|
35480
35826
|
const interactive = body?.interactive === true;
|
|
35481
35827
|
const finalText = typeof body?.finalText === "string" ? body.finalText : "";
|
|
35482
|
-
const
|
|
35483
|
-
const finalBytes =
|
|
35484
|
-
const queueDepth =
|
|
35485
|
-
const pendingTurns =
|
|
35486
|
-
const consumed2 =
|
|
35828
|
+
const num5 = (v) => typeof v === "number" && Number.isFinite(v) ? v : -1;
|
|
35829
|
+
const finalBytes = num5(body?.finalBytes);
|
|
35830
|
+
const queueDepth = num5(body?.queueDepth);
|
|
35831
|
+
const pendingTurns = num5(body?.pendingTurns);
|
|
35832
|
+
const consumed2 = num5(body?.consumed);
|
|
35487
35833
|
const source = typeof body?.source === "string" ? body.source : null;
|
|
35488
35834
|
const sid = sessionId.slice(0, 8);
|
|
35489
35835
|
console.error(
|
|
35490
|
-
`[whatsapp-native] op=turn-context sessionId=${sid} cacheReadTokens=${
|
|
35836
|
+
`[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)}`
|
|
35491
35837
|
);
|
|
35492
35838
|
const role = body?.role;
|
|
35493
35839
|
if (role !== "admin" && role !== "public") {
|
|
@@ -35968,6 +36314,14 @@ var WaGateway = class {
|
|
|
35968
36314
|
liveReplyTarget(accountId, senderId) {
|
|
35969
36315
|
return this.replies.get(hubKey(accountId, normalizeTarget(senderId) ?? senderId)) ?? null;
|
|
35970
36316
|
}
|
|
36317
|
+
/** Task 2557 — every (account, sender) this gateway currently holds a live
|
|
36318
|
+
* delivery closure for. The follower census reconciles against exactly this
|
|
36319
|
+
* set, so "should have a reader" and "can be sent to" are one fact and
|
|
36320
|
+
* cannot drift. Deriving the set from the follower registry instead would
|
|
36321
|
+
* ask the map that already lost the entry. */
|
|
36322
|
+
boundTargets() {
|
|
36323
|
+
return decodeHubKeys(this.replies.keys());
|
|
36324
|
+
}
|
|
35971
36325
|
async sendOutbound(accountId, senderId, text) {
|
|
35972
36326
|
this.outboundSends++;
|
|
35973
36327
|
const reply = this.replies.get(hubKey(accountId, senderId));
|
|
@@ -36441,12 +36795,12 @@ function createWebchatChannelRoutes(deps) {
|
|
|
36441
36795
|
const finalBytes = typeof body?.finalBytes === "number" && Number.isFinite(body.finalBytes) ? body.finalBytes : 0;
|
|
36442
36796
|
const k = key2.slice(0, 8);
|
|
36443
36797
|
const sid = sessionId.slice(0, 8);
|
|
36444
|
-
const
|
|
36798
|
+
const num5 = (v) => typeof v === "number" && Number.isFinite(v) ? v : -1;
|
|
36445
36799
|
console.error(
|
|
36446
|
-
`[webchat-native] op=turn-context sessionId=${sid} cacheReadTokens=${
|
|
36800
|
+
`[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)}`
|
|
36447
36801
|
);
|
|
36448
|
-
const segmentStart =
|
|
36449
|
-
const segmentEnd =
|
|
36802
|
+
const segmentStart = num5(body?.segmentStart);
|
|
36803
|
+
const segmentEnd = num5(body?.segmentEnd);
|
|
36450
36804
|
const resolved = deps.onTurnEnd?.(key2, role, { sessionId, segmentStart, segmentEnd }) ?? "none";
|
|
36451
36805
|
const resolvedField = role === "public" ? ` resolved=${resolved}` : "";
|
|
36452
36806
|
console.error(`[webchat-native] op=turn-end channel=webchat key=${k} role=${role} sessionId=${sid} trailing=${finalBytes}${resolvedField}`);
|
|
@@ -37188,7 +37542,10 @@ function startEmitter(opts) {
|
|
|
37188
37542
|
const abort2 = new AbortController();
|
|
37189
37543
|
const { entry: entry2, tag } = opts;
|
|
37190
37544
|
const answers = new AnswerAccumulator();
|
|
37545
|
+
let answersEmitted = 0;
|
|
37546
|
+
let closeReason = "stream-end";
|
|
37191
37547
|
const emitAnswer = async (answer) => {
|
|
37548
|
+
answersEmitted += 1;
|
|
37192
37549
|
if (answer.text.trim()) await fanOut2(entry2.subscribers, answer.text, opts.onError, tag);
|
|
37193
37550
|
if (!opts.onAnswer) return;
|
|
37194
37551
|
const guarded = { ...answer, text: guardOutbound(entry2.senderId, answer.text) };
|
|
@@ -37361,9 +37718,14 @@ function startEmitter(opts) {
|
|
|
37361
37718
|
for (const answer of answers.flush()) await emitAnswer(answer);
|
|
37362
37719
|
} catch (err) {
|
|
37363
37720
|
if (!abort2.signal.aborted) {
|
|
37721
|
+
closeReason = "error";
|
|
37364
37722
|
opts.onError?.(`emitter-error: ${err instanceof Error ? err.message : String(err)}`);
|
|
37365
37723
|
}
|
|
37366
37724
|
} finally {
|
|
37725
|
+
if (abort2.signal.aborted) closeReason = "aborted";
|
|
37726
|
+
console.error(
|
|
37727
|
+
`${tag} op=follower-closed sessionId=${entry2.sessionId.slice(0, 8)} reason=${closeReason} answers=${answersEmitted}`
|
|
37728
|
+
);
|
|
37367
37729
|
opts.onClose();
|
|
37368
37730
|
}
|
|
37369
37731
|
})();
|
|
@@ -37564,6 +37926,12 @@ function startWebchatNativeFileFollower(input) {
|
|
|
37564
37926
|
return startEmitter({
|
|
37565
37927
|
entry: entry2,
|
|
37566
37928
|
tag: "[webchat-adaptor]",
|
|
37929
|
+
// Task 2557 — the emitter has always had this channel and no follower ever
|
|
37930
|
+
// passed one, so every cause it reported was discarded. The close line
|
|
37931
|
+
// carries the outcome; this carries the cause.
|
|
37932
|
+
onError: (reason) => {
|
|
37933
|
+
console.error(`[webchat-adaptor] op=follower-error sessionId=${input.sessionId.slice(0, 8)} reason=${reason}`);
|
|
37934
|
+
},
|
|
37567
37935
|
fileDelivery: makeWebchatFileDelivery(entry2),
|
|
37568
37936
|
// A resumed session's JSONL already holds prior SendUserFile tool_uses;
|
|
37569
37937
|
// suppress replay so historical calls are not re-reconciled on attach.
|
|
@@ -37728,6 +38096,14 @@ function startNativeFileFollower(input) {
|
|
|
37728
38096
|
return startEmitter({
|
|
37729
38097
|
entry: entry2,
|
|
37730
38098
|
tag: "[whatsapp-adaptor]",
|
|
38099
|
+
// Task 2557 — the emitter has always had this channel and no follower ever
|
|
38100
|
+
// passed one, so every cause it reported (`follow-status-404`,
|
|
38101
|
+
// `emitter-error: …`, `subscriber-error: …`) was discarded. The close line
|
|
38102
|
+
// carries the outcome; this carries the cause, and it can fire without the
|
|
38103
|
+
// stream ending, so neither substitutes for the other.
|
|
38104
|
+
onError: (reason) => {
|
|
38105
|
+
console.error(`[whatsapp-adaptor] op=follower-error sessionId=${input.sessionId.slice(0, 8)} reason=${reason}`);
|
|
38106
|
+
},
|
|
37731
38107
|
// Task 2521 — admin-only. A public spawn has no tools (Task 2078), so a
|
|
37732
38108
|
// handler here would reconcile a call that can never fire.
|
|
37733
38109
|
fileDelivery: input.role === "admin" ? makeWhatsAppFileDelivery(entry2, input.maxyAccountId) : null,
|
|
@@ -37752,6 +38128,69 @@ function startNativeFileFollower(input) {
|
|
|
37752
38128
|
});
|
|
37753
38129
|
}
|
|
37754
38130
|
|
|
38131
|
+
// app/lib/channel-delivery/follower-registry.ts
|
|
38132
|
+
function createFollowerRegistry(start) {
|
|
38133
|
+
const controllers = /* @__PURE__ */ new Map();
|
|
38134
|
+
const params = /* @__PURE__ */ new Map();
|
|
38135
|
+
return {
|
|
38136
|
+
attach(p) {
|
|
38137
|
+
controllers.get(p.senderId)?.abort();
|
|
38138
|
+
params.set(p.senderId, p);
|
|
38139
|
+
let ac;
|
|
38140
|
+
ac = start(p, () => {
|
|
38141
|
+
if (controllers.get(p.senderId) === ac) controllers.delete(p.senderId);
|
|
38142
|
+
});
|
|
38143
|
+
controllers.set(p.senderId, ac);
|
|
38144
|
+
},
|
|
38145
|
+
live: () => [...controllers.keys()],
|
|
38146
|
+
paramsFor: (senderId) => params.get(senderId) ?? null
|
|
38147
|
+
};
|
|
38148
|
+
}
|
|
38149
|
+
|
|
38150
|
+
// app/lib/channel-delivery/follower-census.ts
|
|
38151
|
+
var CENSUS_MAX_REATTEMPTS = 3;
|
|
38152
|
+
function makeCensusState() {
|
|
38153
|
+
return { attempts: /* @__PURE__ */ new Map() };
|
|
38154
|
+
}
|
|
38155
|
+
var id82 = (s) => s.slice(0, 8);
|
|
38156
|
+
function followerCensusTick(deps, state = makeCensusState()) {
|
|
38157
|
+
const bound = deps.bound();
|
|
38158
|
+
const live = new Set(deps.live());
|
|
38159
|
+
for (const senderId of live) state.attempts.delete(senderId);
|
|
38160
|
+
const missing = bound.filter((senderId) => !live.has(senderId));
|
|
38161
|
+
console.error(
|
|
38162
|
+
`[channel-follower] op=follower-census channel=${deps.channel} bound=${bound.length} attached=${live.size} missing=${missing.length} missingSenders=${missing.map(id82).join(",") || "none"}`
|
|
38163
|
+
);
|
|
38164
|
+
for (const senderId of missing) {
|
|
38165
|
+
const used = state.attempts.get(senderId) ?? 0;
|
|
38166
|
+
if (used >= CENSUS_MAX_REATTEMPTS) {
|
|
38167
|
+
console.error(
|
|
38168
|
+
`[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=${id82(deps.paramsFor(senderId)?.sessionId ?? "none")} senderId=${id82(senderId)} outcome=skipped reason=reattach-exhausted attempts=${used}`
|
|
38169
|
+
);
|
|
38170
|
+
continue;
|
|
38171
|
+
}
|
|
38172
|
+
const p = deps.paramsFor(senderId);
|
|
38173
|
+
if (!p) {
|
|
38174
|
+
console.error(
|
|
38175
|
+
`[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=none senderId=${id82(senderId)} outcome=failed reason=no-attach-params`
|
|
38176
|
+
);
|
|
38177
|
+
continue;
|
|
38178
|
+
}
|
|
38179
|
+
try {
|
|
38180
|
+
state.attempts.set(senderId, used + 1);
|
|
38181
|
+
deps.attach(p);
|
|
38182
|
+
console.error(
|
|
38183
|
+
`[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=${id82(p.sessionId)} senderId=${id82(p.senderId)} outcome=attached reason=none`
|
|
38184
|
+
);
|
|
38185
|
+
} catch (err) {
|
|
38186
|
+
console.error(
|
|
38187
|
+
`[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=${id82(p.sessionId)} senderId=${id82(p.senderId)} outcome=failed reason=${err instanceof Error ? err.message : String(err)}`
|
|
38188
|
+
);
|
|
38189
|
+
}
|
|
38190
|
+
}
|
|
38191
|
+
return `${deps.channel}: ${bound.length} door(s), ${missing.length} without a reader`;
|
|
38192
|
+
}
|
|
38193
|
+
|
|
37755
38194
|
// app/lib/telegram/gateway/inbound-hub.ts
|
|
37756
38195
|
var InboundHub3 = class {
|
|
37757
38196
|
keys = /* @__PURE__ */ new Map();
|
|
@@ -37905,7 +38344,8 @@ function createTelegramChannelRoutes(deps) {
|
|
|
37905
38344
|
if (typeof key2 !== "string" || typeof messageId !== "string") {
|
|
37906
38345
|
return c.json({ error: "key and messageId required" }, 400);
|
|
37907
38346
|
}
|
|
37908
|
-
|
|
38347
|
+
const provenance = typeof body?.provenance === "boolean" ? body.provenance : void 0;
|
|
38348
|
+
deps.onReceived?.(key2, messageId, provenance);
|
|
37909
38349
|
return c.json({ ok: true });
|
|
37910
38350
|
});
|
|
37911
38351
|
app81.post("/tg-channel/turn-end", async (c) => {
|
|
@@ -37919,9 +38359,9 @@ function createTelegramChannelRoutes(deps) {
|
|
|
37919
38359
|
const interactive = body?.interactive === true;
|
|
37920
38360
|
const finalText = typeof body?.finalText === "string" ? body.finalText : "";
|
|
37921
38361
|
const sid = sessionId.slice(0, 8);
|
|
37922
|
-
const
|
|
38362
|
+
const num5 = (v) => typeof v === "number" && Number.isFinite(v) ? v : -1;
|
|
37923
38363
|
console.error(
|
|
37924
|
-
`[telegram-native] op=turn-context sessionId=${sid} cacheReadTokens=${
|
|
38364
|
+
`[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)}`
|
|
37925
38365
|
);
|
|
37926
38366
|
const role = body?.role;
|
|
37927
38367
|
if (role !== "admin" && role !== "public") {
|
|
@@ -38039,6 +38479,16 @@ var TelegramGateway = class {
|
|
|
38039
38479
|
}
|
|
38040
38480
|
hub = new InboundHub3();
|
|
38041
38481
|
replies = /* @__PURE__ */ new Map();
|
|
38482
|
+
/** Task 2559 — the keys whose last inbound was `role:'admin'`. The follower
|
|
38483
|
+
* census's bound set is the doors that SHOULD have a reader, and a public
|
|
38484
|
+
* Telegram session is given none by design: the public branch of
|
|
38485
|
+
* `buildTelegramEnsureSession` returns before the attach block, because the
|
|
38486
|
+
* file follower is an admin surface. Including a public key would report it
|
|
38487
|
+
* missing on every tick for ever and stop `missing=` ever reaching zero,
|
|
38488
|
+
* which is the one signal the census exists to give. Removed on a public
|
|
38489
|
+
* inbound, because the role is read from account config on every inbound and
|
|
38490
|
+
* a bot can be flipped between the two. */
|
|
38491
|
+
adminKeys = /* @__PURE__ */ new Set();
|
|
38042
38492
|
spawning = /* @__PURE__ */ new Set();
|
|
38043
38493
|
/** Task 2428 — keys whose channel server signalled ready while their spawn was
|
|
38044
38494
|
* still in flight. The harness discards a channel notification that arrives
|
|
@@ -38066,7 +38516,7 @@ var TelegramGateway = class {
|
|
|
38066
38516
|
}
|
|
38067
38517
|
this.drain(key2);
|
|
38068
38518
|
},
|
|
38069
|
-
onReceived: (key2, messageId) => {
|
|
38519
|
+
onReceived: (key2, messageId, provenance) => {
|
|
38070
38520
|
const acked = this.hub.ack(key2, messageId);
|
|
38071
38521
|
if (acked !== null && (acked.source ?? "user") === "user") {
|
|
38072
38522
|
noteReceived(key2, parseTelegramChannelKey(key2)?.botId ?? "unknown", messageId, Date.now());
|
|
@@ -38074,6 +38524,10 @@ var TelegramGateway = class {
|
|
|
38074
38524
|
console.error(
|
|
38075
38525
|
`[telegram-native] op=channel-received key=${key2} messageId=${messageId}${acked !== null ? "" : " note=unknown"}`
|
|
38076
38526
|
);
|
|
38527
|
+
const prov = provenance === void 0 ? "unknown" : provenance ? "yes" : "no";
|
|
38528
|
+
console.error(
|
|
38529
|
+
`[telegram:reframe] op=compose key=${key2} provenance=${prov} source=${acked?.source ?? "unknown"}`
|
|
38530
|
+
);
|
|
38077
38531
|
}
|
|
38078
38532
|
});
|
|
38079
38533
|
}
|
|
@@ -38146,6 +38600,8 @@ var TelegramGateway = class {
|
|
|
38146
38600
|
const willDeliverNow = this.hub.isReady(key2);
|
|
38147
38601
|
const source = input.source ?? "user";
|
|
38148
38602
|
this.replies.set(key2, input.reply);
|
|
38603
|
+
if (input.role === "admin") this.adminKeys.add(key2);
|
|
38604
|
+
else this.adminKeys.delete(key2);
|
|
38149
38605
|
let standingRules;
|
|
38150
38606
|
if (input.role === "admin" && this.deps.fetchStandingRules) {
|
|
38151
38607
|
const res = await this.deps.fetchStandingRules(input.accountId);
|
|
@@ -38157,7 +38613,21 @@ var TelegramGateway = class {
|
|
|
38157
38613
|
);
|
|
38158
38614
|
}
|
|
38159
38615
|
this.hub.deliver(
|
|
38160
|
-
{
|
|
38616
|
+
{
|
|
38617
|
+
key: key2,
|
|
38618
|
+
text: input.text,
|
|
38619
|
+
media: input.media,
|
|
38620
|
+
messageId: `tg-${++this.seq}`,
|
|
38621
|
+
source,
|
|
38622
|
+
// Task 2561 — passed explicitly rather than parsed out of `key`:
|
|
38623
|
+
// accountId is not in the key, and the channel service imports nothing
|
|
38624
|
+
// from app/lib. Without them the composer cannot name who is speaking,
|
|
38625
|
+
// and the turn opens straight into rules written about the owner.
|
|
38626
|
+
senderId: input.senderId,
|
|
38627
|
+
accountId: input.accountId,
|
|
38628
|
+
standingRules,
|
|
38629
|
+
scheduleProvenance: input.scheduleProvenance
|
|
38630
|
+
},
|
|
38161
38631
|
Date.now()
|
|
38162
38632
|
);
|
|
38163
38633
|
const resolvedSessionId = this.sessionIdFor(input);
|
|
@@ -38197,6 +38667,18 @@ var TelegramGateway = class {
|
|
|
38197
38667
|
liveReplyTarget(key2) {
|
|
38198
38668
|
return this.replies.get(key2) ?? null;
|
|
38199
38669
|
}
|
|
38670
|
+
/** Task 2559 — every admin door this gateway currently holds a live delivery
|
|
38671
|
+
* closure for, as the `<botId>:<senderId>` channel key the follower registry
|
|
38672
|
+
* keys on. The twin of `WaGateway.boundTargets()`: the census reconciles
|
|
38673
|
+
* against exactly this set, so "should have a reader" and "can be sent to"
|
|
38674
|
+
* are one fact and cannot drift. A key `parseTelegramChannelKey` rejects is
|
|
38675
|
+
* the pre-2368 bare-sender shape, skipped rather than halved at an arbitrary
|
|
38676
|
+
* offset, which would yield a door id nothing keys on. */
|
|
38677
|
+
boundTargets() {
|
|
38678
|
+
return [...this.replies.keys()].filter(
|
|
38679
|
+
(key2) => this.adminKeys.has(key2) && parseTelegramChannelKey(key2) !== null
|
|
38680
|
+
);
|
|
38681
|
+
}
|
|
38200
38682
|
async sendOutbound(key2, text) {
|
|
38201
38683
|
const reply = this.replies.get(key2);
|
|
38202
38684
|
if (!reply) throw new Error(`telegram-gateway: no reply route for key ${key2}`);
|
|
@@ -38291,24 +38773,13 @@ function buildTelegramEnsureSession(deps) {
|
|
|
38291
38773
|
`${TAG62} op=spawn botId=${input.botId} sessionId=${req.sessionId} targetAccountId=${input.accountId} role=admin`
|
|
38292
38774
|
);
|
|
38293
38775
|
noteSpawnOk2(input.botId);
|
|
38294
|
-
deps.followers.
|
|
38295
|
-
let ac;
|
|
38296
|
-
ac = deps.startFileFollower({
|
|
38776
|
+
deps.followers.attach({
|
|
38297
38777
|
sessionId: req.sessionId,
|
|
38298
38778
|
senderId: req.channelKey,
|
|
38299
38779
|
accountId: input.accountId,
|
|
38300
38780
|
chatId: input.chatId,
|
|
38301
|
-
botToken: input.botToken
|
|
38302
|
-
// Task 2521 — resolved per answer, not captured once, so a door that goes
|
|
38303
|
-
// live after this session started still receives.
|
|
38304
|
-
fanTargets: () => deps.fanTargets(req.channelKey),
|
|
38305
|
-
// Identity-guarded eviction: only delete if this controller is still the
|
|
38306
|
-
// current one, so a stale self-close never removes a live follower.
|
|
38307
|
-
onClose: () => {
|
|
38308
|
-
if (deps.followers.get(req.channelKey) === ac) deps.followers.delete(req.channelKey);
|
|
38309
|
-
}
|
|
38781
|
+
botToken: input.botToken
|
|
38310
38782
|
});
|
|
38311
|
-
deps.followers.set(req.channelKey, ac);
|
|
38312
38783
|
};
|
|
38313
38784
|
}
|
|
38314
38785
|
|
|
@@ -38449,6 +38920,12 @@ function startTelegramNativeFileFollower(input) {
|
|
|
38449
38920
|
return startEmitter({
|
|
38450
38921
|
entry: entry2,
|
|
38451
38922
|
tag: "[telegram-adaptor]",
|
|
38923
|
+
// Task 2557 — the emitter has always had this channel and no follower ever
|
|
38924
|
+
// passed one, so every cause it reported was discarded. The close line
|
|
38925
|
+
// carries the outcome; this carries the cause.
|
|
38926
|
+
onError: (reason) => {
|
|
38927
|
+
console.error(`[telegram-adaptor] op=follower-error sessionId=${input.sessionId.slice(0, 8)} reason=${reason}`);
|
|
38928
|
+
},
|
|
38452
38929
|
fileDelivery: makeTelegramFileDelivery(entry2, input.botToken),
|
|
38453
38930
|
// A resumed session's JSONL already holds prior SendUserFile tool_uses;
|
|
38454
38931
|
// suppress replay so historical files are not re-sent on attach.
|
|
@@ -39367,7 +39844,27 @@ function resolveChannelOverride(accountId, channel, senderId) {
|
|
|
39367
39844
|
return null;
|
|
39368
39845
|
}
|
|
39369
39846
|
}
|
|
39370
|
-
var nativeFileFollowers =
|
|
39847
|
+
var nativeFileFollowers = createFollowerRegistry(
|
|
39848
|
+
(p, onClose) => startNativeFileFollower({
|
|
39849
|
+
sessionId: p.sessionId,
|
|
39850
|
+
senderId: p.senderId,
|
|
39851
|
+
// Task 1390 — the file-path validation scope is the sender's effective
|
|
39852
|
+
// session account (the sub-account for an account-manager), not the house
|
|
39853
|
+
// socket account. `accountId` stays the getSocket key.
|
|
39854
|
+
accountId: p.accountId,
|
|
39855
|
+
maxyAccountId: p.effectiveAccountId,
|
|
39856
|
+
// Task 2521 — file delivery is admin-only; the text emitter is not.
|
|
39857
|
+
role: p.role,
|
|
39858
|
+
// Task 2521 — resolved per answer, not captured once, so a door that goes
|
|
39859
|
+
// live after this session started still receives.
|
|
39860
|
+
// Task 2531 — `effectiveAccountId`, NOT `accountId`. The reply map is keyed
|
|
39861
|
+
// on the platform UUID (wa-gateway.ts:567) while `accountId` here is the
|
|
39862
|
+
// Baileys credential dirname. Passing the dirname missed every lookup and
|
|
39863
|
+
// delivered session 8e5bc4e8's answer to nothing.
|
|
39864
|
+
fanTargets: () => whatsappFanTargets(p.effectiveAccountId, p.senderId),
|
|
39865
|
+
onClose
|
|
39866
|
+
})
|
|
39867
|
+
);
|
|
39371
39868
|
var webchatFileFollowers = /* @__PURE__ */ new Map();
|
|
39372
39869
|
async function fetchAccountStandingRules(accountId) {
|
|
39373
39870
|
const session = getSession();
|
|
@@ -39598,32 +40095,13 @@ var waGateway = new WaGateway({
|
|
|
39598
40095
|
console.error(`[whatsapp-native] spawn-failed senderId=${senderId} role=${role} error=${result.error} status=${result.status}`);
|
|
39599
40096
|
return;
|
|
39600
40097
|
}
|
|
39601
|
-
|
|
39602
|
-
if (prior) prior.abort();
|
|
39603
|
-
let ac;
|
|
39604
|
-
ac = startNativeFileFollower({
|
|
40098
|
+
nativeFileFollowers.attach({
|
|
39605
40099
|
sessionId: req.sessionId,
|
|
39606
40100
|
senderId,
|
|
39607
40101
|
accountId,
|
|
39608
|
-
|
|
39609
|
-
|
|
39610
|
-
// socket account. accountId stays the getSocket key.
|
|
39611
|
-
maxyAccountId: effectiveAccountId,
|
|
39612
|
-
// Task 2521 — file delivery is admin-only; the text emitter is not.
|
|
39613
|
-
role,
|
|
39614
|
-
// Task 2521 — resolved per answer, not captured once, so a door that goes
|
|
39615
|
-
// live after this session started still receives.
|
|
39616
|
-
// Task 2531 — `effectiveAccountId`, NOT `accountId`. The reply map is
|
|
39617
|
-
// keyed on the platform UUID (wa-gateway.ts:567) while `accountId` here is
|
|
39618
|
-
// the Baileys credential dirname (see the Task 1850 note above). Passing
|
|
39619
|
-
// the dirname missed every lookup and delivered session 8e5bc4e8's answer
|
|
39620
|
-
// to nothing. `accountId` stays correct on line 718: that is the socket.
|
|
39621
|
-
fanTargets: () => whatsappFanTargets(effectiveAccountId, senderId),
|
|
39622
|
-
onClose: () => {
|
|
39623
|
-
if (nativeFileFollowers.get(senderId) === ac) nativeFileFollowers.delete(senderId);
|
|
39624
|
-
}
|
|
40102
|
+
effectiveAccountId,
|
|
40103
|
+
role
|
|
39625
40104
|
});
|
|
39626
|
-
nativeFileFollowers.set(senderId, ac);
|
|
39627
40105
|
}
|
|
39628
40106
|
});
|
|
39629
40107
|
app80.route("/", waGateway.routes());
|
|
@@ -39793,7 +40271,6 @@ setWebchatGateway(webchatGateway);
|
|
|
39793
40271
|
app80.route("/", webchatGateway.routes());
|
|
39794
40272
|
webchatGateway.startSweeper();
|
|
39795
40273
|
webchatGateway.startPublicReaper();
|
|
39796
|
-
var telegramFileFollowers = /* @__PURE__ */ new Map();
|
|
39797
40274
|
var telegramFanTargets = (key2) => {
|
|
39798
40275
|
const account = listValidAccounts().find(
|
|
39799
40276
|
(a) => listBotEntries(a.config.telegram).some((e) => e.id === key2.slice(0, key2.indexOf(":")))
|
|
@@ -39810,6 +40287,19 @@ var telegramFanTargets = (key2) => {
|
|
|
39810
40287
|
deps: adminFanDeps("telegram", account.accountId)
|
|
39811
40288
|
});
|
|
39812
40289
|
};
|
|
40290
|
+
var telegramFileFollowers = createFollowerRegistry(
|
|
40291
|
+
(p, onClose) => startTelegramNativeFileFollower({
|
|
40292
|
+
sessionId: p.sessionId,
|
|
40293
|
+
senderId: p.senderId,
|
|
40294
|
+
accountId: p.accountId,
|
|
40295
|
+
chatId: p.chatId,
|
|
40296
|
+
botToken: p.botToken,
|
|
40297
|
+
// Task 2521 — resolved per answer, not captured once, so a door that goes
|
|
40298
|
+
// live after this session started still receives.
|
|
40299
|
+
fanTargets: () => telegramFanTargets(p.senderId),
|
|
40300
|
+
onClose
|
|
40301
|
+
})
|
|
40302
|
+
);
|
|
39813
40303
|
var telegramGateway = new TelegramGateway({
|
|
39814
40304
|
fetchStandingRules: fetchAccountStandingRules,
|
|
39815
40305
|
// Task 2353 — no turn-instruction resolver on this door. See the dep comment in
|
|
@@ -39830,14 +40320,39 @@ var telegramGateway = new TelegramGateway({
|
|
|
39830
40320
|
managerRcSpawn,
|
|
39831
40321
|
managerSpawn,
|
|
39832
40322
|
resolveSessionOverride: (accountId, channelKey) => resolveChannelOverride(accountId, "telegram", channelKey),
|
|
39833
|
-
startFileFollower: startTelegramNativeFileFollower,
|
|
39834
|
-
fanTargets: telegramFanTargets,
|
|
39835
40323
|
followers: telegramFileFollowers
|
|
39836
40324
|
})
|
|
39837
40325
|
});
|
|
39838
40326
|
setTelegramGateway(telegramGateway);
|
|
39839
40327
|
app80.route("/", telegramGateway.routes());
|
|
39840
40328
|
telegramGateway.startSweeper();
|
|
40329
|
+
var followerCensusState = { whatsapp: makeCensusState(), telegram: makeCensusState() };
|
|
40330
|
+
registerLoop({
|
|
40331
|
+
name: "channel-follower",
|
|
40332
|
+
intervalMs: 6e4,
|
|
40333
|
+
run: () => [
|
|
40334
|
+
followerCensusTick(
|
|
40335
|
+
{
|
|
40336
|
+
channel: "whatsapp",
|
|
40337
|
+
bound: () => waGateway.boundTargets().map((t) => t.senderId),
|
|
40338
|
+
live: () => nativeFileFollowers.live(),
|
|
40339
|
+
paramsFor: (senderId) => nativeFileFollowers.paramsFor(senderId),
|
|
40340
|
+
attach: (p) => nativeFileFollowers.attach(p)
|
|
40341
|
+
},
|
|
40342
|
+
followerCensusState.whatsapp
|
|
40343
|
+
),
|
|
40344
|
+
followerCensusTick(
|
|
40345
|
+
{
|
|
40346
|
+
channel: "telegram",
|
|
40347
|
+
bound: () => telegramGateway.boundTargets(),
|
|
40348
|
+
live: () => telegramFileFollowers.live(),
|
|
40349
|
+
paramsFor: (senderId) => telegramFileFollowers.paramsFor(senderId),
|
|
40350
|
+
attach: (p) => telegramFileFollowers.attach(p)
|
|
40351
|
+
},
|
|
40352
|
+
followerCensusState.telegram
|
|
40353
|
+
)
|
|
40354
|
+
].join("; ")
|
|
40355
|
+
});
|
|
39841
40356
|
var chatRoutes = createChatRoutes({
|
|
39842
40357
|
handleInbound: (input) => webchatGateway.handleInbound(input),
|
|
39843
40358
|
awaitReply: (key2, timeoutMs) => webchatGateway.awaitReply(key2, timeoutMs)
|