@volter/supercode-teams 0.3.196 → 0.3.198
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/README.md +2 -2
- package/machine.mjs +6 -20
- package/mail/send.mjs +16 -14
- package/mail/sqlite.mjs +7 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,8 +61,8 @@ Before the first SQLite start, `supercode teams mail migration-check --home /abs
|
|
|
61
61
|
reads that guard locally without a daemon connection or migration. Admission is not proof that the import will finish.
|
|
62
62
|
The [source boundary and pending qualification](../../docs/evidence/legacy-wrapper-migration-2026-10-08.md)
|
|
63
63
|
name the trusted legacy ABI, loss of later legacy diagnostics and existing retirement recovery.
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
Mail bodies are kept like their headers. Read headers whose body is not kept show an explicit placeholder and are
|
|
65
|
+
capped at 1,000,000; carrier/terminal event log tables keep 10,000 rows each. Read the
|
|
66
66
|
[dated qualification scope](../../docs/evidence/sqlite-mail-store-2026-10-08.md) before inferring platform support.
|
|
67
67
|
|
|
68
68
|
Native mail-history reads process the opened file's initial extent in 64 KiB blocks,
|
package/machine.mjs
CHANGED
|
@@ -84,7 +84,7 @@ import { WorkspaceContexts, fromContext } from '@volter/teams/context';
|
|
|
84
84
|
import { TeamsClient } from '@volter/teams/http-client';
|
|
85
85
|
import { supercodeHome } from '@volter/teams/home';
|
|
86
86
|
import { readMailHistory, machineName } from './mail-history.mjs';
|
|
87
|
-
import { randomBytes, createHash
|
|
87
|
+
import { randomBytes, createHash } from 'node:crypto';
|
|
88
88
|
import { WebSocket } from 'ws';
|
|
89
89
|
import { TmuxTerminalHost, classifyAttentionSignal, composerHoldsDraft, composerReadyForInput } from '@volter/supercode-terminal';
|
|
90
90
|
// The ConPTY host is read from the namespace, where a terminal package too old to have it gives undefined rather
|
|
@@ -909,30 +909,16 @@ export class TeamsMachine extends MachineCore {
|
|
|
909
909
|
const reading = await this.#serveRequest('harness.v1.sessions.activity_under', { pids: [session.rootPid] }, 4_000);
|
|
910
910
|
const native = reading?.activities?.find(row => row.pid === session.rootPid)?.activity;
|
|
911
911
|
if (native?.harness !== expected.harness || native.session_id !== expected.session) return { delivered: false, reason: 'the pane no longer runs the addressed native session; no input submitted' };
|
|
912
|
-
// A
|
|
913
|
-
//
|
|
914
|
-
//
|
|
912
|
+
// A board wake is never typed into a Claude /btw panel, and the daemon sends
|
|
913
|
+
// no keys to close it: the wake waits unread and the board reports the
|
|
914
|
+
// session's `dialog open` turn to the manager.
|
|
915
915
|
const boardWake = messageIds?.length && messageIds.every(id => {
|
|
916
916
|
const stored = this.mailStore.findIn(address, id);
|
|
917
917
|
const envelope = stored ? this.mailStore.body(stored) : null;
|
|
918
918
|
return envelope?.kind === 'notice' && parseAddress(envelope.from)?.harness === 'board';
|
|
919
919
|
});
|
|
920
|
-
if (boardWake && claudeSideQuestionPanel(await this.host.capture(session.id, { lines: 200, preserveEscapes: true }), native))
|
|
921
|
-
|
|
922
|
-
if (await this.host.acquireInput?.(session.id, owner)) {
|
|
923
|
-
try {
|
|
924
|
-
await new Promise(resolve => setTimeout(resolve, COMPOSER_RECHECK_MS));
|
|
925
|
-
const latest = await this.#serveRequest('harness.v1.sessions.activity_under', { pids: [session.rootPid] }, 4_000);
|
|
926
|
-
const current = latest?.activities?.find(row => row.pid === session.rootPid)?.activity;
|
|
927
|
-
const screen = await this.host.capture(session.id, { lines: 200, preserveEscapes: true });
|
|
928
|
-
if (!this.stopping && current?.session_id === expected.session && claudeSideQuestionPanel(screen, current)) {
|
|
929
|
-
await this.host.sendInput(session.id, '\u001b', { submitMode: 'single', inputOwner: owner, deadlineMs: Date.now() + 5_000 });
|
|
930
|
-
this.log.append({ principal: null, cap: 'control', target: `pane:${pane}`, outcome: 'board-side-question-dismissed', detail: JSON.stringify({ address, message_ids: messageIds }) });
|
|
931
|
-
await new Promise(resolve => setTimeout(resolve, COMPOSER_RECHECK_MS));
|
|
932
|
-
}
|
|
933
|
-
} finally { await this.host.releaseInput?.(session.id, owner); }
|
|
934
|
-
}
|
|
935
|
-
}
|
|
920
|
+
if (boardWake && claudeSideQuestionPanel(await this.host.capture(session.id, { lines: 200, preserveEscapes: true }), native))
|
|
921
|
+
return { delivered: false, reason: 'a Claude /btw panel is open on the pane; the board wake waits unread and no keys were sent' };
|
|
936
922
|
const result = await this.#paneTurn(session, pane, text, { messageIds, harness });
|
|
937
923
|
if (result.refused) throw new Error(result.refused.reason);
|
|
938
924
|
return result.waits ?? result;
|
package/mail/send.mjs
CHANGED
|
@@ -427,7 +427,8 @@ export class MailSend {
|
|
|
427
427
|
const [code, what] = {
|
|
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'],
|
|
431
432
|
native_idle: [0, 'it was idle, so the message starts its next turn'],
|
|
432
433
|
hooked: [0, 'queued in its mailbox; terminal delivery waits for native transcript confirmation'],
|
|
433
434
|
hook_woken: [0, 'it was idle, so its pane was told to read its mailbox; its hook shows it the message in that turn'],
|
|
@@ -497,15 +498,8 @@ export class MailSend {
|
|
|
497
498
|
if (held?.queued) return 'pane_held';
|
|
498
499
|
const busy = door.session.status !== 'idle' || door.session.panes_current === false;
|
|
499
500
|
if (!wake && !busy) throw new Refused('queue');
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
this.store.deliver(to, sent);
|
|
503
|
-
this.store.requestWake(to, sent.id, { keep: true });
|
|
504
|
-
const state = this.store.wakeState(to, sent.id);
|
|
505
|
-
this.store.setWakeState(to, sent.id, { ...state, waiting_for_idle: true, next_at_ms: Date.now() + 300_000 });
|
|
506
|
-
if (notifyWhenIdle && !cameThroughLink(sent)) this.store.subscribeIdle(to, { message_id: sent.id, subscriber: sent.from, notice: true, final_reply: false });
|
|
507
|
-
return 'native_waits';
|
|
508
|
-
}
|
|
501
|
+
// 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). Only the pane fallback below waits for idle, since a busy pane is never typed into
|
|
509
503
|
// its reply line names the door this session really has: its tools when it runs supercode's MCP server
|
|
510
504
|
const handed = sent.reply_via?.mode === 'command' && door.session.tools ? { ...sent, reply_via: { mode: 'tool' } } : sent;
|
|
511
505
|
const text = render(handed);
|
|
@@ -555,7 +549,15 @@ export class MailSend {
|
|
|
555
549
|
const received = this.store.claimOf(to, handed.id)?.recipient === true;
|
|
556
550
|
// the relay produced no receipt and cannot send it late (known not made, or not in the receiver's transcript after
|
|
557
551
|
// its bound, its queue gone): a session with a daemon pane has the same copy typed there instead
|
|
558
|
-
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) && busy) {
|
|
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)) {
|
|
559
561
|
this.log(`relay could not deliver ${shortId(handed.id)} to ${to} (${relayed.detail}); typing it into its pane ${door.pane}`);
|
|
560
562
|
this.relaying.add(key);
|
|
561
563
|
try { delivered = await this.#typeIntoPane(handed, to, door.pane, wake, attempt); }
|
|
@@ -573,7 +575,7 @@ export class MailSend {
|
|
|
573
575
|
const now = this.store.findIn(to, handed.id);
|
|
574
576
|
if (now?.state === 'unread') { try { this.store.markRead(to, now.path); } catch { /* moved meanwhile */ } }
|
|
575
577
|
}
|
|
576
|
-
delivered = 'native_idle';
|
|
578
|
+
delivered = busy ? 'native_busy' : 'native_idle';
|
|
577
579
|
}
|
|
578
580
|
} else if (door.kind === 'hook') {
|
|
579
581
|
attempt?.begin('hook-mailbox');
|
|
@@ -745,7 +747,7 @@ export class MailSend {
|
|
|
745
747
|
if (error instanceof Refused) throw new Error(`${error.bytes} bytes; the limit is ${MAX_RELAYED_BYTES}`);
|
|
746
748
|
throw error;
|
|
747
749
|
}
|
|
748
|
-
return { how, text: { steered: 'steered into its running turn', started: 'started a turn', native_waits: 'waits unread until its native turn is idle', 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] };
|
|
750
|
+
return { how, text: { steered: 'steered into its running turn', started: 'started a turn', native_busy: 'read at its next tool call', native_waits: 'waits unread until its native turn is idle', 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] };
|
|
749
751
|
}
|
|
750
752
|
|
|
751
753
|
// ---- a filing --------------------------------------------------------------------------------------------------
|
|
@@ -848,7 +850,7 @@ export class MailSend {
|
|
|
848
850
|
return {
|
|
849
851
|
delivered_to_bus: door.kind !== 'stored' && !['pane_waits', 'pane_held', 'native_waits'].includes(how), message_id: sent.id, reply_to: sender,
|
|
850
852
|
target: { harness, session_id: sessionId, name: door.kind === 'native' ? door.session.name.split('@')[0] : null },
|
|
851
|
-
delivery: { door: door.kind, how: { steered: 'steered', started: 'started', native_waits: 'waiting_for_idle', native_idle: 'started', hooked: 'hook', queued: 'queued', stored: 'stored', operator: 'filed', already: 'already', typed: 'typed', pane_held: 'held', pane_waits: 'stored' }[how] },
|
|
853
|
+
delivery: { door: door.kind, how: { steered: 'steered', started: 'started', native_busy: 'next_tool_call', native_waits: 'waiting_for_idle', native_idle: 'started', hooked: 'hook', queued: 'queued', stored: 'stored', operator: 'filed', already: 'already', typed: 'typed', pane_held: 'held', pane_waits: 'stored' }[how] },
|
|
852
854
|
};
|
|
853
855
|
}
|
|
854
856
|
|
package/mail/sqlite.mjs
CHANGED
|
@@ -10,12 +10,12 @@ import { RECORD_SCHEMA } from '../record-state.mjs';
|
|
|
10
10
|
|
|
11
11
|
export const MAIL_SCHEMA_VERSION = 3;
|
|
12
12
|
export const MAIL_RETENTION = Object.freeze({
|
|
13
|
-
|
|
13
|
+
relayTextDays: 30,
|
|
14
14
|
maxReadHeaders: 1_000_000,
|
|
15
15
|
maxCarrierRows: 10_000,
|
|
16
16
|
maxTerminalLogRows: 10_000,
|
|
17
17
|
});
|
|
18
|
-
export const RETAINED_BODY = `[this message's body was
|
|
18
|
+
export const RETAINED_BODY = `[this message's body was not kept; its header and thread are kept]`;
|
|
19
19
|
|
|
20
20
|
const SCHEMA = `
|
|
21
21
|
CREATE TABLE IF NOT EXISTS mail_metadata (
|
|
@@ -331,51 +331,24 @@ export class MailDatabase extends EventEmitter {
|
|
|
331
331
|
});
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
-
/**
|
|
335
|
-
*
|
|
334
|
+
/** Mail bodies are kept. Retention removes old relay scratch copies (their
|
|
335
|
+
* originals stay in the mailbox) and caps headers whose body is already gone. */
|
|
336
336
|
retain(now = Date.now()) {
|
|
337
|
-
const cutoff = now - this.retention.
|
|
337
|
+
const cutoff = now - this.retention.relayTextDays * 86_400_000;
|
|
338
338
|
return this.transaction(() => {
|
|
339
|
-
const expired = this.statement(`UPDATE messages SET body_digest = coalesce(body_digest, supercode_sha256(body)), body = NULL, first_line = '', body_expired_at_ms = ?
|
|
340
|
-
WHERE state = 'cur' AND created_at_ms < ? AND body IS NOT NULL
|
|
341
|
-
AND NOT EXISTS (SELECT 1 FROM deliveries d WHERE d.mailbox = messages.mailbox AND d.message = messages.id)
|
|
342
|
-
AND NOT EXISTS (SELECT 1 FROM subscriptions s WHERE s.mailbox = messages.mailbox AND s.message = messages.id)`)
|
|
343
|
-
.run(now, cutoff).changes;
|
|
344
|
-
// Completed native questions duplicate an envelope and its answer input.
|
|
345
|
-
// Keep the claim/binding/header so a repeat cannot submit another answer,
|
|
346
|
-
// but let go of those text copies after the mailbox copies have expired.
|
|
347
|
-
const questions = this.statement(`UPDATE mail_records AS r SET record = json_set(
|
|
348
|
-
json_remove(record, '$.native', '$.answers'), '$.envelope.body', ?, '$.body_expired_at_ms', ?)
|
|
349
|
-
WHERE collection = 'native_questions' AND json_type(record, '$.envelope') = 'object'
|
|
350
|
-
AND json_type(record, '$.body_expired_at_ms') IS NULL
|
|
351
|
-
AND json_extract(record, '$.envelope.created_at_ms') < ?
|
|
352
|
-
AND EXISTS (SELECT 1 FROM mail_records d WHERE d.collection = 'native_questions'
|
|
353
|
-
AND d.key = substr(r.key, 1, instr(r.key, '.') - 1) || '.delivered' AND CAST(d.record AS INTEGER) < ?)
|
|
354
|
-
AND NOT EXISTS (SELECT 1 FROM mail_records w WHERE w.collection = 'native_questions'
|
|
355
|
-
AND w.key = substr(r.key, 1, instr(r.key, '.') - 1) || '.waiting')
|
|
356
|
-
AND NOT EXISTS (SELECT 1 FROM messages m WHERE m.id = json_extract(r.record, '$.envelope.id') AND m.body IS NOT NULL)`)
|
|
357
|
-
.run(RETAINED_BODY, now, cutoff, cutoff).changes;
|
|
358
339
|
const relayRecords = this.statement(`DELETE FROM mail_records AS r WHERE collection = 'relays'
|
|
359
340
|
AND (key LIKE '%/turn.txt' OR key LIKE '%/receipt.json') AND updated_at_ms < ?
|
|
360
341
|
AND NOT EXISTS (SELECT 1 FROM mail_records q WHERE q.collection = 'relays'
|
|
361
342
|
AND q.key = substr(r.key, 1, instr(r.key, '/')) || 'queue.json')`).run(cutoff).changes;
|
|
362
|
-
const launchInputs = this.statement(`UPDATE mail_records SET record = json_set(
|
|
363
|
-
json_remove(record, '$.initialInput', '$.claudeInput.text'), '$.input_body_expired_at_ms', ?)
|
|
364
|
-
WHERE collection = 'native_launches' AND json_type(record, '$.input_body_expired_at_ms') IS NULL
|
|
365
|
-
AND json_type(record, '$.input_settled_at_ms') = 'integer'
|
|
366
|
-
AND json_extract(record, '$.input_settled_at_ms') < ?
|
|
367
|
-
AND (json_type(record, '$.initialInput') = 'text' OR json_type(record, '$.claudeInput.text') = 'text')`)
|
|
368
|
-
.run(now, cutoff).changes;
|
|
369
343
|
const removed = this.statement(`DELETE FROM messages WHERE (mailbox, id) IN (
|
|
370
344
|
SELECT mailbox, id FROM messages m WHERE state = 'cur' AND body_expired_at_ms IS NOT NULL
|
|
371
345
|
AND NOT EXISTS (SELECT 1 FROM deliveries d WHERE d.mailbox = m.mailbox AND d.message = m.id)
|
|
372
346
|
AND NOT EXISTS (SELECT 1 FROM subscriptions s WHERE s.mailbox = m.mailbox AND s.message = m.id)
|
|
373
347
|
ORDER BY created_at_ms DESC, id DESC LIMIT -1 OFFSET ?)`)
|
|
374
348
|
.run(this.retention.maxReadHeaders).changes;
|
|
375
|
-
const result = { at_ms: now,
|
|
376
|
-
removed_relay_text_records: relayRecords, expired_launch_inputs: launchInputs, removed_headers: removed, policy: this.retention };
|
|
349
|
+
const result = { at_ms: now, removed_relay_text_records: relayRecords, removed_headers: removed, policy: this.retention };
|
|
377
350
|
this.setMetadata('retention', result);
|
|
378
|
-
if (
|
|
351
|
+
if (relayRecords || removed) this.changed('retention', result);
|
|
379
352
|
return result;
|
|
380
353
|
});
|
|
381
354
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.198",
|
|
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",
|