@volter/supercode-teams 0.3.159 → 0.3.161
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/connector/index.mjs +2 -2
- package/live-sessions.mjs +12 -3
- package/machine-records.mjs +113 -45
- package/machine.mjs +75 -63
- package/mail/carrier.mjs +202 -157
- package/mail/change-stream.mjs +143 -0
- package/mail/delivery-status.mjs +42 -0
- package/mail/hooks.mjs +90 -15
- package/mail/reads.mjs +2 -1
- package/mail/relay-routing.mjs +41 -0
- package/mail/relay.mjs +120 -204
- package/mail/send.mjs +56 -32
- package/mail/sqlite-store.mjs +166 -16
- package/mail/sqlite.mjs +42 -4
- package/package.json +1 -1
- package/record-state.mjs +143 -0
package/machine.mjs
CHANGED
|
@@ -654,7 +654,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
654
654
|
if (!allowed.size) return refuse(RECORDS_NOT_GRANTED);
|
|
655
655
|
if (!this.records) return refuse('this machine keeps no record yet');
|
|
656
656
|
accept();
|
|
657
|
-
const unsubscribe = this.records.producer.subscribe((frame) => {
|
|
657
|
+
const unsubscribe = this.records.producer.subscribe((frame) => { stream.write(frame); }, allowed, stream.params ?? {});
|
|
658
658
|
const readmit = local ? null : setInterval(() => void (async () => {
|
|
659
659
|
const admitted = await entry.verify?.().catch(() => null);
|
|
660
660
|
const now = admitted ? sectionsFor(admitted, false) : new Set();
|
|
@@ -663,6 +663,29 @@ export class TeamsMachine extends MachineCore {
|
|
|
663
663
|
readmit?.unref?.();
|
|
664
664
|
stream.on('close', () => { clearInterval(readmit); unsubscribe(); });
|
|
665
665
|
});
|
|
666
|
+
// Held peer records never leave this OS user's local socket. Apps and the
|
|
667
|
+
// resident board follow committed row changes instead of full fleet copies.
|
|
668
|
+
this.registerDoor('machine-record-changes', (entry, stream, accept, refuse) => {
|
|
669
|
+
if (entry.principal !== 'local' || entry.appDoor) return refuse('held machine records are read only by this machine\'s own processes');
|
|
670
|
+
if (!this.records) return refuse('this daemon keeps no records yet');
|
|
671
|
+
accept();
|
|
672
|
+
const changes = this.mailStore.changes;
|
|
673
|
+
let cursor = stream.params?.after ?? null;
|
|
674
|
+
const stop = changes.follow((page) => {
|
|
675
|
+
if (page.error) return stream.close(page.error);
|
|
676
|
+
if (page.reset) {
|
|
677
|
+
const snapshot = this.#recordSnapshot();
|
|
678
|
+
cursor = changes.cursor();
|
|
679
|
+
stream.write({ type: 'snapshot', cursor, ...snapshot });
|
|
680
|
+
return cursor;
|
|
681
|
+
}
|
|
682
|
+
cursor = page.cursor;
|
|
683
|
+
stream.write({ type: 'changes', ...page });
|
|
684
|
+
}, { after: cursor, topics: ['record-head', 'record-row'], initial: true });
|
|
685
|
+
const beat = setInterval(() => { try { stream.write({ type: 'beat', cursor, at: Date.now() }); } catch { stop(); clearInterval(beat); } }, 15_000);
|
|
686
|
+
beat.unref?.();
|
|
687
|
+
stream.on('close', () => { clearInterval(beat); stop(); });
|
|
688
|
+
});
|
|
666
689
|
// This machine's processes read the team's published events here, from the one stream its connector holds
|
|
667
690
|
// (machine-events.mjs PublishedEvents); a caller it refuses reads the server itself.
|
|
668
691
|
this.registerDoor('events', (entry, stream, accept, refuse) => {
|
|
@@ -826,13 +849,19 @@ export class TeamsMachine extends MachineCore {
|
|
|
826
849
|
runtimeOf: (harness, id) => live.runtimeReceipt(harness, id), runtimeNamed: (receiptId) => live.receiptNamed(receiptId),
|
|
827
850
|
runtimesIn: (harness, cwd) => live.runtimesIn(harness, cwd),
|
|
828
851
|
endpointOf: async () => (await this.relayEndpoint)?.url ?? null,
|
|
852
|
+
inboundSettled: () => this.mailHooks?.drainPoolReplies(),
|
|
829
853
|
arrived: (sessionId, landed) => claudeTranscriptHolds([...live.claudeDirs.keys()].map((dir) => join(dirname(dir), 'projects')), sessionId, landed),
|
|
830
854
|
log: (line) => this.emit('mail-log', line) });
|
|
831
855
|
// a turn or a wake typed into a daemon pane's empty composer, in process (as `panes.turn` types one)
|
|
832
|
-
const typeTurn = async (pane, harness, text, messageIds) => {
|
|
856
|
+
const typeTurn = async (pane, harness, text, messageIds, address) => {
|
|
833
857
|
if (this.stopping) throw new Error('the daemon is stopping; no pane input was submitted');
|
|
834
858
|
const session = await this.#paneByContext(pane);
|
|
835
859
|
if (!session) throw new Error(`no pane with context ${pane}`);
|
|
860
|
+
const expected = parseAddress(address);
|
|
861
|
+
if (!expected || expected.machine !== machine || expected.harness !== harness) return { delivered: false, reason: 'mail recipient binding is missing' };
|
|
862
|
+
const reading = await this.#serveRequest('harness.v1.sessions.activity_under', { pids: [session.rootPid] }, 4_000);
|
|
863
|
+
const native = reading?.activities?.find(row => row.pid === session.rootPid)?.activity;
|
|
864
|
+
if (native?.harness !== expected.harness || native.session_id !== expected.session) return { delivered: false, reason: 'the pane no longer runs the addressed native session; no input submitted' };
|
|
836
865
|
const result = await this.#paneTurn(session, pane, text, { messageIds, harness });
|
|
837
866
|
if (result.refused) throw new Error(result.refused.reason);
|
|
838
867
|
return result.waits ?? result;
|
|
@@ -859,11 +888,13 @@ export class TeamsMachine extends MachineCore {
|
|
|
859
888
|
// minute, and before each relay's inbound call
|
|
860
889
|
this.hookSpool = join(supercodeHome(this.env), 'hook-spool', 'relay-inbound');
|
|
861
890
|
this.mailHooks.drainSpool(this.hookSpool);
|
|
891
|
+
this.mailHooks.drainPoolReplies();
|
|
862
892
|
this.hookSpoolTimer = setInterval(() => this.mailHooks.drainSpool(this.hookSpool), 60_000); this.hookSpoolTimer.unref?.();
|
|
863
893
|
// the mailbox's own delivery (step 4b): each wake attempted by its session's door, or carried to its machine
|
|
864
894
|
this.carrier = new MailCarrier({ machine, mailRoot, store: this.mailStore, live,
|
|
865
895
|
handOver: (address, id) => this.mailSend.handOver(address, id),
|
|
866
|
-
typeTurn,
|
|
896
|
+
typeTurn, typeUserTurns: (address, pane) => this.mailSend.typeUserTurns(address, pane), paneOf: (session) => this.mail.paneOf(session),
|
|
897
|
+
askMachine: (to, request) => this.#askMailDoor(to, request, MAIL_SEND_MS),
|
|
867
898
|
log: (line) => this.emit('mail-log', line) }).start();
|
|
868
899
|
// idle notices and final replies (step 4c): each subscribed session's activity followed through harness serve
|
|
869
900
|
this.idleNotices = new IdleNotices({ machine, store: this.mailStore, live,
|
|
@@ -877,26 +908,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
877
908
|
claudeProjects: () => [...live.claudeDirs.keys()].map((dir) => join(dirname(dir), 'projects')),
|
|
878
909
|
lastMessageAt: (address) => this.sessionDescriptors?.get(address)?.updated_at_ms ?? null,
|
|
879
910
|
program: this.supercodeBin, env: this.env, log: (line) => this.emit('mail-log', line) }).start();
|
|
880
|
-
//
|
|
881
|
-
// its composer is free: tried at each change of its mailbox, and every 2 s while one waits (mail_watch.rs
|
|
882
|
-
// `deliver_waiting_user_turns` read every pane's mailbox every 2 s)
|
|
883
|
-
const turnsWaiting = new Map(); // address → timer
|
|
884
|
-
const typeWaiting = async (address) => {
|
|
885
|
-
clearTimeout(turnsWaiting.get(address)); turnsWaiting.delete(address);
|
|
886
|
-
if (this.stopping) return;
|
|
887
|
-
const session = live.sessions().find((row) => row.address === address);
|
|
888
|
-
const pane = session ? this.mail.paneOf(session) : null;
|
|
889
|
-
if (!pane || !this.mailStore.userTurns(address).length) return;
|
|
890
|
-
try { await this.mailSend.typeUserTurns(address, pane); } catch (error) { this.emit('mail-log', `the user's turns for ${address} wait: ${error.message}`); }
|
|
891
|
-
if (!this.stopping && this.mailStore.userTurns(address).length) { const timer = setTimeout(() => void typeWaiting(address), 2000); timer.unref?.(); turnsWaiting.set(address, timer); }
|
|
892
|
-
};
|
|
893
|
-
this.mailStore.on('changed', (address) => { if (address && !turnsWaiting.has(address)) void typeWaiting(address); });
|
|
894
|
-
// the turns already waiting when this listener came (the store read every mailbox at its start, before it), and every
|
|
895
|
-
// mailbox again once per 5 min, so a turn no event named is typed within that bound
|
|
896
|
-
const everyMailbox = () => { for (const address of this.mailStore.addresses()) if (!turnsWaiting.has(address)) void typeWaiting(address); };
|
|
897
|
-
everyMailbox();
|
|
898
|
-
this.turnsSweep = setInterval(everyMailbox, 300_000); this.turnsSweep.unref?.();
|
|
899
|
-
this.stopWaitingTurns = () => { clearInterval(this.turnsSweep); for (const timer of turnsWaiting.values()) clearTimeout(timer); turnsWaiting.clear(); };
|
|
911
|
+
// User turns share the persisted delivery schedule; only their own raw-input door types them.
|
|
900
912
|
// an agent's verbs (3b-2): its threads, a delegation, its record
|
|
901
913
|
this.agentVerbs = new AgentVerbs({ machine, store: this.mailStore, agents: this.agents, live, supercodeBin: this.supercodeBin, env: this.env,
|
|
902
914
|
paneOf: (session) => this.mail.paneOf(session), mailRoot,
|
|
@@ -905,23 +917,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
905
917
|
// the names sessions ran under (`<mail>/names.json`): a name an agent was given reaches the same session after it
|
|
906
918
|
// runs under another, kept from the map's changes (at most once a second)
|
|
907
919
|
let naming = null;
|
|
908
|
-
|
|
909
|
-
// sessions that ended while no daemon ran are retired once, at start, when the live sessions are known
|
|
910
|
-
let wasLive = new Map(live.sessions().map((row) => [row.address, row.name]));
|
|
911
|
-
live.on('changed', () => {
|
|
912
|
-
// while the Codex sessions are not known (a start, a serve restart) a Codex session missing has not ended: the last
|
|
913
|
-
// known view stands until they are known again
|
|
914
|
-
if (this.stopping || !live.codexKnown) return;
|
|
915
|
-
const now = new Map(live.sessions().map((row) => [row.address, row.name]));
|
|
916
|
-
// a Codex session that leaves the map has not provably ended (idle over a day, it is no longer followed): kept
|
|
917
|
-
for (const [address, name] of wasLive) if (!now.has(address) && address.split(':')[2] !== 'codex') void this.relays.retire(address, name).catch(() => {});
|
|
918
|
-
wasLive = now;
|
|
919
|
-
});
|
|
920
|
-
void live.whenCodexKnown(120_000).then((known) => {
|
|
921
|
-
if (this.stopping) return;
|
|
922
|
-
if (!known) { this.emit('mail-log', 'relays: the live sessions were not known within two minutes of start; relays of ended sessions are retired at their next end'); return; }
|
|
923
|
-
return this.relays.sweepAtStart((address) => live.sessions().some((row) => row.address === address));
|
|
924
|
-
}).catch((error) => this.emit('mail-log', `relays: the start sweep failed: ${error.message}`));
|
|
920
|
+
void this.relays.sweepAtStart().catch((error) => this.emit('mail-log', `relay pool: predecessors retained (${error.message})`));
|
|
925
921
|
live.on('changed', () => {
|
|
926
922
|
if (this.stopping) return;
|
|
927
923
|
if (naming) return;
|
|
@@ -930,6 +926,16 @@ export class TeamsMachine extends MachineCore {
|
|
|
930
926
|
});
|
|
931
927
|
}
|
|
932
928
|
|
|
929
|
+
#recordSnapshot() {
|
|
930
|
+
const self = machineName(this.env);
|
|
931
|
+
if (!this.records) return { machine: self, at: Date.now(), records: {}, unread: 'this daemon keeps no records yet' };
|
|
932
|
+
const following = this.machineEvents ? null : 'this machine\'s daemon runs with no team connector, so it follows no other machine';
|
|
933
|
+
return { machine: self, at: Date.now(), ...(following ? { unread: following } : {}), records: {
|
|
934
|
+
[self]: { record: this.records.producer.record, stream: 'self', since: null, why: null, received_at: Date.now() },
|
|
935
|
+
...this.records.holder.read(),
|
|
936
|
+
} };
|
|
937
|
+
}
|
|
938
|
+
|
|
933
939
|
/** One send of this machine's own, made in its daemon's process (the connector's machine events): from the sender
|
|
934
940
|
* the request names, which is this machine's own operator address. */
|
|
935
941
|
async sendOwn(request) {
|
|
@@ -966,7 +972,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
966
972
|
|
|
967
973
|
#startRecords() {
|
|
968
974
|
const machine = machineName(this.env);
|
|
969
|
-
const producer = new RecordProducer({ machine, log: (line) => this.emit('records-log', line), readers: {
|
|
975
|
+
const producer = new RecordProducer({ machine, store: this.mailStore, log: (line) => this.emit('records-log', line), readers: {
|
|
970
976
|
// this machine's sessions, from the live-session map (no process)
|
|
971
977
|
sessions: async () => {
|
|
972
978
|
const sessions = await this.mail.localRows();
|
|
@@ -974,18 +980,21 @@ export class TeamsMachine extends MachineCore {
|
|
|
974
980
|
return sessions;
|
|
975
981
|
},
|
|
976
982
|
panes: async () => {
|
|
977
|
-
|
|
983
|
+
let rows;
|
|
984
|
+
try { rows = await this.attentionEvidence(); }
|
|
985
|
+
catch (error) { this.live?.panes([], { current: false }); throw error; }
|
|
978
986
|
const titles = readJson(this.p.titles, { v: 1, titles: {} }).titles;
|
|
979
987
|
const panes = rows.filter((row) => row.session?.id).map(({ session: s }) => ({ pane: s.contextKey, dead: Boolean(s.dead), pid: s.rootPid ?? null, label: s.label ?? null, title: titles[`pane:${s.contextKey}`] ?? s.label ?? null, cwd: s.cwd ?? null }));
|
|
980
988
|
const live = panes.filter((pane) => !pane.dead && pane.pid);
|
|
981
989
|
// the pane each session whose record names none runs in (Grok's; Claude's on Windows): the one rooted at it
|
|
982
|
-
this.live?.panes(live.map((pane) => ({ pane: pane.pane, pid: pane.pid })));
|
|
983
990
|
// the harness session under each live pane, as the board reads it: unread is said, never no session
|
|
984
991
|
let activities = [], sessions_unread = null;
|
|
985
992
|
if (live.length) {
|
|
986
993
|
try { activities = (await this.#serveRequest('harness.v1.sessions.activity_under', { pids: live.map((pane) => pane.pid) }, 30_000))?.activities ?? []; }
|
|
987
994
|
catch (error) { sessions_unread = String(error?.message ?? error).slice(0, 200); }
|
|
988
995
|
}
|
|
996
|
+
this.live?.panes(live.map((pane) => ({ pane: pane.pane, pid: pane.pid,
|
|
997
|
+
activity: sessions_unread ? null : activities.find(row => row.pid === pane.pid)?.activity ?? null })), { current: sessions_unread == null });
|
|
989
998
|
// the sessions under the panes unread: the panes are read again on the section's backoff until they are (MR4)
|
|
990
999
|
if (sessions_unread) producer.retry('panes', null);
|
|
991
1000
|
else this.live?.codexPanes(activities.map((row) => ({ pane: live.find((pane) => pane.pid === row.pid)?.pane ?? null, activity: row.activity })));
|
|
@@ -1001,12 +1010,12 @@ export class TeamsMachine extends MachineCore {
|
|
|
1001
1010
|
return answer.mail;
|
|
1002
1011
|
},
|
|
1003
1012
|
} });
|
|
1004
|
-
const holder = new RecordHolder({ self: machine, file: join(this.home, 'machine-records.json'), log: (line) => this.emit('records-log', line),
|
|
1013
|
+
const holder = new RecordHolder({ self: machine, store: this.mailStore, file: join(this.home, 'machine-records.json'), log: (line) => this.emit('records-log', line),
|
|
1005
1014
|
// the holder's sink is attached at once, in the same turn the stream is handed over: nothing it delivers is missed
|
|
1006
1015
|
open: async (peer, sink) => {
|
|
1007
1016
|
const channel = await openMachine({ machine: peer, env: this.env });
|
|
1008
1017
|
let stream;
|
|
1009
|
-
try { stream = await channel.open('machine-records', {}); } catch (error) { channel.close(); throw error; }
|
|
1018
|
+
try { stream = await channel.open('machine-records', sink.resume ?? {}); } catch (error) { channel.close(); throw error; }
|
|
1010
1019
|
stream.on('data', sink.data);
|
|
1011
1020
|
stream.on('close', (reason) => { channel.close(); sink.close(reason); });
|
|
1012
1021
|
stream.on('error', (error) => sink.close(error?.message));
|
|
@@ -1121,9 +1130,15 @@ export class TeamsMachine extends MachineCore {
|
|
|
1121
1130
|
start();
|
|
1122
1131
|
};
|
|
1123
1132
|
this.recordsWatches = new Set(); this.recordsRetries = new Set();
|
|
1124
|
-
//
|
|
1133
|
+
// Mail rows follow their durable sequence. Reconciliation/gaps name a
|
|
1134
|
+
// snapshot; ordinary pages touch only the mailboxes whose rows changed.
|
|
1125
1135
|
const mailState = () => producer.note('mail_watch', this.mailStore.unread ?? null);
|
|
1126
|
-
this.mailStore.
|
|
1136
|
+
this.stopRecordMail = this.mailStore.changes.follow((page) => {
|
|
1137
|
+
if (this.recordsStopped || page.error) return;
|
|
1138
|
+
mailState();
|
|
1139
|
+
if (page.reset) producer.touch('mail');
|
|
1140
|
+
else for (const address of new Set(page.changes.map((change) => change.key.address).filter(Boolean))) producer.touch('mail', address);
|
|
1141
|
+
}, { after: this.mailStore.changes.cursor(), topics: ['message', 'delivery', 'claim'] });
|
|
1127
1142
|
mailState();
|
|
1128
1143
|
// the live sessions' changes, as the live-session map takes them (Claude's registry in every home it follows, the
|
|
1129
1144
|
// hosted runtimes, Codex), and whether each of its sources is current
|
|
@@ -1208,6 +1223,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
1208
1223
|
for (const retry of this.recordsRetries ?? []) clearTimeout(retry);
|
|
1209
1224
|
for (const watcher of this.recordsWatches ?? []) { try { watcher.close(); } catch { /* closed */ } }
|
|
1210
1225
|
this.records?.producer.stop(); this.records?.holder.close();
|
|
1226
|
+
this.stopRecordMail?.();
|
|
1211
1227
|
for (const child of this.voiceProcesses.values()) child.kill('SIGTERM');
|
|
1212
1228
|
this.voiceProcesses.clear();
|
|
1213
1229
|
this.jobs?.stopAll();
|
|
@@ -1215,7 +1231,6 @@ export class TeamsMachine extends MachineCore {
|
|
|
1215
1231
|
this.stopLiveFollow?.();
|
|
1216
1232
|
this.#stopMailWorkers();
|
|
1217
1233
|
this.mailStore?.stop();
|
|
1218
|
-
clearInterval(this.turnsSweep);
|
|
1219
1234
|
this.stoppingServe = true;
|
|
1220
1235
|
clearTimeout(this.serveRestart);
|
|
1221
1236
|
// said, so a stop reads as one in the log and never as a serve that broke
|
|
@@ -1237,7 +1252,6 @@ export class TeamsMachine extends MachineCore {
|
|
|
1237
1252
|
this.nativeQuestions?.stop();
|
|
1238
1253
|
this.mailHooks?.stop();
|
|
1239
1254
|
this.relays?.stop();
|
|
1240
|
-
this.stopWaitingTurns?.();
|
|
1241
1255
|
}
|
|
1242
1256
|
|
|
1243
1257
|
connectionClosed(index) {
|
|
@@ -3580,11 +3594,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
3580
3594
|
// this machine's record and the held records of the machines it follows, each with how current it is: for this
|
|
3581
3595
|
// machine's own processes only (the records it holds are other machines', given to it, never passed on)
|
|
3582
3596
|
if (principal !== 'local') { this.log.append({ principal, cap: method, target: '*', outcome: 'refused' }); throw Object.assign(new Error('machine records are read by this machine\'s own processes'), { code: 403 }); }
|
|
3583
|
-
|
|
3584
|
-
const self = machineName(this.env);
|
|
3585
|
-
// a daemon with no team presence stream follows no other machine: said, so no reader takes the team as empty
|
|
3586
|
-
const following = this.machineEvents ? null : 'this machine\'s daemon runs with no team connector, so it follows no other machine';
|
|
3587
|
-
return { machine: self, at: Date.now(), ...(following ? { unread: following } : {}), records: { [self]: { record: this.records.producer.record, stream: 'self', since: null, why: null, received_at: Date.now() }, ...this.records.holder.read() } };
|
|
3597
|
+
return { ...this.#recordSnapshot(), cursor: this.mailStore.changes.cursor() };
|
|
3588
3598
|
}
|
|
3589
3599
|
case 'harness.v1.panes.list':
|
|
3590
3600
|
case 'panes.list': {
|
|
@@ -4220,8 +4230,8 @@ export class TeamsMachine extends MachineCore {
|
|
|
4220
4230
|
// `via`: the door the claims record (`inbox`, or `read_messages` for `message mcp`'s tool)
|
|
4221
4231
|
return this.#claimInbox(caller, { all: params.all === true, last, pid: Number(params.claim?.pid) || process.pid, via: params.via === 'read_messages' ? 'read_messages' : 'inbox' });
|
|
4222
4232
|
}
|
|
4223
|
-
// `message wait
|
|
4224
|
-
//
|
|
4233
|
+
// `message wait`: one durable cursor followed for the caller's mailbox,
|
|
4234
|
+
// then the ordinary tokenized claim. No folder scan or read/listen gap.
|
|
4225
4235
|
case 'harness.v1.mail.wait': {
|
|
4226
4236
|
if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('mail is read here by this machine\'s own processes'), { code: 403 });
|
|
4227
4237
|
const caller = await this.#callerOf(entry, params);
|
|
@@ -4233,16 +4243,12 @@ export class TeamsMachine extends MachineCore {
|
|
|
4233
4243
|
// waiting for its pane, so a wait never wakes for mail it then cannot claim; an abandoned claim is given back first
|
|
4234
4244
|
const waiting = () => this.mailStore.hasClaimable(caller, wanted ? (envelope) => envelope.from === wanted || String(envelope.from_name ?? '').startsWith(wanted) : null);
|
|
4235
4245
|
if (!waiting()) {
|
|
4236
|
-
const arrived = await
|
|
4237
|
-
|
|
4238
|
-
const done = (value) => { clearTimeout(timer); this.mailStore.off('changed', changed); resolve(value); };
|
|
4239
|
-
const timer = setTimeout(() => done(false), seconds * 1000);
|
|
4240
|
-
this.mailStore.on('changed', changed);
|
|
4241
|
-
});
|
|
4246
|
+
const arrived = await this.mailStore.changes.wait(waiting, { topics: ['message', 'claim'], addresses: [caller], timeoutMs: seconds * 1000 });
|
|
4247
|
+
if (this.stopping) throw Object.assign(new Error('the daemon is stopping; the wait ended without claiming mail'), { code: 503 });
|
|
4242
4248
|
if (!arrived) return { code: 6, timeout: true, text: `No mail within ${seconds} seconds; nothing is lost.` };
|
|
4243
4249
|
}
|
|
4244
4250
|
// everything unread is handed over, not only the awaited sender's: a message not shown now would stay silent
|
|
4245
|
-
return this.#claimInbox(caller, { all: false, last: null, pid: Number(params.claim?.pid) || process.pid, via: 'wait' });
|
|
4251
|
+
return { ...await this.#claimInbox(caller, { all: false, last: null, pid: Number(params.claim?.pid) || process.pid, via: 'wait' }), cursor: this.mailStore.changes.cursor() };
|
|
4246
4252
|
}
|
|
4247
4253
|
// harness serve's `sessions.inbox` (step 1), asked here by serve: an operator's own mailbox (`from_name`) has its
|
|
4248
4254
|
// unread mail claimed and acknowledged, so a second read does not return it; any other address (`address`) is only
|
|
@@ -4402,6 +4408,12 @@ export class TeamsMachine extends MachineCore {
|
|
|
4402
4408
|
if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('mail storage diagnostics are read only by this machine\'s own processes'), { code: 403 });
|
|
4403
4409
|
return this.mailStore.statistics();
|
|
4404
4410
|
}
|
|
4411
|
+
case 'harness.v1.mail.changes': {
|
|
4412
|
+
if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('mail changes are read by this machine\'s own processes'), { code: 403 });
|
|
4413
|
+
const addresses = params.addresses ?? [];
|
|
4414
|
+
if (!Array.isArray(addresses) || addresses.length > 256 || addresses.some((address) => !parseAddress(address))) throw Object.assign(new Error('changes takes at most 256 mailbox addresses'), { code: 400 });
|
|
4415
|
+
return this.mailStore.changes.read(params.after, { topics: ['message', 'delivery', 'claim', 'subscription'], addresses });
|
|
4416
|
+
}
|
|
4405
4417
|
// Who a local client is, from its claim (live-sessions.mjs callerOf): what a command acting on another machine sends
|
|
4406
4418
|
// there as its caller, resolved here, where the map that holds it is. Local only; never the app door's.
|
|
4407
4419
|
// The pane this daemon launched that a local process runs in, and who started that pane: what a process with no
|