@volter/supercode-teams 0.2.150 → 0.2.152
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/fleet.mjs +1 -0
- package/bin/teams.mjs +3 -3
- package/machine.mjs +55 -12
- package/package.json +1 -1
- package/terminal-mail.mjs +120 -0
package/bin/fleet.mjs
CHANGED
|
@@ -30,6 +30,7 @@ for (let i = 0; i < argv.length; i += 1) {
|
|
|
30
30
|
const eq = a.indexOf('=');
|
|
31
31
|
if (eq > 0) { flags[a.slice(2, eq)] = a.slice(eq + 1); continue; } // --args=--norc
|
|
32
32
|
const key = a.slice(2);
|
|
33
|
+
if (key === 'act-for-caller') { flags[key] = true; continue; }
|
|
33
34
|
const next = argv[i + 1];
|
|
34
35
|
if (next !== undefined && !next.startsWith('--')) { flags[key] = next; i += 1; } else flags[key] = true;
|
|
35
36
|
} else positional.push(a);
|
package/bin/teams.mjs
CHANGED
|
@@ -47,7 +47,7 @@ import { NODE_PROTOCOL } from '@volter/teams/channel';
|
|
|
47
47
|
|
|
48
48
|
const MACHINE_VERBS = new Set(['machine', 'describe', 'panes', 'input', 'suspend', 'resume', 'files', 'ports', 'port', 'browser', 'stream', 'jobs', 'workspace', 'title', 'attention', 'watch', 'rpc', 'launches', 'log', 'mail']);
|
|
49
49
|
// Every flag a machine verb reads; any other is refused rather than silently ignored.
|
|
50
|
-
const KNOWN_FLAGS = new Set(['home', 'machine', 'context', 'floor', 'supercode', 'tmux-socket', 'verbose', 'lines', 'input', 'name', 'for', 'urgency', 'reason', 'interrupt', 'query', 'out', 'in', 'interval', 'no-enter', 'when-composer-empty', 'key', 'listen', 'list', 'from', 'to', 'path', 'to-path', 'repo', 'branch', 'solo', 'card', 'create', 'presence-only', 'clear-draft']);
|
|
50
|
+
const KNOWN_FLAGS = new Set(['home', 'machine', 'context', 'floor', 'supercode', 'tmux-socket', 'verbose', 'lines', 'input', 'name', 'for', 'urgency', 'reason', 'interrupt', 'query', 'out', 'in', 'interval', 'no-enter', 'when-composer-empty', 'key', 'listen', 'list', 'from', 'to', 'path', 'to-path', 'repo', 'branch', 'solo', 'card', 'create', 'presence-only', 'clear-draft', 'message-ids']);
|
|
51
51
|
|
|
52
52
|
const argv = process.argv.slice(2);
|
|
53
53
|
const flags = {};
|
|
@@ -237,8 +237,8 @@ async function main() {
|
|
|
237
237
|
if (flags.key === true || rest.length) die('input <pane> --key NAMES: keys by tmux name, space- or comma-separated (Escape, Enter, Tab, BTab, BSpace, Space, Up, Down, Left, Right, Home, End, PPage, NPage, DC, C-a … C-z)');
|
|
238
238
|
return withNode(async (rpc) => out(await rpc.call('panes.input', { pane: sub, text: keyBytes(String(flags.key)), submit: false, keys: true, caller: caller() })));
|
|
239
239
|
}
|
|
240
|
-
if (flags['when-composer-empty']) return withNode(async (rpc) => out(await rpc.call('panes.turn', { pane: sub, text: rest.join(' '), caller: caller() })));
|
|
241
|
-
return withNode(async (rpc) => out(await rpc.call('panes.input', { pane: sub, text: rest.join(' '), submit: !flags['no-enter'], caller: caller() })));
|
|
240
|
+
if (flags['when-composer-empty']) return withNode(async (rpc) => out(await rpc.call('panes.turn', { pane: sub, text: rest.join(' '), message_ids: flags['message-ids'] ? String(flags['message-ids']).split(',') : undefined, caller: caller() })));
|
|
241
|
+
return withNode(async (rpc) => out(await rpc.call('panes.input', { pane: sub, text: rest.join(' '), submit: !flags['no-enter'], message_ids: flags['message-ids'] ? String(flags['message-ids']).split(',') : undefined, caller: caller() })));
|
|
242
242
|
case 'port': {
|
|
243
243
|
// `port <pane>/<n> [--listen host:port]`: a machine port forwarded here. Stays up until ctrl-c.
|
|
244
244
|
const spec = String(sub ?? '');
|
package/machine.mjs
CHANGED
|
@@ -50,6 +50,7 @@ import { request as httpRequest } from 'node:http';
|
|
|
50
50
|
import { DEFAULT_RANK, normalizeRank, judge } from './attention.mjs';
|
|
51
51
|
import { popupTerminal, openPopup } from './popup-terminals.mjs';
|
|
52
52
|
import { JobOwner, executionCapabilities, streamProgram, serveWorkspace } from './execution.mjs';
|
|
53
|
+
import { TerminalMailbox, retainedTerminalMail } from './terminal-mail.mjs';
|
|
53
54
|
import { confirmNativeInitialInput } from './native-initial-input.mjs';
|
|
54
55
|
import { canonicalCodexFolderTrust, settleCodexHookReview } from './codex-startup.mjs';
|
|
55
56
|
|
|
@@ -197,6 +198,13 @@ export class TeamsMachine extends MachineCore {
|
|
|
197
198
|
// longer than the caller's own RPC timeout — so the wait lives here, where
|
|
198
199
|
// `stop()` can abandon it rather than leave a timer running past the daemon.
|
|
199
200
|
this.voiceProcesses = new Map(); // session address -> optional voice pack child
|
|
201
|
+
this.terminalMailboxes = new Map();
|
|
202
|
+
this.terminalMailRoot = join(this.env.SUPERCODE_HOME || join(this.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'supercode'), 'mail');
|
|
203
|
+
this.terminalMailTimer = setInterval(() => {
|
|
204
|
+
if (this.terminalMailWork) return;
|
|
205
|
+
this.terminalMailWork = this.#drainTerminalMail().catch(error => this.emit('mail-watch-stderr', `terminal mailbox: ${error.message}`)).finally(() => { this.terminalMailWork = null; });
|
|
206
|
+
}, 1000);
|
|
207
|
+
this.terminalMailTimer.unref?.();
|
|
200
208
|
this.inputInFlight = new Set(); // one input transaction per pane
|
|
201
209
|
this.inputRepairsPath = join(this.home, 'input-repairs.json');
|
|
202
210
|
this.inputRepairs = readJson(this.inputRepairsPath, {});
|
|
@@ -229,6 +237,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
229
237
|
}
|
|
230
238
|
|
|
231
239
|
async beforeClose() {
|
|
240
|
+
clearInterval(this.terminalMailTimer); await this.terminalMailWork;
|
|
232
241
|
clearInterval(this.inputRepairTimer);this.inputRepairTimer=null;
|
|
233
242
|
await this.inputRepairWork;
|
|
234
243
|
clearInterval(this.endWatch); this.endWatch = null;
|
|
@@ -894,13 +903,46 @@ export class TeamsMachine extends MachineCore {
|
|
|
894
903
|
this.pendingInitialInputs.set(key, { abort, task });
|
|
895
904
|
}
|
|
896
905
|
|
|
906
|
+
async #drainTerminalMail() {
|
|
907
|
+
for (const saved of retainedTerminalMail(this.terminalMailRoot)) {
|
|
908
|
+
if (this.terminalMailboxes.has(saved.recipient) || !saved.binding) continue;
|
|
909
|
+
const session = await this.#paneByContext(saved.binding.context);
|
|
910
|
+
if (!session || session.id !== saved.binding.terminal || session.rootPid !== saved.binding.pid) continue;
|
|
911
|
+
const queue = new TerminalMailbox(this.terminalMailRoot, saved.recipient, receipt => this.log.append({ principal: null, cap: 'control',
|
|
912
|
+
target: `pane:${saved.binding.context}`, outcome: receipt.delivered ? 'input-delivered' : 'input-pending', detail: JSON.stringify(receipt) }), (locator, options) => this.#serveRequest('harness.v1.sessions.load', { locator, options: { ...options, include_subagents: false } }, 10000));
|
|
913
|
+
queue.identity = saved.binding.identity; queue.host = this.host; queue.session = session;
|
|
914
|
+
this.terminalMailboxes.set(saved.recipient, queue);
|
|
915
|
+
}
|
|
916
|
+
await Promise.all([...this.terminalMailboxes.values()].filter(queue => !this.inputRepairs[queue.state.binding?.context] && !this.inputInFlight.has(queue.session?.id)).map(queue => queue.kick()));
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
async #terminalMail(session, contextKey, kind, text, options) {
|
|
920
|
+
const activity = await this.#serveRequest('harness.v1.sessions.activity_under', { pids: [session.rootPid] });
|
|
921
|
+
const native = activity?.activities?.find(row => row.pid === session.rootPid)?.activity;
|
|
922
|
+
if (!native?.session_id) return { delivered: false, reason: 'native-session-unresolved' };
|
|
923
|
+
const found = await this.#serveRequest('harness.v1.sessions.discover', { query: native.session_id, harnesses: [kind], limit: 20 });
|
|
924
|
+
const row = found?.sessions?.find(row => row.locator?.session_id === native.session_id);
|
|
925
|
+
const path = row?.locator?.storage?.path;
|
|
926
|
+
if (!row?.locator || (kind === 'codex' && !path)) return { delivered: false, reason: 'native-transcript-unresolved' };
|
|
927
|
+
const key = `${kind}:${native.session_id}`;
|
|
928
|
+
let queue = this.terminalMailboxes.get(key);
|
|
929
|
+
if (!queue) {
|
|
930
|
+
queue = new TerminalMailbox(this.terminalMailRoot, key, receipt => this.log.append({ principal: null, cap: 'control', target: `pane:${contextKey}`,
|
|
931
|
+
outcome: receipt.delivered ? 'input-delivered' : 'input-pending', detail: JSON.stringify(receipt) }), (locator, options) => this.#serveRequest('harness.v1.sessions.load', { locator, options: { ...options, include_subagents: false } }, 10000));
|
|
932
|
+
this.terminalMailboxes.set(key, queue);
|
|
933
|
+
}
|
|
934
|
+
const ids = queue.enqueue(text, options.messageIds, { kind, path, locator: row.locator }, this.host, session);
|
|
935
|
+
return queue.receipt(ids);
|
|
936
|
+
}
|
|
937
|
+
|
|
897
938
|
async #deliverPaneText(session, contextKey, text, options = {}) {
|
|
939
|
+
const kind = this.#paneKind(contextKey, session);
|
|
940
|
+
if (['codex', 'grok', 'gemini', 'goose', 'opencode', 'pi', 'hermes', 'openclaw'].includes(kind)) return this.#terminalMail(session, contextKey, kind, submission(text, false), options);
|
|
898
941
|
if(this.inputRepairs[contextKey])return {delivered:false,reason:'input-cleanup-pending',cleanup_required:true};
|
|
899
942
|
if (this.inputInFlight.has(session.id)) return { delivered: false, reason: 'input-in-flight' };
|
|
900
943
|
this.inputInFlight.add(session.id);
|
|
901
944
|
try {
|
|
902
|
-
|
|
903
|
-
if (kind === 'claude-code' || kind === 'codex') {
|
|
945
|
+
if (kind === 'claude-code') {
|
|
904
946
|
if (typeof terminal.deliverComposerInput !== 'function') return { delivered: false, reason: 'terminal-package-too-old' };
|
|
905
947
|
const result=await terminal.deliverComposerInput(this.host, session.id, kind, submission(text, false), options);
|
|
906
948
|
if(result.cleanup_required){
|
|
@@ -1542,15 +1584,16 @@ export class TeamsMachine extends MachineCore {
|
|
|
1542
1584
|
if (this.inputInFlight.has(session.id)) return { delivered: false, reason: 'input-in-flight' };
|
|
1543
1585
|
let result;
|
|
1544
1586
|
if (params.keys || params.submit === false) {
|
|
1587
|
+
if (!params.keys && [...this.terminalMailboxes.values()].some(queue => queue.session?.id === session.id && (queue.state.active || queue.state.pending.length))) return { delivered: false, reason: 'terminal mailbox has an unconfirmed paste' };
|
|
1545
1588
|
this.inputInFlight.add(session.id);
|
|
1546
1589
|
try {
|
|
1547
1590
|
const delivery = await this.host.sendInput(session.id, params.keys ? String(params.text ?? '') : submission(params.text, false), { submitMode: 'single', deadlineMs });
|
|
1548
1591
|
result = { delivered: true, delivery };
|
|
1549
1592
|
} finally { this.inputInFlight.delete(session.id); }
|
|
1550
1593
|
} else {
|
|
1551
|
-
result = await this.#deliverPaneText(session, params.pane, params.text, { deadlineMs });
|
|
1594
|
+
result = await this.#deliverPaneText(session, params.pane, params.text, { deadlineMs, messageIds: params.message_ids });
|
|
1552
1595
|
}
|
|
1553
|
-
act('control', `pane:${params.pane}`, result.delivered ? 'input' : 'failed', result
|
|
1596
|
+
act('control', `pane:${params.pane}`, result.delivered ? 'input' : 'failed', JSON.stringify(result));
|
|
1554
1597
|
return result;
|
|
1555
1598
|
}
|
|
1556
1599
|
case 'panes.clearDraft': {
|
|
@@ -1620,15 +1663,15 @@ export class TeamsMachine extends MachineCore {
|
|
|
1620
1663
|
if (!session) throw Object.assign(new Error(`no pane with context ${params.pane}`), { code: 404 });
|
|
1621
1664
|
if (this.suspended.has(params.pane)) throw Object.assign(new Error(`pane ${params.pane} is suspended: remote input is refused until resume`), { code: 423 });
|
|
1622
1665
|
const kind = this.#paneKind(params.pane, session);
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
return { delivered: false, reason: first === true || second === true ? 'draft' : 'no-composer' };
|
|
1666
|
+
if (kind === 'claude-code') {
|
|
1667
|
+
const draftOf = async () => composerHoldsDraft(kind, await this.host.capture(session.id, { lines: 200, preserveEscapes: true }));
|
|
1668
|
+
const first = await draftOf();
|
|
1669
|
+
await new Promise(resolve => setTimeout(resolve, COMPOSER_RECHECK_MS));
|
|
1670
|
+
const second = await draftOf();
|
|
1671
|
+
if (first !== false || second !== false) return { delivered: false, reason: first === true || second === true ? 'draft' : 'no-composer' };
|
|
1629
1672
|
}
|
|
1630
|
-
const result = await this.#deliverPaneText(session, params.pane, params.text, { deadlineMs: Date.now() + 10_000 });
|
|
1631
|
-
act('control', `pane:${params.pane}`, result.delivered ? 'input' : 'failed', result
|
|
1673
|
+
const result = await this.#deliverPaneText(session, params.pane, params.text, { deadlineMs: Date.now() + 10_000, messageIds: params.message_ids });
|
|
1674
|
+
act('control', `pane:${params.pane}`, result.delivered ? 'input' : 'failed', JSON.stringify(result));
|
|
1632
1675
|
return result;
|
|
1633
1676
|
}
|
|
1634
1677
|
case 'panes.capture': {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.152",
|
|
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",
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Per-recipient terminal mailbox. A retained paste is never repeated: retries
|
|
2
|
+
// press Enter and look for the exact user message in the native transcript.
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync, renameSync, appendFileSync, statSync, openSync, readSync, closeSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
6
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
7
|
+
|
|
8
|
+
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
9
|
+
const textOf = content => typeof content === 'string' ? content : Array.isArray(content) && content.every(p => ['text', 'input_text'].includes(p.type)) ? content.map(p => p.text).join('') : null;
|
|
10
|
+
|
|
11
|
+
function confirmed(batch) {
|
|
12
|
+
const fd = openSync(batch.path, 'r');
|
|
13
|
+
try {
|
|
14
|
+
let offset = batch.offset, buffered = '';
|
|
15
|
+
const decoder = new StringDecoder('utf8');
|
|
16
|
+
const bytes = Buffer.alloc(64 * 1024);
|
|
17
|
+
for (;;) {
|
|
18
|
+
const count = readSync(fd, bytes, 0, bytes.length, offset);
|
|
19
|
+
if (!count) return false;
|
|
20
|
+
offset += count; buffered += decoder.write(bytes.subarray(0, count));
|
|
21
|
+
let end;
|
|
22
|
+
while ((end = buffered.indexOf('\n')) >= 0) {
|
|
23
|
+
const line = buffered.slice(0, end); buffered = buffered.slice(end + 1);
|
|
24
|
+
let row; try { row = JSON.parse(line); } catch { continue; }
|
|
25
|
+
const content = batch.kind === 'codex' && row.type === 'response_item' && row.payload?.type === 'message' && row.payload.role === 'user'
|
|
26
|
+
? row.payload.content : batch.kind === 'grok' && row.type === 'user' ? row.content : null;
|
|
27
|
+
if (textOf(content) === batch.text) return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} finally { closeSync(fd); }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class TerminalMailbox {
|
|
34
|
+
constructor(root, recipient, log, load) {
|
|
35
|
+
this.dir = join(root, 'terminal-delivery', hash(recipient));
|
|
36
|
+
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
37
|
+
this.file = join(this.dir, 'queue.json'); this.log = log; this.load = load;
|
|
38
|
+
try { this.state = JSON.parse(readFileSync(this.file, 'utf8')); }
|
|
39
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; this.state = { recipient, pending: [], active: null, receipts: [] }; }
|
|
40
|
+
this.work = null;
|
|
41
|
+
}
|
|
42
|
+
save() {
|
|
43
|
+
const encoded = JSON.stringify(this.state) + '\n';
|
|
44
|
+
if (encoded === this.saved) return;
|
|
45
|
+
const temporary = `${this.file}.${process.pid}.tmp`;
|
|
46
|
+
writeFileSync(temporary, encoded, { mode: 0o600 });
|
|
47
|
+
renameSync(temporary, this.file); this.saved = encoded;
|
|
48
|
+
}
|
|
49
|
+
receipt(ids) {
|
|
50
|
+
const delivered = ids.every(id => this.state.receipts.some(r => r.message_ids.includes(id)));
|
|
51
|
+
const batch = this.state.active;
|
|
52
|
+
return { delivered, queued: !delivered, message_ids: ids,
|
|
53
|
+
...(delivered ? { receipt: 'native-user-message' } : { reason: batch?.error ?? 'queued', batch: batch?.id ?? null }) };
|
|
54
|
+
}
|
|
55
|
+
enqueue(text, ids, identity, host, session) {
|
|
56
|
+
const messageIds = ids?.length ? ids : [`input-${randomUUID()}`];
|
|
57
|
+
const known = new Set([...this.state.pending.flatMap(m => m.ids), ...(this.state.active?.message_ids ?? []), ...this.state.receipts.flatMap(r => r.message_ids)]);
|
|
58
|
+
if (messageIds.some(id => !known.has(id))) {
|
|
59
|
+
// One caller retry is one queued envelope, including when a prior batch is
|
|
60
|
+
// waiting for confirmation. New mail joins the next paste, never this one.
|
|
61
|
+
this.state.pending.push({ ids: messageIds, text }); this.save();
|
|
62
|
+
}
|
|
63
|
+
this.identity = identity; this.host = host; this.session = session;
|
|
64
|
+
this.state.binding = { context: session.contextKey, terminal: session.id, pid: session.rootPid, identity }; this.save();
|
|
65
|
+
return messageIds;
|
|
66
|
+
}
|
|
67
|
+
kick() {
|
|
68
|
+
if (this.work) return this.work;
|
|
69
|
+
this.work = this.drain().catch(error => {
|
|
70
|
+
if (this.state.active) { this.state.active.error = error.message; this.save(); }
|
|
71
|
+
this.log({ delivered: false, reason: error.message, message_ids: this.state.active?.message_ids ?? [] });
|
|
72
|
+
}).finally(() => { this.work = null; });
|
|
73
|
+
return this.work;
|
|
74
|
+
}
|
|
75
|
+
async drain() {
|
|
76
|
+
let batch = this.state.active;
|
|
77
|
+
if (!batch && this.state.pending.length) {
|
|
78
|
+
const items = this.state.pending.slice();
|
|
79
|
+
const text = items.map(m => m.text).join('\n\n');
|
|
80
|
+
const incremental = this.identity.kind === 'codex';
|
|
81
|
+
const baseline = incremental ? null : await this.load(this.identity.locator, { message_tail: 1 });
|
|
82
|
+
batch = { id: randomUUID(), message_ids: items.flatMap(m => m.ids), text, kind: this.identity.kind,
|
|
83
|
+
path: this.identity.path, offset: incremental ? statSync(this.identity.path).size : null,
|
|
84
|
+
locator: this.identity.locator, messageOffset: baseline?.window?.total_messages ?? null, phase: 'prepared', enters: 0, last_enter: 0 };
|
|
85
|
+
this.state.active = batch; this.state.pending = this.state.pending.filter(message => !items.includes(message)); this.save();
|
|
86
|
+
}
|
|
87
|
+
if (!batch) return;
|
|
88
|
+
const seen = batch.offset !== null ? confirmed(batch) : (await this.load(batch.locator, { message_offset: batch.messageOffset, message_limit: 1000 })).session?.messages?.some(message => message.role === 'user' && textOf(message.content) === batch.text);
|
|
89
|
+
if (seen) {
|
|
90
|
+
const receipt = { batch: batch.id, message_ids: batch.message_ids, delivered: true, receipt: 'native-user-message', at: Date.now() };
|
|
91
|
+
appendFileSync(join(this.dir, 'receipts.jsonl'), JSON.stringify(receipt) + '\n', { mode: 0o600 });
|
|
92
|
+
this.state.receipts.push(receipt);
|
|
93
|
+
this.state.active = null; this.save(); this.log(receipt); return;
|
|
94
|
+
}
|
|
95
|
+
if (batch.phase === 'prepared') {
|
|
96
|
+
// Persist uncertainty before I/O. A daemon restart checks the transcript;
|
|
97
|
+
// it never guesses that an unacknowledged paste did not happen.
|
|
98
|
+
batch.phase = 'paste_started'; this.save();
|
|
99
|
+
await this.host.sendInput(this.session.id, batch.text, { submitMode: 'none' });
|
|
100
|
+
batch.phase = 'pasted'; this.save();
|
|
101
|
+
}
|
|
102
|
+
if (batch.phase === 'paste_started') {
|
|
103
|
+
batch.error = 'paste outcome uncertain; native transcript confirmation pending'; this.save(); return;
|
|
104
|
+
}
|
|
105
|
+
if (batch.enters < 3 && Date.now() - batch.last_enter >= 5000) {
|
|
106
|
+
batch.enters += 1; batch.last_enter = Date.now(); this.save();
|
|
107
|
+
await this.host.sendInput(this.session.id, '\r', { submitMode: 'single' });
|
|
108
|
+
this.log({ delivered: false, queued: true, batch: batch.id, message_ids: batch.message_ids, attempt: batch.enters });
|
|
109
|
+
}
|
|
110
|
+
batch.error = batch.enters >= 3 ? 'native transcript unconfirmed after Enter retries' : 'native transcript confirmation pending';
|
|
111
|
+
this.save();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function retainedTerminalMail(root) {
|
|
116
|
+
const directory = join(root, 'terminal-delivery');
|
|
117
|
+
let names; try { names = readdirSync(directory); } catch (error) { if (error.code === 'ENOENT') return []; throw error; }
|
|
118
|
+
return names.map(name => JSON.parse(readFileSync(join(directory, name, 'queue.json'), 'utf8')))
|
|
119
|
+
.filter(state => state.active || state.pending.length);
|
|
120
|
+
}
|