@volter/supercode-teams 0.3.113 → 0.3.115
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 +124 -24
- 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
|
|
|
@@ -124,6 +126,13 @@ export function claudeUsageLimitWait(screen) {
|
|
|
124
126
|
/** The settle floor for a pane whose kind has no composer pattern: the screen
|
|
125
127
|
* must have stopped changing for this long before the pane counts as ready. */
|
|
126
128
|
const INITIAL_INPUT_SETTLE_MS = 1_000;
|
|
129
|
+
/** A Windows Codex's typed first input waits, before pasting, for an empty composer on a screen unchanged this long
|
|
130
|
+
* (CODEX_PASTE_SETTLE_DEADLINE_MS at most); gives the paste CODEX_PASTE_SHOWN_MS to show, and watches
|
|
131
|
+
* CODEX_PASTE_LATE_MS more to clear one shown after that. */
|
|
132
|
+
const CODEX_PASTE_SETTLE_MS = 1_500;
|
|
133
|
+
const CODEX_PASTE_SETTLE_DEADLINE_MS = 30_000;
|
|
134
|
+
const CODEX_PASTE_SHOWN_MS = 5_000;
|
|
135
|
+
const CODEX_PASTE_LATE_MS = 15_000;
|
|
127
136
|
|
|
128
137
|
/**
|
|
129
138
|
* Claude asks whether to trust a folder it has not opened before, whatever its permission mode, and has no flag that
|
|
@@ -1927,6 +1936,30 @@ export class TeamsMachine extends MachineCore {
|
|
|
1927
1936
|
* a claim is the same user's word, held to what the map holds. A request the Teams connector forwards from another
|
|
1928
1937
|
* machine keeps the caller it carries. A local claim the map does not hold is refused.
|
|
1929
1938
|
*/
|
|
1939
|
+
/** The caller's unread mail claimed under its reader's process (a reader that ends gives it back), with the token its
|
|
1940
|
+
* reader acknowledges once it has shown it: `message inbox`, `read_messages` and `message wait`. */
|
|
1941
|
+
async #claimInbox(caller, { all, last, pid, via }) {
|
|
1942
|
+
const read = await this.mail.inbox(caller, { all, last, pid, via });
|
|
1943
|
+
let token = null;
|
|
1944
|
+
if (read.claimed.length) {
|
|
1945
|
+
token = randomBytes(12).toString('hex');
|
|
1946
|
+
this.inboxClaims ??= new Map();
|
|
1947
|
+
// a reader that never acknowledges leaves its claims under its own pid (given back when it ends): its token
|
|
1948
|
+
// is let go after a while, never held for good
|
|
1949
|
+
for (const [old] of [...this.inboxClaims].slice(0, Math.max(0, this.inboxClaims.size - 255))) this.inboxClaims.delete(old);
|
|
1950
|
+
this.inboxClaims.set(token, { address: caller, claimed: read.claimed });
|
|
1951
|
+
}
|
|
1952
|
+
// `plain`: each message as its reader is shown it (the envelope rendered), with no listing's line about when
|
|
1953
|
+
const plain = read.stored.map((item) => render(item.envelope));
|
|
1954
|
+
const claimedIds = new Set(read.claimed.map((item) => item.envelope.id));
|
|
1955
|
+
const kept = inboxKept(read.stored.length,
|
|
1956
|
+
(index) => Buffer.byteLength(read.rendered[index] ?? '') + Buffer.byteLength(plain[index] ?? '') + Buffer.byteLength(JSON.stringify(read.rows[index] ?? null)),
|
|
1957
|
+
(index) => claimedIds.has(read.stored[index].envelope.id));
|
|
1958
|
+
const left = read.stored.length - kept.length;
|
|
1959
|
+
const note = left ? [inboxNote(left)] : [];
|
|
1960
|
+
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 };
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1930
1963
|
#callerOf(entry, params) {
|
|
1931
1964
|
const named = typeof params.caller === 'string' && params.caller ? params.caller : null;
|
|
1932
1965
|
if (entry.principal !== 'local') return Promise.resolve(named);
|
|
@@ -2341,18 +2374,37 @@ export class TeamsMachine extends MachineCore {
|
|
|
2341
2374
|
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2342
2375
|
// Styled, so the composer's dim placeholder ("Ask Codex to do anything") reads as empty.
|
|
2343
2376
|
const composer = async () => terminal.composerText?.('codex', await this.host.capture(session.id, { preserveEscapes: true })) ?? null;
|
|
2344
|
-
|
|
2377
|
+
// A composer that reads ready can still be mid-layout: a fresh Codex redraws its screen for about a second more
|
|
2378
|
+
// (measured on volter-desktop: ready from the first frame, redrawn at ~1.1 s and ~1.3 s), and a paste sent into
|
|
2379
|
+
// that is shown late or not at all. Paste only into an empty composer on a screen that has stopped changing.
|
|
2380
|
+
let settled = false;
|
|
2381
|
+
for (const until = Date.now() + CODEX_PASTE_SETTLE_DEADLINE_MS; !settled && !signal.aborted && Date.now() < until;) {
|
|
2382
|
+
const capture = await this.host.capture(session.id, { preserveEscapes: true });
|
|
2383
|
+
settled = terminal.composerText?.('codex', capture) === '' &&
|
|
2384
|
+
this.#screenUnchangedMs(session.id, capture, Date.now()) >= CODEX_PASTE_SETTLE_MS;
|
|
2385
|
+
if (!settled) await pause(150);
|
|
2386
|
+
}
|
|
2387
|
+
if (signal.aborted) return { delivered: false, reason: 'aborted' };
|
|
2388
|
+
if (!settled) return { delivered: false, reason: await composer() === '' ? 'screen-unsettled' : 'composer-not-empty' };
|
|
2345
2389
|
for (const paste of codexWindowsPastes(body)) {
|
|
2346
2390
|
if (signal.aborted) return { delivered: false, reason: 'aborted' };
|
|
2347
2391
|
await this.host.sendInput(session.id, paste, { submitMode: 'none' });
|
|
2348
2392
|
await pause(300);
|
|
2349
2393
|
}
|
|
2350
2394
|
let held = false;
|
|
2351
|
-
for (
|
|
2395
|
+
for (const until = Date.now() + CODEX_PASTE_SHOWN_MS; !held && !signal.aborted && Date.now() < until;) {
|
|
2352
2396
|
held = Boolean(await composer());
|
|
2353
2397
|
if (!held) await pause(150);
|
|
2354
2398
|
}
|
|
2355
|
-
if (!held)
|
|
2399
|
+
if (!held) {
|
|
2400
|
+
// A paste shown after this gave up would sit in the composer unsent: watch a while longer and clear it, so the
|
|
2401
|
+
// pane is left as it was found (the launch records the input as undelivered either way).
|
|
2402
|
+
for (const until = Date.now() + CODEX_PASTE_LATE_MS; !signal.aborted && Date.now() < until;) {
|
|
2403
|
+
if (await composer()) { await this.#clearCodexComposer(session, composer, pause); break; }
|
|
2404
|
+
await pause(250);
|
|
2405
|
+
}
|
|
2406
|
+
return { delivered: false, reason: 'paste-unconfirmed' };
|
|
2407
|
+
}
|
|
2356
2408
|
await this.host.sendInput(session.id, '\r', { submitMode: 'single' });
|
|
2357
2409
|
return { delivered: true };
|
|
2358
2410
|
} catch (error) {
|
|
@@ -2362,6 +2414,17 @@ export class TeamsMachine extends MachineCore {
|
|
|
2362
2414
|
}
|
|
2363
2415
|
}
|
|
2364
2416
|
|
|
2417
|
+
/** Empty a Windows Codex composer this launch pasted into: line clear, then deletes until it reads empty (a paste
|
|
2418
|
+
* placeholder goes with one delete). Never Enter, Escape or Ctrl-C. */
|
|
2419
|
+
async #clearCodexComposer(session, composer, pause) {
|
|
2420
|
+
await this.host.sendInput(session.id, '\u0015', { submitMode: 'none' });
|
|
2421
|
+
await pause(300);
|
|
2422
|
+
for (let round = 0; round < 40 && await composer(); round += 1) {
|
|
2423
|
+
await this.host.sendInput(session.id, '\u007f'.repeat(200), { submitMode: 'none' });
|
|
2424
|
+
await pause(200);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2365
2428
|
async #deliverPaneText(session, contextKey, text, options = {}) {
|
|
2366
2429
|
const kind = this.#paneKind(contextKey, session);
|
|
2367
2430
|
if (kind === 'codex' && codexUpdatePrompt(await this.host.capture(session.id, { lines: 120 }))) return { delivered: false, reason: 'codex-update-prompt: delivery refused; restart Codex with check_for_update_on_startup=false' };
|
|
@@ -3673,28 +3736,65 @@ export class TeamsMachine extends MachineCore {
|
|
|
3673
3736
|
// the caller's own inbox: its unread mail claimed under its own process (a reader that ends gives it back), then
|
|
3674
3737
|
// acknowledged once it has printed it (harness.v1.mail.acknowledge)
|
|
3675
3738
|
const caller = await this.#callerOf(entry, params);
|
|
3676
|
-
if (typeof caller !== 'string' || /^sc:[^:]+:board:/.test(caller)) return { code: 2, text:
|
|
3739
|
+
if (typeof caller !== 'string' || /^sc:[^:]+:board:/.test(caller)) return { code: 2, text: NO_CALLER };
|
|
3677
3740
|
// `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
|
-
|
|
3741
|
+
return this.#claimInbox(caller, { all: params.all === true, last, pid: Number(params.claim?.pid) || process.pid, via: params.via === 'read_messages' ? 'read_messages' : 'inbox' });
|
|
3742
|
+
}
|
|
3743
|
+
// `message wait` (step 1): the caller's next unread mail (from one sender, with `from`), waited for on the store's
|
|
3744
|
+
// own change events, at most 100 s, then claimed as its inbox claims it; nothing polls
|
|
3745
|
+
case 'harness.v1.mail.wait': {
|
|
3746
|
+
if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('mail is read here by this machine\'s own processes'), { code: 403 });
|
|
3747
|
+
const caller = await this.#callerOf(entry, params);
|
|
3748
|
+
if (typeof caller !== 'string' || /^sc:[^:]+:board:/.test(caller)) return { code: 2, text: NO_CALLER };
|
|
3749
|
+
let wanted = null;
|
|
3750
|
+
if (typeof params.from === 'string' && params.from) { const found = this.mail.resolve(params.from); wanted = found.error ? params.from : found.address; }
|
|
3751
|
+
const seconds = Math.min(100, Math.max(0, Number.isFinite(Number(params.timeout)) ? Math.floor(Number(params.timeout)) : 100));
|
|
3752
|
+
// exactly what its claim would take (store.claimUnread): never a live reader's claimed copy or a user's own turn
|
|
3753
|
+
// waiting for its pane, so a wait never wakes for mail it then cannot claim; an abandoned claim is given back first
|
|
3754
|
+
const waiting = () => this.mailStore.hasClaimable(caller, wanted ? (envelope) => envelope.from === wanted || String(envelope.from_name ?? '').startsWith(wanted) : null);
|
|
3755
|
+
if (!waiting()) {
|
|
3756
|
+
const arrived = await new Promise((resolve) => {
|
|
3757
|
+
const changed = (address) => { if (address === caller && waiting()) done(true); };
|
|
3758
|
+
const done = (value) => { clearTimeout(timer); this.mailStore.off('changed', changed); resolve(value); };
|
|
3759
|
+
const timer = setTimeout(() => done(false), seconds * 1000);
|
|
3760
|
+
this.mailStore.on('changed', changed);
|
|
3761
|
+
});
|
|
3762
|
+
if (!arrived) return { code: 6, timeout: true, text: `No mail within ${seconds} seconds; nothing is lost.` };
|
|
3763
|
+
}
|
|
3764
|
+
// everything unread is handed over, not only the awaited sender's: a message not shown now would stay silent
|
|
3765
|
+
return this.#claimInbox(caller, { all: false, last: null, pid: Number(params.claim?.pid) || process.pid, via: 'wait' });
|
|
3766
|
+
}
|
|
3767
|
+
// harness serve's `sessions.inbox` (step 1), asked here by serve: an operator's own mailbox (`from_name`) has its
|
|
3768
|
+
// unread mail claimed and acknowledged, so a second read does not return it; any other address (`address`) is only
|
|
3769
|
+
// looked at, never consumed
|
|
3770
|
+
case 'harness.v1.mail.operator_inbox': {
|
|
3771
|
+
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 });
|
|
3772
|
+
if (params.address != null && params.from_name != null) throw Object.assign(new Error('sessions.inbox takes from_name or address, not both'), { code: 400 });
|
|
3773
|
+
let address;
|
|
3774
|
+
if (typeof params.address === 'string') {
|
|
3775
|
+
if (!parseAddress(params.address)) throw Object.assign(new Error(`not an address: ${params.address}`), { code: 400 });
|
|
3776
|
+
address = params.address;
|
|
3777
|
+
} else {
|
|
3778
|
+
const name = Array.from(String(params.from_name ?? 'supercode').trim(), (c) => /\s|@/.test(c) ? '-' : c).join('');
|
|
3779
|
+
if (!name) throw Object.assign(new Error('from_name must not be empty'), { code: 400 });
|
|
3780
|
+
address = makeAddress(machineName(this.env), 'operator', name);
|
|
3781
|
+
if (!address) throw Object.assign(new Error(`from_name is not a mailbox name: ${name}`), { code: 400 });
|
|
3782
|
+
}
|
|
3783
|
+
const all = params.all === true;
|
|
3784
|
+
const shown = (item, state) => { const envelope = this.mailStore.body(item) ?? item.envelope; return { state, envelope, rendered: render(envelope) }; };
|
|
3785
|
+
if (typeof params.address === 'string') {
|
|
3786
|
+
const { stored } = this.mailStore.inventory(address);
|
|
3787
|
+
return { address, messages: stored.filter((item) => all || item.state === 'unread').map((item) => shown(item, item.state)), looked: true };
|
|
3688
3788
|
}
|
|
3689
|
-
|
|
3690
|
-
const
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
return { code: 0,
|
|
3789
|
+
const read = await this.mail.inbox(address, { all, last: null, pid: process.pid, via: 'sessions.inbox' });
|
|
3790
|
+
const messages = [...(all ? read.stored.filter((item) => item.state === 'read').map((item) => shown(item, 'read')) : []), ...read.claimed.map((item) => shown(item, 'unread'))];
|
|
3791
|
+
this.mail.acknowledge(address, read.claimed);
|
|
3792
|
+
return { address, messages };
|
|
3793
|
+
}
|
|
3794
|
+
// `message waiting` and `message expired` (step 1): the carrier's own records
|
|
3795
|
+
case 'harness.v1.mail.waiting': case 'harness.v1.mail.expired': {
|
|
3796
|
+
if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('mail is read here by this machine\'s own processes'), { code: 403 });
|
|
3797
|
+
return { code: 0, rows: method === 'harness.v1.mail.waiting' ? this.carrier.waiting() : this.carrier.expired() };
|
|
3698
3798
|
}
|
|
3699
3799
|
// Who a local client is, from its claim (live-sessions.mjs callerOf): what a command acting on another machine sends
|
|
3700
3800
|
// 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.115",
|
|
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",
|