@volter/supercode-teams 0.3.85 → 0.3.87
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/bin/workspace/calendar.mjs +13 -1
- package/bin/workspace.mjs +5 -0
- package/connector/index.mjs +20 -2
- package/execution.mjs +3 -1
- package/live-sessions.mjs +56 -4
- package/machine-events.mjs +7 -9
- package/machine.mjs +269 -64
- package/mail/agents.mjs +386 -0
- package/mail/compose.mjs +97 -0
- package/mail/envelope.mjs +155 -0
- package/mail/reads.mjs +389 -0
- package/mail/relay.mjs +233 -0
- package/mail/runtime.mjs +20 -0
- package/mail/send.mjs +723 -0
- package/mail/store.mjs +419 -0
- package/mail/transcript.mjs +287 -0
- package/mail/verbs.mjs +232 -0
- package/mail-history.mjs +1 -1
- package/package.json +2 -1
package/machine.mjs
CHANGED
|
@@ -36,6 +36,13 @@ import { RecordHolder, RecordProducer, RECENT_PER_MAILBOX, RECORDS_NOT_GRANTED }
|
|
|
36
36
|
import { LiveSessions, ACTING_VARIABLES } from './live-sessions.mjs';
|
|
37
37
|
import { MailStore } from './mail/store.mjs';
|
|
38
38
|
import { MailReads, panePrompt } from './mail/reads.mjs';
|
|
39
|
+
import { Agents } from './mail/agents.mjs';
|
|
40
|
+
import { Relays } from './mail/relay.mjs';
|
|
41
|
+
import { MailSend } from './mail/send.mjs';
|
|
42
|
+
import { AgentVerbs } from './mail/verbs.mjs';
|
|
43
|
+
import { deliverToRuntime } from './mail/runtime.mjs';
|
|
44
|
+
import { envelope as envelopeOf, linkOf, parseAddress, senderIdentity } from './mail/compose.mjs';
|
|
45
|
+
import { codexQuestions } from './native-questions.mjs';
|
|
39
46
|
import { Duplex, PassThrough } from 'node:stream';
|
|
40
47
|
|
|
41
48
|
/** Two ends of a connection made inside this process: what one writes, the other reads. Ending or destroying either
|
|
@@ -139,6 +146,22 @@ function trustClaudeFolder(cwd) {
|
|
|
139
146
|
|
|
140
147
|
/** How long a launch watches its new pane before answering, so a program that ends at once is reported. */
|
|
141
148
|
const LAUNCH_SETTLE_MS = 500;
|
|
149
|
+
/** How long a send to another machine waits for its mail door's answer (its receiver's door there: a relay's receipt
|
|
150
|
+
* alone may take 90 s). An answer that does not come is unanswered, never refused or failed. */
|
|
151
|
+
const MAIL_SEND_MS = 120_000;
|
|
152
|
+
/**
|
|
153
|
+
* The doors whose work waits on a session, with the bound each answers within (the node door's own is 30 s): a send or
|
|
154
|
+
* a hand-over waits on its receiver's door (a relay's receipt, up to 90 s; another machine's door, MAIL_SEND_MS), a
|
|
155
|
+
* delegation or a declaration that starts a session waits for it to appear (up to a minute). Past its bound a call is
|
|
156
|
+
* answered 504, which its client reads as unanswered: the work may still finish.
|
|
157
|
+
*/
|
|
158
|
+
const DOOR_BOUNDS_MS = {
|
|
159
|
+
'harness.v1.mail.send': 170_000,
|
|
160
|
+
'harness.v1.mail.message': 170_000,
|
|
161
|
+
'harness.v1.mail.handover': 140_000,
|
|
162
|
+
'harness.v1.mail.delegate': 110_000,
|
|
163
|
+
'harness.v1.agent.declare': 110_000,
|
|
164
|
+
};
|
|
142
165
|
/** How many typed lines' keys a machine remembers; a server retries a line for minutes, not thousands of lines. */
|
|
143
166
|
const TURN_KEYS_KEPT = 1000;
|
|
144
167
|
/** How long a typed line's door gets to confirm it: a loaded machine's reader types it in seconds and answers late. */
|
|
@@ -257,7 +280,11 @@ export class TeamsMachine extends MachineCore {
|
|
|
257
280
|
async sessionBindings(machineId,incarnationId) {
|
|
258
281
|
const panes=await this.host.listSessions();
|
|
259
282
|
const live=panes.filter(pane=>!pane.dead&&pane.rootPid);
|
|
260
|
-
|
|
283
|
+
// a harness serve that is not there (between restarts, or still starting) leaves the bindings as last read: a
|
|
284
|
+
// heartbeat never fails, nor marks every session unknown, for want of one reading
|
|
285
|
+
let reading;
|
|
286
|
+
try{reading=live.length?await this.#serveRequest('harness.v1.sessions.activity_under',{pids:live.map(pane=>pane.rootPid)}):{activities:[]};}
|
|
287
|
+
catch{return [...this.#sessionBindings.values()];}
|
|
261
288
|
const byPid=new Map((reading.activities??[]).map(item=>[item.pid,item.activity]));
|
|
262
289
|
const current=new Set(panes.filter(pane=>!pane.dead).map(pane=>pane.contextKey));
|
|
263
290
|
let launchAgents=null;
|
|
@@ -593,7 +620,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
593
620
|
// ---- this machine's mail (mail/; docs/architecture/overview.md, "The mail operations") ---------------------------
|
|
594
621
|
#startMail() {
|
|
595
622
|
const mailRoot = join(supercodeHome(this.env), 'mail');
|
|
596
|
-
this.mailStore = new MailStore({ mailRoot, log: (line) => this.emit('records-log', line) }).start();
|
|
623
|
+
this.mailStore = new MailStore({ mailRoot, machine: machineName(this.env), log: (line) => this.emit('records-log', line) }).start();
|
|
597
624
|
// the session index's rows by address (its descriptors, as the records' index subscription hands them over)
|
|
598
625
|
this.sessionDescriptors = new Map();
|
|
599
626
|
const live = this.live;
|
|
@@ -609,20 +636,57 @@ export class TeamsMachine extends MachineCore {
|
|
|
609
636
|
paneQuestion: async (pane) => { const session = await this.#paneByContext(pane); return session ? panePrompt(await this.host.capture(session.id, { lines: 60 })) : null; },
|
|
610
637
|
askMachine: (machine, request) => this.#askMailDoor(machine, request),
|
|
611
638
|
});
|
|
639
|
+
// a send to a session, for every entry (3b-1): its routing on the live map, an agent's plan and its doors
|
|
640
|
+
const machine = machineName(this.env);
|
|
641
|
+
this.agents = new Agents({ mailRoot, machine, supercodeBin: this.supercodeBin, env: this.env,
|
|
642
|
+
discover: (params) => this.#serveRequest('harness.v1.sessions.discover', params, 10_000),
|
|
643
|
+
recordedConfig: (locator) => this.#serveRequest('harness.v1.sessions.recorded_config', { locator }, 15_000) });
|
|
644
|
+
this.relays = new Relays({ mailRoot, machine, supercodeBin: this.supercodeBin, serve: (method, params, timeoutMs) => this.#serveRequest(method, params, timeoutMs),
|
|
645
|
+
runtimeOf: (harness, id) => live.runtimeReceipt(harness, id), runtimeNamed: (receiptId) => live.receiptNamed(receiptId) });
|
|
646
|
+
this.mailSend = new MailSend({ machine, mailRoot, live, store: this.mailStore, agents: this.agents, relays: this.relays,
|
|
647
|
+
// a send waits on its receiver's door there (a relay's receipt takes up to 90 s), never past MAIL_SEND_MS
|
|
648
|
+
askMachine: (to, request) => this.#askMailDoor(to, request, MAIL_SEND_MS),
|
|
649
|
+
deliverRuntime: deliverToRuntime, codexAnswer: (request) => codexQuestions(request) });
|
|
650
|
+
// an agent's verbs (3b-2): its threads, a delegation, its record
|
|
651
|
+
this.agentVerbs = new AgentVerbs({ machine, store: this.mailStore, agents: this.agents, live, supercodeBin: this.supercodeBin, env: this.env,
|
|
652
|
+
paneOf: (session) => this.mail.paneOf(session), mailRoot });
|
|
653
|
+
// the names sessions ran under (`<mail>/names.json`): a name an agent was given reaches the same session after it
|
|
654
|
+
// runs under another, kept from the map's changes (at most once a second)
|
|
655
|
+
let naming = null;
|
|
656
|
+
live.on('changed', () => {
|
|
657
|
+
if (naming) return;
|
|
658
|
+
naming = setTimeout(() => { naming = null; try { this.mailSend.rememberNames(live.sessions()); } catch { /* kept at the next change */ } }, 1000);
|
|
659
|
+
naming.unref?.();
|
|
660
|
+
});
|
|
612
661
|
}
|
|
613
662
|
|
|
614
|
-
/** One
|
|
615
|
-
*
|
|
616
|
-
async
|
|
617
|
-
|
|
663
|
+
/** One send of this machine's own, made in its daemon's process (the connector's machine events): from the sender
|
|
664
|
+
* the request names, which is this machine's own operator address. */
|
|
665
|
+
async sendOwn(request) {
|
|
666
|
+
if (!this.mailSend) throw new Error('this machine\'s daemon has not started its mail yet');
|
|
667
|
+
return this.mailSend.send({ address: request.from, name: request.from_name ?? '' }, request.to, request.body, { id: request.id ?? null, queue: request.queue === true, notice: request.notice === true });
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** The name a caller is known by in its envelopes: its session's, else an operator's or a board's `name@machine`. */
|
|
671
|
+
#callerName(address) {
|
|
672
|
+
const row = this.live.sessions().find((session) => session.address === address);
|
|
673
|
+
if (row) return row.name;
|
|
674
|
+
const parsed = parseAddress(address);
|
|
675
|
+
return parsed ? `${parsed.session}@${parsed.machine}` : String(address);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** One request to another machine's mail door, over Teams, given up after `boundMs` with a reason: a named read (the
|
|
679
|
+
* `show` of a message that machine's record names, 10 s) or a send (MAIL_SEND_MS). */
|
|
680
|
+
async #askMailDoor(machine, request, boundMs = 10_000) {
|
|
618
681
|
const channel = await openMachine({ machine, env: this.env });
|
|
619
682
|
try {
|
|
620
|
-
const stream = await channel.open('mail', { request }, { timeoutMs:
|
|
683
|
+
const stream = await channel.open('mail', { request }, { timeoutMs: Math.min(boundMs, 15_000) });
|
|
684
|
+
// once the door took the request, an answer that never comes is unanswered: what it did there is unknown
|
|
621
685
|
const text = await new Promise((resolve, reject) => {
|
|
622
686
|
let answer = '';
|
|
623
|
-
const timer = setTimeout(() => { try { stream.close('timed out'); } catch { /* closed */ } reject(new Error(`${machine} did not answer within ${
|
|
687
|
+
const timer = setTimeout(() => { try { stream.close('timed out'); } catch { /* closed */ } reject(Object.assign(new Error(`${machine} did not answer within ${boundMs / 1000} s`), { unanswered: true })); }, boundMs);
|
|
624
688
|
stream.on('data', (chunk) => { answer += chunk; });
|
|
625
|
-
stream.on('close', (reason) => { clearTimeout(timer); answer.trim() ? resolve(answer) : reject(new Error(String(reason ?? 'its mail door closed without answering'))); });
|
|
689
|
+
stream.on('close', (reason) => { clearTimeout(timer); answer.trim() ? resolve(answer) : reject(Object.assign(new Error(String(reason ?? 'its mail door closed without answering')), { unanswered: true })); });
|
|
626
690
|
});
|
|
627
691
|
const answer = JSON.parse(text.trim().split('\n').pop());
|
|
628
692
|
if (answer?.outcome === 'failed') throw new Error(answer.detail ?? 'its mail door failed');
|
|
@@ -873,6 +937,8 @@ export class TeamsMachine extends MachineCore {
|
|
|
873
937
|
this.jobs?.stopAll();
|
|
874
938
|
this.stoppingMailWatch = true;
|
|
875
939
|
this.mailWatch?.kill('SIGTERM');
|
|
940
|
+
this.stoppingServe = true;
|
|
941
|
+
clearTimeout(this.serveRestart);
|
|
876
942
|
this.serve?.kill('SIGTERM');
|
|
877
943
|
this.host.dispose();
|
|
878
944
|
}
|
|
@@ -881,21 +947,69 @@ export class TeamsMachine extends MachineCore {
|
|
|
881
947
|
for (const [key, owners] of this.routes) { owners.delete(index); if (owners.size === 0) this.routes.delete(key); }
|
|
882
948
|
}
|
|
883
949
|
|
|
950
|
+
/**
|
|
951
|
+
* The daemon's `harness serve`. Its pipes are a peer that can go away: a write into them after it has ended (EPIPE) is
|
|
952
|
+
* that pipe's error, handled here, never the daemon's end. When it ends, every request waiting on it is answered at
|
|
953
|
+
* once (failed, saying so), every harness stream relayed into it is closed, and it is started again (a growing wait
|
|
954
|
+
* between restarts, from 1 s up to 1 min, reset once one has run a minute); the subscriptions made on it are made
|
|
955
|
+
* again on its `serve-exit`. Only the daemon's own stop ends it for good.
|
|
956
|
+
*/
|
|
884
957
|
#startServe() {
|
|
885
958
|
const child = spawn(this.supercodeBin, ['harness', 'serve'], { stdio: ['pipe', 'pipe', 'pipe'], env: this.env });
|
|
959
|
+
const started = Date.now();
|
|
886
960
|
child.stdout.setEncoding('utf8');
|
|
887
961
|
// A restarted serve starts on a line of its own, never on the tail its predecessor left unfinished.
|
|
888
962
|
const serveLines = lineSplitter();
|
|
963
|
+
// a serve already replaced never answers into the streams that its successor serves (its late answers would reach a
|
|
964
|
+
// reopened stream that uses the same ids)
|
|
889
965
|
child.stdout.on('data', (chunk) => {
|
|
966
|
+
if (this.serve !== child) return;
|
|
890
967
|
for (const line of serveLines(chunk)) if (line) this.#routeServeLine(line);
|
|
891
968
|
});
|
|
892
969
|
child.stderr.setEncoding('utf8');
|
|
893
970
|
child.stderr.on('data', (text) => this.emit('serve-stderr', text));
|
|
971
|
+
let ended = false;
|
|
972
|
+
const end = (why) => {
|
|
973
|
+
if (ended) return;
|
|
974
|
+
ended = true;
|
|
975
|
+
if (this.serve === child) this.serve = null;
|
|
976
|
+
// what waited on it is answered now, never left to its own bound
|
|
977
|
+
for (const [id, pending] of this.internalRequests ?? []) { this.internalRequests.delete(id); pending.reject(new Error(`harness serve ${why}`)); }
|
|
978
|
+
for (const [, entry] of this.connections) entry.harness?.close(`harness serve ${why}`);
|
|
979
|
+
if (this.stoppingServe) return;
|
|
980
|
+
const ranLong = Date.now() - started > 60_000;
|
|
981
|
+
this.serveBackoff = ranLong ? 1000 : Math.min(60_000, Math.max(1000, (this.serveBackoff ?? 500) * 2));
|
|
982
|
+
this.emit('serve-stderr', `harness serve ${why}; started again in ${Math.round(this.serveBackoff / 1000)} s\n`);
|
|
983
|
+
this.serveRestart = setTimeout(() => { if (!this.stoppingServe) this.#startServe(); }, this.serveBackoff);
|
|
984
|
+
this.serveRestart.unref?.();
|
|
985
|
+
};
|
|
986
|
+
// a write into a serve that has gone (EPIPE, ECONNRESET) is its pipe's error: the serve is over, the daemon is not
|
|
987
|
+
child.stdin.on('error', (error) => {
|
|
988
|
+
this.emit('serve-stderr', `harness serve's input: ${error.message}\n`);
|
|
989
|
+
try { child.kill('SIGTERM'); } catch { /* gone */ }
|
|
990
|
+
// one that does not end on SIGTERM within 5 s is killed: a serve that no longer reads is never left behind
|
|
991
|
+
const grace = setTimeout(() => { if (child.exitCode === null && child.signalCode === null) { try { child.kill('SIGKILL'); } catch { /* gone */ } } }, 5000);
|
|
992
|
+
grace.unref?.();
|
|
993
|
+
child.once('exit', () => clearTimeout(grace));
|
|
994
|
+
end(`stopped taking input (${error.code ?? error.message})`);
|
|
995
|
+
});
|
|
996
|
+
child.stdout.on('error', () => {});
|
|
997
|
+
child.stderr.on('error', () => {});
|
|
998
|
+
child.on('error', (error) => end(`could not run (${error.message})`));
|
|
894
999
|
child.on('exit', (code, signal) => {
|
|
895
1000
|
this.emit('serve-exit', { code, signal });
|
|
896
|
-
|
|
1001
|
+
end(`exited (${signal ?? `code ${code}`})`);
|
|
897
1002
|
});
|
|
898
1003
|
this.serve = child;
|
|
1004
|
+
child.once('spawn', () => { if (this.serve === child) this.emit('serve-started'); });
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/** One line into the daemon's `harness serve`, when it is there to take it: false when it is not (ended, or between
|
|
1008
|
+
* restarts). A write's own failure is its pipe's error (handled in #startServe), never a throw here. */
|
|
1009
|
+
#toServe(line) {
|
|
1010
|
+
const stdin = this.serve?.stdin;
|
|
1011
|
+
if (!stdin || !stdin.writable || stdin.destroyed) return false;
|
|
1012
|
+
try { stdin.write(line); return true; } catch { return false; }
|
|
899
1013
|
}
|
|
900
1014
|
|
|
901
1015
|
// Idle notices for every session on this machine (`supercode message watch`):
|
|
@@ -921,10 +1035,12 @@ export class TeamsMachine extends MachineCore {
|
|
|
921
1035
|
this.internalRequests ??= new Map();
|
|
922
1036
|
const id = (this.internalNext = (this.internalNext ?? 0) + 1);
|
|
923
1037
|
return new Promise((resolve, reject) => {
|
|
924
|
-
if (!this.serve?.stdin?.writable) return reject(new Error('harness serve is not running'));
|
|
925
1038
|
this.internalRequests.set(id, { resolve, reject });
|
|
1039
|
+
if (!this.#toServe(`${JSON.stringify({ jsonrpc: '2.0', id: joinId(INTERNAL_CONNECTION, id), method, params })}\n`)) {
|
|
1040
|
+
this.internalRequests.delete(id);
|
|
1041
|
+
return reject(new Error('harness serve is not running'));
|
|
1042
|
+
}
|
|
926
1043
|
setTimeout(() => { if (this.internalRequests.delete(id)) reject(new Error(`${method} timed out`)); }, timeoutMs).unref();
|
|
927
|
-
this.serve.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: joinId(INTERNAL_CONNECTION, id), method, params })}\n`);
|
|
928
1044
|
});
|
|
929
1045
|
}
|
|
930
1046
|
|
|
@@ -1016,8 +1132,12 @@ export class TeamsMachine extends MachineCore {
|
|
|
1016
1132
|
if (!line) continue;
|
|
1017
1133
|
let message;
|
|
1018
1134
|
try { message = JSON.parse(line); } catch { continue; }
|
|
1019
|
-
|
|
1020
|
-
|
|
1135
|
+
const asked = message.id !== undefined && message.id !== null ? message.id : null;
|
|
1136
|
+
if (asked !== null) message.id = joinId(index, message.id);
|
|
1137
|
+
// a serve that is not there to take it answers the request at once, on this stream only
|
|
1138
|
+
if (!this.#toServe(JSON.stringify(message) + '\n') && asked !== null && !stream.closed) {
|
|
1139
|
+
try { stream.write(`${JSON.stringify({ jsonrpc: '2.0', id: asked, error: { code: -32000, message: 'harness serve is not running; it is being started again' } })}\n`); } catch { /* the stream went */ }
|
|
1140
|
+
}
|
|
1021
1141
|
}
|
|
1022
1142
|
});
|
|
1023
1143
|
stream.on('close', () => { entry.harness = null; });
|
|
@@ -1128,16 +1248,12 @@ export class TeamsMachine extends MachineCore {
|
|
|
1128
1248
|
}
|
|
1129
1249
|
}
|
|
1130
1250
|
|
|
1131
|
-
/** One mail request from this machine: its own
|
|
1132
|
-
*
|
|
1251
|
+
/** One mail request from this machine (its launch notices): its own send, which delivers here or sends on through
|
|
1252
|
+
* the one way out to another machine, in this process. */
|
|
1133
1253
|
#sendMail(request, target) {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
child.stderr.on('data', () => {});
|
|
1138
|
-
child.on('error', (error) => this.log.append({ principal: null, cap: 'mail', target, outcome: 'failed', detail: error.message }));
|
|
1139
|
-
child.on('close', (code) => this.log.append({ principal: null, cap: 'mail', target, outcome: code === 0 ? 'send' : 'failed', detail: out.trim().slice(0, 200) || undefined }));
|
|
1140
|
-
child.stdin.end(JSON.stringify(request));
|
|
1254
|
+
void this.mailSend.send({ address: request.from, name: request.from_name }, request.to, request.body, { queue: request.queue === true, notice: request.notice === true })
|
|
1255
|
+
.then((answer) => this.log.append({ principal: null, cap: 'mail', target, outcome: answer.code === 0 ? 'send' : 'failed', detail: answer.text.slice(0, 200) || undefined }),
|
|
1256
|
+
(error) => this.log.append({ principal: null, cap: 'mail', target, outcome: 'failed', detail: String(error.message).slice(0, 200) }));
|
|
1141
1257
|
}
|
|
1142
1258
|
|
|
1143
1259
|
// ---- mail ----------------------------------------------------------------
|
|
@@ -1174,8 +1290,8 @@ export class TeamsMachine extends MachineCore {
|
|
|
1174
1290
|
}
|
|
1175
1291
|
|
|
1176
1292
|
#serveMail(entry, stream, accept, refuse,requestOverride=null) {
|
|
1177
|
-
// One message into a session on this machine, delivered by
|
|
1178
|
-
//
|
|
1293
|
+
// One message into a session on this machine, delivered by this daemon's own send (mail/send.mjs) through the same
|
|
1294
|
+
// doors a local send uses. The receiver is the
|
|
1179
1295
|
// target: `mail:*` reaches every session here, `mail:<address>` one of them; a
|
|
1180
1296
|
// receiver named rather than addressed needs `mail:*`.
|
|
1181
1297
|
const { admitted, principal } = entry;
|
|
@@ -1217,7 +1333,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
1217
1333
|
try {
|
|
1218
1334
|
const answer = request.op === 'list'
|
|
1219
1335
|
? { code: 0, sessions: await this.mail.localRows() }
|
|
1220
|
-
: { code: 0, rows: this.mail.showRows((Array.isArray(request.ids) ? request.ids : []).filter((id) => typeof id === 'string')) };
|
|
1336
|
+
: { code: 0, rows: await this.mail.showRows((Array.isArray(request.ids) ? request.ids : []).filter((id) => typeof id === 'string')) };
|
|
1221
1337
|
this.log.append({ principal, cap: 'mail', target, outcome: request.op });
|
|
1222
1338
|
if (!stream.closed) { stream.write(JSON.stringify(answer)); stream.close('answered'); }
|
|
1223
1339
|
} catch (error) {
|
|
@@ -1227,27 +1343,51 @@ export class TeamsMachine extends MachineCore {
|
|
|
1227
1343
|
})();
|
|
1228
1344
|
return;
|
|
1229
1345
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1346
|
+
// Who sent it is the stream's principal, as Teams authenticated it, and the name Teams has for it (never a name the
|
|
1347
|
+
// caller's app asserts): an envelope made here says so, never the return address the request claims. A local caller
|
|
1348
|
+
// is no link.
|
|
1349
|
+
const link = linkOf(principal, entry.principalName ?? null);
|
|
1350
|
+
void (async () => {
|
|
1351
|
+
let answer;
|
|
1352
|
+
try { answer = request.op === 'file' ? this.#fileHere(request, link) : await this.#sendHere(request, link); }
|
|
1353
|
+
catch (error) { answer = { outcome: 'failed', detail: error.message }; }
|
|
1354
|
+
this.log.append({ principal, cap: 'mail', target, outcome: answer.outcome === 'failed' || (answer.code ?? 0) !== 0 ? 'failed' : request.op, detail: answer.outcome === 'failed' ? String(answer.detail).slice(0, 200) : undefined });
|
|
1238
1355
|
if (stream.closed) return;
|
|
1239
|
-
stream.write(
|
|
1356
|
+
stream.write(JSON.stringify(answer));
|
|
1240
1357
|
stream.close('delivered');
|
|
1241
|
-
});
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1358
|
+
})();
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/** A send through the mail door, for a session here (message.rs `remote_deliver` send): one for another machine is
|
|
1362
|
+
* refused when it came through a link (never forwarded on this machine's own credential), and this machine's own
|
|
1363
|
+
* daemon sends on through the one way out. */
|
|
1364
|
+
async #sendHere(request, link) {
|
|
1365
|
+
const from = parseAddress(request.from)?.address;
|
|
1366
|
+
if (!from) return { code: 2, text: `\`${request.from}\` is not a session address; addresses look like sc:<machine>:<harness>:<session-id>`, receipt: null };
|
|
1367
|
+
const text = (key) => (typeof request[key] === 'string' ? request[key] : null);
|
|
1368
|
+
return this.mailSend.send({ address: from, name: text('from_name') ?? '' }, text('to') ?? '', text('body') ?? '', {
|
|
1369
|
+
subject: text('subject'), in_reply_to: text('in_reply_to'), notify_when_idle: request.notify_when_idle === true, queue: request.queue === true,
|
|
1370
|
+
id: text('id'), notice: request.notice === true, thread: text('thread'),
|
|
1371
|
+
}, { link });
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/** A message another machine's mailbox carried here, filed in a mailbox here (message.rs `remote_deliver` file): the
|
|
1375
|
+
* sender is resolved here from the link it came through, never from the filer's own claim; a wake it carries is
|
|
1376
|
+
* requested once, for a message new here. */
|
|
1377
|
+
#fileHere(request, link) {
|
|
1378
|
+
const to = parseAddress(request.to);
|
|
1379
|
+
const envelope = request.envelope;
|
|
1380
|
+
const valid = envelope && typeof envelope === 'object' && typeof envelope.id === 'string' && /^[A-Za-z0-9_-]+$/.test(envelope.id) && parseAddress(envelope.from)
|
|
1381
|
+
&& typeof envelope.from_name === 'string' && typeof envelope.body === 'string' && Number.isSafeInteger(envelope.created_at_ms) && typeof envelope.kind === 'string' && envelope.reply_via && typeof envelope.reply_via.mode === 'string';
|
|
1382
|
+
if (!to || !valid) return { code: 2, text: 'Not filed: the mail door files one envelope for one address here.', receipt: null };
|
|
1383
|
+
if (to.machine !== machineName(this.env)) return { code: 3, text: `${to.address} is not on this machine.`, receipt: null };
|
|
1384
|
+
const { sender_identity: _claimed, ...rest } = envelope;
|
|
1385
|
+
const filed = envelopeOf({ ...rest, sender_identity: senderIdentity(envelope.from, link, machineName(this.env)) });
|
|
1386
|
+
const fresh = this.mailStore.findIn(to.address, filed.id) == null;
|
|
1387
|
+
this.mailStore.deliver(to.address, filed);
|
|
1388
|
+
// carried here by another machine's mailbox: its wake comes with it, once, and this machine's watch delivers it
|
|
1389
|
+
if (fresh && request.wake === true) this.mailStore.requestWake(to.address, filed.id);
|
|
1390
|
+
return { code: 0, text: `filed for ${to.address}`, receipt: null };
|
|
1251
1391
|
}
|
|
1252
1392
|
|
|
1253
1393
|
/**
|
|
@@ -1577,17 +1717,12 @@ export class TeamsMachine extends MachineCore {
|
|
|
1577
1717
|
return { observation: `mailbox:${from}`, principal: null, classification: 'unresolved', label: 'unresolved', why: `it claims ${machine ?? 'another machine'}'s address but did not come through that machine's link` };
|
|
1578
1718
|
}
|
|
1579
1719
|
|
|
1580
|
-
/**
|
|
1581
|
-
#runFiling(request) {
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
child.stdout.setEncoding('utf8').on('data', (chunk) => { out += chunk; });
|
|
1587
|
-
child.on('error', (error) => { clearTimeout(timer); resolve({ code: 1, text: error.message }); });
|
|
1588
|
-
child.on('close', () => { clearTimeout(timer); try { resolve(JSON.parse(out.trim().split('\n').pop())); } catch { resolve({ code: 1, text: out.trim().slice(-300) || 'the mailbox\'s filing door answered nothing' }); } });
|
|
1589
|
-
child.stdin.on('error', () => {});
|
|
1590
|
-
child.stdin.end(JSON.stringify(request));
|
|
1720
|
+
/** A filing in this machine's mail (mail/send.mjs `file`), in this process. */
|
|
1721
|
+
async #runFiling(request) {
|
|
1722
|
+
if (!this.mailSend) return { code: 1, text: 'this machine\'s daemon has not started its mail yet' };
|
|
1723
|
+
return this.mailSend.file({ address: request.from, name: request.from_name ?? '' }, request.to, request.body ?? '', {
|
|
1724
|
+
subject: request.subject ?? null, key: request.id ?? null, wake: request.wake !== false,
|
|
1725
|
+
sender_identity: request.sender_identity && typeof request.sender_identity === 'object' ? request.sender_identity : null, typed: request.typed === true,
|
|
1591
1726
|
});
|
|
1592
1727
|
}
|
|
1593
1728
|
|
|
@@ -2352,8 +2487,10 @@ export class TeamsMachine extends MachineCore {
|
|
|
2352
2487
|
const reply = (envelope) => { if (stream.closed) return; try { stream.write(JSON.stringify(envelope) + '\n'); } catch { /* peer gone */ } };
|
|
2353
2488
|
try {
|
|
2354
2489
|
// Every door answers or fails within a bound; a hung substrate call becomes an error, never silence.
|
|
2355
|
-
// The caller may shorten the bound, never lengthen it past the daemon's own 30 s
|
|
2356
|
-
|
|
2490
|
+
// The caller may shorten the bound, never lengthen it past the daemon's own: 30 s, or the longer one a door
|
|
2491
|
+
// that waits on a session states (DOOR_BOUNDS_MS), each above its own inner waits.
|
|
2492
|
+
const own = DOOR_BOUNDS_MS[request.method] ?? 30_000;
|
|
2493
|
+
const bound = Math.min(Number(request.params?.timeoutMs) || own, own);
|
|
2357
2494
|
const result = await withTimeout(this.#dispatch(entry, request.method, request.params ?? {}), bound, request.method);
|
|
2358
2495
|
reply({ id: request.id, result });
|
|
2359
2496
|
this.emit('door', { method: request.method, ms: Date.now() - startedAt, outcome: 'ok' });
|
|
@@ -3095,12 +3232,80 @@ export class TeamsMachine extends MachineCore {
|
|
|
3095
3232
|
act('title', params.target, 'ok', renamed);
|
|
3096
3233
|
return { ok: true, target: params.target, title: params.title ?? null, window: renamed };
|
|
3097
3234
|
}
|
|
3235
|
+
// A send to a session (docs/architecture/overview.md, "The mail operations", 3b-1): `supercode message send` and
|
|
3236
|
+
// `reply`, a session's `send_message` and the native callers' client (D207, pending) on this machine's own socket,
|
|
3237
|
+
// each with its claim; the caller is who the claim resolves to, never who the request says it is.
|
|
3238
|
+
case 'harness.v1.mail.send': {
|
|
3239
|
+
if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('a send is made here by this machine\'s own processes; another machine\'s goes through its mail door'), { code: 403 });
|
|
3240
|
+
const caller = await this.#callerOf(entry, params);
|
|
3241
|
+
if (typeof caller !== 'string' || !caller) return { code: 2, text: "Can't tell which session is running this command, so replies would have nowhere to go. Nothing was sent. Run it from your agent session's own shell tool.", receipt: null };
|
|
3242
|
+
const text = (key) => (typeof params[key] === 'string' ? params[key] : null);
|
|
3243
|
+
const body = text('body') ?? '';
|
|
3244
|
+
if (!body.trim()) return { code: 1, text: 'Nothing was sent: the message is empty.', receipt: null };
|
|
3245
|
+
const from = { address: caller, name: this.#callerName(caller) };
|
|
3246
|
+
let to = text('to'), options = { subject: text('subject'), in_reply_to: text('in_reply_to'), notify_when_idle: params.notify_when_idle === true, queue: params.queue === true, id: text('id'), message_id: text('message_id') };
|
|
3247
|
+
if (text('reply')) {
|
|
3248
|
+
// a reply: to the message's sender, in its thread, its subject kept (message.rs `Reply`)
|
|
3249
|
+
const found = this.mailStore.findPrefixIn(caller, text('reply'));
|
|
3250
|
+
if (found.length !== 1) return { code: 2, text: `Not sent: ${text('reply')} names ${found.length} messages in your mailbox. Name the one you answer as its envelope shows it.`, receipt: null };
|
|
3251
|
+
const answered = found[0].envelope;
|
|
3252
|
+
if (answered.kind === 'notice') return { code: 2, text: `Not sent: ${text('reply')} is an automated notice; nothing reads a reply to it.`, receipt: null };
|
|
3253
|
+
to = answered.from;
|
|
3254
|
+
options = { ...options, subject: typeof answered.subject === 'string' ? (answered.subject.startsWith('Re: ') ? answered.subject : `Re: ${answered.subject}`) : null, in_reply_to: answered.id, id: null, thread: answered.thread ?? answered.id };
|
|
3255
|
+
}
|
|
3256
|
+
if (!to) return { code: 2, text: 'Not sent: a send names its receiver.', receipt: null };
|
|
3257
|
+
// a bare name no session here answers to may be a session on the team's other machines (their held records)
|
|
3258
|
+
const team = text('reply') ? { to: null } : this.mail.teamAddress(to, (name) => this.mailSend.remembered(name));
|
|
3259
|
+
if (team.error) return { code: 5, text: `Not sent: ${team.error}. Name it as \`name@machine\` or its address.`, receipt: null };
|
|
3260
|
+
const answer = { ...(await this.mailSend.send(from, team.to ?? to, body, options)), ...(team.notes?.length ? { notes: team.notes } : {}) };
|
|
3261
|
+
act('mail.send', to, answer.code === 0 ? 'ok' : 'failed');
|
|
3262
|
+
return answer;
|
|
3263
|
+
}
|
|
3264
|
+
// An agent's verbs (docs/architecture/overview.md, 3b-2): `message thread`, `threads`, `delegate`, and `supercode
|
|
3265
|
+
// agent declare|show`, this machine's own processes only; a delegation and a thread list without --agent are the
|
|
3266
|
+
// caller's, as its claim names it.
|
|
3267
|
+
case 'harness.v1.mail.thread': case 'harness.v1.mail.threads': case 'harness.v1.mail.delegate': case 'harness.v1.agent.declare': case 'harness.v1.agent.show': {
|
|
3268
|
+
if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('an agent\'s verbs are answered here for this machine\'s own processes'), { code: 403 });
|
|
3269
|
+
const text = (key) => (typeof params[key] === 'string' && params[key] ? params[key] : null);
|
|
3270
|
+
if (method === 'harness.v1.mail.thread') {
|
|
3271
|
+
if (!text('id')) throw Object.assign(new Error('thread names a message id'), { code: 400 });
|
|
3272
|
+
return this.agentVerbs.thread(text('id'));
|
|
3273
|
+
}
|
|
3274
|
+
if (method === 'harness.v1.agent.show') return this.agentVerbs.show(text('name'));
|
|
3275
|
+
if (method === 'harness.v1.agent.declare') {
|
|
3276
|
+
if (!text('name')) throw Object.assign(new Error('declare names its agent'), { code: 400 });
|
|
3277
|
+
const answer = await this.agentVerbs.declare({ name: text('name'), main: text('main'), open: text('open'), input: text('input'), folder: text('folder'), harness: text('harness'),
|
|
3278
|
+
idle_minutes: Number.isInteger(params.idle_minutes) && params.idle_minutes >= 0 ? params.idle_minutes : null, owners_account_manager: params.owners_account_manager === true });
|
|
3279
|
+
act('agent.declare', text('name'), answer.code === 0 ? 'ok' : 'failed');
|
|
3280
|
+
return answer;
|
|
3281
|
+
}
|
|
3282
|
+
let caller = null;
|
|
3283
|
+
try { caller = await this.#callerOf(entry, params); } catch (error) { if (method === 'harness.v1.mail.delegate') throw error; }
|
|
3284
|
+
if (method === 'harness.v1.mail.threads') return this.agentVerbs.threads(text('agent'), typeof caller === 'string' ? caller : null);
|
|
3285
|
+
if (typeof caller !== 'string' || !caller) return { code: 2, text: "Can't tell which session is running this command, so replies would have nowhere to go. Nothing was sent. Run it from your agent session's own shell tool." };
|
|
3286
|
+
if (!text('root')) throw Object.assign(new Error('delegate names its root'), { code: 400 });
|
|
3287
|
+
const answer = await this.agentVerbs.delegate(caller, text('root'), { folder: text('folder'), harness: text('harness') });
|
|
3288
|
+
act('mail.delegate', caller, answer.code === 0 ? 'ok' : 'failed');
|
|
3289
|
+
return answer;
|
|
3290
|
+
}
|
|
3291
|
+
// A filed message handed to its session's door (the native watch's carrier and a relay's `message push`, until
|
|
3292
|
+
// step 4): once, by its id, recorded on its wake.
|
|
3293
|
+
case 'harness.v1.mail.handover': {
|
|
3294
|
+
if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('mail is handed over here by this machine\'s own processes'), { code: 403 });
|
|
3295
|
+
if (typeof params.address !== 'string' || typeof params.id !== 'string' || !parseAddress(params.address)) throw Object.assign(new Error('a hand-over names an address and a message id'), { code: 400 });
|
|
3296
|
+
return this.mailSend.handOver(params.address, params.id, { markRead: params.mark_read === true, wake: params.wake === true });
|
|
3297
|
+
}
|
|
3298
|
+
// harness serve's `sessions.message` (its mail to a session or an agent, not its `as_user` turn), asked here by
|
|
3299
|
+
// harness serve itself: one implementation of a send, whatever the entry.
|
|
3300
|
+
case 'harness.v1.mail.message': {
|
|
3301
|
+
if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('sessions.message is answered here for this machine\'s own harness serve'), { code: 403 });
|
|
3302
|
+
return this.mailSend.message(params);
|
|
3303
|
+
}
|
|
3098
3304
|
case 'harness.v1.mail.file': {
|
|
3099
3305
|
// One message filed in a receiver's mailbox here (a board owner's notices and mail, D138 step 6): the sender's
|
|
3100
|
-
// identity is observed by this daemon, in this process, and
|
|
3101
|
-
//
|
|
3102
|
-
//
|
|
3103
|
-
// sender a local operator names, as `supercode message file` itself does.
|
|
3306
|
+
// identity is observed by this daemon, in this process, and filed with the message here (mail/send.mjs `file`):
|
|
3307
|
+
// no filing starts a process. An observation that cannot be made is said in the record (unresolved, with why),
|
|
3308
|
+
// never waited on past its bound. Operators only: the filing takes the sender a local operator names.
|
|
3104
3309
|
if (!operator) throw Object.assign(new Error('operators only'), { code: 403 });
|
|
3105
3310
|
for (const key of ['from', 'to', 'body']) if (typeof params[key] !== 'string' || !params[key]) throw Object.assign(new Error(`a filing names its ${key}`), { code: 400 });
|
|
3106
3311
|
const sender_identity = await this.#observeSender(params.from, params.from_name ?? null, typeof params.context === 'string' && params.context ? params.context : null);
|
|
@@ -3135,7 +3340,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
3135
3340
|
address = found.address;
|
|
3136
3341
|
}
|
|
3137
3342
|
const machine = address.split(':')[1];
|
|
3138
|
-
if (machine === machineName(this.env)) return { code: 0, address, rows: this.mail.inboxRows(address, last) };
|
|
3343
|
+
if (machine === machineName(this.env)) return { code: 0, address, rows: await this.mail.inboxRows(address, last) };
|
|
3139
3344
|
try {
|
|
3140
3345
|
const answer = await this.#askMailDoor(machine, { op: 'inbox', session: address, recent: last });
|
|
3141
3346
|
return answer?.code === 0 ? { code: 0, address, rows: Array.isArray(answer.rows) ? answer.rows : [] } : { code: 3, text: answer?.text ?? 'its machine refused' };
|
|
@@ -3145,7 +3350,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
3145
3350
|
// acknowledged once it has printed it (harness.v1.mail.acknowledge)
|
|
3146
3351
|
const caller = await this.#callerOf(entry, params);
|
|
3147
3352
|
if (typeof caller !== 'string' || /^sc:[^:]+:board:/.test(caller)) return { code: 2, text: "Can't tell which session is running this command, so replies would have nowhere to go. Nothing was sent. Run it from your agent session's own shell tool." };
|
|
3148
|
-
const read = this.mail.inbox(caller, { all: params.all === true, last, pid: Number(params.claim?.pid) || process.pid });
|
|
3353
|
+
const read = await this.mail.inbox(caller, { all: params.all === true, last, pid: Number(params.claim?.pid) || process.pid });
|
|
3149
3354
|
let token = null;
|
|
3150
3355
|
if (read.claimed.length) {
|
|
3151
3356
|
token = randomBytes(12).toString('hex');
|