@agentcomms/resend 0.27.1 → 0.29.0

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.
@@ -1,19 +1,20 @@
1
- import { C as unknown, S as union, b as strictObject, c as boolean, g as object, h as number, i as _enum, l as discriminatedUnion, m as looseObject, n as datetime, p as literal$1, s as array, x as string, y as record } from "./iso-5-9R47LC.mjs";
2
- import { access, chmod, constants, link, lstat, mkdir, open, readFile, readdir, readlink, realpath, rename, rm, stat, unlink, utimes } from "node:fs/promises";
1
+ import { C as unknown, S as union$1, b as strictObject, c as boolean, g as object, h as number, i as _enum, l as discriminatedUnion, m as looseObject, n as datetime, p as literal$1, s as array, x as string, y as record } from "./iso-5-9R47LC.mjs";
2
+ import { execFile, spawn } from "node:child_process";
3
+ import { createHash, randomBytes, randomInt, timingSafeEqual } from "node:crypto";
4
+ import { accessSync, constants, existsSync, lstatSync, readFileSync, readlinkSync, realpathSync, statSync } from "node:fs";
5
+ import { access, chmod, constants as constants$1, link, lstat, mkdir, open, readFile, readdir, readlink, realpath, rename, rm, stat, unlink, utimes } from "node:fs/promises";
6
+ import { homedir, platform, userInfo } from "node:os";
3
7
  import path, { basename, delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
4
8
  import { domainToASCII, fileURLToPath, pathToFileURL } from "node:url";
5
- import { createHash, randomBytes, randomInt, timingSafeEqual } from "node:crypto";
6
9
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
7
10
  import "node:readline/promises";
8
11
  import { styleText } from "node:util";
9
- import { homedir, platform, userInfo } from "node:os";
10
- import { accessSync, constants as constants$1, lstatSync, readFileSync, readlinkSync, realpathSync, statSync } from "node:fs";
11
- import { execFile, spawn } from "node:child_process";
12
12
  import { gunzipSync } from "node:zlib";
13
+ import { AsyncLocalStorage } from "node:async_hooks";
13
14
  //#endregion
14
15
  //#region src/version.ts
15
16
  /** The package version, read from package.json at build time. */
16
- const VERSION$2 = "0.27.1";
17
+ const VERSION$2 = "0.29.0";
17
18
  //#endregion
18
19
  //#region src/api/routes.ts
19
20
  /**
@@ -201,8 +202,70 @@ const RESEND_CALLER = Object.freeze({
201
202
  url: import.meta.url,
202
203
  packageName: PACKAGE_NAME
203
204
  });
204
- //#endregion
205
- //#region ../core/dist/update-check-BVv--MbW.mjs
205
+ Object.freeze(["on", "off"]);
206
+ /** Whether `value` is a retention a person can set: a whole number of days from 1 to 365. */
207
+ function isActivityRetention(value) {
208
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 365;
209
+ }
210
+ function isActivityRecordSetting(value) {
211
+ return value === "on" || value === "off";
212
+ }
213
+ /** The `activity` block as the file holds it, raw: an object, or nothing. */
214
+ function storedActivity(config) {
215
+ const stored = config.activity;
216
+ return typeof stored === "object" && stored !== null && !Array.isArray(stored) ? stored : null;
217
+ }
218
+ /**
219
+ * The settings in force for `config`. A missing block, or a missing key, is the default. Anything else this release
220
+ * cannot read is named in `problems`, and read so that nothing is recorded and nothing is deleted: a word other than
221
+ * `on` or `off` as `off`; a retention that is not a whole number from 1 to 365 — out of range, a fraction, not a
222
+ * number — as 365 days with recording `off`; a block that is not an object as both.
223
+ */
224
+ function readActivitySettings(config) {
225
+ const problems = [];
226
+ if (config.activity === void 0) return {
227
+ record: "on",
228
+ retentionDays: 30,
229
+ problems
230
+ };
231
+ const stored = storedActivity(config);
232
+ if (stored === null) {
233
+ problems.push(`activity in config.json is not an object, so recording is off and 365 days are kept`);
234
+ return {
235
+ record: "off",
236
+ retentionDays: 365,
237
+ problems
238
+ };
239
+ }
240
+ let record = "on";
241
+ if (stored.record !== void 0) {
242
+ if (isActivityRecordSetting(stored.record)) record = stored.record;
243
+ else {
244
+ record = "off";
245
+ problems.push(`activity.record is ${describe(stored.record)}, not on or off, so recording is off`);
246
+ }
247
+ }
248
+ let retentionDays = 30;
249
+ const days = stored.retentionDays;
250
+ if (days !== void 0) {
251
+ if (isActivityRetention(days)) retentionDays = days;
252
+ else {
253
+ retentionDays = 365;
254
+ record = "off";
255
+ problems.push(`activity.retentionDays is ${describe(days)}, not a whole number from 1 to 365, so recording is off and 365 days are kept`);
256
+ }
257
+ }
258
+ return {
259
+ record,
260
+ retentionDays,
261
+ problems
262
+ };
263
+ }
264
+ /** A stored value in a sentence: short, and never more than a few dozen characters of whatever the file held. */
265
+ function describe(value) {
266
+ const text = typeof value === "string" ? JSON.stringify(value) : String(JSON.stringify(value) ?? value);
267
+ return text.length > 40 ? `${text.slice(0, 39)}…` : text;
268
+ }
206
269
  /**
207
270
  * Every failure a user or an agent can act on carries a specific, stable code from this registry, a message, an
208
271
  * optional one-line hint and structured `details`. The code decides the process exit status; agents and skills branch
@@ -402,22 +465,6 @@ function toCommsError(value) {
402
465
  if (isCommsError(value)) return value;
403
466
  return new CommsError("UNEXPECTED", value instanceof Error ? value.message : String(value), { cause: value });
404
467
  }
405
- function sha256Hex(data) {
406
- return createHash("sha256").update(data).digest("hex");
407
- }
408
- /** Lower-cases the address part of `Name <addr>` or a bare address; display names are dropped. */
409
- function normaliseAddress(value) {
410
- return (/<([^<>]+)>\s*$/.exec(value)?.[1] ?? value).trim().toLowerCase();
411
- }
412
- function collapseWhitespace(text) {
413
- return text.replace(/\s+/g, " ").trim();
414
- }
415
- /** Deterministic JSON: keys sorted at every level, undefined members dropped. */
416
- function canonicalJson$1(value) {
417
- if (Array.isArray(value)) return `[${value.map(canonicalJson$1).join(",")}]`;
418
- if (value && typeof value === "object") return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson$1(v)}`).join(",")}}`;
419
- return JSON.stringify(value);
420
- }
421
468
  /** Creates a directory (and parents) readable only by the current user. Tightens an existing one. */
