@rubytech/create-realagent-code 0.1.575 → 0.1.576
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/plugins/admin/skills/whats-new/SKILL.md +4 -0
- 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/server/server.js +187 -63
- package/dist/__tests__/converge-client-admins.test.js +0 -50
package/payload/server/server.js
CHANGED
|
@@ -4259,9 +4259,9 @@ var require_dist5 = __commonJS({
|
|
|
4259
4259
|
try {
|
|
4260
4260
|
await onEmpty(c);
|
|
4261
4261
|
} catch (err) {
|
|
4262
|
-
const
|
|
4262
|
+
const id83 = c.elementId.slice(0, 8);
|
|
4263
4263
|
const message = err instanceof Error ? err.message : String(err);
|
|
4264
|
-
process.stderr.write(`[trash:empty-run] reason=onEmpty-failed elementId=${
|
|
4264
|
+
process.stderr.write(`[trash:empty-run] reason=onEmpty-failed elementId=${id83} labels=${c.labels.join(",")} message=${message}
|
|
4265
4265
|
`);
|
|
4266
4266
|
continue;
|
|
4267
4267
|
}
|
|
@@ -21386,7 +21386,7 @@ init_neo4j_store();
|
|
|
21386
21386
|
async function purgeAdminConversationJsonls(args) {
|
|
21387
21387
|
const fetchImpl = args.fetchImpl ?? fetch;
|
|
21388
21388
|
const log2 = args.logger ?? ((line) => console.log(line));
|
|
21389
|
-
const
|
|
21389
|
+
const id83 = args.sessionId.slice(0, 8);
|
|
21390
21390
|
const purged = [];
|
|
21391
21391
|
for (const sessionId of args.claudeSessionIds) {
|
|
21392
21392
|
const url = `${args.managerBase}/${encodeURIComponent(sessionId)}`;
|
|
@@ -21396,7 +21396,7 @@ async function purgeAdminConversationJsonls(args) {
|
|
|
21396
21396
|
} catch (err) {
|
|
21397
21397
|
const message2 = err instanceof Error ? err.message : String(err);
|
|
21398
21398
|
log2(
|
|
21399
|
-
`[admin-conversation-delete] sessionId=${
|
|
21399
|
+
`[admin-conversation-delete] sessionId=${id83} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message2}`
|
|
21400
21400
|
);
|
|
21401
21401
|
return {
|
|
21402
21402
|
ok: false,
|
|
@@ -21413,7 +21413,7 @@ async function purgeAdminConversationJsonls(args) {
|
|
|
21413
21413
|
}
|
|
21414
21414
|
if (res.status === 409) {
|
|
21415
21415
|
log2(
|
|
21416
|
-
`[admin-conversation-delete] sessionId=${
|
|
21416
|
+
`[admin-conversation-delete] sessionId=${id83} reason=pty-still-alive claudeSessionId=${sessionId.slice(0, 8)}`
|
|
21417
21417
|
);
|
|
21418
21418
|
return {
|
|
21419
21419
|
ok: false,
|
|
@@ -21430,7 +21430,7 @@ async function purgeAdminConversationJsonls(args) {
|
|
|
21430
21430
|
} catch {
|
|
21431
21431
|
}
|
|
21432
21432
|
log2(
|
|
21433
|
-
`[admin-conversation-delete] sessionId=${
|
|
21433
|
+
`[admin-conversation-delete] sessionId=${id83} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message}`
|
|
21434
21434
|
);
|
|
21435
21435
|
return {
|
|
21436
21436
|
ok: false,
|
|
@@ -21449,13 +21449,13 @@ async function cascadeAdminConversationDelete(args) {
|
|
|
21449
21449
|
const started2 = Date.now();
|
|
21450
21450
|
const fetchImpl = args.fetchImpl ?? fetch;
|
|
21451
21451
|
const log2 = args.logger ?? ((line) => console.log(line));
|
|
21452
|
-
const
|
|
21452
|
+
const id83 = args.sessionId.slice(0, 8);
|
|
21453
21453
|
let claudeSessionIds;
|
|
21454
21454
|
try {
|
|
21455
21455
|
claudeSessionIds = args.claudeSessionIds ?? await getConversationClaudeSessionIds(args.sessionId, args.accountId);
|
|
21456
21456
|
} catch (err) {
|
|
21457
21457
|
const message = err instanceof Error ? err.message : String(err);
|
|
21458
|
-
log2(`[admin-conversation-delete] sessionId=${
|
|
21458
|
+
log2(`[admin-conversation-delete] sessionId=${id83} reason=graph-delete-failed claudeSessionIds-purged= message=transcript-resolve:${message}`);
|
|
21459
21459
|
return { ok: false, reason: "graph-delete-failed", claudeSessionIdsPurged: [], message };
|
|
21460
21460
|
}
|
|
21461
21461
|
const purgeOutcome = await purgeAdminConversationJsonls({
|
|
@@ -21486,7 +21486,7 @@ async function cascadeAdminConversationDelete(args) {
|
|
|
21486
21486
|
} catch (err) {
|
|
21487
21487
|
const message = err instanceof Error ? err.message : String(err);
|
|
21488
21488
|
const csv3 = purged.map((s) => s.slice(0, 8)).join(",");
|
|
21489
|
-
log2(`[admin-conversation-delete] sessionId=${
|
|
21489
|
+
log2(`[admin-conversation-delete] sessionId=${id83} reason=graph-delete-failed claudeSessionIds-purged=${csv3} message=${message}`);
|
|
21490
21490
|
return {
|
|
21491
21491
|
ok: false,
|
|
21492
21492
|
reason: "graph-delete-failed",
|
|
@@ -21499,7 +21499,7 @@ async function cascadeAdminConversationDelete(args) {
|
|
|
21499
21499
|
}
|
|
21500
21500
|
const ms = Date.now() - started2;
|
|
21501
21501
|
const csv2 = purged.map((s) => s.slice(0, 8)).join(",");
|
|
21502
|
-
log2(`[admin-conversation-delete] sessionId=${
|
|
21502
|
+
log2(`[admin-conversation-delete] sessionId=${id83} claudeSessionIds=${csv2} ms=${ms}`);
|
|
21503
21503
|
return { ok: true, claudeSessionIds: purged, ms };
|
|
21504
21504
|
}
|
|
21505
21505
|
|
|
@@ -29716,21 +29716,21 @@ app52.get("/booking-link", requireAdminSession, (c) => {
|
|
|
29716
29716
|
console.error("[calendar] op=booking-link auth-rejected reason=no-account-for-session accountId=none");
|
|
29717
29717
|
return c.json({ error: "no account for session", reason: "no-account-for-session" }, 401);
|
|
29718
29718
|
}
|
|
29719
|
-
const
|
|
29719
|
+
const id83 = account.accountId.slice(0, 8);
|
|
29720
29720
|
try {
|
|
29721
29721
|
if (!existsSync41(join50(account.accountDir, AVAILABILITY_FILENAME))) {
|
|
29722
|
-
console.log(`[calendar] op=booking-link accountId=${
|
|
29722
|
+
console.log(`[calendar] op=booking-link accountId=${id83} bookingDomain=none source=availability-config`);
|
|
29723
29723
|
return c.json({ bookingDomain: null });
|
|
29724
29724
|
}
|
|
29725
29725
|
const config = readAvailabilityConfig(account.accountDir);
|
|
29726
29726
|
const bookingDomain = typeof config.bookingDomain === "string" && config.bookingDomain.trim() ? config.bookingDomain.trim() : null;
|
|
29727
29727
|
console.log(
|
|
29728
|
-
`[calendar] op=booking-link accountId=${
|
|
29728
|
+
`[calendar] op=booking-link accountId=${id83} bookingDomain=${bookingDomain ?? "none"} source=availability-config`
|
|
29729
29729
|
);
|
|
29730
29730
|
return c.json({ bookingDomain });
|
|
29731
29731
|
} catch (err) {
|
|
29732
29732
|
const message = err instanceof Error ? err.message : String(err);
|
|
29733
|
-
console.error(`[calendar] op=booking-link-fail accountId=${
|
|
29733
|
+
console.error(`[calendar] op=booking-link-fail accountId=${id83} err="${message}"`);
|
|
29734
29734
|
return c.json({ bookingDomain: null });
|
|
29735
29735
|
}
|
|
29736
29736
|
});
|
|
@@ -29750,20 +29750,20 @@ app53.get("/portal-link", requireAdminSession, (c) => {
|
|
|
29750
29750
|
console.error("[data-portal] op=portal-link auth-rejected reason=no-account-for-session accountId=none");
|
|
29751
29751
|
return c.json({ error: "no account for session", reason: "no-account-for-session" }, 401);
|
|
29752
29752
|
}
|
|
29753
|
-
const
|
|
29753
|
+
const id83 = account.accountId.slice(0, 8);
|
|
29754
29754
|
try {
|
|
29755
29755
|
const path = join51(account.accountDir, PORTAL_FILENAME);
|
|
29756
29756
|
if (!existsSync42(path)) {
|
|
29757
|
-
console.log(`[data-portal] op=portal-link accountId=${
|
|
29757
|
+
console.log(`[data-portal] op=portal-link accountId=${id83} portalDomain=none source=data-portal-config`);
|
|
29758
29758
|
return c.json({ portalDomain: null });
|
|
29759
29759
|
}
|
|
29760
29760
|
const cfg = JSON.parse(readFileSync42(path, "utf-8"));
|
|
29761
29761
|
const portalDomain = typeof cfg.portalDomain === "string" && cfg.portalDomain.trim() ? cfg.portalDomain.trim() : null;
|
|
29762
|
-
console.log(`[data-portal] op=portal-link accountId=${
|
|
29762
|
+
console.log(`[data-portal] op=portal-link accountId=${id83} portalDomain=${portalDomain ?? "none"} source=data-portal-config`);
|
|
29763
29763
|
return c.json({ portalDomain });
|
|
29764
29764
|
} catch (err) {
|
|
29765
29765
|
const message = err instanceof Error ? err.message : String(err);
|
|
29766
|
-
console.error(`[data-portal] op=portal-link-fail accountId=${
|
|
29766
|
+
console.error(`[data-portal] op=portal-link-fail accountId=${id83} err="${message}"`);
|
|
29767
29767
|
return c.json({ portalDomain: null });
|
|
29768
29768
|
}
|
|
29769
29769
|
});
|
|
@@ -29785,13 +29785,13 @@ app54.get("/planner-link", requireAdminSession, (c) => {
|
|
|
29785
29785
|
);
|
|
29786
29786
|
return c.json({ error: "no account for session", reason: "no-account-for-session" }, 401);
|
|
29787
29787
|
}
|
|
29788
|
-
const
|
|
29788
|
+
const id83 = account.accountId.slice(0, 8);
|
|
29789
29789
|
try {
|
|
29790
29790
|
const portalPath = join52(account.accountDir, PORTAL_FILENAME2);
|
|
29791
29791
|
const dispatchConfigured = existsSync43(join52(account.accountDir, "dispatch", "config.json"));
|
|
29792
29792
|
if (!existsSync43(portalPath) || !dispatchConfigured) {
|
|
29793
29793
|
console.log(
|
|
29794
|
-
`[dispatch-planner] op=planner-link accountId=${
|
|
29794
|
+
`[dispatch-planner] op=planner-link accountId=${id83} plannerUrl=none portal=${existsSync43(portalPath)} dispatch=${dispatchConfigured}`
|
|
29795
29795
|
);
|
|
29796
29796
|
return c.json({ plannerUrl: null });
|
|
29797
29797
|
}
|
|
@@ -29799,12 +29799,12 @@ app54.get("/planner-link", requireAdminSession, (c) => {
|
|
|
29799
29799
|
const domain = typeof cfg.portalDomain === "string" && cfg.portalDomain.trim() ? cfg.portalDomain.trim() : null;
|
|
29800
29800
|
const plannerUrl = domain ? `https://${domain}/planner` : null;
|
|
29801
29801
|
console.log(
|
|
29802
|
-
`[dispatch-planner] op=planner-link accountId=${
|
|
29802
|
+
`[dispatch-planner] op=planner-link accountId=${id83} plannerUrl=${plannerUrl ?? "none"}`
|
|
29803
29803
|
);
|
|
29804
29804
|
return c.json({ plannerUrl });
|
|
29805
29805
|
} catch (err) {
|
|
29806
29806
|
const message = err instanceof Error ? err.message : String(err);
|
|
29807
|
-
console.error(`[dispatch-planner] op=planner-link-fail accountId=${
|
|
29807
|
+
console.error(`[dispatch-planner] op=planner-link-fail accountId=${id83} err="${message}"`);
|
|
29808
29808
|
return c.json({ plannerUrl: null });
|
|
29809
29809
|
}
|
|
29810
29810
|
});
|
|
@@ -30385,7 +30385,7 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30385
30385
|
if (!accountId) return c.json({ error: "Account not found for session" }, 401);
|
|
30386
30386
|
const eventId = c.req.param("eventId");
|
|
30387
30387
|
const body = await c.req.json().catch(() => ({}));
|
|
30388
|
-
const
|
|
30388
|
+
const id83 = eventId.slice(0, 8);
|
|
30389
30389
|
const setClauses = [];
|
|
30390
30390
|
const params = { eventId, accountId };
|
|
30391
30391
|
const fields = [];
|
|
@@ -30397,11 +30397,11 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30397
30397
|
const clearing = bindingTouched && body.agentChannel === null && body.agentDestination === null;
|
|
30398
30398
|
const binding = bindingTouched && typeof body.agentChannel === "string" && body.agentChannel.length > 0 && typeof body.agentDestination === "string" && body.agentDestination.length > 0;
|
|
30399
30399
|
if (bindingTouched && !clearing && !binding) {
|
|
30400
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30400
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=agentChannel+agentDestination outcome=rejected reason=partial-binding`);
|
|
30401
30401
|
return c.json({ error: "A dispatch binding needs both a channel and a destination, or both cleared." }, 400);
|
|
30402
30402
|
}
|
|
30403
30403
|
if (binding && !DISPATCH_CHANNELS2.has(body.agentChannel)) {
|
|
30404
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30404
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=agentChannel+agentDestination outcome=rejected reason=invalid-channel channel=${body.agentChannel}`);
|
|
30405
30405
|
return c.json({ error: `Unknown dispatch channel "${body.agentChannel}".` }, 400);
|
|
30406
30406
|
}
|
|
30407
30407
|
const describing = "description" in body;
|
|
@@ -30430,7 +30430,7 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30430
30430
|
storedDescription = pre.records[0]?.get("description") ?? null;
|
|
30431
30431
|
} catch (err) {
|
|
30432
30432
|
const message = err instanceof Error ? err.message : String(err);
|
|
30433
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30433
|
+
console.error(`[admin:routines] op=patch id=${id83} outcome=rejected reason=tz-read-fail err="${message}"`);
|
|
30434
30434
|
return c.json({ error: `Routine unavailable: ${message}` }, 503);
|
|
30435
30435
|
} finally {
|
|
30436
30436
|
await preSession.close();
|
|
@@ -30438,7 +30438,7 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30438
30438
|
}
|
|
30439
30439
|
if (binding) {
|
|
30440
30440
|
if (storedActionPlugin !== null) {
|
|
30441
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30441
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=agentChannel+agentDestination outcome=rejected reason=action-routine`);
|
|
30442
30442
|
return c.json(
|
|
30443
30443
|
{ 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
30444
|
400
|
|
@@ -30446,7 +30446,7 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30446
30446
|
}
|
|
30447
30447
|
const effectivePrompt = typeof body.agentPrompt === "string" ? body.agentPrompt : storedPrompt ?? "";
|
|
30448
30448
|
if (effectivePrompt.trim().length === 0) {
|
|
30449
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30449
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=agentChannel+agentDestination outcome=rejected reason=no-prompt`);
|
|
30450
30450
|
return c.json({ error: "A bound routine needs an instruction, or it will fire and do nothing." }, 400);
|
|
30451
30451
|
}
|
|
30452
30452
|
const channel = body.agentChannel;
|
|
@@ -30456,12 +30456,12 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30456
30456
|
houseAccountId = channel === "whatsapp" ? resolveHouseOrSoleAccountId(ACCOUNTS_DIR) : accountId;
|
|
30457
30457
|
} catch (err) {
|
|
30458
30458
|
const message = err instanceof Error ? err.message : String(err);
|
|
30459
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30459
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=agentChannel+agentDestination outcome=rejected reason=house-unresolved err="${message}"`);
|
|
30460
30460
|
return c.json({ error: `WhatsApp destinations cannot be validated on this install: ${message}` }, 503);
|
|
30461
30461
|
}
|
|
30462
30462
|
const check = validateAgentDestination(ACCOUNTS_DIR, houseAccountId, accountId, channel, destination);
|
|
30463
30463
|
if (!check.valid) {
|
|
30464
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30464
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=agentChannel+agentDestination outcome=rejected reason=${check.reason}`);
|
|
30465
30465
|
return c.json(
|
|
30466
30466
|
{
|
|
30467
30467
|
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 +30478,7 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30478
30478
|
if (clearing) {
|
|
30479
30479
|
const keepsRow = typeof body.recurrence === "string" || storedRecurrence !== null || storedActionPlugin !== null;
|
|
30480
30480
|
if (!keepsRow) {
|
|
30481
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30481
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=agentChannel+agentDestination outcome=rejected reason=clear-would-orphan`);
|
|
30482
30482
|
return c.json(
|
|
30483
30483
|
{ error: "Clearing the dispatch on a one-time routine would leave nothing to show. Give it a schedule first, or delete it." },
|
|
30484
30484
|
400
|
|
@@ -30498,12 +30498,12 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30498
30498
|
let nextName = null;
|
|
30499
30499
|
if (naming) {
|
|
30500
30500
|
if (typeof body.name !== "string" || body.name.trim().length === 0) {
|
|
30501
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30501
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=name outcome=rejected reason=empty-name`);
|
|
30502
30502
|
return c.json({ error: "A routine needs a name." }, 400);
|
|
30503
30503
|
}
|
|
30504
30504
|
nextName = body.name.trim();
|
|
30505
30505
|
if (nextName.length > 200) {
|
|
30506
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30506
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=name outcome=rejected reason=too-long len=${nextName.length}`);
|
|
30507
30507
|
return c.json({ error: "A routine name is at most 200 characters." }, 400);
|
|
30508
30508
|
}
|
|
30509
30509
|
setClauses.push("e.name = $name");
|
|
@@ -30516,7 +30516,7 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30516
30516
|
if (describing) {
|
|
30517
30517
|
const raw = body.description;
|
|
30518
30518
|
if (typeof raw !== "string" && raw !== null) {
|
|
30519
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30519
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=description outcome=rejected reason=not-a-string`);
|
|
30520
30520
|
return c.json({ error: "A description must be text, or null to clear it." }, 400);
|
|
30521
30521
|
}
|
|
30522
30522
|
nextDescription = typeof raw === "string" && raw.trim().length > 0 ? raw : null;
|
|
@@ -30537,7 +30537,7 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30537
30537
|
params.embedding = embedding;
|
|
30538
30538
|
} else {
|
|
30539
30539
|
embedLog = " embed=failed";
|
|
30540
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30540
|
+
console.error(`[admin:routines] op=patch id=${id83} fields=${fields.join("+")} embed=failed`);
|
|
30541
30541
|
}
|
|
30542
30542
|
}
|
|
30543
30543
|
if (typeof body.recurrence === "string") {
|
|
@@ -30551,7 +30551,7 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30551
30551
|
nextRunLog = nextRun;
|
|
30552
30552
|
} catch (err) {
|
|
30553
30553
|
const reason = err instanceof Error ? err.message : String(err);
|
|
30554
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30554
|
+
console.error(`[admin:routines] op=patch id=${id83} outcome=rejected reason=invalid-cron cron=${body.recurrence}`);
|
|
30555
30555
|
return c.json({ error: `Invalid recurrence: ${reason}` }, 400);
|
|
30556
30556
|
}
|
|
30557
30557
|
}
|
|
@@ -30569,15 +30569,15 @@ app55.patch("/:eventId", requireAdminSession, async (c) => {
|
|
|
30569
30569
|
params
|
|
30570
30570
|
);
|
|
30571
30571
|
if (res.records.length === 0) {
|
|
30572
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30572
|
+
console.error(`[admin:routines] op=patch id=${id83} outcome=rejected reason=not-found`);
|
|
30573
30573
|
return c.json({ error: "Routine not found" }, 404);
|
|
30574
30574
|
}
|
|
30575
30575
|
const nameLog = nextName === null ? "" : ` name=len:${nextName.length} operatorEdited=stamped`;
|
|
30576
|
-
console.log(`[admin:routines] op=patch id=${
|
|
30576
|
+
console.log(`[admin:routines] op=patch id=${id83} fields=${fields.join("+")} outcome=ok nextRun=${nextRunLog} cron=${params.recurrence ?? "unchanged"}${bindingLog}${nameLog}${embedLog}`);
|
|
30577
30577
|
return c.json({ routine: mapRoutine(res.records[0]) });
|
|
30578
30578
|
} catch (err) {
|
|
30579
30579
|
const message = err instanceof Error ? err.message : String(err);
|
|
30580
|
-
console.error(`[admin:routines] op=patch id=${
|
|
30580
|
+
console.error(`[admin:routines] op=patch id=${id83} outcome=rejected reason=${message}`);
|
|
30581
30581
|
return c.json({ error: `Update failed: ${message}` }, 503);
|
|
30582
30582
|
} finally {
|
|
30583
30583
|
await session.close();
|
|
@@ -34975,8 +34975,18 @@ async function migrateSidecarAccountIds(opts = {}) {
|
|
|
34975
34975
|
init_src2();
|
|
34976
34976
|
|
|
34977
34977
|
// app/lib/whatsapp/gateway/inbound-hub.ts
|
|
34978
|
+
var HUB_SEPARATOR = "\0";
|
|
34978
34979
|
function hubKey(accountId, senderId) {
|
|
34979
|
-
return `${accountId}
|
|
34980
|
+
return `${accountId}${HUB_SEPARATOR}${senderId}`;
|
|
34981
|
+
}
|
|
34982
|
+
function decodeHubKeys(keys) {
|
|
34983
|
+
const out = [];
|
|
34984
|
+
for (const key2 of keys) {
|
|
34985
|
+
const i = key2.indexOf(HUB_SEPARATOR);
|
|
34986
|
+
if (i < 0) continue;
|
|
34987
|
+
out.push({ accountId: key2.slice(0, i), senderId: key2.slice(i + HUB_SEPARATOR.length) });
|
|
34988
|
+
}
|
|
34989
|
+
return out;
|
|
34980
34990
|
}
|
|
34981
34991
|
var InboundHub = class {
|
|
34982
34992
|
senders = /* @__PURE__ */ new Map();
|
|
@@ -35968,6 +35978,14 @@ var WaGateway = class {
|
|
|
35968
35978
|
liveReplyTarget(accountId, senderId) {
|
|
35969
35979
|
return this.replies.get(hubKey(accountId, normalizeTarget(senderId) ?? senderId)) ?? null;
|
|
35970
35980
|
}
|
|
35981
|
+
/** Task 2557 — every (account, sender) this gateway currently holds a live
|
|
35982
|
+
* delivery closure for. The follower census reconciles against exactly this
|
|
35983
|
+
* set, so "should have a reader" and "can be sent to" are one fact and
|
|
35984
|
+
* cannot drift. Deriving the set from the follower registry instead would
|
|
35985
|
+
* ask the map that already lost the entry. */
|
|
35986
|
+
boundTargets() {
|
|
35987
|
+
return decodeHubKeys(this.replies.keys());
|
|
35988
|
+
}
|
|
35971
35989
|
async sendOutbound(accountId, senderId, text) {
|
|
35972
35990
|
this.outboundSends++;
|
|
35973
35991
|
const reply = this.replies.get(hubKey(accountId, senderId));
|
|
@@ -37188,7 +37206,10 @@ function startEmitter(opts) {
|
|
|
37188
37206
|
const abort2 = new AbortController();
|
|
37189
37207
|
const { entry: entry2, tag } = opts;
|
|
37190
37208
|
const answers = new AnswerAccumulator();
|
|
37209
|
+
let answersEmitted = 0;
|
|
37210
|
+
let closeReason = "stream-end";
|
|
37191
37211
|
const emitAnswer = async (answer) => {
|
|
37212
|
+
answersEmitted += 1;
|
|
37192
37213
|
if (answer.text.trim()) await fanOut2(entry2.subscribers, answer.text, opts.onError, tag);
|
|
37193
37214
|
if (!opts.onAnswer) return;
|
|
37194
37215
|
const guarded = { ...answer, text: guardOutbound(entry2.senderId, answer.text) };
|
|
@@ -37361,9 +37382,14 @@ function startEmitter(opts) {
|
|
|
37361
37382
|
for (const answer of answers.flush()) await emitAnswer(answer);
|
|
37362
37383
|
} catch (err) {
|
|
37363
37384
|
if (!abort2.signal.aborted) {
|
|
37385
|
+
closeReason = "error";
|
|
37364
37386
|
opts.onError?.(`emitter-error: ${err instanceof Error ? err.message : String(err)}`);
|
|
37365
37387
|
}
|
|
37366
37388
|
} finally {
|
|
37389
|
+
if (abort2.signal.aborted) closeReason = "aborted";
|
|
37390
|
+
console.error(
|
|
37391
|
+
`${tag} op=follower-closed sessionId=${entry2.sessionId.slice(0, 8)} reason=${closeReason} answers=${answersEmitted}`
|
|
37392
|
+
);
|
|
37367
37393
|
opts.onClose();
|
|
37368
37394
|
}
|
|
37369
37395
|
})();
|
|
@@ -37564,6 +37590,12 @@ function startWebchatNativeFileFollower(input) {
|
|
|
37564
37590
|
return startEmitter({
|
|
37565
37591
|
entry: entry2,
|
|
37566
37592
|
tag: "[webchat-adaptor]",
|
|
37593
|
+
// Task 2557 — the emitter has always had this channel and no follower ever
|
|
37594
|
+
// passed one, so every cause it reported was discarded. The close line
|
|
37595
|
+
// carries the outcome; this carries the cause.
|
|
37596
|
+
onError: (reason) => {
|
|
37597
|
+
console.error(`[webchat-adaptor] op=follower-error sessionId=${input.sessionId.slice(0, 8)} reason=${reason}`);
|
|
37598
|
+
},
|
|
37567
37599
|
fileDelivery: makeWebchatFileDelivery(entry2),
|
|
37568
37600
|
// A resumed session's JSONL already holds prior SendUserFile tool_uses;
|
|
37569
37601
|
// suppress replay so historical calls are not re-reconciled on attach.
|
|
@@ -37728,6 +37760,14 @@ function startNativeFileFollower(input) {
|
|
|
37728
37760
|
return startEmitter({
|
|
37729
37761
|
entry: entry2,
|
|
37730
37762
|
tag: "[whatsapp-adaptor]",
|
|
37763
|
+
// Task 2557 — the emitter has always had this channel and no follower ever
|
|
37764
|
+
// passed one, so every cause it reported (`follow-status-404`,
|
|
37765
|
+
// `emitter-error: …`, `subscriber-error: …`) was discarded. The close line
|
|
37766
|
+
// carries the outcome; this carries the cause, and it can fire without the
|
|
37767
|
+
// stream ending, so neither substitutes for the other.
|
|
37768
|
+
onError: (reason) => {
|
|
37769
|
+
console.error(`[whatsapp-adaptor] op=follower-error sessionId=${input.sessionId.slice(0, 8)} reason=${reason}`);
|
|
37770
|
+
},
|
|
37731
37771
|
// Task 2521 — admin-only. A public spawn has no tools (Task 2078), so a
|
|
37732
37772
|
// handler here would reconcile a call that can never fire.
|
|
37733
37773
|
fileDelivery: input.role === "admin" ? makeWhatsAppFileDelivery(entry2, input.maxyAccountId) : null,
|
|
@@ -37752,6 +37792,69 @@ function startNativeFileFollower(input) {
|
|
|
37752
37792
|
});
|
|
37753
37793
|
}
|
|
37754
37794
|
|
|
37795
|
+
// app/lib/channel-delivery/follower-registry.ts
|
|
37796
|
+
function createFollowerRegistry(start) {
|
|
37797
|
+
const controllers = /* @__PURE__ */ new Map();
|
|
37798
|
+
const params = /* @__PURE__ */ new Map();
|
|
37799
|
+
return {
|
|
37800
|
+
attach(p) {
|
|
37801
|
+
controllers.get(p.senderId)?.abort();
|
|
37802
|
+
params.set(p.senderId, p);
|
|
37803
|
+
let ac;
|
|
37804
|
+
ac = start(p, () => {
|
|
37805
|
+
if (controllers.get(p.senderId) === ac) controllers.delete(p.senderId);
|
|
37806
|
+
});
|
|
37807
|
+
controllers.set(p.senderId, ac);
|
|
37808
|
+
},
|
|
37809
|
+
live: () => [...controllers.keys()],
|
|
37810
|
+
paramsFor: (senderId) => params.get(senderId) ?? null
|
|
37811
|
+
};
|
|
37812
|
+
}
|
|
37813
|
+
|
|
37814
|
+
// app/lib/channel-delivery/follower-census.ts
|
|
37815
|
+
var CENSUS_MAX_REATTEMPTS = 3;
|
|
37816
|
+
function makeCensusState() {
|
|
37817
|
+
return { attempts: /* @__PURE__ */ new Map() };
|
|
37818
|
+
}
|
|
37819
|
+
var id82 = (s) => s.slice(0, 8);
|
|
37820
|
+
function followerCensusTick(deps, state = makeCensusState()) {
|
|
37821
|
+
const bound = deps.bound();
|
|
37822
|
+
const live = new Set(deps.live());
|
|
37823
|
+
for (const senderId of live) state.attempts.delete(senderId);
|
|
37824
|
+
const missing = bound.filter((b) => !live.has(b.senderId));
|
|
37825
|
+
console.error(
|
|
37826
|
+
`[channel-follower] op=follower-census bound=${bound.length} attached=${live.size} missing=${missing.length} missingSenders=${missing.map((m) => id82(m.senderId)).join(",") || "none"}`
|
|
37827
|
+
);
|
|
37828
|
+
for (const m of missing) {
|
|
37829
|
+
const used = state.attempts.get(m.senderId) ?? 0;
|
|
37830
|
+
if (used >= CENSUS_MAX_REATTEMPTS) {
|
|
37831
|
+
console.error(
|
|
37832
|
+
`[channel-follower] op=follower-reattach sessionId=${id82(deps.paramsFor(m.senderId)?.sessionId ?? "none")} senderId=${id82(m.senderId)} outcome=skipped reason=reattach-exhausted attempts=${used}`
|
|
37833
|
+
);
|
|
37834
|
+
continue;
|
|
37835
|
+
}
|
|
37836
|
+
const p = deps.paramsFor(m.senderId);
|
|
37837
|
+
if (!p) {
|
|
37838
|
+
console.error(
|
|
37839
|
+
`[channel-follower] op=follower-reattach sessionId=none senderId=${id82(m.senderId)} outcome=failed reason=no-attach-params`
|
|
37840
|
+
);
|
|
37841
|
+
continue;
|
|
37842
|
+
}
|
|
37843
|
+
try {
|
|
37844
|
+
state.attempts.set(m.senderId, used + 1);
|
|
37845
|
+
deps.attach(p);
|
|
37846
|
+
console.error(
|
|
37847
|
+
`[channel-follower] op=follower-reattach sessionId=${id82(p.sessionId)} senderId=${id82(p.senderId)} outcome=attached reason=none`
|
|
37848
|
+
);
|
|
37849
|
+
} catch (err) {
|
|
37850
|
+
console.error(
|
|
37851
|
+
`[channel-follower] op=follower-reattach sessionId=${id82(p.sessionId)} senderId=${id82(p.senderId)} outcome=failed reason=${err instanceof Error ? err.message : String(err)}`
|
|
37852
|
+
);
|
|
37853
|
+
}
|
|
37854
|
+
}
|
|
37855
|
+
return `${bound.length} door(s), ${missing.length} without a reader`;
|
|
37856
|
+
}
|
|
37857
|
+
|
|
37755
37858
|
// app/lib/telegram/gateway/inbound-hub.ts
|
|
37756
37859
|
var InboundHub3 = class {
|
|
37757
37860
|
keys = /* @__PURE__ */ new Map();
|
|
@@ -38449,6 +38552,12 @@ function startTelegramNativeFileFollower(input) {
|
|
|
38449
38552
|
return startEmitter({
|
|
38450
38553
|
entry: entry2,
|
|
38451
38554
|
tag: "[telegram-adaptor]",
|
|
38555
|
+
// Task 2557 — the emitter has always had this channel and no follower ever
|
|
38556
|
+
// passed one, so every cause it reported was discarded. The close line
|
|
38557
|
+
// carries the outcome; this carries the cause.
|
|
38558
|
+
onError: (reason) => {
|
|
38559
|
+
console.error(`[telegram-adaptor] op=follower-error sessionId=${input.sessionId.slice(0, 8)} reason=${reason}`);
|
|
38560
|
+
},
|
|
38452
38561
|
fileDelivery: makeTelegramFileDelivery(entry2, input.botToken),
|
|
38453
38562
|
// A resumed session's JSONL already holds prior SendUserFile tool_uses;
|
|
38454
38563
|
// suppress replay so historical files are not re-sent on attach.
|
|
@@ -39367,7 +39476,27 @@ function resolveChannelOverride(accountId, channel, senderId) {
|
|
|
39367
39476
|
return null;
|
|
39368
39477
|
}
|
|
39369
39478
|
}
|
|
39370
|
-
var nativeFileFollowers =
|
|
39479
|
+
var nativeFileFollowers = createFollowerRegistry(
|
|
39480
|
+
(p, onClose) => startNativeFileFollower({
|
|
39481
|
+
sessionId: p.sessionId,
|
|
39482
|
+
senderId: p.senderId,
|
|
39483
|
+
// Task 1390 — the file-path validation scope is the sender's effective
|
|
39484
|
+
// session account (the sub-account for an account-manager), not the house
|
|
39485
|
+
// socket account. `accountId` stays the getSocket key.
|
|
39486
|
+
accountId: p.accountId,
|
|
39487
|
+
maxyAccountId: p.effectiveAccountId,
|
|
39488
|
+
// Task 2521 — file delivery is admin-only; the text emitter is not.
|
|
39489
|
+
role: p.role,
|
|
39490
|
+
// Task 2521 — resolved per answer, not captured once, so a door that goes
|
|
39491
|
+
// live after this session started still receives.
|
|
39492
|
+
// Task 2531 — `effectiveAccountId`, NOT `accountId`. The reply map is keyed
|
|
39493
|
+
// on the platform UUID (wa-gateway.ts:567) while `accountId` here is the
|
|
39494
|
+
// Baileys credential dirname. Passing the dirname missed every lookup and
|
|
39495
|
+
// delivered session 8e5bc4e8's answer to nothing.
|
|
39496
|
+
fanTargets: () => whatsappFanTargets(p.effectiveAccountId, p.senderId),
|
|
39497
|
+
onClose
|
|
39498
|
+
})
|
|
39499
|
+
);
|
|
39371
39500
|
var webchatFileFollowers = /* @__PURE__ */ new Map();
|
|
39372
39501
|
async function fetchAccountStandingRules(accountId) {
|
|
39373
39502
|
const session = getSession();
|
|
@@ -39598,34 +39727,29 @@ var waGateway = new WaGateway({
|
|
|
39598
39727
|
console.error(`[whatsapp-native] spawn-failed senderId=${senderId} role=${role} error=${result.error} status=${result.status}`);
|
|
39599
39728
|
return;
|
|
39600
39729
|
}
|
|
39601
|
-
|
|
39602
|
-
if (prior) prior.abort();
|
|
39603
|
-
let ac;
|
|
39604
|
-
ac = startNativeFileFollower({
|
|
39730
|
+
nativeFileFollowers.attach({
|
|
39605
39731
|
sessionId: req.sessionId,
|
|
39606
39732
|
senderId,
|
|
39607
39733
|
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
|
-
}
|
|
39734
|
+
effectiveAccountId,
|
|
39735
|
+
role
|
|
39625
39736
|
});
|
|
39626
|
-
nativeFileFollowers.set(senderId, ac);
|
|
39627
39737
|
}
|
|
39628
39738
|
});
|
|
39739
|
+
var followerCensusState = makeCensusState();
|
|
39740
|
+
registerLoop({
|
|
39741
|
+
name: "channel-follower",
|
|
39742
|
+
intervalMs: 6e4,
|
|
39743
|
+
run: () => followerCensusTick(
|
|
39744
|
+
{
|
|
39745
|
+
bound: () => waGateway.boundTargets(),
|
|
39746
|
+
live: () => nativeFileFollowers.live(),
|
|
39747
|
+
paramsFor: (senderId) => nativeFileFollowers.paramsFor(senderId),
|
|
39748
|
+
attach: (p) => nativeFileFollowers.attach(p)
|
|
39749
|
+
},
|
|
39750
|
+
followerCensusState
|
|
39751
|
+
)
|
|
39752
|
+
});
|
|
39629
39753
|
app80.route("/", waGateway.routes());
|
|
39630
39754
|
waGateway.startSweeper();
|
|
39631
39755
|
registerLoop({
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
// Task 1623 — acceptance for the installer convergence that drops the vestigial
|
|
2
|
-
// admins[] key from existing client account.json. Filesystem fixtures under a
|
|
3
|
-
// tmp dir; no installer side effects. Runs under `node --test dist/__tests__/*`.
|
|
4
|
-
import test from "node:test";
|
|
5
|
-
import assert from "node:assert/strict";
|
|
6
|
-
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs";
|
|
7
|
-
import { tmpdir } from "node:os";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import { removeClientAdminsKey } from "../converge-client-admins.js";
|
|
10
|
-
function makeRoot() {
|
|
11
|
-
return mkdtempSync(join(tmpdir(), "conv-1623-"));
|
|
12
|
-
}
|
|
13
|
-
function acct(root, id, cfg) {
|
|
14
|
-
mkdirSync(join(root, id), { recursive: true });
|
|
15
|
-
writeFileSync(join(root, id, "account.json"), JSON.stringify(cfg, null, 2));
|
|
16
|
-
}
|
|
17
|
-
function hasAdmins(root, id) {
|
|
18
|
-
return "admins" in JSON.parse(readFileSync(join(root, id, "account.json"), "utf-8"));
|
|
19
|
-
}
|
|
20
|
-
test("removes admins from a client, keeps it on the house, is idempotent", () => {
|
|
21
|
-
const root = makeRoot();
|
|
22
|
-
try {
|
|
23
|
-
acct(root, "c1", { accountId: "c1", role: "client", admins: [] });
|
|
24
|
-
acct(root, "h1", { accountId: "h1", role: "house", admins: [{ userId: "u1", role: "owner" }] });
|
|
25
|
-
acct(root, "c2", { accountId: "c2", role: "client" }); // already stripped
|
|
26
|
-
const first = removeClientAdminsKey(root);
|
|
27
|
-
assert.equal(first.find((r) => r.accountId === "c1")?.action, "removed");
|
|
28
|
-
assert.equal(first.find((r) => r.accountId === "h1")?.action, "house-kept");
|
|
29
|
-
assert.equal(first.find((r) => r.accountId === "c2")?.action, "absent");
|
|
30
|
-
assert.equal(hasAdmins(root, "c1"), false);
|
|
31
|
-
assert.equal(hasAdmins(root, "h1"), true);
|
|
32
|
-
const second = removeClientAdminsKey(root);
|
|
33
|
-
assert.equal(second.find((r) => r.accountId === "c1")?.action, "absent");
|
|
34
|
-
}
|
|
35
|
-
finally {
|
|
36
|
-
rmSync(root, { recursive: true, force: true });
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
test("unreadable account.json is reported, not thrown", () => {
|
|
40
|
-
const root = makeRoot();
|
|
41
|
-
try {
|
|
42
|
-
mkdirSync(join(root, "bad"), { recursive: true });
|
|
43
|
-
writeFileSync(join(root, "bad", "account.json"), "{ not json");
|
|
44
|
-
const res = removeClientAdminsKey(root);
|
|
45
|
-
assert.equal(res.find((r) => r.accountId === "bad")?.action, "unreadable");
|
|
46
|
-
}
|
|
47
|
-
finally {
|
|
48
|
-
rmSync(root, { recursive: true, force: true });
|
|
49
|
-
}
|
|
50
|
-
});
|