@volter/supercode-teams 0.3.113 → 0.3.114
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.mjs +9 -3
- package/machine.mjs +84 -21
- package/mail/carrier.mjs +43 -0
- package/mail/store.mjs +16 -0
- package/package.json +1 -1
package/bin/workspace.mjs
CHANGED
|
@@ -1010,13 +1010,19 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
|
|
|
1010
1010
|
const sync = saved ? { ...(readSyncStatus(state.directory) ?? { not_syncing: [], last_error: null, queued_captures: 0 }), agent_identity: agentIdentityStatus(teamsHome()) } : null;
|
|
1011
1011
|
// The mail this machine's mailbox still has to deliver: after a failed attempt, or for a machine that is offline.
|
|
1012
1012
|
let mailWaiting = [];
|
|
1013
|
-
|
|
1014
|
-
|
|
1013
|
+
// asked of the daemon, which keeps these records (mail step 1); a daemon that does not answer is said, never read as none
|
|
1014
|
+
let mailWaitingError = null;
|
|
1015
|
+
try {
|
|
1016
|
+
const channel = await openLocalMachine(paths(teamsHome()).socket);
|
|
1017
|
+
try { const rpc = await nodeRpc(channel); mailWaiting = (await rpc.call('harness.v1.mail.waiting', {}, { timeoutMs: 15000 }))?.rows ?? []; }
|
|
1018
|
+
finally { channel.close?.(); }
|
|
1019
|
+
} catch (error) { mailWaitingError = error.message; }
|
|
1020
|
+
const value = { version: 1, context: scope(context), data: { mail_waiting: mailWaiting, ...(mailWaitingError ? { mail_waiting_error: mailWaitingError } : {}), reachable, account, connector: saved ? { enrolled: true, running: running || busy, ...(busy ? { busy: true } : {}), machine: saved.machine, source: saved.source, sync } : { enrolled: false, running: false }, sources: reachable && !context.machine_only ? (await team.sources.list()).items : [] } };
|
|
1015
1021
|
if (flags.json) return output(value);
|
|
1016
1022
|
const lines = [`Team ${context.team_name ?? context.team_id} · ${new URL(context.origin).host}${reachable ? '' : ' · unreachable'}`];
|
|
1017
1023
|
lines.push(saved?.machine ? `Connector ${saved.machine.name ?? saved.machine.id} · ${running ? 'running' : busy ? 'running (busy: its control socket did not answer within 1 s)' : 'not running'}${saved.service?.kind ? ` (${saved.service.kind})` : ''}` : 'Connector not connected (supercode teams connect)');
|
|
1018
1024
|
lines.push(...syncStatusLines(sync));
|
|
1019
|
-
lines.push(...mailWaitingLines(mailWaiting));
|
|
1025
|
+
lines.push(...(mailWaitingError ? [`Mail waiting mail unknown: this machine's daemon did not answer (${mailWaitingError})`] : mailWaitingLines(mailWaiting)));
|
|
1020
1026
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
1021
1027
|
return;
|
|
1022
1028
|
}
|
package/machine.mjs
CHANGED
|
@@ -42,10 +42,12 @@ import { Agents } from './mail/agents.mjs';
|
|
|
42
42
|
import { Relays } from './mail/relay.mjs';
|
|
43
43
|
import { serveRelayEndpoint } from './mail/relay-endpoint.mjs';
|
|
44
44
|
import { MailCarrier } from './mail/carrier.mjs';
|
|
45
|
+
|
|
46
|
+
const NO_CALLER = "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.";
|
|
45
47
|
import { MailSend } from './mail/send.mjs';
|
|
46
48
|
import { AgentVerbs } from './mail/verbs.mjs';
|
|
47
49
|
import { deliverToRuntime } from './mail/runtime.mjs';
|
|
48
|
-
import { envelope as envelopeOf, linkOf, parseAddress, senderIdentity } from './mail/compose.mjs';
|
|
50
|
+
import { envelope as envelopeOf, linkOf, makeAddress, parseAddress, senderIdentity } from './mail/compose.mjs';
|
|
49
51
|
import { codexQuestions } from './native-questions.mjs';
|
|
50
52
|
import { Duplex, PassThrough } from 'node:stream';
|
|
51
53
|
|
|
@@ -1927,6 +1929,30 @@ export class TeamsMachine extends MachineCore {
|
|
|
1927
1929
|
* a claim is the same user's word, held to what the map holds. A request the Teams connector forwards from another
|
|
1928
1930
|
* machine keeps the caller it carries. A local claim the map does not hold is refused.
|
|
1929
1931
|
*/
|
|
1932
|
+
/** The caller's unread mail claimed under its reader's process (a reader that ends gives it back), with the token its
|
|
1933
|
+
* reader acknowledges once it has shown it: `message inbox`, `read_messages` and `message wait`. */
|
|
1934
|
+
async #claimInbox(caller, { all, last, pid, via }) {
|
|
1935
|
+
const read = await this.mail.inbox(caller, { all, last, pid, via });
|
|
1936
|
+
let token = null;
|
|
1937
|
+
if (read.claimed.length) {
|
|
1938
|
+
token = randomBytes(12).toString('hex');
|
|
1939
|
+
this.inboxClaims ??= new Map();
|
|
1940
|
+
// a reader that never acknowledges leaves its claims under its own pid (given back when it ends): its token
|
|
1941
|
+
// is let go after a while, never held for good
|
|
1942
|
+
for (const [old] of [...this.inboxClaims].slice(0, Math.max(0, this.inboxClaims.size - 255))) this.inboxClaims.delete(old);
|
|
1943
|
+
this.inboxClaims.set(token, { address: caller, claimed: read.claimed });
|
|
1944
|
+
}
|
|
1945
|
+
// `plain`: each message as its reader is shown it (the envelope rendered), with no listing's line about when
|
|
1946
|
+
const plain = read.stored.map((item) => render(item.envelope));
|
|
1947
|
+
const claimedIds = new Set(read.claimed.map((item) => item.envelope.id));
|
|
1948
|
+
const kept = inboxKept(read.stored.length,
|
|
1949
|
+
(index) => Buffer.byteLength(read.rendered[index] ?? '') + Buffer.byteLength(plain[index] ?? '') + Buffer.byteLength(JSON.stringify(read.rows[index] ?? null)),
|
|
1950
|
+
(index) => claimedIds.has(read.stored[index].envelope.id));
|
|
1951
|
+
const left = read.stored.length - kept.length;
|
|
1952
|
+
const note = left ? [inboxNote(left)] : [];
|
|
1953
|
+
return { code: 0, address: caller, rendered: [...note, ...kept.map((index) => read.rendered[index])], plain: [...note, ...kept.map((index) => plain[index])], rows: kept.map((index) => read.rows[index]), omitted: left, token };
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1930
1956
|
#callerOf(entry, params) {
|
|
1931
1957
|
const named = typeof params.caller === 'string' && params.caller ? params.caller : null;
|
|
1932
1958
|
if (entry.principal !== 'local') return Promise.resolve(named);
|
|
@@ -3673,28 +3699,65 @@ export class TeamsMachine extends MachineCore {
|
|
|
3673
3699
|
// the caller's own inbox: its unread mail claimed under its own process (a reader that ends gives it back), then
|
|
3674
3700
|
// acknowledged once it has printed it (harness.v1.mail.acknowledge)
|
|
3675
3701
|
const caller = await this.#callerOf(entry, params);
|
|
3676
|
-
if (typeof caller !== 'string' || /^sc:[^:]+:board:/.test(caller)) return { code: 2, text:
|
|
3702
|
+
if (typeof caller !== 'string' || /^sc:[^:]+:board:/.test(caller)) return { code: 2, text: NO_CALLER };
|
|
3677
3703
|
// `via`: the door the claims record (`inbox`, or `read_messages` for `message mcp`'s tool)
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3704
|
+
return this.#claimInbox(caller, { all: params.all === true, last, pid: Number(params.claim?.pid) || process.pid, via: params.via === 'read_messages' ? 'read_messages' : 'inbox' });
|
|
3705
|
+
}
|
|
3706
|
+
// `message wait` (step 1): the caller's next unread mail (from one sender, with `from`), waited for on the store's
|
|
3707
|
+
// own change events, at most 100 s, then claimed as its inbox claims it; nothing polls
|
|
3708
|
+
case 'harness.v1.mail.wait': {
|
|
3709
|
+
if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('mail is read here by this machine\'s own processes'), { code: 403 });
|
|
3710
|
+
const caller = await this.#callerOf(entry, params);
|
|
3711
|
+
if (typeof caller !== 'string' || /^sc:[^:]+:board:/.test(caller)) return { code: 2, text: NO_CALLER };
|
|
3712
|
+
let wanted = null;
|
|
3713
|
+
if (typeof params.from === 'string' && params.from) { const found = this.mail.resolve(params.from); wanted = found.error ? params.from : found.address; }
|
|
3714
|
+
const seconds = Math.min(100, Math.max(0, Number.isFinite(Number(params.timeout)) ? Math.floor(Number(params.timeout)) : 100));
|
|
3715
|
+
// exactly what its claim would take (store.claimUnread): never a live reader's claimed copy or a user's own turn
|
|
3716
|
+
// waiting for its pane, so a wait never wakes for mail it then cannot claim; an abandoned claim is given back first
|
|
3717
|
+
const waiting = () => this.mailStore.hasClaimable(caller, wanted ? (envelope) => envelope.from === wanted || String(envelope.from_name ?? '').startsWith(wanted) : null);
|
|
3718
|
+
if (!waiting()) {
|
|
3719
|
+
const arrived = await new Promise((resolve) => {
|
|
3720
|
+
const changed = (address) => { if (address === caller && waiting()) done(true); };
|
|
3721
|
+
const done = (value) => { clearTimeout(timer); this.mailStore.off('changed', changed); resolve(value); };
|
|
3722
|
+
const timer = setTimeout(() => done(false), seconds * 1000);
|
|
3723
|
+
this.mailStore.on('changed', changed);
|
|
3724
|
+
});
|
|
3725
|
+
if (!arrived) return { code: 6, timeout: true, text: `No mail within ${seconds} seconds; nothing is lost.` };
|
|
3726
|
+
}
|
|
3727
|
+
// everything unread is handed over, not only the awaited sender's: a message not shown now would stay silent
|
|
3728
|
+
return this.#claimInbox(caller, { all: false, last: null, pid: Number(params.claim?.pid) || process.pid, via: 'wait' });
|
|
3729
|
+
}
|
|
3730
|
+
// harness serve's `sessions.inbox` (step 1), asked here by serve: an operator's own mailbox (`from_name`) has its
|
|
3731
|
+
// unread mail claimed and acknowledged, so a second read does not return it; any other address (`address`) is only
|
|
3732
|
+
// looked at, never consumed
|
|
3733
|
+
case 'harness.v1.mail.operator_inbox': {
|
|
3734
|
+
if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('sessions.inbox is answered here for this machine\'s own harness serve'), { code: 403 });
|
|
3735
|
+
if (params.address != null && params.from_name != null) throw Object.assign(new Error('sessions.inbox takes from_name or address, not both'), { code: 400 });
|
|
3736
|
+
let address;
|
|
3737
|
+
if (typeof params.address === 'string') {
|
|
3738
|
+
if (!parseAddress(params.address)) throw Object.assign(new Error(`not an address: ${params.address}`), { code: 400 });
|
|
3739
|
+
address = params.address;
|
|
3740
|
+
} else {
|
|
3741
|
+
const name = Array.from(String(params.from_name ?? 'supercode').trim(), (c) => /\s|@/.test(c) ? '-' : c).join('');
|
|
3742
|
+
if (!name) throw Object.assign(new Error('from_name must not be empty'), { code: 400 });
|
|
3743
|
+
address = makeAddress(machineName(this.env), 'operator', name);
|
|
3744
|
+
if (!address) throw Object.assign(new Error(`from_name is not a mailbox name: ${name}`), { code: 400 });
|
|
3688
3745
|
}
|
|
3689
|
-
|
|
3690
|
-
const
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
const
|
|
3696
|
-
const
|
|
3697
|
-
|
|
3746
|
+
const all = params.all === true;
|
|
3747
|
+
const shown = (item, state) => { const envelope = this.mailStore.body(item) ?? item.envelope; return { state, envelope, rendered: render(envelope) }; };
|
|
3748
|
+
if (typeof params.address === 'string') {
|
|
3749
|
+
const { stored } = this.mailStore.inventory(address);
|
|
3750
|
+
return { address, messages: stored.filter((item) => all || item.state === 'unread').map((item) => shown(item, item.state)), looked: true };
|
|
3751
|
+
}
|
|
3752
|
+
const read = await this.mail.inbox(address, { all, last: null, pid: process.pid, via: 'sessions.inbox' });
|
|
3753
|
+
const messages = [...(all ? read.stored.filter((item) => item.state === 'read').map((item) => shown(item, 'read')) : []), ...read.claimed.map((item) => shown(item, 'unread'))];
|
|
3754
|
+
this.mail.acknowledge(address, read.claimed);
|
|
3755
|
+
return { address, messages };
|
|
3756
|
+
}
|
|
3757
|
+
// `message waiting` and `message expired` (step 1): the carrier's own records
|
|
3758
|
+
case 'harness.v1.mail.waiting': case 'harness.v1.mail.expired': {
|
|
3759
|
+
if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('mail is read here by this machine\'s own processes'), { code: 403 });
|
|
3760
|
+
return { code: 0, rows: method === 'harness.v1.mail.waiting' ? this.carrier.waiting() : this.carrier.expired() };
|
|
3698
3761
|
}
|
|
3699
3762
|
// Who a local client is, from its claim (live-sessions.mjs callerOf): what a command acting on another machine sends
|
|
3700
3763
|
// there as its caller, resolved here, where the map that holds it is. Local only; never the app door's.
|
package/mail/carrier.mjs
CHANGED
|
@@ -262,6 +262,49 @@ export class MailCarrier {
|
|
|
262
262
|
catch (error) { this.log(`carrier: ${machine}'s wait was not kept: ${error.message}`); }
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
// ---- what waits, and what expired (`message waiting`, `message expired`) -----------------------------------------
|
|
266
|
+
|
|
267
|
+
/** Every message here still waiting to be delivered after a failed attempt, or for a machine that is offline, oldest
|
|
268
|
+
* filed first (mailbox.rs `waiting_mail`, field for field). */
|
|
269
|
+
waiting() {
|
|
270
|
+
const all = [];
|
|
271
|
+
let folders = [];
|
|
272
|
+
try { folders = readdirSync(this.mailRoot); } catch { return all; }
|
|
273
|
+
for (const folder of folders) {
|
|
274
|
+
let ids = [];
|
|
275
|
+
try { ids = readdirSync(join(this.mailRoot, folder, 'wake')); } catch { continue; }
|
|
276
|
+
if (!ids.length) continue;
|
|
277
|
+
const address = this.#address(folder);
|
|
278
|
+
if (!address) continue;
|
|
279
|
+
const machine = address.split(':')[1];
|
|
280
|
+
const wait = machine !== this.machine ? readJson(this.#machineWaitPath(machine)) : null;
|
|
281
|
+
for (const id of ids) {
|
|
282
|
+
const state = this.store.wakeState(address, id);
|
|
283
|
+
if (!wait && !(state.attempts > 0) && state.last_error == null) continue;
|
|
284
|
+
const found = this.store.findIn(address, id);
|
|
285
|
+
const envelope = found ? readJson(found.path) : null;
|
|
286
|
+
if (!envelope) continue;
|
|
287
|
+
all.push({ id, to: address, from: envelope.from ?? '', subject: envelope.subject ?? null, filed_at_ms: Number(envelope.created_at_ms) || 0,
|
|
288
|
+
waiting_for: wait ? wait.machine : null, since_ms: wait?.offline_since_ms ?? state.failing_since_ms ?? (Number(envelope.created_at_ms) || 0),
|
|
289
|
+
attempts: state.attempts ?? 0, last_error: wait?.last_error ?? state.last_error ?? null });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return all.sort((a, b) => a.filed_at_ms - b.filed_at_ms);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Every wake that expired here, oldest first (mailbox.rs `expired_wakes`). */
|
|
296
|
+
expired() {
|
|
297
|
+
const all = [];
|
|
298
|
+
let folders = [];
|
|
299
|
+
try { folders = readdirSync(this.mailRoot); } catch { return all; }
|
|
300
|
+
for (const folder of folders) {
|
|
301
|
+
let names = [];
|
|
302
|
+
try { names = readdirSync(join(this.mailRoot, folder, 'expired')); } catch { continue; }
|
|
303
|
+
for (const name of names) { const expiry = readJson(join(this.mailRoot, folder, 'expired', name)); if (expiry?.id) all.push(expiry); }
|
|
304
|
+
}
|
|
305
|
+
return all.sort((a, b) => (a.expired_at_ms ?? 0) - (b.expired_at_ms ?? 0));
|
|
306
|
+
}
|
|
307
|
+
|
|
265
308
|
// ---- how an attempt went ---------------------------------------------------------------------------------------
|
|
266
309
|
|
|
267
310
|
/** Record how a wake went: done, waiting for its next attempt, or expired at its bound (mail_watch.rs `settle`). */
|
package/mail/store.mjs
CHANGED
|
@@ -191,6 +191,22 @@ export class MailStore extends EventEmitter {
|
|
|
191
191
|
/** The unread mail of `address` (not the user's own turns) moved into `claimed/` under its reader's pid (`pid`, the
|
|
192
192
|
* reading process: a claim whose reader ends goes back to `new/`), each claim recorded; another reader that took one
|
|
193
193
|
* first keeps it (mailbox.rs `claim_unread`). Oldest first. */
|
|
194
|
+
/** Whether `claimUnread` would take anything now, taking nothing: abandoned claims given back first, as it does, then
|
|
195
|
+
* any envelope in new/ that is not a user's own turn (`message wait` wakes only for mail it can claim). From the
|
|
196
|
+
* mailbox's own folders, as claimUnread reads them. */
|
|
197
|
+
hasClaimable(address, wanted = null) {
|
|
198
|
+
const box = this.boxOf(address) ?? this.#named(address);
|
|
199
|
+
if (!box) return false;
|
|
200
|
+
this.#recoverAbandoned(box);
|
|
201
|
+
let names = [];
|
|
202
|
+
try { names = readdirSync(join(box.dir, 'new')); } catch { return false; }
|
|
203
|
+
return names.some((file) => {
|
|
204
|
+
if (!file.endsWith('.json')) return false;
|
|
205
|
+
const envelope = readEnvelope(join(box.dir, 'new', file));
|
|
206
|
+
return envelope && typeof envelope.id === 'string' && envelope.kind !== 'user' && (!wanted || wanted(envelope));
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
194
210
|
claimUnread(address, via, caller, pid = process.pid) {
|
|
195
211
|
// the mailbox's own folders, read here (one named read): what decides a delivery never comes from the watch-fed
|
|
196
212
|
// index alone, whose events a platform can drop (an FSEvents drop, an inotify overflow)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.114",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
|
|
6
6
|
"license": "MIT",
|