@volter/supercode-teams 0.3.198 → 0.3.200
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/machine.mjs +1 -1
- package/mail/carrier.mjs +2 -15
- package/mail/send.mjs +9 -17
- package/mail/sqlite-store.mjs +1 -12
- package/package.json +1 -1
- package/terminal-mail.mjs +26 -8
package/machine.mjs
CHANGED
|
@@ -3729,7 +3729,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
3729
3729
|
if (config.adapter?.kind !== 'rh2') throw new Error('machine-owned voice needs a persistent meeting adapter; stdio runs in its caller');
|
|
3730
3730
|
const voiceEntry = this.env.SUPERCODE_VOICE_ENTRY || fileURLToPath(new URL('../voice/bin/voice.mjs', import.meta.url));
|
|
3731
3731
|
if (!existsSync(voiceEntry)) throw new Error('voice pack is not installed; configure SUPERCODE_VOICE_ENTRY');
|
|
3732
|
-
const child = spawn(process.execPath, [voiceEntry, 'run', '--config', configPath], { env: { ...this.env, SUPERCODE_BIN: this.supercodeBin, SUPERCODE_CALLER: params.session }, stdio: ['ignore', 'ignore', 'ignore'] });
|
|
3732
|
+
const child = spawn(process.execPath, [voiceEntry, 'run', '--config', configPath], { env: { ...this.env, SUPERCODE_BIN: this.supercodeBin, SUPERCODE_CALLER: params.session }, stdio: ['ignore', 'ignore', 'ignore'], windowsHide: true });
|
|
3733
3733
|
this.voiceProcesses.set(params.session, child);
|
|
3734
3734
|
const ended = () => { if (this.voiceProcesses.get(params.session) === child) this.voiceProcesses.delete(params.session); };
|
|
3735
3735
|
child.once('error', ended); child.once('exit', ended);
|
package/mail/carrier.mjs
CHANGED
|
@@ -53,12 +53,6 @@ export class MailCarrier {
|
|
|
53
53
|
start() {
|
|
54
54
|
this.onWake = () => this.#schedule();
|
|
55
55
|
this.store.on('wake', this.onWake);
|
|
56
|
-
this.onLive = () => {
|
|
57
|
-
if (this.stopped) return;
|
|
58
|
-
try { this.store.wakeIdleSessions(this.live.sessions().filter((row) => row.status === 'idle').map((row) => row.address)); }
|
|
59
|
-
catch (error) { this.log(`carrier: idle waits could not be resumed: ${error.message}`); }
|
|
60
|
-
};
|
|
61
|
-
this.live.on('changed', this.onLive);
|
|
62
56
|
this.#sweep();
|
|
63
57
|
this.sweeper = setInterval(() => this.#sweep(), LAST_RETRY_MS);
|
|
64
58
|
this.sweeper.unref?.();
|
|
@@ -74,7 +68,6 @@ export class MailCarrier {
|
|
|
74
68
|
stop() {
|
|
75
69
|
this.stopped = true;
|
|
76
70
|
this.store.off('wake', this.onWake);
|
|
77
|
-
this.live.off('changed', this.onLive);
|
|
78
71
|
clearInterval(this.sweeper); clearTimeout(this.timer);
|
|
79
72
|
this.timer = null; this.timerAt = null;
|
|
80
73
|
}
|
|
@@ -135,7 +128,7 @@ export class MailCarrier {
|
|
|
135
128
|
let due = this.store.dueWakes(address, now);
|
|
136
129
|
if (!due.length) return;
|
|
137
130
|
// a session in a daemon pane whose mail is typed there: a hooked one (its hook shows it the rest), and one with no
|
|
138
|
-
// hook and no relay (Grok), whose mail that found its composer
|
|
131
|
+
// hook and no relay (Grok), whose mail that found a draft in its composer waits here until it is free
|
|
139
132
|
const session = this.live.sessions().find((row) => row.address === address);
|
|
140
133
|
const pane = session ? this.paneOf(session) : null;
|
|
141
134
|
const users = due.filter((id) => this.store.findIn(address, id)?.envelope.kind === 'user');
|
|
@@ -198,8 +191,7 @@ export class MailCarrier {
|
|
|
198
191
|
new Promise((settle) => { bound = setTimeout(() => settle({ outcome: 'later', detail: `its hand-over was still running after ${HANDOVER_BOUND_MS / 1000} s` }), HANDOVER_BOUND_MS); bound.unref?.(); }),
|
|
199
192
|
]).finally(() => clearTimeout(bound));
|
|
200
193
|
carried = answer?.outcome === 'delivered' ? { kind: 'delivered' } : answer?.outcome === 'filed' ? { kind: 'filed' }
|
|
201
|
-
: answer?.outcome === '
|
|
202
|
-
: answer?.outcome === 'pending' ? { kind: 'pending', reason: answer.detail } : { kind: 'later', reason: answer?.detail ?? 'not handed over' };
|
|
194
|
+
: answer?.outcome === 'pending' ? { kind: 'pending', reason: answer.detail } : { kind: 'later', reason: answer?.detail ?? 'not handed over' };
|
|
203
195
|
} catch (error) { carried = { kind: 'later', reason: `this machine's daemon did not hand it over: ${error.message}` }; }
|
|
204
196
|
}
|
|
205
197
|
if (this.stopped) return;
|
|
@@ -325,11 +317,6 @@ export class MailCarrier {
|
|
|
325
317
|
this.store.setWakeState(address, id, { ...state, next_at_ms: Date.now() + FIRST_RETRY_MS, last_error: carried.reason });
|
|
326
318
|
return;
|
|
327
319
|
}
|
|
328
|
-
if (carried.kind === 'waiting') {
|
|
329
|
-
const state = this.store.wakeState(address, id);
|
|
330
|
-
this.store.setWakeState(address, id, { ...state, waiting_for_idle: true, next_at_ms: Date.now() + LAST_RETRY_MS });
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
320
|
const now = Date.now();
|
|
334
321
|
const state = this.store.wakeState(address, id);
|
|
335
322
|
const envelope = this.store.findIn(address, id)?.envelope;
|
package/mail/send.mjs
CHANGED
|
@@ -31,7 +31,7 @@ const CODEX_KNOWN_WAIT_MS = 20_000;
|
|
|
31
31
|
const RELAY_GRACE_MS = 30_000;
|
|
32
32
|
/** The kinds of delivery that hand nothing over (#deliverWoken's `how`): the copy waits filed. An operator's mailbox,
|
|
33
33
|
* which its operator reads, and a session's hook, which shows it, are deliveries. */
|
|
34
|
-
const LEFT_FILED = new Set(['queued', 'stored', 'pane_held', 'pane_waits', '
|
|
34
|
+
const LEFT_FILED = new Set(['queued', 'stored', 'pane_held', 'pane_waits', 'not_running']);
|
|
35
35
|
|
|
36
36
|
/** Replies in one unbroken chain between two sessions beyond which another is refused. */
|
|
37
37
|
const REPLY_CHAIN_LIMIT = 32;
|
|
@@ -428,7 +428,6 @@ export class MailSend {
|
|
|
428
428
|
steered: [0, 'steered into its running turn'],
|
|
429
429
|
started: [0, 'it was idle, so the message started a turn'],
|
|
430
430
|
native_busy: [0, 'it will read it at its next tool call'],
|
|
431
|
-
native_waits: [EXIT_STORED, 'Stored: its relay could not deliver it and its session is busy, so its pane fallback waits unread until it is idle. Do not resend'],
|
|
432
431
|
native_idle: [0, 'it was idle, so the message starts its next turn'],
|
|
433
432
|
hooked: [0, 'queued in its mailbox; terminal delivery waits for native transcript confirmation'],
|
|
434
433
|
hook_woken: [0, 'it was idle, so its pane was told to read its mailbox; its hook shows it the message in that turn'],
|
|
@@ -499,7 +498,8 @@ export class MailSend {
|
|
|
499
498
|
const busy = door.session.status !== 'idle' || door.session.panes_current === false;
|
|
500
499
|
if (!wake && !busy) throw new Refused('queue');
|
|
501
500
|
// a busy session is handed it at once, as an idle one is: Claude Code's own cross-session message surfaces at the
|
|
502
|
-
// receiver's next tool call (D391).
|
|
501
|
+
// receiver's next tool call (D391). Its pane fallback below types it at once too: a prompt submitted mid-turn is queued
|
|
502
|
+
// and absorbed into the running turn
|
|
503
503
|
// its reply line names the door this session really has: its tools when it runs supercode's MCP server
|
|
504
504
|
const handed = sent.reply_via?.mode === 'command' && door.session.tools ? { ...sent, reply_via: { mode: 'tool' } } : sent;
|
|
505
505
|
const text = render(handed);
|
|
@@ -549,15 +549,7 @@ export class MailSend {
|
|
|
549
549
|
const received = this.store.claimOf(to, handed.id)?.recipient === true;
|
|
550
550
|
// the relay produced no receipt and cannot send it late (known not made, or not in the receiver's transcript after
|
|
551
551
|
// its bound, its queue gone): a session with a daemon pane has the same copy typed there instead
|
|
552
|
-
if (!relayed.delivered && !received && wake && door.pane && (!relayed.unknown || relayed.landed === false)
|
|
553
|
-
// its pane is typed into only once its turn is idle: the copy waits unread, its wake resumed when it goes idle
|
|
554
|
-
// (carrier.mjs), which hands it to the relay again first
|
|
555
|
-
this.log(`relay could not deliver ${shortId(handed.id)} to busy ${to} (${relayed.detail}); its pane fallback waits for idle`);
|
|
556
|
-
try { this.store.requestWake(to, handed.id, { keep: true }); } catch { /* the wake filed above stands */ }
|
|
557
|
-
const state = this.store.wakeState(to, handed.id);
|
|
558
|
-
this.store.setWakeState(to, handed.id, { ...state, waiting_for_idle: true, next_at_ms: Date.now() + 300_000 });
|
|
559
|
-
delivered = 'native_waits';
|
|
560
|
-
} else if (!relayed.delivered && !received && wake && door.pane && (!relayed.unknown || relayed.landed === false)) {
|
|
552
|
+
if (!relayed.delivered && !received && wake && door.pane && (!relayed.unknown || relayed.landed === false)) {
|
|
561
553
|
this.log(`relay could not deliver ${shortId(handed.id)} to ${to} (${relayed.detail}); typing it into its pane ${door.pane}`);
|
|
562
554
|
this.relaying.add(key);
|
|
563
555
|
try { delivered = await this.#typeIntoPane(handed, to, door.pane, wake, attempt); }
|
|
@@ -610,7 +602,7 @@ export class MailSend {
|
|
|
610
602
|
}
|
|
611
603
|
// held by the daemon's queue for the pane, typed once its composer frees: not typed yet, so kept unread
|
|
612
604
|
if (typed?.queued) { this.store.deliver(to, sent); if (wake) this.store.requestWake(to, sent.id, { keep: true }); return 'pane_held'; }
|
|
613
|
-
// not typed (its composer held a draft or
|
|
605
|
+
// not typed (its composer held a draft, or showed none): it waits unread with a wake, so the carrier tries it again once
|
|
614
606
|
// the composer is empty (mail/carrier.mjs), never left for a read a session with no hook never makes
|
|
615
607
|
this.store.deliver(to, sent);
|
|
616
608
|
if (!wake) return 'queued';
|
|
@@ -747,7 +739,7 @@ export class MailSend {
|
|
|
747
739
|
if (error instanceof Refused) throw new Error(`${error.bytes} bytes; the limit is ${MAX_RELAYED_BYTES}`);
|
|
748
740
|
throw error;
|
|
749
741
|
}
|
|
750
|
-
return { how, text: { steered: 'steered into its running turn', started: 'started a turn', native_busy: 'read at its next tool call',
|
|
742
|
+
return { how, text: { steered: 'steered into its running turn', started: 'started a turn', native_busy: 'read at its next tool call', native_idle: 'starts its next turn', hooked: 'in its mailbox, its hook shows it', queued: 'waits in its mailbox', operator: 'filed', already: 'it already had it', stored: 'stored; it has no delivery door', typed: 'typed into its pane', pane_held: 'held for its pane until its composer is free', pane_waits: 'waits in its mailbox (its composer was not empty)' }[how] };
|
|
751
743
|
}
|
|
752
744
|
|
|
753
745
|
// ---- a filing --------------------------------------------------------------------------------------------------
|
|
@@ -848,9 +840,9 @@ export class MailSend {
|
|
|
848
840
|
return refused('delivery_failed', error.message);
|
|
849
841
|
}
|
|
850
842
|
return {
|
|
851
|
-
delivered_to_bus: door.kind !== 'stored' && !['pane_waits', 'pane_held'
|
|
843
|
+
delivered_to_bus: door.kind !== 'stored' && !['pane_waits', 'pane_held'].includes(how), message_id: sent.id, reply_to: sender,
|
|
852
844
|
target: { harness, session_id: sessionId, name: door.kind === 'native' ? door.session.name.split('@')[0] : null },
|
|
853
|
-
delivery: { door: door.kind, how: { steered: 'steered', started: 'started', native_busy: 'next_tool_call',
|
|
845
|
+
delivery: { door: door.kind, how: { steered: 'steered', started: 'started', native_busy: 'next_tool_call', native_idle: 'started', hooked: 'hook', queued: 'queued', stored: 'stored', operator: 'filed', already: 'already', typed: 'typed', pane_held: 'held', pane_waits: 'stored' }[how] },
|
|
854
846
|
};
|
|
855
847
|
}
|
|
856
848
|
|
|
@@ -1038,7 +1030,7 @@ export class MailSend {
|
|
|
1038
1030
|
try {
|
|
1039
1031
|
const how = await this.route(sent, address, door, true, false);
|
|
1040
1032
|
// handed to the session (or already its): moved to cur/ (one only queued for a runtime not woken would stay unread)
|
|
1041
|
-
if (LEFT_FILED.has(how)) return { outcome: how === '
|
|
1033
|
+
if (LEFT_FILED.has(how)) return { outcome: how === 'pane_held' ? 'pending' : 'later', detail: how === 'pane_waits' ? this.store.wakeState(address, id).last_error : how };
|
|
1042
1034
|
const now = this.store.findIn(address, id);
|
|
1043
1035
|
if (how !== 'queued' && now?.state === 'unread') this.store.markRead(address, now.path);
|
|
1044
1036
|
return { outcome: 'delivered', detail: null };
|
package/mail/sqlite-store.mjs
CHANGED
|
@@ -472,7 +472,7 @@ export class SqliteMailStore extends EventEmitter {
|
|
|
472
472
|
}
|
|
473
473
|
setWakeState(address, id, state) {
|
|
474
474
|
const record = { attempts: state.attempts ?? 0, next_at_ms: state.next_at_ms ?? 0, last_error: state.last_error ?? null,
|
|
475
|
-
handover_at_ms: state.handover_at_ms ?? null, failing_since_ms: state.failing_since_ms ?? null
|
|
475
|
+
handover_at_ms: state.handover_at_ms ?? null, failing_since_ms: state.failing_since_ms ?? null };
|
|
476
476
|
this.transaction(() => {
|
|
477
477
|
if (this.statement('UPDATE deliveries SET record = ?, due_at_ms = ?, attempts = ? WHERE mailbox = ? AND message = ?')
|
|
478
478
|
.run(JSON.stringify(record), record.next_at_ms, record.attempts, address, id).changes) {
|
|
@@ -519,17 +519,6 @@ export class SqliteMailStore extends EventEmitter {
|
|
|
519
519
|
});
|
|
520
520
|
}
|
|
521
521
|
|
|
522
|
-
/** A live idle transition wakes only the native waits for those addresses. */
|
|
523
|
-
wakeIdleSessions(addresses) {
|
|
524
|
-
this.transaction(() => {
|
|
525
|
-
const changed = this.statement(`UPDATE deliveries SET due_at_ms = 0, record = json_set(record, '$.next_at_ms', 0)
|
|
526
|
-
WHERE mailbox IN (SELECT value FROM json_each(?)) AND json_extract(record, '$.waiting_for_idle') = 1
|
|
527
|
-
RETURNING mailbox`).all(JSON.stringify(addresses));
|
|
528
|
-
for (const address of new Set(changed.map((row) => row.mailbox))) this.refreshDeliveryTarget('mailbox', address);
|
|
529
|
-
if (changed.length) this.database.changed('wake', {});
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
|
|
533
522
|
nextDeliveryTarget(excluded = []) {
|
|
534
523
|
return this.statement(`SELECT t.kind, t.target, t.due_at_ms FROM delivery_targets t
|
|
535
524
|
WHERE NOT EXISTS (SELECT 1 FROM json_each(?) e WHERE json_extract(e.value, '$.kind') = t.kind AND json_extract(e.value, '$.target') = t.target)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.200",
|
|
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",
|
package/terminal-mail.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
// Per-recipient terminal mailbox.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Per-recipient terminal mailbox. Mail is typed and submitted at once, into an idle or a busy session alike: a prompt
|
|
2
|
+
// submitted mid-turn is queued by Claude Code and absorbed into the running turn, and held by Codex and written as a
|
|
3
|
+
// user message in that same turn. Only a composer holding a person's unsent text is waited for. A retained paste is
|
|
4
|
+
// never repeated: retries press Enter and look for the exact message in the native transcript (a user row, or the
|
|
5
|
+
// queued_command attachment a running Claude Code turn absorbs it as).
|
|
4
6
|
import { statSync, openSync, readSync, closeSync } from 'node:fs';
|
|
5
7
|
import { createHash, randomUUID } from 'node:crypto';
|
|
6
8
|
import { spawnSync } from 'node:child_process';
|
|
@@ -70,6 +72,10 @@ export function confirmed(batch) {
|
|
|
70
72
|
const decoder = new StringDecoder('utf8');
|
|
71
73
|
const bytes = Buffer.alloc(64 * 1024);
|
|
72
74
|
let matched = 0, fragments = 0;
|
|
75
|
+
// Claude Code holds a prompt submitted mid-turn in its queue (queue-operation enqueue, with its content) until the
|
|
76
|
+
// turn absorbs it or the next turn takes it (remove/dequeue). While held it is not yet delivered, but it will land:
|
|
77
|
+
// the batch is kept, never refused at its deadline, until the transcript shows it or the queue lets it go.
|
|
78
|
+
if (batch.kind === 'claude-code') batch.claudeQueued = false;
|
|
73
79
|
for (;;) {
|
|
74
80
|
const count = readSync(fd, bytes, 0, bytes.length, offset);
|
|
75
81
|
if (!count) return false;
|
|
@@ -78,6 +84,11 @@ export function confirmed(batch) {
|
|
|
78
84
|
while ((end = buffered.indexOf('\n')) >= 0) {
|
|
79
85
|
const line = buffered.slice(0, end); buffered = buffered.slice(end + 1);
|
|
80
86
|
let row; try { row = JSON.parse(line); } catch { continue; }
|
|
87
|
+
if (batch.kind === 'claude-code' && row.type === 'queue-operation' && (!batch.sessionId || row.sessionId === batch.sessionId)) {
|
|
88
|
+
if (row.operation === 'enqueue' && typeof row.content === 'string' && claudeSame(row.content, batch.text)) batch.claudeQueued = true;
|
|
89
|
+
else if (row.operation !== 'enqueue') batch.claudeQueued = false;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
81
92
|
// A prompt submitted while a turn runs is queued, and reaches the model as a `queued_command` attachment when
|
|
82
93
|
// the turn absorbs it (queue-operation remove, reason absorbed_mid_turn); no `user` row is written for it.
|
|
83
94
|
// That attachment is its delivery. An enqueue alone is not: a queued prompt can still be withdrawn.
|
|
@@ -174,7 +185,7 @@ export class TerminalMailbox {
|
|
|
174
185
|
const own = claudeHolds(batch, shown, true);
|
|
175
186
|
// One final read/Enter at the deadline, then a bounded native receipt
|
|
176
187
|
// grace. A badge that painted late never goes straight to refusal.
|
|
177
|
-
if (own && !batch.confirmationError && !batch.finalEnter
|
|
188
|
+
if (own && !batch.confirmationError && !batch.finalEnter) {
|
|
178
189
|
batch.finalEnter = true; batch.phase = 'pasted'; batch.expiresAt = Date.now() + 5000;
|
|
179
190
|
batch.enters++; batch.last_enter = Date.now(); this.save();
|
|
180
191
|
await this.host.sendInput(this.session.id, '\r', { submitMode: 'single', inputOwner: batch.inputOwner });
|
|
@@ -295,6 +306,11 @@ export class TerminalMailbox {
|
|
|
295
306
|
if (batch.kind === 'claude-code' && batch.phase === 'prepared' && batch.expiresAt && Date.now() >= batch.expiresAt) {
|
|
296
307
|
await this.recordRefusal(batch, 'delivery deadline elapsed; no input pasted'); return;
|
|
297
308
|
}
|
|
309
|
+
if (batch.kind === 'claude-code' && batch.phase !== 'refusing' && batch.claudeQueued) {
|
|
310
|
+
// submitted and held in Claude Code's own queue: it lands when the running turn absorbs it, so it is never
|
|
311
|
+
// refused, pressed again or cleaned up meanwhile
|
|
312
|
+
batch.error = 'queued in its running turn; native transcript confirmation pending'; this.save(); return;
|
|
313
|
+
}
|
|
298
314
|
if (batch.kind === 'claude-code' && (batch.phase === 'refusing' || (batch.expiresAt && Date.now() >= batch.expiresAt))) {
|
|
299
315
|
await this.refuseClaude(batch); return;
|
|
300
316
|
}
|
|
@@ -311,12 +327,13 @@ export class TerminalMailbox {
|
|
|
311
327
|
} catch (error) {
|
|
312
328
|
await this.recordRefusal(batch, `input ownership unavailable (${error.code ?? error.message}); no input pasted`); return;
|
|
313
329
|
}
|
|
330
|
+
// A running turn is no reason to hold: only a person's unsent text (or a screen with no composer) is.
|
|
314
331
|
for (let check = 0; check < 2; check++) {
|
|
315
332
|
const screen = await this.host.capture(this.session.id, { lines: 200, preserveEscapes: true });
|
|
316
333
|
const held = composerText(batch.kind, screen);
|
|
317
|
-
if (held !== ''
|
|
334
|
+
if (held !== '') {
|
|
318
335
|
await this.host.releaseInput(this.session.id, batch.inputOwner);
|
|
319
|
-
batch.error = held == null ? 'no-composer' :
|
|
336
|
+
batch.error = held == null ? 'no-composer' : 'draft'; this.save(); return;
|
|
320
337
|
}
|
|
321
338
|
if (check === 0) await pause(200);
|
|
322
339
|
}
|
|
@@ -357,8 +374,9 @@ export class TerminalMailbox {
|
|
|
357
374
|
const screen = await this.host.capture(this.session.id, { lines: 200, preserveEscapes: true });
|
|
358
375
|
// Re-read until the deadline: a Windows paste badge can appear well
|
|
359
376
|
// after one second. Ownership is held by the pane host, across daemon
|
|
360
|
-
// restart, from before the empty reads through receipt/refusal.
|
|
361
|
-
|
|
377
|
+
// restart, from before the empty reads through receipt/refusal. A
|
|
378
|
+
// running turn takes the Enter too: it queues the prompt.
|
|
379
|
+
if (!claudeHolds(batch, composerText(batch.kind, screen), true)) {
|
|
362
380
|
batch.error = 'composer changed or unreadable; native transcript confirmation pending'; this.save(); return;
|
|
363
381
|
}
|
|
364
382
|
}
|