422
469
  async function ensurePrivateDir(path) {
423
470
  await mkdir(path, {
@@ -535,6 +582,1267 @@ async function syncDirectory(path) {
535
582
  await handle.close();
536
583
  }
537
584
  }
585
+ /** True when a path exists and is readable or writable by someone other than its owner (POSIX only). */
586
+ async function isGroupOrWorldAccessible(path) {
587
+ if (process.platform === "win32") return false;
588
+ return ((await stat(path)).mode & 63) !== 0;
589
+ }
590
+ /**
591
+ * The activity log (design 2026-10-25-activity, §4 with S1–S5): one line for every call an agent session makes that
592
+ * reads or writes account data, tied to the session that made it — what it touched, never what it said.
593
+ *
594
+ * - **Where:** `<state>/activity/YYYY-MM-DD.jsonl` (the UTC day of the line), each file owner-only (600) in an
595
+ * owner-only folder (700), each line appended whole with `appendPrivateLine`, as the audit log's are. Daily files,
596
+ * so retention drops whole days (`prune`).
597
+ * - **What a line holds:** `ActivityLine`, and nothing else. Every line is rebuilt field by field from the fields a
598
+ * line has (`activityLineOf`), so a channel that hands over a whole message — body, text, HTML, attachment bytes —
599
+ * still writes only its id, subject and sender: the type says so, and the copy makes it so. Subjects and names are
600
+ * kept as received, cut to their bounds, and shown only inside the untrusted-content envelope.
601
+ * - **Never in the way:** `append` never throws into its caller. A line that cannot be written is said once per
602
+ * process on stderr, remembered for `doctor`, and the call it describes goes on (§4.6).
603
+ * - **The audit log is unchanged.** It stays small, content-free and shareable; this one is private.
604
+ */
605
+ /** What the call did with account data: read it, wrote it, was told of it (an event delivery, S1), or approved it. */
606
+ const ACTIVITY_KINDS = Object.freeze([
607
+ "read",
608
+ "write",
609
+ "told",
610
+ "approval"
611
+ ]);
612
+ /** How it ended: done, refused by a policy, an approval or a rule, or failed. */
613
+ const ACTIVITY_OUTCOMES = Object.freeze([
614
+ "ok",
615
+ "refused",
616
+ "failed"
617
+ ]);
618
+ /** Which kind of process recorded it: an MCP server, a CLI call, or the event service's own work. */
619
+ const ACTIVITY_SURFACES = Object.freeze([
620
+ "mcp",
621
+ "cli",
622
+ "daemon"
623
+ ]);
624
+ /**
625
+ * The most a line may take as written, its newline included: 16 KiB, well inside the one write `appendPrivateLine`
626
+ * makes of it, so a line is always whole or absent, whatever its items hold. Past it, `fitted` cuts it.
627
+ */
628
+ const MAX_ACTIVITY_LINE_BYTES = 16384;
629
+ const BOUNDS = {
630
+ id: 200,
631
+ name: 200,
632
+ address: 320,
633
+ fileName: 255,
634
+ conversation: 200,
635
+ query: 500,
636
+ reason: 300,
637
+ word: 100,
638
+ alias: 128,
639
+ folder: 4096,
640
+ startedAt: 64,
641
+ version: 64
642
+ };
643
+ const MAX_RECIPIENTS$1 = 50;
644
+ const MAX_COUNTS = 10;
645
+ const SESSION_KEY = /^[0-9a-f]{16}$/;
646
+ const ISO_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
647
+ const DAY_FILE = /^(\d{4}-\d{2}-\d{2})\.jsonl$/;
648
+ const DAY_MS$1 = 864e5;
649
+ /** `value` cut to `max` characters — whole characters, never half a surrogate pair — ending in `…` when cut. */
650
+ function cut(value, max) {
651
+ if (typeof value !== "string") return void 0;
652
+ if (value.length <= max) return value;
653
+ const chars = Array.from(value.slice(0, max * 2 + 2));
654
+ return chars.length <= max ? chars.join("") : `${chars.slice(0, max - 1).join("")}…`;
655
+ }
656
+ function whole(value) {
657
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.round(value) : void 0;
658
+ }
659
+ function isoDay(at) {
660
+ return new Date(at).toISOString().slice(0, 10);
661
+ }
662
+ function own$3(value) {
663
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
664
+ }
665
+ function itemOf$1(raw) {
666
+ const item = own$3(raw);
667
+ const id = cut(item?.id, BOUNDS.id);
668
+ if (item === void 0 || id === void 0) return null;
669
+ const sender = own$3(item.from);
670
+ const from = sender === void 0 ? void 0 : {
671
+ ...typeof sender.name === "string" ? { name: cut(sender.name, BOUNDS.name) } : {},
672
+ ...typeof sender.address === "string" ? { address: cut(sender.address, BOUNDS.address) } : {}
673
+ };
674
+ const size = whole(item.size);
675
+ return {
676
+ id,
677
+ ...typeof item.subject === "string" ? { subject: cut(item.subject, 200) } : {},
678
+ ...from !== void 0 && Object.keys(from).length > 0 ? { from } : {},
679
+ ...typeof item.name === "string" ? { name: cut(item.name, BOUNDS.fileName) } : {},
680
+ ...size !== void 0 ? { size } : {},
681
+ ...typeof item.conversation === "string" ? { conversation: cut(item.conversation, BOUNDS.conversation) } : {}
682
+ };
683
+ }
684
+ function clientOf(raw) {
685
+ const client = own$3(raw);
686
+ const name = cut(client?.name, BOUNDS.word);
687
+ if (name === void 0 || name === "") return void 0;
688
+ const version = cut(client?.version, BOUNDS.version);
689
+ return {
690
+ name,
691
+ ...version ? { version } : {}
692
+ };
693
+ }
694
+ /** A parent in this exact key order — the order `sessionOfAncestor` finds it by. */
695
+ function parentOf(raw) {
696
+ const parent = own$3(raw);
697
+ const pid = whole(parent?.pid);
698
+ const startedAt = cut(parent?.startedAt, BOUNDS.startedAt);
699
+ if (pid === void 0 || startedAt === void 0) return void 0;
700
+ return {
701
+ pid,
702
+ startedAt,
703
+ name: cut(parent?.name, BOUNDS.word) ?? ""
704
+ };
705
+ }
706
+ function sessionOf(raw) {
707
+ const session = own$3(raw) ?? {};
708
+ const key = typeof session.key === "string" && SESSION_KEY.test(session.key) ? session.key : null;
709
+ const surface = ACTIVITY_SURFACES.includes(session.surface) ? session.surface : "mcp";
710
+ const client = clientOf(session.client);
711
+ const parent = parentOf(session.parent);
712
+ const ancestors = Array.isArray(session.ancestors) ? session.ancestors.slice(0, 3).flatMap((name) => typeof name === "string" ? [cut(name, BOUNDS.word) ?? ""] : []) : void 0;
713
+ return {
714
+ key: key ?? randomKey(),
715
+ process: cut(session.process, BOUNDS.word) ?? PROCESS_ID,
716
+ pid: whole(session.pid) ?? 0,
717
+ ...client === void 0 ? {} : { client },
718
+ folder: typeof session.folder === "string" ? cut(session.folder, BOUNDS.folder) ?? null : null,
719
+ ...parent === void 0 ? {} : { parent },
720
+ ...ancestors === void 0 ? {} : { ancestors },
721
+ grouped: key !== null && session.grouped === true,
722
+ surface
723
+ };
724
+ }
725
+ /**
726
+ * The line as it is written: every field copied from what a line has, each cut to its bound, in one fixed order.
727
+ * Anything else the input carries — a body, a text, an HTML part, bytes — is not copied, whatever it is called.
728
+ */
729
+ function activityLineOf(input, now = /* @__PURE__ */ new Date()) {
730
+ return fitted(boundedLine(input, now));
731
+ }
732
+ /** How many bytes a line takes as written, its newline included. */
733
+ function bytesOf(line) {
734
+ return Buffer.byteLength(JSON.stringify(line)) + 1;
735
+ }
736
+ /**
737
+ * The line, cut until it fits in `MAX_ACTIVITY_LINE_BYTES`, and marked `truncated` when it was: items from the end
738
+ * first — `count` keeps how many there were — then recipients from the end, then the longer words shortened, and
739
+ * last the words that are not the line's own dropped. Text a sender wrote can be many times its length as JSON (a
740
+ * control character is six bytes), so the bounds on each field alone do not bound the line.
741
+ */
742
+ function fitted(line) {
743
+ if (bytesOf(line) <= 16384) return line;
744
+ const items = [...line.items ?? []];
745
+ const recipients = [...line.recipients ?? []];
746
+ let cutLine = {
747
+ ...line,
748
+ truncated: true
749
+ };
750
+ const fits = () => bytesOf(cutLine) <= MAX_ACTIVITY_LINE_BYTES;
751
+ while (items.length > 0 && !fits()) {
752
+ items.pop();
753
+ cutLine = {
754
+ ...cutLine,
755
+ items: [...items]
756
+ };
757
+ }
758
+ while (recipients.length > 0 && !fits()) {
759
+ recipients.pop();
760
+ cutLine = {
761
+ ...cutLine,
762
+ recipients: [...recipients]
763
+ };
764
+ }
765
+ if (fits()) return cutLine;
766
+ const short = (text, max) => text === void 0 ? void 0 : cut(text, max);
767
+ const { ancestors: _ancestors, ...session } = cutLine.session;
768
+ cutLine = {
769
+ ...cutLine,
770
+ channel: short(cutLine.channel, 40) ?? "",
771
+ operation: short(cutLine.operation, 64) ?? "",
772
+ account: {
773
+ id: short(cutLine.account.id, 64) ?? "",
774
+ alias: short(cutLine.account.alias, 64) ?? ""
775
+ },
776
+ session: {
777
+ ...session,
778
+ folder: cutLine.session.folder === null ? null : short(cutLine.session.folder, 256) ?? null,
779
+ ...session.client === void 0 ? {} : { client: {
780
+ name: short(session.client.name, 40) ?? "",
781
+ ...session.client.version === void 0 ? {} : { version: short(session.client.version, 20) }
782
+ } },
783
+ ...session.parent === void 0 ? {} : { parent: {
784
+ ...session.parent,
785
+ name: short(session.parent.name, 40) ?? ""
786
+ } }
787
+ },
788
+ ...cutLine.reason === void 0 ? {} : { reason: short(cutLine.reason, 100) },
789
+ ...cutLine.query === void 0 ? {} : { query: short(cutLine.query, 100) },
790
+ ...cutLine.told === void 0 ? {} : { told: {
791
+ rule: short(cutLine.told.rule, 60) ?? "",
792
+ target: short(cutLine.told.target, 60) ?? ""
793
+ } }
794
+ };
795
+ if (fits()) return cutLine;
796
+ const { reason: _reason, query: _query, told: _told, counts: _counts, ...rest } = cutLine;
797
+ return {
798
+ ...rest,
799
+ truncated: true
800
+ };
801
+ }
802
+ /** The line every field of it bounded, before the line as a whole is (`fitted`). */
803
+ function boundedLine(input, now) {
804
+ const raw = input;
805
+ const at = typeof raw.at === "string" && ISO_TIME.test(raw.at) ? raw.at : now.toISOString();
806
+ const account = own$3(raw.account);
807
+ const items = Array.isArray(raw.items) ? raw.items : void 0;
808
+ const kept = items?.slice(0, 50).flatMap((item) => itemOf$1(item) ?? []);
809
+ const given = whole(raw.count);
810
+ const count = items === void 0 ? given : Math.max(items.length, given ?? 0);
811
+ const counts = own$3(raw.counts);
812
+ const keptCounts = counts === void 0 ? void 0 : Object.fromEntries(Object.entries(counts).filter(([name, value]) => /^[a-z][A-Za-z]{0,31}$/.test(name) && whole(value) !== void 0).slice(0, MAX_COUNTS).map(([name, value]) => [name, whole(value)]));
813
+ const recipients = Array.isArray(raw.recipients) ? raw.recipients.slice(0, MAX_RECIPIENTS$1).flatMap((address) => typeof address === "string" ? [cut(address, BOUNDS.address)] : []) : void 0;
814
+ const told = own$3(raw.told);
815
+ const reason = cut(raw.reason, BOUNDS.reason);
816
+ const query = cut(raw.query, BOUNDS.query);
817
+ const approvalId = typeof raw.approvalId === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(raw.approvalId) ? raw.approvalId : void 0;
818
+ return {
819
+ at,
820
+ channel: cut(raw.channel, BOUNDS.word) ?? "",
821
+ account: {
822
+ id: cut(account?.id, BOUNDS.alias) ?? "",
823
+ alias: cut(account?.alias, BOUNDS.alias) ?? ""
824
+ },
825
+ operation: cut(raw.operation, BOUNDS.word) ?? "",
826
+ kind: ACTIVITY_KINDS.includes(raw.kind) ? raw.kind : "read",
827
+ outcome: ACTIVITY_OUTCOMES.includes(raw.outcome) ? raw.outcome : "ok",
828
+ ...reason ? { reason } : {},
829
+ session: sessionOf(raw.session),
830
+ ms: whole(raw.ms) ?? 0,
831
+ ...kept === void 0 ? {} : { items: kept },
832
+ ...count === void 0 ? {} : { count },
833
+ ...keptCounts === void 0 || Object.keys(keptCounts).length === 0 ? {} : { counts: keptCounts },
834
+ ...query === void 0 ? {} : { query },
835
+ ...recipients === void 0 ? {} : { recipients },
836
+ ...approvalId === void 0 ? {} : { approvalId },
837
+ ...told === void 0 ? {} : { told: {
838
+ rule: cut(told.rule, BOUNDS.name) ?? "",
839
+ target: cut(told.target, BOUNDS.name) ?? ""
840
+ } },
841
+ ...raw.truncated === true ? { truncated: true } : {}
842
+ };
843
+ }
844
+ /** A line read back from a file: one this release can show, or null for anything else. */
845
+ function readLine(text) {
846
+ let raw;
847
+ try {
848
+ raw = JSON.parse(text);
849
+ } catch {
850
+ return null;
851
+ }
852
+ const line = own$3(raw);
853
+ if (line === void 0 || typeof line.at !== "string" || !ISO_TIME.test(line.at)) return null;
854
+ const session = own$3(line.session);
855
+ if (typeof session?.key !== "string" || !SESSION_KEY.test(session.key)) return null;
856
+ return activityLineOf(line);
857
+ }
858
+ function emptyKinds() {
859
+ return {
860
+ read: 0,
861
+ write: 0,
862
+ told: 0,
863
+ approval: 0
864
+ };
865
+ }
866
+ function emptyOutcomes() {
867
+ return {
868
+ ok: 0,
869
+ refused: 0,
870
+ failed: 0
871
+ };
872
+ }
873
+ function aggregateOf(line) {
874
+ const kind = emptyKinds();
875
+ kind[line.kind] += 1;
876
+ const outcome = emptyOutcomes();
877
+ outcome[line.outcome] += 1;
878
+ return {
879
+ key: line.session.key,
880
+ grouped: line.session.grouped,
881
+ surfaces: [line.session.surface],
882
+ primary: {
883
+ at: line.at,
884
+ cli: line.session.surface === "cli",
885
+ folder: line.session.folder,
886
+ client: line.session.client ?? null,
887
+ parent: line.session.parent ?? null
888
+ },
889
+ firstSeen: line.at,
890
+ lastSeen: line.at,
891
+ processes: [line.session.process],
892
+ channels: line.channel ? [line.channel] : [],
893
+ accounts: line.account.alias ? [line.account.alias] : [],
894
+ kind,
895
+ outcome
896
+ };
897
+ }
898
+ function union(a, b) {
899
+ return [.../* @__PURE__ */ new Set([...a, ...b])];
900
+ }
901
+ /** Two aggregates of one session, added together. */
902
+ function merge(a, b) {
903
+ const first = (x, y) => x.cli !== y.cli ? x.cli ? y : x : x.at <= y.at ? x : y;
904
+ const primary = first(a.primary, b.primary);
905
+ const other = primary === a.primary ? b.primary : a.primary;
906
+ const kind = emptyKinds();
907
+ const outcome = emptyOutcomes();
908
+ for (const name of ACTIVITY_KINDS) kind[name] = a.kind[name] + b.kind[name];
909
+ for (const name of ACTIVITY_OUTCOMES) outcome[name] = a.outcome[name] + b.outcome[name];
910
+ return {
911
+ key: a.key,
912
+ grouped: a.grouped || b.grouped,
913
+ surfaces: union(a.surfaces, b.surfaces),
914
+ primary: {
915
+ ...primary,
916
+ client: primary.client ?? other.client,
917
+ parent: primary.parent ?? other.parent
918
+ },
919
+ firstSeen: a.firstSeen <= b.firstSeen ? a.firstSeen : b.firstSeen,
920
+ lastSeen: a.lastSeen >= b.lastSeen ? a.lastSeen : b.lastSeen,
921
+ processes: union(a.processes, b.processes),
922
+ channels: union(a.channels, b.channels),
923
+ accounts: union(a.accounts, b.accounts),
924
+ kind,
925
+ outcome
926
+ };
927
+ }
928
+ function add(groups, aggregate) {
929
+ const held = groups.get(aggregate.key);
930
+ groups.set(aggregate.key, held === void 0 ? aggregate : merge(held, aggregate));
931
+ }
932
+ function summaryOf$1(aggregate) {
933
+ return {
934
+ key: aggregate.key,
935
+ grouped: aggregate.grouped,
936
+ surfaces: [...aggregate.surfaces].sort(),
937
+ folder: aggregate.primary.folder,
938
+ client: aggregate.primary.client,
939
+ parent: aggregate.primary.parent,
940
+ firstSeen: aggregate.firstSeen,
941
+ lastSeen: aggregate.lastSeen,
942
+ processes: aggregate.processes.length,
943
+ channels: [...aggregate.channels].sort(),
944
+ accounts: [...aggregate.accounts].sort(),
945
+ counts: {
946
+ kind: { ...aggregate.kind },
947
+ outcome: { ...aggregate.outcome }
948
+ }
949
+ };
950
+ }
951
+ /** A day's aggregates read back from its index: trusted only as far as their shape, and dropped whole otherwise. */
952
+ function aggregatesOf(raw) {
953
+ if (!Array.isArray(raw)) return null;
954
+ const strings = (value) => Array.isArray(value) && value.every((each) => typeof each === "string");
955
+ for (const each of raw) {
956
+ const held = own$3(each);
957
+ if (held === void 0 || typeof held.key !== "string" || !SESSION_KEY.test(held.key) || typeof held.firstSeen !== "string" || typeof held.lastSeen !== "string" || own$3(held.primary) === void 0 || own$3(held.kind) === void 0 || own$3(held.outcome) === void 0 || !strings(held.processes) || !strings(held.channels) || !strings(held.accounts) || !strings(held.surfaces)) return null;
958
+ }
959
+ return raw;
960
+ }
961
+ const FAILURE_FILE = "activity-failure.json";
962
+ const INDEX_DIR = ".index";
963
+ /**
964
+ * Each day's server windows: one small line for each server process's window on that day — its key, parent and
965
+ * folder — so a CLI call finds the window it runs in without reading the day (`sessionOfAncestor`).
966
+ */
967
+ const PARENTS_DIR = ".parents";
968
+ const DEFAULT_SHOW_LIMIT = 1e3;
969
+ const ON_BY_DEFAULT = Object.freeze({
970
+ record: "on",
971
+ retentionDays: 30,
972
+ problems: []
973
+ });
974
+ const UNREADABLE = Object.freeze({
975
+ record: "off",
976
+ retentionDays: 365,
977
+ problems: ["the configuration could not be read, so nothing is recorded"]
978
+ });
979
+ /** The once-a-process line, when the stderr default is used: one process, one line, however many logs it opens. */
980
+ let warnedOnStderr = false;
981
+ function codeOf$1(error) {
982
+ if (error instanceof CommsError) return error.code;
983
+ const code = error?.code;
984
+ return typeof code === "string" ? code : "error";
985
+ }
986
+ function messageOf$2(error) {
987
+ return cut(error instanceof Error ? error.message : String(error), BOUNDS.reason) ?? "unknown";
988
+ }
989
+ var ActivityLog = class {
990
+ directory;
991
+ #failureFile;
992
+ #now;
993
+ #settings;
994
+ #warn;
995
+ #failure = null;
996
+ #failureWrittenAt = 0;
997
+ #warned = false;
998
+ #prunedDay = null;
999
+ /** The days this process has made sure of a parents file for, and the windows it has noted there, by day. */
1000
+ #parentsReady = /* @__PURE__ */ new Set();
1001
+ #parentsNoted = /* @__PURE__ */ new Set();
1002
+ constructor(stateDir, options = {}) {
1003
+ this.directory = join(stateDir, "activity");
1004
+ this.#failureFile = join(stateDir, FAILURE_FILE);
1005
+ this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
1006
+ this.#settings = options.settings ?? (async () => ON_BY_DEFAULT);
1007
+ this.#warn = options.warn;
1008
+ }
1009
+ /** The last line this process could not write, or null. */
1010
+ get lastFailure() {
1011
+ return this.#failure;
1012
+ }
1013
+ /**
1014
+ * Appends one line, unless recording is off. True when it was written. Never throws: a line that cannot be written
1015
+ * is said once per process (`warn`, stderr by default), recorded beside the log for `doctor`, and returns false.
1016
+ *
1017
+ * The first call a process makes on each day — this, or any read — first prunes days older than the retention in
1018
+ * force, whether or not recording is on: the log's daily maintenance, best effort.
1019
+ */
1020
+ async append(input) {
1021
+ if ((await this.#maintained()).record !== "on") return false;
1022
+ try {
1023
+ const line = activityLineOf(input, this.#now());
1024
+ await appendPrivateLine(join(this.directory, `${line.at.slice(0, 10)}.jsonl`), JSON.stringify(line));
1025
+ await this.#noteParent(line);
1026
+ if (this.#failure !== null) {
1027
+ this.#failure = null;
1028
+ await rm(this.#failureFile, { force: true }).catch(() => void 0);
1029
+ }
1030
+ } catch (error) {
1031
+ await this.#failed(error);
1032
+ return false;
1033
+ }
1034
+ return true;
1035
+ }
1036
+ async #failed(error) {
1037
+ const now = this.#now();
1038
+ const failure = {
1039
+ at: now.toISOString(),
1040
+ code: codeOf$1(error),
1041
+ message: messageOf$2(error),
1042
+ pid: process.pid
1043
+ };
1044
+ this.#failure = failure;
1045
+ const said = `agent-communications: activity is not being recorded: ${failure.message}`;
1046
+ if (this.#warn !== void 0) {
1047
+ if (!this.#warned) this.#warn(said);
1048
+ } else if (!warnedOnStderr) {
1049
+ warnedOnStderr = true;
1050
+ process.stderr.write(`${said}\n`);
1051
+ }
1052
+ this.#warned = true;
1053
+ if (now.getTime() - this.#failureWrittenAt < 6e4) return;
1054
+ this.#failureWrittenAt = now.getTime();
1055
+ await writeFileAtomic(this.#failureFile, `${JSON.stringify(failure)}\n`).catch(() => void 0);
1056
+ }
1057
+ /**
1058
+ * The day's parents file, made on a process's first line of the day — so a reader can tell a day with no window
1059
+ * from one an earlier build wrote, with no index — and a grouped server's window noted in it once a day. Best
1060
+ * effort: without it a CLI call reads the day itself.
1061
+ */
1062
+ async #noteParent(line) {
1063
+ const day = line.at.slice(0, 10);
1064
+ const path = join(this.directory, PARENTS_DIR, `${day}.jsonl`);
1065
+ try {
1066
+ if (!this.#parentsReady.has(day)) {
1067
+ await ensurePrivateDir(join(this.directory, PARENTS_DIR));
1068
+ await (await open(path, "a", 384)).close();
1069
+ this.#parentsReady.add(day);
1070
+ }
1071
+ const { session } = line;
1072
+ if (session.surface !== "mcp" || !session.grouped || session.parent === void 0) return;
1073
+ const noted = `${day}\0${session.key}`;
1074
+ if (this.#parentsNoted.has(noted)) return;
1075
+ await appendPrivateLine(path, JSON.stringify({
1076
+ key: session.key,
1077
+ parent: session.parent,
1078
+ folder: session.folder
1079
+ }));
1080
+ this.#parentsNoted.add(noted);
1081
+ } catch {}
1082
+ }
1083
+ /** The windows noted on `day`, or null when the day has no parents file (an earlier build wrote it, or none). */
1084
+ async #parentsOf(day) {
1085
+ let text;
1086
+ try {
1087
+ text = await readFile(join(this.directory, PARENTS_DIR, `${day}.jsonl`), "utf8");
1088
+ } catch {
1089
+ return null;
1090
+ }
1091
+ return text.split("\n").flatMap((raw) => {
1092
+ if (!raw) return [];
1093
+ try {
1094
+ const held = own$3(JSON.parse(raw));
1095
+ const parent = parentOf(held?.parent);
1096
+ const key = held?.key;
1097
+ if (typeof key !== "string" || !SESSION_KEY.test(key) || parent === void 0) return [];
1098
+ return [{
1099
+ key,
1100
+ parent,
1101
+ folder: typeof held?.folder === "string" ? held.folder : null
1102
+ }];
1103
+ } catch {
1104
+ return [];
1105
+ }
1106
+ });
1107
+ }
1108
+ /** The settings in force, read afresh; a reader that throws reads as `UNREADABLE` — off, and every day kept. */
1109
+ async #settingsNow() {
1110
+ try {
1111
+ return await this.#settings();
1112
+ } catch {
1113
+ return UNREADABLE;
1114
+ }
1115
+ }
1116
+ /**
1117
+ * The settings in force, after the day's maintenance: the first call a process makes on a day — an append or a read,
1118
+ * recording on or off — deletes the days past the retention. Never throws.
1119
+ */
1120
+ async #maintained() {
1121
+ const settings = await this.#settingsNow();
1122
+ const today = isoDay(this.#now().getTime());
1123
+ if (this.#prunedDay !== today) {
1124
+ this.#prunedDay = today;
1125
+ await this.prune(settings.retentionDays).catch(() => void 0);
1126
+ }
1127
+ return settings;
1128
+ }
1129
+ /** The first day the retention in force keeps: any day before it is never shown, deleted yet or not. */
1130
+ #cutoff(retentionDays) {
1131
+ return isoDay(this.#now().getTime() - retentionDays * DAY_MS$1);
1132
+ }
1133
+ /** The days held, oldest first. */
1134
+ async days() {
1135
+ let names;
1136
+ try {
1137
+ names = await readdir(this.directory);
1138
+ } catch (error) {
1139
+ const code = error.code;
1140
+ if (code === "ENOENT" || code === "ENOTDIR") return [];
1141
+ throw error;
1142
+ }
1143
+ return names.flatMap((name) => DAY_FILE.exec(name)?.[1] ?? []).sort();
1144
+ }
1145
+ /**
1146
+ * Deletes every day whose last line is older than `retentionDays` days: a day goes once all of it is that old, so
1147
+ * every line is kept at least that long and at most a day longer. Whole days only; every other day is left whole.
1148
+ */
1149
+ async prune(retentionDays) {
1150
+ if (!isActivityRetention(retentionDays)) throw new CommsError("USAGE", `the activity log keeps a whole number of days, 1 to 365, not ${String(retentionDays)}`);
1151
+ const cutoff = this.#cutoff(retentionDays);
1152
+ const deleted = [];
1153
+ for (const day of await this.days()) {
1154
+ if (day >= cutoff) continue;
1155
+ await rm(join(this.directory, `${day}.jsonl`), { force: true });
1156
+ await rm(join(this.directory, INDEX_DIR, `${day}.json`), { force: true }).catch(() => void 0);
1157
+ await rm(join(this.directory, PARENTS_DIR, `${day}.jsonl`), { force: true }).catch(() => void 0);
1158
+ deleted.push(day);
1159
+ }
1160
+ return { deleted };
1161
+ }
1162
+ async #lines(day) {
1163
+ const text = await this.#text(day);
1164
+ return text === null ? [] : text.split("\n").flatMap((raw) => raw ? readLine(raw) ?? [] : []);
1165
+ }
1166
+ async #text(day) {
1167
+ try {
1168
+ return await readFile(join(this.directory, `${day}.jsonl`), "utf8");
1169
+ } catch (error) {
1170
+ if (error.code === "ENOENT") return null;
1171
+ throw error;
1172
+ }
1173
+ }
1174
+ /**
1175
+ * A whole past day's sessions, from its index when the index was made from the file as it is now, else from the
1176
+ * file — and the index written again, best effort. Today's is never indexed: it is still being written.
1177
+ */
1178
+ async #dayAggregates(day) {
1179
+ const file = join(this.directory, `${day}.jsonl`);
1180
+ const index = join(this.directory, INDEX_DIR, `${day}.json`);
1181
+ let mark;
1182
+ try {
1183
+ const info = await stat(file);
1184
+ mark = `${info.size}:${info.mtimeMs}`;
1185
+ } catch {
1186
+ return [];
1187
+ }
1188
+ try {
1189
+ const held = JSON.parse(await readFile(index, "utf8"));
1190
+ const sessions = held.version === 1 && held.mark === mark ? aggregatesOf(held.sessions) : null;
1191
+ if (sessions !== null) return sessions;
1192
+ } catch {}
1193
+ const groups = /* @__PURE__ */ new Map();
1194
+ for (const line of await this.#lines(day)) add(groups, aggregateOf(line));
1195
+ const sessions = [...groups.values()];
1196
+ await writeFileAtomic(index, `${JSON.stringify({
1197
+ version: 1,
1198
+ mark,
1199
+ sessions
1200
+ })}\n`).catch(() => void 0);
1201
+ return sessions;
1202
+ }
1203
+ /**
1204
+ * Every session with a line at or after `since` (every day the retention keeps, when left out), newest first. A day
1205
+ * past the retention is never read, whether or not it has been deleted yet.
1206
+ */
1207
+ async sessions(options = {}) {
1208
+ const since = options.since;
1209
+ const today = isoDay(this.#now().getTime());
1210
+ const cutoff = this.#cutoff((await this.#maintained()).retentionDays);
1211
+ const groups = /* @__PURE__ */ new Map();
1212
+ for (const day of await this.days()) {
1213
+ if (day < cutoff) continue;
1214
+ if (since !== void 0 && `${day}T23:59:59.999Z` < since) continue;
1215
+ if ((since === void 0 || since <= `${day}T00:00:00.000Z`) && day < today) {
1216
+ for (const aggregate of await this.#dayAggregates(day)) add(groups, aggregate);
1217
+ continue;
1218
+ }
1219
+ for (const line of await this.#lines(day)) if (since === void 0 || line.at >= since) add(groups, aggregateOf(line));
1220
+ }
1221
+ return [...groups.values()].map(summaryOf$1).sort((a, b) => a.lastSeen === b.lastSeen ? a.key.localeCompare(b.key) : a.lastSeen < b.lastSeen ? 1 : -1);
1222
+ }
1223
+ /**
1224
+ * One session's lines, newest first, that match every filter given; and its summary over the time asked. Only
1225
+ * lines whose session key is exactly `key` are ever read into the answer, and none of a day past the retention.
1226
+ */
1227
+ async show(key, filters = {}) {
1228
+ if (!SESSION_KEY.test(key)) throw new CommsError("USAGE", `"${cut(key, 40)}" is not a session key`);
1229
+ const cutoff = this.#cutoff((await this.#maintained()).retentionDays);
1230
+ const limit = filters.limit ?? DEFAULT_SHOW_LIMIT;
1231
+ const since = filters.since;
1232
+ const needle = `"key":"${key}"`;
1233
+ const lines = [];
1234
+ let matched = 0;
1235
+ let summary = null;
1236
+ const days = (await this.days()).filter((day) => day >= cutoff).reverse();
1237
+ for (const day of days) {
1238
+ if (since !== void 0 && `${day}T23:59:59.999Z` < since) break;
1239
+ const text = await this.#text(day);
1240
+ if (text === null) continue;
1241
+ const raws = text.split("\n");
1242
+ for (let index = raws.length - 1; index >= 0; index -= 1) {
1243
+ const raw = raws[index];
1244
+ if (!raw.includes(needle)) continue;
1245
+ const line = readLine(raw);
1246
+ if (line === null || line.session.key !== key) continue;
1247
+ if (since !== void 0 && line.at < since) continue;
1248
+ const aggregate = aggregateOf(line);
1249
+ summary = summary === null ? aggregate : merge(summary, aggregate);
1250
+ if (!matches(line, filters)) continue;
1251
+ lines.push(line);
1252
+ matched += 1;
1253
+ if (lines.length >= limit * 2) {
1254
+ lines.sort(newestFirst);
1255
+ lines.length = limit;
1256
+ }
1257
+ }
1258
+ }
1259
+ lines.sort(newestFirst);
1260
+ if (lines.length > limit) lines.length = limit;
1261
+ return {
1262
+ lines,
1263
+ more: matched > limit,
1264
+ summary: summary === null ? null : summaryOf$1(summary)
1265
+ };
1266
+ }
1267
+ /**
1268
+ * The window a CLI call belongs to (S2): the session of a server process, today or yesterday, whose parent is one of
1269
+ * `ancestors` — the nearest first. Null when none is. Read from each day's parents file; a day with none — written
1270
+ * by an earlier build — is read whole, as before.
1271
+ */
1272
+ async sessionOfAncestor(ancestors) {
1273
+ const now = this.#now().getTime();
1274
+ const noted = [];
1275
+ const texts = [];
1276
+ for (const day of [isoDay(now), isoDay(now - DAY_MS$1)]) {
1277
+ const parents = await this.#parentsOf(day);
1278
+ if (parents !== null) {
1279
+ noted.push(...parents);
1280
+ continue;
1281
+ }
1282
+ const text = await this.#text(day).catch(() => null);
1283
+ if (text !== null) texts.push(text);
1284
+ }
1285
+ for (const ancestor of ancestors) {
1286
+ const found = noted.find((each) => each.parent.pid === ancestor.pid && each.parent.startedAt === ancestor.startedAt);
1287
+ if (found !== void 0) return found;
1288
+ const needle = `"parent":{"pid":${ancestor.pid},"startedAt":${JSON.stringify(ancestor.startedAt)}`;
1289
+ for (const text of texts) {
1290
+ let from = text.indexOf(needle);
1291
+ while (from !== -1) {
1292
+ const start = text.lastIndexOf("\n", from) + 1;
1293
+ const end = text.indexOf("\n", from);
1294
+ const line = readLine(text.slice(start, end === -1 ? text.length : end));
1295
+ const parent = line?.session.parent;
1296
+ if (line?.session.grouped && line.session.surface === "mcp" && parent?.pid === ancestor.pid && parent.startedAt === ancestor.startedAt) return {
1297
+ key: line.session.key,
1298
+ parent,
1299
+ folder: line.session.folder
1300
+ };
1301
+ from = text.indexOf(needle, from + needle.length);
1302
+ }
1303
+ }
1304
+ }
1305
+ return null;
1306
+ }
1307
+ /** The last failure recorded by any process — the newer of the file beside the log and this process's own. */
1308
+ async readFailure() {
1309
+ let stored = null;
1310
+ try {
1311
+ const raw = own$3(JSON.parse(await readFile(this.#failureFile, "utf8")));
1312
+ if (raw !== void 0 && typeof raw.at === "string" && ISO_TIME.test(raw.at)) stored = {
1313
+ at: raw.at,
1314
+ code: cut(raw.code, BOUNDS.word) ?? "error",
1315
+ message: cut(raw.message, BOUNDS.reason) ?? "unknown",
1316
+ pid: whole(raw.pid) ?? 0
1317
+ };
1318
+ } catch {}
1319
+ const mine = this.#failure;
1320
+ if (mine === null) return stored;
1321
+ if (stored === null) return mine;
1322
+ return mine.at >= stored.at ? mine : stored;
1323
+ }
1324
+ /**
1325
+ * What `doctor` shows of the log. It creates nothing; like every read it first deletes the days past the retention,
1326
+ * once a day per process, so the days it lists past the retention are those that could not be deleted.
1327
+ */
1328
+ async health() {
1329
+ await this.#maintained();
1330
+ const today = isoDay(this.#now().getTime());
1331
+ let exists = false;
1332
+ let isDirectory = true;
1333
+ try {
1334
+ const info = await stat(this.directory);
1335
+ exists = true;
1336
+ isDirectory = info.isDirectory();
1337
+ } catch {}
1338
+ const days = exists && isDirectory ? await this.days().catch(() => []) : [];
1339
+ let lastWritten = null;
1340
+ const newest = days.at(-1);
1341
+ if (newest !== void 0) lastWritten = await stat(join(this.directory, `${newest}.jsonl`)).then((info) => info.mtime.toISOString(), () => null);
1342
+ const state = join(this.directory, "..");
1343
+ const target = days.includes(today) ? join(this.directory, `${today}.jsonl`) : exists ? this.directory : existsSync(state) ? state : null;
1344
+ const writable = isDirectory && (target === null || await access(target, constants$1.W_OK).then(() => true, () => false));
1345
+ const loose = exists && isDirectory ? await isGroupOrWorldAccessible(this.directory).catch(() => false) : false;
1346
+ return {
1347
+ directory: this.directory,
1348
+ exists,
1349
+ isDirectory,
1350
+ private: !exists || !isDirectory || process.platform === "win32" ? null : !loose,
1351
+ writable,
1352
+ days,
1353
+ lastWritten,
1354
+ failure: await this.readFailure()
1355
+ };
1356
+ }
1357
+ };
1358
+ /** Newest first; lines of one time in the order they were met. */
1359
+ function newestFirst(a, b) {
1360
+ return a.at === b.at ? 0 : a.at < b.at ? 1 : -1;
1361
+ }
1362
+ function matches(line, filters) {
1363
+ if (filters.kind !== void 0 && line.kind !== filters.kind) return false;
1364
+ if (filters.outcome !== void 0 && line.outcome !== filters.outcome) return false;
1365
+ if (filters.channel !== void 0 && line.channel !== filters.channel) return false;
1366
+ if (filters.account !== void 0 && line.account.alias !== filters.account && line.account.id !== filters.account) return false;
1367
+ return true;
1368
+ }
1369
+ /** `ps -o ppid=,lstart=,comm=` in the C locale: the parent, the start as five words, and the executable — last. */
1370
+ const PS_LINE = /^\s*(\d+)\s+([A-Z][a-z]{2}\s+[A-Z][a-z]{2}\s+\d{1,2}\s+\d{1,2}:\d{2}:\d{2}\s+\d{4})\s+(.+?)\s*$/;
1371
+ /** One process's line of `ps -o ppid=,lstart=,comm=`, or null for anything else. The name is the executable's own. */
1372
+ function parsePsLine(text) {
1373
+ const first = text.split("\n").find((row) => row.trim() !== "");
1374
+ const match = first === void 0 ? null : PS_LINE.exec(first);
1375
+ if (match === null) return null;
1376
+ const command = match[3];
1377
+ const name = command.includes("/") ? command.split("/").at(-1) ?? command : command;
1378
+ return {
1379
+ ppid: Number(match[1]),
1380
+ startedAt: match[2].replace(/\s+/g, " "),
1381
+ name
1382
+ };
1383
+ }
1384
+ const PS_PROGRAMS = ["/bin/ps", "/usr/bin/ps"];
1385
+ /**
1386
+ * This machine's `ps`, by its full path, in the C locale so the start reads the same in every process, bounded by
1387
+ * `timeoutMs`. Never on Windows: no query there answers inside the bound (PowerShell alone takes longer to start),
1388
+ * so a Windows process is ungrouped.
1389
+ */
1390
+ const psProbe = (pid, timeoutMs) => new Promise((resolve) => {
1391
+ const program = PS_PROGRAMS.find((path) => existsSync(path));
1392
+ if (process.platform === "win32" || program === void 0) {
1393
+ resolve(null);
1394
+ return;
1395
+ }
1396
+ try {
1397
+ execFile(program, [
1398
+ "-o",
1399
+ "ppid=,lstart=,comm=",
1400
+ "-p",
1401
+ String(pid)
1402
+ ], {
1403
+ timeout: Math.max(1, Math.round(timeoutMs)),
1404
+ env: {
1405
+ LC_ALL: "C",
1406
+ LANG: "C",
1407
+ PATH: "/bin:/usr/bin"
1408
+ },
1409
+ maxBuffer: 65536,
1410
+ windowsHide: true
1411
+ }, (error, stdout) => resolve(error ? null : parsePsLine(String(stdout)))).on("error", () => resolve(null));
1412
+ } catch {
1413
+ resolve(null);
1414
+ }
1415
+ });
1416
+ function randomKey() {
1417
+ return randomBytes(8).toString("hex");
1418
+ }
1419
+ /**
1420
+ * What a process has worked out of its session: its random ids, its background reads of its parent, and a CLI call's
1421
+ * identity. One per process (`sessionIdentity` keeps its own); a test makes its own.
1422
+ */
1423
+ var SessionIdentityCache = class {
1424
+ /** A random id this process made when it started (§4.3). */
1425
+ processId = randomBytes(6).toString("hex");
1426
+ /** The key of this process's own session, when it is not grouped with others. */
1427
+ ownKey = randomKey();
1428
+ };
1429
+ /** Each cache's background reads, and its CLI identity by log: held here, so the class shows only its ids. */
1430
+ const READS = /* @__PURE__ */ new WeakMap();
1431
+ const CLI_IDENTITIES = /* @__PURE__ */ new WeakMap();
1432
+ function heldIn(map, cache) {
1433
+ let held = map.get(cache);
1434
+ if (held === void 0) {
1435
+ held = /* @__PURE__ */ new Map();
1436
+ map.set(cache, held);
1437
+ }
1438
+ return held;
1439
+ }
1440
+ const PROCESS_CACHE = new SessionIdentityCache();
1441
+ /** This process's own random id: a line handed in without one names this process. */
1442
+ const PROCESS_ID = PROCESS_CACHE.processId;
1443
+ /** `promise`, or null after `ms`. Its timer is not unref'd: whoever awaits it keeps the process alive till then. */
1444
+ function within$2(promise, ms) {
1445
+ return new Promise((resolve) => {
1446
+ const timer = setTimeout(() => resolve(null), Math.max(0, ms));
1447
+ promise.then((value) => {
1448
+ clearTimeout(timer);
1449
+ resolve(value);
1450
+ }, () => {
1451
+ clearTimeout(timer);
1452
+ resolve(null);
1453
+ });
1454
+ });
1455
+ }
1456
+ function folderOf(options) {
1457
+ try {
1458
+ return (options.cwd ?? (() => process.cwd()))();
1459
+ } catch {
1460
+ return null;
1461
+ }
1462
+ }
1463
+ async function readAncestry(options, depth, initIsParent, budgetMs) {
1464
+ const platform = options.platform ?? process.platform;
1465
+ const pid = options.pid ?? process.pid;
1466
+ const folder = folderOf(options);
1467
+ const chain = [];
1468
+ if (platform === "win32") return {
1469
+ pid,
1470
+ folder,
1471
+ chain,
1472
+ complete: true
1473
+ };
1474
+ const probe = options.probe ?? psProbe;
1475
+ const deadline = Date.now() + budgetMs;
1476
+ let next = options.ppid ?? process.ppid;
1477
+ while (chain.length < depth && (next > 1 || initIsParent && next === 1 && chain.length === 0)) {
1478
+ const left = deadline - Date.now();
1479
+ if (left <= 0) return {
1480
+ pid,
1481
+ folder,
1482
+ chain,
1483
+ complete: false
1484
+ };
1485
+ const facts = await within$2(probe(next, left), left);
1486
+ if (facts === null) return {
1487
+ pid,
1488
+ folder,
1489
+ chain,
1490
+ complete: false
1491
+ };
1492
+ chain.push({
1493
+ pid: next,
1494
+ startedAt: facts.startedAt,
1495
+ name: facts.name,
1496
+ ppid: facts.ppid
1497
+ });
1498
+ if (next === 1) break;
1499
+ next = facts.ppid;
1500
+ }
1501
+ return {
1502
+ pid,
1503
+ folder,
1504
+ chain,
1505
+ complete: true
1506
+ };
1507
+ }
1508
+ /**
1509
+ * The background read of this process's parent, begun if none is under way or kept: a read that finished is kept for
1510
+ * good, one under way is waited on, and one that ran out of time is begun again once a minute has passed.
1511
+ */
1512
+ function ancestryRead(cache, options, initIsParent) {
1513
+ const id = initIsParent ? "daemon" : "server";
1514
+ const now = (options.now ?? Date.now)();
1515
+ const reads = heldIn(READS, cache);
1516
+ const held = reads.get(id);
1517
+ if (held !== void 0 && (held.settled === null || held.settled.complete || now - held.at < 6e4)) return held;
1518
+ const read = {
1519
+ at: now,
1520
+ settled: null,
1521
+ promise: readAncestry(options, 1, initIsParent, options.budgetMs ?? 2e3)
1522
+ };
1523
+ read.promise.then((ancestry) => {
1524
+ read.settled = ancestry;
1525
+ }, () => void 0);
1526
+ reads.set(id, read);
1527
+ return read;
1528
+ }
1529
+ /**
1530
+ * Begins working out this process's session in the background, as an MCP server or the event service starts, so its
1531
+ * first calls find it done: a read of its parent of up to `SESSION_IDENTITY_BACKGROUND_MS`. Returns at once; never
1532
+ * throws. A CLI call reads its own, and is not warmed.
1533
+ */
1534
+ function warmSessionIdentity(surface = "mcp", options = {}) {
1535
+ if (surface === "cli") return;
1536
+ try {
1537
+ ancestryRead(options.cache ?? PROCESS_CACHE, options, surface === "daemon");
1538
+ } catch {}
1539
+ }
1540
+ function windowKey(parent, folder, connection) {
1541
+ const named = connection === void 0 || connection === "" ? "" : `\0${connection}`;
1542
+ return createHash("sha256").update(`${parent.pid}\0${parent.startedAt}\0${folder}${named}`).digest("hex").slice(0, 16);
1543
+ }
1544
+ /** System folders, each with everything under it: no project is started there. */
1545
+ const POSIX_SYSTEM_TREES = [
1546
+ "/System",
1547
+ "/Library",
1548
+ "/Applications",
1549
+ "/usr",
1550
+ "/bin",
1551
+ "/sbin",
1552
+ "/etc",
1553
+ "/dev",
1554
+ "/proc",
1555
+ "/sys",
1556
+ "/boot"
1557
+ ];
1558
+ /** Folders that hold everyone's projects, or anyone's temporary files — only the folder itself, not what is in it. */
1559
+ const POSIX_SHARED = [
1560
+ "/tmp",
1561
+ "/var",
1562
+ "/private",
1563
+ "/private/tmp",
1564
+ "/private/var",
1565
+ "/opt",
1566
+ "/Volumes",
1567
+ "/mnt",
1568
+ "/media"
1569
+ ];
1570
+ const WINDOWS_SYSTEM_TREES = [
1571
+ "\\windows",
1572
+ "\\program files",
1573
+ "\\program files (x86)",
1574
+ "\\programdata"
1575
+ ];
1576
+ /**
1577
+ * Whether a folder is one that cannot tell projects apart (S3): a filesystem's root, the home folder or a folder above
1578
+ * it, a system folder or anything in one, or a shared one such as `/tmp`. A client that starts every server from one
1579
+ * process there — a desktop app started in `/`, say — would otherwise make every project it serves one session.
1580
+ */
1581
+ function sharedFolder(folder, home, platform = process.platform) {
1582
+ if (platform === "win32") {
1583
+ const plain = (path) => path.replace(/\//g, "\\").replace(/\\+$/, "").toLowerCase();
1584
+ const at = plain(folder);
1585
+ const homeAt = plain(home);
1586
+ if (/^[a-z]:$/.test(at) || at === "") return true;
1587
+ if (homeAt !== "" && (at === homeAt || homeAt.startsWith(`${at}\\`))) return true;
1588
+ const rest = at.replace(/^[a-z]:/, "");
1589
+ return rest === "\\users" || WINDOWS_SYSTEM_TREES.some((tree) => rest === tree || rest.startsWith(`${tree}\\`));
1590
+ }
1591
+ const plain = (path) => (path.length > 1 ? path.replace(/\/+$/, "") : path) || "/";
1592
+ const at = plain(folder);
1593
+ const homeAt = plain(home);
1594
+ if (at === "/") return true;
1595
+ if (homeAt !== "/" && (at === homeAt || homeAt.startsWith(`${at}/`))) return true;
1596
+ if (POSIX_SHARED.includes(at)) return true;
1597
+ return POSIX_SYSTEM_TREES.some((tree) => at === tree || at.startsWith(`${tree}/`));
1598
+ }
1599
+ /**
1600
+ * Which session this process's calls belong to (S2): its id, its parent's id, start and name, its folder and the
1601
+ * session key. Never throws.
1602
+ *
1603
+ * - **An MCP server** (`mcp`) is grouped with every other server its parent started in the same folder: one agent
1604
+ * window. Its parent is read in the background — begun by `warmSessionIdentity` as the server starts, or by its first
1605
+ * call — for up to two seconds; a call waits for it about 200 ms and, when it is not done, goes on alone for now. A
1606
+ * read that ran out of time is not kept: one a minute or more later tries again. A parent that has gone, a folder
1607
+ * that cannot be read, and a folder that cannot tell projects apart (`sharedFolder`: `/`, the home folder, a system
1608
+ * folder) leave it ungrouped, with a key of its own.
1609
+ * - **A CLI call** (`cli`) reads up to three processes above it, within about 200 ms. When one of them is the parent
1610
+ * of a server process that recorded a line today (or yesterday) in `options.log`, the call joins that window's
1611
+ * session; otherwise it is a session of its own. Its `ancestors` name those processes either way.
1612
+ * - **The event service** (`daemon`) is grouped by its parent too, process 1 included, whatever its folder — it serves
1613
+ * no project — so it is one session since it started. It waits for the whole read: it records on a timer, and keeps
1614
+ * what it is given.
1615
+ * - **On Windows** no process is grouped: a query of a process's parent and start does not answer within the bound.
1616
+ *
1617
+ * `clientInfo` is what an MCP client said it is; it is stored as that, and nothing depends on it.
1618
+ */
1619
+ async function sessionIdentity(surface, clientInfo, options = {}) {
1620
+ const seams = options.pid !== void 0 || options.ppid !== void 0 || options.cwd !== void 0 || options.probe !== void 0 || options.platform !== void 0 || options.budgetMs !== void 0 || options.waitMs !== void 0 || options.home !== void 0 || options.now !== void 0;
1621
+ const cache = options.cache ?? (seams ? null : PROCESS_CACHE);
1622
+ const client = clientOf(clientInfo);
1623
+ try {
1624
+ if (surface === "cli" && cache !== null) {
1625
+ const directory = options.log?.directory ?? "";
1626
+ const identities = heldIn(CLI_IDENTITIES, cache);
1627
+ let held = identities.get(directory);
1628
+ if (held === void 0) {
1629
+ held = identityOf$1(surface, options, cache);
1630
+ identities.set(directory, held);
1631
+ }
1632
+ return {
1633
+ ...await held,
1634
+ ...client === void 0 ? {} : { client }
1635
+ };
1636
+ }
1637
+ return {
1638
+ ...await identityOf$1(surface, options, cache),
1639
+ ...client === void 0 ? {} : { client }
1640
+ };
1641
+ } catch {
1642
+ return {
1643
+ key: cache?.ownKey ?? randomKey(),
1644
+ process: cache?.processId ?? PROCESS_ID,
1645
+ pid: options.pid ?? process.pid,
1646
+ ...client === void 0 ? {} : { client },
1647
+ folder: null,
1648
+ grouped: false,
1649
+ surface
1650
+ };
1651
+ }
1652
+ }
1653
+ async function identityOf$1(surface, options, cache) {
1654
+ const own = cache?.ownKey ?? randomKey();
1655
+ const processId = cache?.processId ?? randomBytes(6).toString("hex");
1656
+ const parentOfChain = (chain, index) => {
1657
+ const held = chain[index];
1658
+ return held === void 0 ? void 0 : {
1659
+ pid: held.pid,
1660
+ startedAt: held.startedAt,
1661
+ name: held.name
1662
+ };
1663
+ };
1664
+ if (surface === "cli") {
1665
+ const { pid, folder, chain } = await readAncestry(options, 3, false, options.budgetMs ?? 200);
1666
+ const base = {
1667
+ process: processId,
1668
+ pid,
1669
+ folder,
1670
+ surface
1671
+ };
1672
+ const ancestors = chain.map((held) => held.name);
1673
+ const parents = chain.map((_, index) => parentOfChain(chain, index));
1674
+ const joined = parents.length > 0 && options.log ? await options.log.sessionOfAncestor(parents) : null;
1675
+ if (joined !== null) return {
1676
+ key: joined.key,
1677
+ ...base,
1678
+ parent: joined.parent,
1679
+ ancestors,
1680
+ grouped: true
1681
+ };
1682
+ const parent = parentOfChain(chain, 0);
1683
+ return {
1684
+ key: own,
1685
+ ...base,
1686
+ ...parent === void 0 ? {} : { parent },
1687
+ ancestors,
1688
+ grouped: false
1689
+ };
1690
+ }
1691
+ const initIsParent = surface === "daemon";
1692
+ let ancestry;
1693
+ if (cache === null) ancestry = await readAncestry(options, 1, initIsParent, options.budgetMs ?? 200);
1694
+ else {
1695
+ const read = ancestryRead(cache, options, initIsParent);
1696
+ const wait = initIsParent ? options.budgetMs ?? 2e3 : options.waitMs ?? 200;
1697
+ ancestry = read.settled ?? await within$2(read.promise, wait);
1698
+ }
1699
+ if (ancestry === null) return {
1700
+ key: own,
1701
+ process: processId,
1702
+ pid: options.pid ?? process.pid,
1703
+ folder: folderOf(options),
1704
+ grouped: false,
1705
+ surface
1706
+ };
1707
+ const { pid, folder, chain } = ancestry;
1708
+ const base = {
1709
+ process: processId,
1710
+ pid,
1711
+ folder,
1712
+ surface
1713
+ };
1714
+ const parent = parentOfChain(chain, 0);
1715
+ const home = options.home ?? homedir();
1716
+ const shared = !initIsParent && folder !== null && sharedFolder(folder, home, options.platform ?? process.platform);
1717
+ if (parent === void 0 || folder === null || shared) return {
1718
+ key: own,
1719
+ ...base,
1720
+ ...parent === void 0 ? {} : { parent },
1721
+ grouped: false
1722
+ };
1723
+ return {
1724
+ key: windowKey(parent, folder, options.connection),
1725
+ ...base,
1726
+ parent,
1727
+ grouped: true
1728
+ };
1729
+ }
1730
+ /**
1731
+ * Records one line in `core`'s activity log. Never throws, and never fails the call it describes: a line that cannot
1732
+ * be written is reported by the log itself (`ActivityLog.append`). Recording off writes nothing.
1733
+ *
1734
+ * For a line a channel builds itself — the event service's `told` lines (S1), say. Around an operation, use
1735
+ * `withActivity`, which times it and records how it ended.
1736
+ */
1737
+ async function recordActivity(core, line) {
1738
+ try {
1739
+ await core.activity.append(line);
1740
+ } catch {}
1741
+ }
1742
+ /** The refusals a call can end in — a policy, an approval, a rule, a cap or a usage mistake — rather than a failure. */
1743
+ const REFUSED_CODES = /* @__PURE__ */ new Set([
1744
+ "APPROVAL_REQUIRED",
1745
+ "APPROVAL_PENDING",
1746
+ "APPROVAL_EXPIRED",
1747
+ "APPROVAL_VOID",
1748
+ "SEND_REFUSED",
1749
+ "POLICY_NEVER",
1750
+ "ACT_NOT_ALLOWED",
1751
+ "STANDING_REFUSED",
1752
+ "STANDING_LIMIT",
1753
+ "RATE_CAPPED",
1754
+ "LOOSENING_REFUSED",
1755
+ "USAGE",
1756
+ "WHATSAPP_VISIBILITY_SEAM_REQUIRED"
1757
+ ]);
1758
+ function failureDetail(error) {
1759
+ const code = error instanceof CommsError ? error.code : "UNEXPECTED";
1760
+ return {
1761
+ outcome: REFUSED_CODES.has(code) ? "refused" : "failed",
1762
+ reason: `${code}: ${messageOf$2(error)}`
1763
+ };
1764
+ }
1765
+ async function recordCall(context, operation, kind, ms, detail) {
1766
+ try {
1767
+ const clientInfo = typeof context.clientInfo === "function" ? context.clientInfo() : context.clientInfo;
1768
+ const session = context.session ?? await sessionIdentity(context.surface, clientInfo, {
1769
+ log: context.core.activity,
1770
+ connection: typeof context.connection === "function" ? context.connection() : context.connection
1771
+ });
1772
+ const query = detail.query ?? context.query;
1773
+ await recordActivity(context.core, {
1774
+ channel: context.channel,
1775
+ account: detail.account ?? context.account,
1776
+ operation,
1777
+ kind,
1778
+ outcome: detail.outcome ?? "ok",
1779
+ ...detail.reason === void 0 ? {} : { reason: detail.reason },
1780
+ session,
1781
+ ms,
1782
+ ...detail.items === void 0 ? {} : { items: detail.items },
1783
+ ...detail.count === void 0 ? {} : { count: detail.count },
1784
+ ...detail.counts === void 0 ? {} : { counts: detail.counts },
1785
+ ...query === void 0 ? {} : { query },
1786
+ ...detail.recipients === void 0 ? {} : { recipients: detail.recipients },
1787
+ ...detail.approvalId === void 0 ? {} : { approvalId: detail.approvalId },
1788
+ ...detail.told === void 0 ? {} : { told: detail.told }
1789
+ });
1790
+ } catch {}
1791
+ }
1792
+ /**
1793
+ * Runs one operation and records its line (§8): times `fn`, and when it returns records `describe(result)` as `ok`
1794
+ * (or the outcome `describe` names); when it throws, records `refused` for a policy's, an approval's or a rule's
1795
+ * refusal and `failed` for anything else, with the error's code and message as the reason, and throws the same error
1796
+ * on. What it returns, or throws, is exactly what `fn` did: recording never changes or fails the call, and a
1797
+ * `describe` that throws records the line without its details.
1798
+ *
1799
+ * `operationId` is the capability id in `capabilities.json` (`gmail.search`); `kind` is `read`, `write`, `told` or
1800
+ * `approval`.
1801
+ *
1802
+ * @example
1803
+ * const found = await withActivity(
1804
+ * { core, channel: 'gmail', surface: 'mcp', account: { id: inbox.id, alias }, clientInfo: () => server.getClientVersion() },
1805
+ * 'gmail.search',
1806
+ * 'read',
1807
+ * () => search(core, request),
1808
+ * (result) => ({ query: request.query, count: result.total, items: result.messages.map((m) => ({ id: m.id, subject: m.subject, from: m.from })) }),
1809
+ * );
1810
+ */
1811
+ async function withActivity(context, operationId, kind, fn, describe) {
1812
+ const started = performance.now();
1813
+ let result;
1814
+ try {
1815
+ result = await fn();
1816
+ } catch (error) {
1817
+ await recordCall(context, operationId, kind, performance.now() - started, failureDetail(error));
1818
+ throw error;
1819
+ }
1820
+ const ms = performance.now() - started;
1821
+ let detail = {};
1822
+ try {
1823
+ detail = describe(result) ?? {};
1824
+ } catch {
1825
+ detail = {};
1826
+ }
1827
+ await recordCall(context, operationId, kind, ms, detail);
1828
+ return result;
1829
+ }
1830
+ function sha256Hex(data) {
1831
+ return createHash("sha256").update(data).digest("hex");
1832
+ }
1833
+ /** Lower-cases the address part of `Name <addr>` or a bare address; display names are dropped. */
1834
+ function normaliseAddress(value) {
1835
+ return (/<([^<>]+)>\s*$/.exec(value)?.[1] ?? value).trim().toLowerCase();
1836
+ }
1837
+ function collapseWhitespace(text) {
1838
+ return text.replace(/\s+/g, " ").trim();
1839
+ }
1840
+ /** Deterministic JSON: keys sorted at every level, undefined members dropped. */
1841
+ function canonicalJson$1(value) {
1842
+ if (Array.isArray(value)) return `[${value.map(canonicalJson$1).join(",")}]`;
1843
+ if (value && typeof value === "object") return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson$1(v)}`).join(",")}}`;
1844
+ return JSON.stringify(value);
1845
+ }
538
1846
  /**
539
1847
  * Opening `wx` failed because someone else holds the path — retry, rather than failing the command.
540
1848
  *
@@ -3681,7 +4989,7 @@ async function launcherCanStart(runtimeDir, name) {
3681
4989
  const cli = launcherCli(runtimeDir, name);
3682
4990
  try {
3683
4991
  if (!(await stat(cli)).isFile()) return false;
3684
- await access(cli, constants$1.R_OK);
4992
+ await access(cli, constants.R_OK);
3685
4993
  return true;
3686
4994
  } catch {
3687
4995
  return false;
@@ -3692,7 +5000,7 @@ function launcherCanStartSync(runtimeDir, name) {
3692
5000
  const cli = launcherCli(runtimeDir, name);
3693
5001
  try {
3694
5002
  if (!statSync(cli).isFile()) return false;
3695
- accessSync(cli, constants$1.R_OK);
5003
+ accessSync(cli, constants.R_OK);
3696
5004
  return true;
3697
5005
  } catch {
3698
5006
  return false;
@@ -3981,7 +5289,7 @@ function nodeProgram(env, platform = process.platform) {
3981
5289
  const candidate = join(directory, name);
3982
5290
  try {
3983
5291
  if (!statSync(candidate).isFile()) continue;
3984
- accessSync(candidate, constants$1.X_OK);
5292
+ accessSync(candidate, constants.X_OK);
3985
5293
  if (!sameRealPath(realpathSync(candidate), real, platform)) continue;
3986
5294
  return sameRealPath(candidate, own, platform) ? own : candidate;
3987
5295
  } catch {}
@@ -4237,8 +5545,8 @@ function managedShimStarting(context, binary, entry) {
4237
5545
  if (recorded === void 0 || recorded.path !== path) return null;
4238
5546
  if (!lstatSync(path).isFile() || sha256(readFileSync(path, "utf8")) !== recorded.digest) return null;
4239
5547
  if (realpathSync(recorded.entry) !== realpathSync(entry)) return null;
4240
- accessSync(recorded.node, constants$1.X_OK);
4241
- accessSync(path, constants$1.X_OK);
5548
+ accessSync(recorded.node, constants.X_OK);
5549
+ accessSync(path, constants.X_OK);
4242
5550
  return publicPath;
4243
5551
  } catch {
4244
5552
  return null;
@@ -4542,7 +5850,7 @@ async function executableIn(name, directories, env) {
4542
5850
  for (const directory of paths) for (const extension of extensions) {
4543
5851
  const candidate = join(directory, name + extension.toLowerCase());
4544
5852
  try {
4545
- await access(candidate, constants.X_OK);
5853
+ await access(candidate, constants$1.X_OK);
4546
5854
  return candidate;
4547
5855
  } catch {}
4548
5856
  }
@@ -4745,7 +6053,7 @@ async function installManagedRuntime(context, product, version) {
4745
6053
  async function findNpmCli() {
4746
6054
  const candidates = [join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"), join(dirname(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")];
4747
6055
  for (const candidate of candidates) try {
4748
- await access(candidate, constants.R_OK);
6056
+ await access(candidate, constants$1.R_OK);
4749
6057
  return resolve(candidate);
4750
6058
  } catch {}
4751
6059
  throw new CommsError("CONFIG", "npm could not be found next to this Node installation", { hint: "Install with `--launcher npx` instead, which runs the published package directly." });
@@ -4879,7 +6187,7 @@ async function localCliEntry(moduleUrl) {
4879
6187
  join(here, "..", "cli.mjs")
4880
6188
  ];
4881
6189
  for (const candidate of candidates) try {
4882
- await access(candidate, constants.R_OK);
6190
+ await access(candidate, constants$1.R_OK);
4883
6191
  return resolve(candidate);
4884
6192
  } catch {}
4885
6193
  throw new CommsError("UNEXPECTED", "cannot find this package's own command to register");
@@ -7127,14 +8435,14 @@ async function resolveInsideRoot(root, target) {
7127
8435
  async function createUniqueFile(directory, filename) {
7128
8436
  const extension = extname(filename);
7129
8437
  const stem = extension ? filename.slice(0, -extension.length) : filename;
7130
- const noFollow = process.platform === "win32" ? 0 : constants$1.O_NOFOLLOW;
8438
+ const noFollow = process.platform === "win32" ? 0 : constants.O_NOFOLLOW;
7131
8439
  for (let attempt = 1; attempt < 1e3; attempt += 1) {
7132
8440
  const name = attempt === 1 ? filename : `${stem}-${attempt}${extension}`;
7133
8441
  const path = join(directory, name);
7134
8442
  try {
7135
8443
  return {
7136
8444
  path,
7137
- handle: await open(path, constants$1.O_WRONLY | constants$1.O_CREAT | constants$1.O_EXCL | noFollow, 384)
8445
+ handle: await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollow, 384)
7138
8446
  };
7139
8447
  } catch (error) {
7140
8448
  if (error.code === "EEXIST") continue;
@@ -7477,7 +8785,7 @@ const configV1Schema = looseObject({
7477
8785
  version: literal$1(1),
7478
8786
  ...v1BodyShape
7479
8787
  }).superRefine(checkV1Body);
7480
- const servesSchema = union([literal$1("any"), looseObject({ domains: array(string().min(1)).min(1) })]);
8788
+ const servesSchema = union$1([literal$1("any"), looseObject({ domains: array(string().min(1)).min(1) })]);
7481
8789
  const generationSchema = looseObject({
7482
8790
  name: aliasSchema,
7483
8791
  clientId: string().min(1),
@@ -7633,7 +8941,7 @@ const configV3Naming2Schema = looseObject({
7633
8941
  ...v2BodyShape,
7634
8942
  ...v3Shape
7635
8943
  }).superRefine(checkV2Body);
7636
- union([configV3Naming1Schema, configV3Naming2Schema]);
8944
+ union$1([configV3Naming1Schema, configV3Naming2Schema]);
7637
8945
  function schemaFor(version, naming = 2) {
7638
8946
  if (version === 3) return naming === 1 ? configV3Naming1Schema : configV3Naming2Schema;
7639
8947
  return version === 2 ? configV2Schema : configV1Schema;
@@ -8440,6 +9748,10 @@ function classifyChange(before, after) {
8440
9748
  const levels = behaviourLevels(behaviour.deltas);
8441
9749
  loosen("behaviour.rules", levels.before, levels.after);
8442
9750
  }
9751
+ const activityBefore = readActivitySettings(before);
9752
+ const activityAfter = readActivitySettings(after);
9753
+ if (activityBefore.record === "off" && activityAfter.record === "on") loosen("activity.record", activityBefore.record, activityAfter.record);
9754
+ if (activityAfter.retentionDays > activityBefore.retentionDays) loosen("activity.retentionDays", activityBefore.retentionDays, activityAfter.retentionDays);
8443
9755
  return {
8444
9756
  loosened: changes.map((change) => change.path),
8445
9757
  changes
@@ -8523,7 +9835,7 @@ const ACCOUNT_SETTINGS = [
8523
9835
  ];
8524
9836
  /**
8525
9837
  * Every setting that differs between `before` and `after`, loosened or tightened: the defaults, the secret store,
8526
- * the agent label's settings, the behaviour rules, and each mailbox's and account's own settings — including those of
9838
+ * the agent label's settings, the behaviour rules, the activity log's, and each mailbox's and account's own settings — including those of
8527
9839
  * one added or removed.
8528
9840
  *
8529
9841
  * What a change approval binds, beside its loosenings. A preview shows the whole change — "sends approved by never;
@@ -8566,6 +9878,8 @@ function changedSettings(before, after) {
8566
9878
  const labelAfter = storedAgentLabel(after);
8567
9879
  for (const key of LABEL_SETTINGS) record(`agentLabel.${key}`, labelBefore[key], labelAfter[key]);
8568
9880
  record("behaviour.rules", storedBehaviourRules(before), storedBehaviourRules(after));
9881
+ record("activity.record", storedActivity(before)?.record, storedActivity(after)?.record);
9882
+ record("activity.retentionDays", storedActivity(before)?.retentionDays, storedActivity(after)?.retentionDays);
8569
9883
  return changes;
8570
9884
  }
8571
9885
  /** The behaviour rules as the file holds them — raw, ids and all, as `changedSettings` records every setting. */
@@ -12996,7 +14310,7 @@ function folding(platform) {
12996
14310
  return platform === "win32" ? (text) => text.toLowerCase() : (text) => text;
12997
14311
  }
12998
14312
  /** The package version, read from package.json at build time. */
12999
- const VERSION = "0.27.1";
14313
+ const VERSION = "0.29.0";
13000
14314
  /**
13001
14315
  * The command a person is told to run for one of this suite's own CLIs (design 2026-10-04, D1 and D5; CUE-403).
13002
14316
  *
@@ -13373,7 +14687,7 @@ function stableLauncher(dataDir, packageName, version, platform) {
13373
14687
  const name = launcherName(packageName);
13374
14688
  const { launcher } = launcherPaths(dataDir, name);
13375
14689
  if (!statSync(launcher).isFile()) return null;
13376
- accessSync(launcher, platform === "win32" ? constants$1.R_OK : constants$1.X_OK);
14690
+ accessSync(launcher, platform === "win32" ? constants.R_OK : constants.X_OK);
13377
14691
  const pointer = readPointerSync(dataDir, name);
13378
14692
  if (pointer === void 0 || !launcherCanStartSync(pointer.current, name)) return null;
13379
14693
  const started = readPackageManifest(join(pointer.current, "node_modules", ...packageName.split("/")));
@@ -13546,7 +14860,7 @@ function checkedEntry(root, entry) {
13546
14860
  ok: false,
13547
14861
  why: `${entry} is not a file`
13548
14862
  };
13549
- accessSync(real, constants$1.R_OK);
14863
+ accessSync(real, constants.R_OK);
13550
14864
  } catch {
13551
14865
  return {
13552
14866
  ok: false,
@@ -14319,7 +15633,9 @@ const SETTING_LABELS = {
14319
15633
  "agentLabel.rules": "who each kind of message appears to come from",
14320
15634
  "agentLabel.slackIcons": "the icon of the agent’s Slack posts",
14321
15635
  "agentLabel.slackStyles": "how the agent’s Slack messages look",
14322
- "behaviour.rules": "what an agent may do"
15636
+ "behaviour.rules": "what an agent may do",
15637
+ "activity.record": "recording what agent sessions read and send",
15638
+ "activity.retentionDays": "how many days the activity log keeps"
14323
15639
  };
14324
15640
  /**
14325
15641
  * What the loosening means for the person, in their terms.
@@ -14351,6 +15667,8 @@ function meaning(field, after, scope) {
14351
15667
  case "agentLabel.slackIcons": return "the app’s Slack posts will show this icon beside your name";
14352
15668
  case "agentLabel.slackStyles": return "every agent message there goes out as you, labelled in its words, even where the app could post: it trades Slack’s own APP tag, which no one can fake, for words in the message";
14353
15669
  case "behaviour.rules": return "where it allows more, an agent may start those acts; each one still waits for your yes";
15670
+ case "activity.record": return "every call an agent session makes to your accounts will be recorded on this machine again: the subjects and senders it read, what it searched for, what it sent and to whom — never a message body";
15671
+ case "activity.retentionDays": return "what agent sessions read and sent — subjects, senders, searches, recipients — will be kept on this machine for longer";
14354
15672
  default: return "";
14355
15673
  }
14356
15674
  }
@@ -14843,7 +16161,7 @@ var FileSecretStore = class {
14843
16161
  }
14844
16162
  async get(ref) {
14845
16163
  try {
14846
- const handle = await open(this.#path(ref), constants$1.O_RDONLY | constants$1.O_NOFOLLOW);
16164
+ const handle = await open(this.#path(ref), constants.O_RDONLY | constants.O_NOFOLLOW);
14847
16165
  let raw;
14848
16166
  try {
14849
16167
  raw = await handle.readFile("utf8");
@@ -15284,7 +16602,7 @@ strictObject({
15284
16602
  clientId: string().regex(GOOGLE_CLIENT_ID_PATTERN, "a Google client id is digits, a hyphen, then .apps.googleusercontent.com"),
15285
16603
  clientSecret: string().regex(/^[\x20-\x7e]{1,256}$/, "the client secret is 1–256 printable ASCII characters"),
15286
16604
  projectId: string().regex(/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/, "a Google Cloud project id is 6–30 lowercase letters, digits or hyphens").optional(),
15287
- serves: union([literal$1("any"), strictObject({ domains: array(string().regex(DOMAIN_PATTERN, "each domain is a lower-case host name")).min(1).max(50) })])
16605
+ serves: union$1([literal$1("any"), strictObject({ domains: array(string().regex(DOMAIN_PATTERN, "each domain is a lower-case host name")).min(1).max(50) })])
15288
16606
  }).optional(),
15289
16607
  slack: strictObject({
15290
16608
  workspace: string().regex(SLACK_WORKSPACE_ID_PATTERN, "a Slack workspace id is T followed by capital letters or digits"),
@@ -15325,7 +16643,7 @@ function shownPath(path) {
15325
16643
  async function readBounded(path, what) {
15326
16644
  let handle;
15327
16645
  try {
15328
- handle = await open(path, constants$1.O_RDONLY | (constants$1.O_NONBLOCK ?? 0));
16646
+ handle = await open(path, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
15329
16647
  } catch (error) {
15330
16648
  return what.unopened(error);
15331
16649
  }
@@ -16050,6 +17368,10 @@ function openCore(options = {}) {
16050
17368
  plans: new PlanStore(paths.stateDir, now),
16051
17369
  taint: new TaintStore(paths.stateDir, now),
16052
17370
  audit,
17371
+ activity: new ActivityLog(paths.stateDir, {
17372
+ now,
17373
+ settings: async () => readActivitySettings(await config.load())
17374
+ }),
16053
17375
  handoffs,
16054
17376
  async secrets(kind) {
16055
17377
  const chosen = kind ?? secretsStoreOf(await config.load());
@@ -16741,7 +18063,7 @@ async function corePackageRoot() {
16741
18063
  }
16742
18064
  throw new CommsError("UNEXPECTED", "cannot find the @agentcomms/core package this is running from");
16743
18065
  }
16744
- const exists = (path) => access(path, constants.R_OK).then(() => true, () => false);
18066
+ const exists = (path) => access(path, constants$1.R_OK).then(() => true, () => false);
16745
18067
  /**
16746
18068
  * A module URL inside a channel's package, for the `local` launcher, which registers a checkout's own code.
16747
18069
  *
@@ -17191,7 +18513,7 @@ function serverPruneChange(core, env, request, own) {
17191
18513
  };
17192
18514
  const { packageName, version } = ownProduct(request.channel, own) ?? {
17193
18515
  packageName: serverFacts(request.channel).packageName,
17194
- version: "0.27.1"
18516
+ version: "0.29.0"
17195
18517
  };
17196
18518
  const product = {
17197
18519
  packageName,
@@ -19339,7 +20661,7 @@ function terminalUpdateHooks(core, env, options = {}) {
19339
20661
  } };
19340
20662
  }
19341
20663
  //#endregion
19342
- //#region ../core/dist/tool-arguments-CxbRW3Ep.mjs
20664
+ //#region ../core/dist/tool-arguments-1XgEf3VM.mjs
19343
20665
  const APPLIED = Symbol.for("agentcomms.strictToolArguments");
19344
20666
  const TARGET = "draft-2020-12";
19345
20667
  /**
@@ -31782,6 +33104,11 @@ var ResendContext = class {
31782
33104
  platform;
31783
33105
  surface;
31784
33106
  /**
33107
+ * What the MCP client said it is (`clientInfo`), for the activity log's session: set by the server once it exists,
33108
+ * read when a line is written, since a client says it only on connecting. Never set for the CLI.
33109
+ */
33110
+ clientInfo = void 0;
33111
+ /**
31785
33112
  * What every result, refusal and hint names for a person to run: Resend's own CLI as this installation runs it, or
31786
33113
  * core's through its installed dependency (`own`, `core`), quoted for `platform` — or why there is none here.
31787
33114
  */
@@ -32973,6 +34300,98 @@ async function runDoctor(context, options = {}) {
32973
34300
  };
32974
34301
  }
32975
34302
  //#endregion
34303
+ //#region src/activity.ts
34304
+ /**
34305
+ * Resend's half of the activity log (design 2026-10-25-activity §4.2, §8 Task 2a): every operation a capability row
34306
+ * names that reads or writes a Resend account records one line, through core's `withActivity`, from inside the
34307
+ * operation — so the command and the tool that run it record the same line.
34308
+ *
34309
+ * - **One line per call.** An operation another recorded operation calls on its way records nothing of its own; the
34310
+ * outer call is known by the async context it runs in, so two calls at once in one server are two lines.
34311
+ * - **What it touched, never what it said.** Ids, counts, subjects, senders' names and addresses, file names and
34312
+ * sizes, a send's recipients and its approval — never a body, a preview or bytes. A field a result carries inside the
34313
+ * untrusted-content envelope is taken out of it for the line (`plain`); the log wraps it again wherever it is shown.
34314
+ * - **Never in the way.** Working out the account never throws, and `withActivity` never changes what the call returns
34315
+ * or throws.
34316
+ */
34317
+ /** Set for the length of a recorded call: an operation it calls on its way records nothing of its own. */
34318
+ const recording = new AsyncLocalStorage();
34319
+ /** The account a line is recorded against, as known before the call: its id and its name, or the name alone. */
34320
+ async function accountOf(context, name) {
34321
+ try {
34322
+ return {
34323
+ id: (await context.accounts.find(name))?.account.id ?? "",
34324
+ alias: name
34325
+ };
34326
+ } catch {
34327
+ return {
34328
+ id: "",
34329
+ alias: name
34330
+ };
34331
+ }
34332
+ }
34333
+ /**
34334
+ * Runs one Resend operation and records its line: `operation` is the capability id (`resend.emails.list`), `kind`
34335
+ * what it did, `account` the account's name as the call gives it. Inside another recorded call it only runs `fn`.
34336
+ */
34337
+ async function recorded(context, operation, kind, account, fn, describe) {
34338
+ if (recording.getStore() === true) return fn();
34339
+ const named = await accountOf(context, account);
34340
+ return recording.run(true, () => withActivity({
34341
+ core: context.core,
34342
+ channel: "resend",
34343
+ surface: context.surface,
34344
+ account: named,
34345
+ clientInfo: () => context.clientInfo?.()
34346
+ }, operation, kind, fn, describe));
34347
+ }
34348
+ /** What a read answers when its key cannot read: a refusal that comes back as a result, recorded as one. */
34349
+ function unavailable(result) {
34350
+ return {
34351
+ outcome: "refused",
34352
+ reason: result.reason
34353
+ };
34354
+ }
34355
+ const ENVELOPE = new RegExp(`^<${UNTRUSTED_TAG} boundary="([A-Za-z0-9_-]+)"[^>\\n]*>\\n([\\s\\S]*)\\n</${UNTRUSTED_TAG} boundary="\\1">$`);
34356
+ /**
34357
+ * A field as its sender wrote it: the text inside its untrusted-content envelope when it is wrapped, the field itself
34358
+ * when it is bare (a plain address), and undefined for nothing at all.
34359
+ */
34360
+ function plain(field) {
34361
+ if (typeof field !== "string" || field === "") return void 0;
34362
+ return ENVELOPE.exec(field)?.[2] ?? field;
34363
+ }
34364
+ /** A sender from a `Name <address>` field, bare or wrapped: the name only when there is one. */
34365
+ function senderOfField(field) {
34366
+ const text = plain(field);
34367
+ if (text === void 0) return void 0;
34368
+ const parsed = parseAddressList(text)[0];
34369
+ if (parsed === void 0) return { address: text };
34370
+ return {
34371
+ ...parsed.name ? { name: parsed.name } : {},
34372
+ address: parsed.address
34373
+ };
34374
+ }
34375
+ /** A sender from its two fields, each bare or wrapped. Undefined when there is neither. */
34376
+ function senderOf(from) {
34377
+ const name = plain(from?.name ?? void 0);
34378
+ const address = plain(from?.address ?? void 0);
34379
+ if (name === void 0 && address === void 0) return void 0;
34380
+ return {
34381
+ ...name === void 0 ? {} : { name },
34382
+ ...address === void 0 ? {} : { address }
34383
+ };
34384
+ }
34385
+ /** One email touched: its id, and its subject and sender when it has them. */
34386
+ function itemOf(id, subject, from) {
34387
+ const text = plain(subject ?? void 0);
34388
+ return {
34389
+ id,
34390
+ ...text === void 0 ? {} : { subject: text },
34391
+ ...from === void 0 ? {} : { from }
34392
+ };
34393
+ }
34394
+ //#endregion
32976
34395
  //#region src/compose/inbound.ts
32977
34396
  /**
32978
34397
  * Turning what arrived — or what the team sent — into what an agent may read.
@@ -33360,8 +34779,11 @@ function domainRow(entry) {
33360
34779
  createdAt: text(entry.created_at)
33361
34780
  };
33362
34781
  }
33363
- /** The team's domains; with `domain`, that one with the DNS records it needs. */
34782
+ /** The team's domains; with `domain`, that one with the DNS records it needs. Recorded by how many there are. */
33364
34783
  async function listDomains(context, name, options = {}) {
34784
+ return recorded(context, "resend.domains", "read", name, () => domainsOf(context, name, options), (result) => result.available ? { count: result.domains.length } : unavailable(result));
34785
+ }
34786
+ async function domainsOf(context, name, options) {
33365
34787
  return whenReadable(context, name, async (named, transport) => {
33366
34788
  const domains = ((await resendRequest(transport, "GET", "/domains")).data ?? []).map(domainRow);
33367
34789
  if (options.domain === void 0) return { domains };
@@ -33417,7 +34839,18 @@ function pageQuery(paging, fallback) {
33417
34839
  after: cursorOf(paging.after)
33418
34840
  };
33419
34841
  }
34842
+ /** A sent email as its activity line keeps it: its id, its subject and who it was from. */
34843
+ function sentItem(row) {
34844
+ return itemOf(row.id, row.subject, senderOfField(row.from));
34845
+ }
34846
+ /** The emails the team sent, recorded by each one's id, subject and sender. */
33420
34847
  async function listSentEmails(context, name, paging = {}) {
34848
+ return recorded(context, "resend.emails.list", "read", name, () => sentEmailsOf(context, name, paging), (result) => result.available ? {
34849
+ count: result.emails.length,
34850
+ items: result.emails.map(sentItem)
34851
+ } : unavailable(result));
34852
+ }
34853
+ async function sentEmailsOf(context, name, paging) {
33421
34854
  const query = pageQuery(paging, 20);
33422
34855
  return whenReadable(context, name, async (_named, transport) => {
33423
34856
  const page = await resendRequest(transport, "GET", "/emails", { query });
@@ -33435,7 +34868,11 @@ async function listSentEmails(context, name, paging = {}) {
33435
34868
  };
33436
34869
  });
33437
34870
  }
34871
+ /** One sent email, recorded by its id, subject and sender. */
33438
34872
  async function showSentEmail(context, name, id) {
34873
+ return recorded(context, "resend.emails.show", "read", name, () => sentEmailOf(context, name, id), (result) => result.available ? { items: [sentItem(result.email)] } : unavailable(result));
34874
+ }
34875
+ async function sentEmailOf(context, name, id) {
33439
34876
  const emailId = resendId(id, "email id");
33440
34877
  return whenReadable(context, name, async (_named, transport) => {
33441
34878
  const entry = await resendRequest(transport, "GET", `/emails/${emailId}`);
@@ -33516,7 +34953,14 @@ async function recordTaint(context, named, transport, collector, entries) {
33516
34953
  internalDomains
33517
34954
  });
33518
34955
  }
34956
+ /** The emails the team received, recorded by each one's id, subject and sender. */
33519
34957
  async function listReceived(context, name, paging = {}) {
34958
+ return recorded(context, "resend.received.list", "read", name, () => receivedOf(context, name, paging), (result) => result.available ? {
34959
+ count: result.emails.length,
34960
+ items: result.emails.map((email) => itemOf(email.id, email.subject, senderOf(email.from)))
34961
+ } : unavailable(result));
34962
+ }
34963
+ async function receivedOf(context, name, paging) {
33520
34964
  const query = pageQuery(paging, 20);
33521
34965
  return whenReadable(context, name, async (named, transport) => {
33522
34966
  const page = await resendRequest(transport, "GET", "/emails/receiving", { query });
@@ -33552,7 +34996,16 @@ async function listReceived(context, name, paging = {}) {
33552
34996
  };
33553
34997
  });
33554
34998
  }
34999
+ /** One received email, recorded by its id, subject and sender. */
33555
35000
  async function showReceived(context, name, id) {
35001
+ return recorded(context, "resend.received.show", "read", name, () => receivedEmailOf(context, name, id), (result) => {
35002
+ if (!result.available) return unavailable(result);
35003
+ const email = result.email;
35004
+ const subject = typeof email.subject === "string" ? email.subject : void 0;
35005
+ return { items: [itemOf(String(email.id ?? ""), subject, senderOf(email.from))] };
35006
+ });
35007
+ }
35008
+ async function receivedEmailOf(context, name, id) {
33556
35009
  const emailId = resendId(id, "email id");
33557
35010
  return whenReadable(context, name, async (named, transport) => {
33558
35011
  const entry = await resendRequest(transport, "GET", `/emails/receiving/${emailId}`);
@@ -33661,7 +35114,25 @@ async function downloadsRoot(context) {
33661
35114
  * path is the sender's: see {@link storedName}. The names they gave, and the types they declared, come back as their
33662
35115
  * own fields, wrapped where they are text; their addresses are recorded as tainted before the result is returned.
33663
35116
  */
35117
+ /** Saves a received email's attachments, recorded by each file's id, the name its sender gave it, and its size. */
33664
35118
  async function downloadReceived(context, name, id, options = {}) {
35119
+ return recorded(context, "resend.received.download", "read", name, () => downloadReceivedOf(context, name, id, options), (result) => {
35120
+ if (!result.available) return unavailable(result);
35121
+ const items = result.files.map((file) => {
35122
+ const filename = plain(file.filename);
35123
+ return {
35124
+ id: file.attachmentId,
35125
+ ...filename === void 0 ? {} : { name: filename },
35126
+ size: file.size
35127
+ };
35128
+ });
35129
+ return {
35130
+ items,
35131
+ counts: { saved: items.length }
35132
+ };
35133
+ });
35134
+ }
35135
+ async function downloadReceivedOf(context, name, id, options) {
33665
35136
  const emailId = resendId(id, "email id");
33666
35137
  const only = options.attachmentId === void 0 ? void 0 : resendId(options.attachmentId, "attachment id");
33667
35138
  const subpath = relativeSubpath(options.out);
@@ -33733,7 +35204,11 @@ function dateOf(raw, name) {
33733
35204
  if (typeof raw !== "string" || !DATE.test(raw.trim())) throw new CommsError("USAGE", `${name} must be a date like 2026-09-01, or an ISO 8601 time with a zone`);
33734
35205
  return raw.trim();
33735
35206
  }
35207
+ /** The team's sending totals, recorded only as asked: they name no email. */
33736
35208
  async function getMetrics(context, name, range = {}) {
35209
+ return recorded(context, "resend.metrics", "read", name, () => metricsOf(context, name, range), (result) => result.available ? {} : unavailable(result));
35210
+ }
35211
+ async function metricsOf(context, name, range) {
33737
35212
  const start = dateOf(range.start, "start");
33738
35213
  const end = dateOf(range.end, "end");
33739
35214
  return whenReadable(context, name, async (_named, transport) => {
@@ -33756,7 +35231,11 @@ const ORIGINS = [
33756
35231
  "complaint",
33757
35232
  "manual"
33758
35233
  ];
35234
+ /** The addresses Resend will not send to, recorded by how many there are — never the addresses. */
33759
35235
  async function listSuppressions(context, name, options = {}) {
35236
+ return recorded(context, "resend.suppressions", "read", name, () => suppressionsOf(context, name, options), (result) => result.available ? { count: result.suppressions.length } : unavailable(result));
35237
+ }
35238
+ async function suppressionsOf(context, name, options) {
33760
35239
  const query = pageQuery(options, 20);
33761
35240
  if (options.origin !== void 0 && !ORIGINS.includes(String(options.origin))) throw new CommsError("USAGE", `"${String(options.origin)}" is not an origin: use bounce, complaint or manual`);
33762
35241
  return whenReadable(context, name, async (_named, transport) => {
@@ -33790,7 +35269,14 @@ async function listSuppressions(context, name, options = {}) {
33790
35269
  });
33791
35270
  }
33792
35271
  const SCHEDULED_PAGES = 3;
35272
+ /** The emails waiting to go, recorded by each one's id, subject and sender. */
33793
35273
  async function listScheduled(context, name) {
35274
+ return recorded(context, "resend.scheduled.list", "read", name, () => scheduledOf(context, name), (result) => result.available ? {
35275
+ count: result.scheduled.length,
35276
+ items: result.scheduled.map(sentItem)
35277
+ } : unavailable(result));
35278
+ }
35279
+ async function scheduledOf(context, name) {
33794
35280
  return whenReadable(context, name, async (named, transport) => {
33795
35281
  const records = new SendRecords(context.core.paths.stateDir, context.now);
33796
35282
  const envelope = {
@@ -33831,7 +35317,31 @@ async function listScheduled(context, name) {
33831
35317
  function messageOf$1(error) {
33832
35318
  return error instanceof Error ? error.message : String(error);
33833
35319
  }
35320
+ /**
35321
+ * Cancelling one scheduled email, as a change the shared change flow plans and applies. Recorded in the activity log
35322
+ * as the write it is — the email's id, and how many recipients it had — when it is applied, or when the look before it
35323
+ * refuses; a call that only prepares the change's approval writes nothing to the account, and the approval store keeps
35324
+ * it.
35325
+ */
33834
35326
  function cancelScheduledChange(context, name, id) {
35327
+ const change = cancelChange(context, name, id);
35328
+ return {
35329
+ ...change,
35330
+ plan: async (config) => {
35331
+ try {
35332
+ return await change.plan(config);
35333
+ } catch (error) {
35334
+ await recorded(context, "resend.scheduled.cancel", "write", name, () => Promise.reject(error), () => ({})).catch(() => void 0);
35335
+ throw error;
35336
+ }
35337
+ },
35338
+ apply: (consent, request) => recorded(context, "resend.scheduled.cancel", "write", name, () => change.apply(consent, request), (cancelled) => ({
35339
+ items: [{ id: cancelled.id }],
35340
+ counts: { recipients: cancelled.recipients }
35341
+ }))
35342
+ };
35343
+ }
35344
+ function cancelChange(context, name, id) {
33835
35345
  const emailId = resendId(id, "email id");
33836
35346
  const look = async () => {
33837
35347
  const named = await context.accounts.require(name);
@@ -34326,6 +35836,21 @@ async function attachPolicy(context) {
34326
35836
  * this exact content. Nothing is sent; preparing twice is free.
34327
35837
  */
34328
35838
  async function prepareSend(context, name, input) {
35839
+ return recorded(context, "resend.send.prepare", "approval", name, () => prepareOneSend(context, name, input), (prepared) => ({
35840
+ approvalId: prepared.approvalId,
35841
+ items: [itemOf(prepared.approvalId, prepared.expect.subject)],
35842
+ recipients: activityRecipients(prepared.expect)
35843
+ }));
35844
+ }
35845
+ /** A send's recipients as its activity line keeps them: To, Cc and Bcc, as addresses. */
35846
+ function activityRecipients(expect) {
35847
+ return [
35848
+ ...expect.to,
35849
+ ...expect.cc,
35850
+ ...expect.bcc
35851
+ ].flatMap((address) => plain(address) ?? []);
35852
+ }
35853
+ async function prepareOneSend(context, name, input) {
34329
35854
  const { legacyDrain } = await ensureSendEpochConfig(context.core, { now: context.now });
34330
35855
  const named = await context.accounts.require(name);
34331
35856
  requireSendMode(named, context.handoffs);
@@ -34583,6 +36108,13 @@ function acceptedSaid(withId, scheduledAt) {
34583
36108
  * request, send — never retried, because a retried send may deliver twice.
34584
36109
  */
34585
36110
  async function executeSend(context, name, options) {
36111
+ return recorded(context, "resend.send.execute", "write", name, () => sendApproved(context, name, options), (sent) => ({
36112
+ approvalId: sent.approvalId,
36113
+ items: [itemOf(sent.resendId ?? sent.approvalId, sent.subject)],
36114
+ recipients: activityRecipients(sent)
36115
+ }));
36116
+ }
36117
+ async function sendApproved(context, name, options) {
34586
36118
  const { legacyDrain } = await ensureSendEpochConfig(context.core, { now: context.now });
34587
36119
  const named = await context.accounts.require(name);
34588
36120
  requireSendMode(named, context.handoffs);
@@ -34823,6 +36355,12 @@ function outcomeWords(outcome) {
34823
36355
  * machine's own record of a cancellation Resend confirmed; without it, a cancellation is as Resend reports it.
34824
36356
  */
34825
36357
  async function sendStatus(context, name, approvalId) {
36358
+ return recorded(context, "resend.send.status", "read", name, () => statusOf(context, name, approvalId), (status) => ({
36359
+ approvalId: status.approvalId,
36360
+ ...status.resend === null ? {} : { items: [{ id: status.resend.id }] }
36361
+ }));
36362
+ }
36363
+ async function statusOf(context, name, approvalId) {
34826
36364
  const named = await context.accounts.require(name);
34827
36365
  const local = await new SendRecords(context.core.paths.stateDir, context.now).summary(named.account.id, approvalId);
34828
36366
  const seen = await context.core.approvals.inspect(approvalId, {
@@ -35096,4 +36634,4 @@ async function revokeSendApproval(context, approvalId) {
35096
36634
  });
35097
36635
  }
35098
36636
  //#endregion
35099
- export { colorEnabled as $, removeAccountChange as A, waitCallOptions as At, strictToolArguments as B, VERSION$2 as Bt, inspectKey as C, serverPruneChange as Ct, policyChange as D, truncateDisplay as Dt, policyApprovalRefusal as E, toCommsError as Et, CHANGE_POLICIES as F, RESEND_CALLER as Ft, UPDATE_CHECK_CHILD_COMMAND as G, CommsError as H, MODES as I, REFUSED as It, approveRefusedHint as J, agentMarker as K, SEND_POLICIES as L, RESEND_API_ORIGIN as Lt, showAccount as M, wholeNumber as Mt, resendRequest as N, writeResult as Nt, policyReport as O, updateNoticeAtTerminal as Ot, ResendContext as P, PACKAGE_NAME as Pt, cliHandoffs as Q, checkNewName as R, RESEND_DOWNLOAD_ORIGIN as Rt, addAccountChange as S, serverInstallChange as St, modeOf as T, terminalUpdateHooks as Tt, EXIT_CODES as U, CHANNEL_SERVERS as V, PATH_OPTIONS as W, changeToolResult as X, canPrompt as Y, checkForUpdates as Z, showSentEmail as _, renderInstall as _t, revokeSendApproval as a, gatedChangeAtTerminal as at, readBody as b, runCommand as bt, downloadReceived as c, installExitStatus as ct, listReceived as d, newBoundary as dt, commandPathOf as et, listScheduled as f, openCore as ft, showReceived as g, renderApprovalWait as gt, resendId as h, refuseUnclaimedApproval as ht, prepareSend as i, gatedChange as it, sendPolicyOf as j, waitForApproval as jt, policyWanted as k, updateToolNotice as kt, getMetrics as l, kindOf as lt, listSuppressions as m, pathOverridesFromCliOptions as mt, executeSend as n, escapeForDisplay as nt, sendStatus as o, handoffSentence as ot, listSentEmails as p, paint as pt, approveChangeAtTerminal as q, finishSendApproval as r, exemptFromUpdateNotice as rt, cancelScheduledChange as s, handoffSentenceToFill as st, beginSendApproval as t, describeFenceReport as tt, listDomains as u, neutralise as ut, addressesOf as v, renderPrune as vt, listAccounts as w, stripInvisible as wt, runDoctor as x, runUpdateCheckChild as xt, attachmentRisks as y, resolvePaths as yt, keyPermissionOf as z, ROUTES as zt };
36637
+ export { colorEnabled as $, removeAccountChange as A, waitCallOptions as At, strictToolArguments as B, ROUTES as Bt, inspectKey as C, serverPruneChange as Ct, policyChange as D, truncateDisplay as Dt, policyApprovalRefusal as E, toCommsError as Et, CHANGE_POLICIES as F, PACKAGE_NAME as Ft, UPDATE_CHECK_CHILD_COMMAND as G, CommsError as H, MODES as I, RESEND_CALLER as It, approveRefusedHint as J, agentMarker as K, SEND_POLICIES as L, REFUSED as Lt, showAccount as M, warmSessionIdentity as Mt, resendRequest as N, wholeNumber as Nt, policyReport as O, updateNoticeAtTerminal as Ot, ResendContext as P, writeResult as Pt, cliHandoffs as Q, checkNewName as R, RESEND_API_ORIGIN as Rt, addAccountChange as S, serverInstallChange as St, modeOf as T, terminalUpdateHooks as Tt, EXIT_CODES as U, CHANNEL_SERVERS as V, VERSION$2 as Vt, PATH_OPTIONS as W, changeToolResult as X, canPrompt as Y, checkForUpdates as Z, showSentEmail as _, renderInstall as _t, revokeSendApproval as a, gatedChangeAtTerminal as at, readBody as b, runCommand as bt, downloadReceived as c, installExitStatus as ct, listReceived as d, newBoundary as dt, commandPathOf as et, listScheduled as f, openCore as ft, showReceived as g, renderApprovalWait as gt, resendId as h, refuseUnclaimedApproval as ht, prepareSend as i, gatedChange as it, sendPolicyOf as j, waitForApproval as jt, policyWanted as k, updateToolNotice as kt, getMetrics as l, kindOf as lt, listSuppressions as m, pathOverridesFromCliOptions as mt, executeSend as n, escapeForDisplay as nt, sendStatus as o, handoffSentence as ot, listSentEmails as p, paint as pt, approveChangeAtTerminal as q, finishSendApproval as r, exemptFromUpdateNotice as rt, cancelScheduledChange as s, handoffSentenceToFill as st, beginSendApproval as t, describeFenceReport as tt, listDomains as u, neutralise as ut, addressesOf as v, renderPrune as vt, listAccounts as w, stripInvisible as wt, runDoctor as x, runUpdateCheckChild as xt, attachmentRisks as y, resolvePaths as yt, keyPermissionOf as z, RESEND_DOWNLOAD_ORIGIN as zt };