@kdonev/termscape 0.1.7 → 0.1.9
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 +7 -1
- package/dist/agents/profiles.d.ts +10 -0
- package/dist/agents/profiles.d.ts.map +1 -1
- package/dist/agents/profiles.js +12 -0
- package/dist/agents/profiles.js.map +1 -1
- package/dist/agents/router.d.ts +15 -7
- package/dist/agents/router.d.ts.map +1 -1
- package/dist/agents/router.js +23 -9
- package/dist/agents/router.js.map +1 -1
- package/dist/agents/wiring.d.ts.map +1 -1
- package/dist/agents/wiring.js +16 -1
- package/dist/agents/wiring.js.map +1 -1
- package/dist/debug.d.ts.map +1 -1
- package/dist/debug.js +7 -0
- package/dist/debug.js.map +1 -1
- package/dist/hub.d.ts +51 -1
- package/dist/hub.d.ts.map +1 -1
- package/dist/hub.js +209 -30
- package/dist/hub.js.map +1 -1
- package/dist/hub.tgz +0 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +5 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +5 -2
- package/dist/server.js.map +1 -1
- package/dist/session/manager.d.ts +6 -1
- package/dist/session/manager.d.ts.map +1 -1
- package/dist/session/manager.js +12 -3
- package/dist/session/manager.js.map +1 -1
- package/dist/session/pty.d.ts +22 -3
- package/dist/session/pty.d.ts.map +1 -1
- package/dist/session/pty.js +110 -33
- package/dist/session/pty.js.map +1 -1
- package/package.json +1 -1
package/dist/hub.js
CHANGED
|
@@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs';
|
|
|
4
4
|
import { encodeInjection, INJECT_SUBMIT, MAX_PENDING_PROPOSALS, parseAddress, slugify, } from './protocol/index.js';
|
|
5
5
|
import { openDb } from './db/index.js';
|
|
6
6
|
import { Store } from './db/store.js';
|
|
7
|
-
import { briefMode, ProfileRegistry } from './agents/profiles.js';
|
|
7
|
+
import { briefMode, isPlainTerminal, ProfileRegistry } from './agents/profiles.js';
|
|
8
8
|
import { AgentDetector } from './agents/detect.js';
|
|
9
9
|
import { TemplateRegistry, overlayCanvasTemplates, validate as validateTemplate, } from './agents/templates.js';
|
|
10
10
|
import { TokenRegistry } from './agents/tokens.js';
|
|
@@ -18,9 +18,10 @@ import { PeerRegistry } from './remote/registry.js';
|
|
|
18
18
|
import { SPAWN_RELAY_TIMEOUT_MS } from './remote/peer-serve.js';
|
|
19
19
|
import { deploy } from './remote/deployer.js';
|
|
20
20
|
import { hubTarballPath } from './remote/tarball.js';
|
|
21
|
+
import { debug } from './debug.js';
|
|
21
22
|
import { paths } from './paths.js';
|
|
22
23
|
import { checkFolder, folderName } from './folders.js';
|
|
23
|
-
export const HUB_VERSION = '0.1.
|
|
24
|
+
export const HUB_VERSION = '0.1.9';
|
|
24
25
|
/**
|
|
25
26
|
* How long changes are pooled before every attached machine is told the
|
|
26
27
|
* canvas has moved on. A status change is a session change, and agents change
|
|
@@ -33,6 +34,17 @@ const ANNOUNCE_COALESCE_MS = 300;
|
|
|
33
34
|
*/
|
|
34
35
|
const OPENING_QUIET_MS = 1500;
|
|
35
36
|
const OPENING_READY_CAP_MS = 20_000;
|
|
37
|
+
/**
|
|
38
|
+
* How long a delivery waits for a starting agent before going in regardless.
|
|
39
|
+
*
|
|
40
|
+
* There has to be a bound. A session's readiness gate deliberately outlasts
|
|
41
|
+
* its own cap while a question is on screen - the instruction is still wanted
|
|
42
|
+
* once the human answers - but a `send_message` that blocks its caller until
|
|
43
|
+
* somebody walks over to a window is worse than a message that arrives badly.
|
|
44
|
+
* So the wait is bounded here, and past it delivery is what it always was:
|
|
45
|
+
* immediate, recorded, and the CLI's business.
|
|
46
|
+
*/
|
|
47
|
+
const DELIVERY_WAIT_CAP_MS = 20_000;
|
|
36
48
|
/** Colours cycled through when a workspace is created, for canvas grouping. */
|
|
37
49
|
const WORKSPACE_COLORS = [
|
|
38
50
|
'#7c9cf5',
|
|
@@ -84,6 +96,16 @@ export class Hub extends EventEmitter {
|
|
|
84
96
|
* two roles a null check rather than a flag.
|
|
85
97
|
*/
|
|
86
98
|
uplink = null;
|
|
99
|
+
/**
|
|
100
|
+
* Per session: resolves once its CLI is reading and its opening instruction,
|
|
101
|
+
* if it had one, has been typed and sent.
|
|
102
|
+
*
|
|
103
|
+
* Every delivery to a local session queues behind this. Without it a message
|
|
104
|
+
* sent to an agent that was still starting - which is exactly what an agent
|
|
105
|
+
* that has just called `spawn_agent` does next - was written into a program
|
|
106
|
+
* not yet reading, and sat in the composer unsent forever (issue 27).
|
|
107
|
+
*/
|
|
108
|
+
opened = new Map();
|
|
87
109
|
announceTimer = null;
|
|
88
110
|
/** Live SSH tunnels, keyed by host id. Not persisted: they die with the hub. */
|
|
89
111
|
tunnels = new Map();
|
|
@@ -120,7 +142,11 @@ export class Hub extends EventEmitter {
|
|
|
120
142
|
this.sessions = new SessionManager(this.store, this.profiles, this.tokens, 'http://127.0.0.1:0');
|
|
121
143
|
this.router = new MessageRouter(this.store, this.sessions, this.profiles, (m) => this.emit('message', m));
|
|
122
144
|
this.sessions.on('session', (s) => this.emit('session', s));
|
|
123
|
-
this.sessions.on('removed', (id, address) =>
|
|
145
|
+
this.sessions.on('removed', (id, address) => {
|
|
146
|
+
// Nothing left to open, and nothing left to hold a delivery for.
|
|
147
|
+
this.opened.delete(id);
|
|
148
|
+
this.emit('removed', id, address);
|
|
149
|
+
});
|
|
124
150
|
this.sessions.on('data', (id, chunk) => this.emit('data', id, chunk));
|
|
125
151
|
this.peers = new PeerRegistry(this.store, HUB_VERSION, (address) => this.sessions.getByAddress(address)?.id ?? null);
|
|
126
152
|
// Peer windows share the canvas, so a new local window must not land on
|
|
@@ -249,6 +275,19 @@ export class Hub extends EventEmitter {
|
|
|
249
275
|
this.uplink?.agents().find((a) => a.address === address);
|
|
250
276
|
return found ? { address, parentAddress: found.parentAddress ?? null } : null;
|
|
251
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Whether the window at `address` is a shell rather than an agent, as far
|
|
280
|
+
* as this hub can tell. Profiles are judged against this hub's own
|
|
281
|
+
* registry, which holds every built-in one; a custom profile only another
|
|
282
|
+
* machine declares reads as an agent, which is what it was before.
|
|
283
|
+
*/
|
|
284
|
+
isShellAt(address) {
|
|
285
|
+
const profile = this.sessions.getByAddress(address)?.profile ??
|
|
286
|
+
this.peers.find(address)?.session.profile ??
|
|
287
|
+
this.uplink?.agents().find((a) => a.address === address)?.profile;
|
|
288
|
+
const p = profile ? this.profiles.get(profile) : null;
|
|
289
|
+
return p ? isPlainTerminal(p) : false;
|
|
290
|
+
}
|
|
252
291
|
/**
|
|
253
292
|
* Refuse to act on an address the viewer may not see, in exactly the words
|
|
254
293
|
* used for one that does not exist: a hidden agent must not be discoverable
|
|
@@ -862,8 +901,7 @@ export class Hub extends EventEmitter {
|
|
|
862
901
|
const opening = [this.typedBrief(session), opts.prompt]
|
|
863
902
|
.filter((part) => !!part)
|
|
864
903
|
.join('\n\n');
|
|
865
|
-
|
|
866
|
-
void this.deliverOpeningInstruction(session.id, opening);
|
|
904
|
+
this.noteOpening(session.id, opening || null);
|
|
867
905
|
this.store.setOpeningPrompt(session.id, opts.restorePrompt === undefined ? (opts.prompt ?? null) : opts.restorePrompt);
|
|
868
906
|
return session;
|
|
869
907
|
}
|
|
@@ -900,9 +938,9 @@ export class Hub extends EventEmitter {
|
|
|
900
938
|
*/
|
|
901
939
|
async resumeSession(sessionId) {
|
|
902
940
|
const session = await this.sessions.resume(sessionId);
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
941
|
+
// A fresh CLI, so a fresh gate: messages must wait for this one to be
|
|
942
|
+
// reading just as they did when it first started.
|
|
943
|
+
this.noteOpening(session.id, this.typedBrief(session));
|
|
906
944
|
return session;
|
|
907
945
|
}
|
|
908
946
|
/**
|
|
@@ -924,8 +962,7 @@ export class Hub extends EventEmitter {
|
|
|
924
962
|
const opening = [this.typedBrief(session), this.store.getOpeningPrompt(sessionId)]
|
|
925
963
|
.filter((part) => !!part)
|
|
926
964
|
.join('\n\n');
|
|
927
|
-
|
|
928
|
-
void this.deliverOpeningInstruction(session.id, opening);
|
|
965
|
+
this.noteOpening(session.id, opening || null);
|
|
929
966
|
return session;
|
|
930
967
|
}
|
|
931
968
|
async resumeWorkspace(workspaceId) {
|
|
@@ -1408,6 +1445,16 @@ export class Hub extends EventEmitter {
|
|
|
1408
1445
|
throw new Error('cannot send a message to yourself');
|
|
1409
1446
|
this.requireVisible(me, to);
|
|
1410
1447
|
const result = await this.deliverFrom(me.address, to, text);
|
|
1448
|
+
// A shell never answers: what it prints stays on its own screen, and
|
|
1449
|
+
// reading that screen is the only way to see it - not polling (issue 30).
|
|
1450
|
+
if (this.isShellAt(to)) {
|
|
1451
|
+
return {
|
|
1452
|
+
...result,
|
|
1453
|
+
note: `${to} is a shell, not an agent: your text was run there as a command, ` +
|
|
1454
|
+
'without a [from ...] prefix, and nothing will be sent back to you. Call ' +
|
|
1455
|
+
'read_screen on it to see the output.',
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1411
1458
|
// Only a successful send counts as a question asked - deliverFrom throws
|
|
1412
1459
|
// on failure, so a message that never arrived does not excuse polling.
|
|
1413
1460
|
this.pollWatch.noteMessage(me.address, to);
|
|
@@ -1478,6 +1525,19 @@ export class Hub extends EventEmitter {
|
|
|
1478
1525
|
// records the failed attempt with its reason, which is what keeps the
|
|
1479
1526
|
// promise that no message is ever dropped silently.
|
|
1480
1527
|
if (where === 'local' || where === null) {
|
|
1528
|
+
// Behind the target's opening, if it is still starting. This is the one
|
|
1529
|
+
// place delivery is not immediate, and the exception earns itself: the
|
|
1530
|
+
// alternative is writing into a CLI that is not reading, which is not a
|
|
1531
|
+
// delivery at all.
|
|
1532
|
+
const target = this.sessions.getByAddress(to);
|
|
1533
|
+
if (target) {
|
|
1534
|
+
const t0 = Date.now();
|
|
1535
|
+
await this.awaitOpened(target.id);
|
|
1536
|
+
const waited = Date.now() - t0;
|
|
1537
|
+
if (waited > 50) {
|
|
1538
|
+
debug('deliver', `${fromAddr} -> ${to}: held ${waited}ms behind the agent's opening`);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1481
1541
|
const r = this.router.send(fromAddr, to, text);
|
|
1482
1542
|
if (!r.delivered)
|
|
1483
1543
|
throw new Error(r.error ?? 'delivery failed');
|
|
@@ -1488,6 +1548,10 @@ export class Hub extends EventEmitter {
|
|
|
1488
1548
|
// complete on this side too.
|
|
1489
1549
|
const id = randomUUID();
|
|
1490
1550
|
const sentAt = Date.now();
|
|
1551
|
+
debug('deliver', `message ${id} ${fromAddr} -> ${to} (${text.length} chars): ` +
|
|
1552
|
+
(where === 'remote'
|
|
1553
|
+
? `to the attached machine ${this.peers.hostIdFor(to) ?? '?'}, which types it`
|
|
1554
|
+
: 'up to the canvas, which routes it'));
|
|
1491
1555
|
try {
|
|
1492
1556
|
if (where === 'remote')
|
|
1493
1557
|
await this.peers.deliver(fromAddr, to, text);
|
|
@@ -1501,9 +1565,11 @@ export class Hub extends EventEmitter {
|
|
|
1501
1565
|
};
|
|
1502
1566
|
this.store.insertMessage(m);
|
|
1503
1567
|
this.emit('message', m);
|
|
1568
|
+
debug('deliver', `message ${id}: accepted after ${Date.now() - sentAt}ms`);
|
|
1504
1569
|
return { delivered: true, to, deliveredAt: m.deliveredAt };
|
|
1505
1570
|
}
|
|
1506
1571
|
catch (err) {
|
|
1572
|
+
debug('deliver', `message ${id}: failed - ${err.message}`);
|
|
1507
1573
|
const m = {
|
|
1508
1574
|
id, fromAddr, toAddr: to, body: text,
|
|
1509
1575
|
sentAt, deliveredAt: null, deliveryState: 'failed',
|
|
@@ -1579,7 +1645,13 @@ export class Hub extends EventEmitter {
|
|
|
1579
1645
|
* gets there.
|
|
1580
1646
|
*/
|
|
1581
1647
|
const picked = await this.pickSpawnTemplate(me, opts.profile);
|
|
1582
|
-
|
|
1648
|
+
// Attributed, except in a shell, which would run the prefix (issue 30).
|
|
1649
|
+
const childProfile = this.profiles.get(picked.agent);
|
|
1650
|
+
const instruction = !opts.prompt
|
|
1651
|
+
? null
|
|
1652
|
+
: childProfile && isPlainTerminal(childProfile)
|
|
1653
|
+
? opts.prompt
|
|
1654
|
+
: `[from ${me.address}] ${opts.prompt}`;
|
|
1583
1655
|
const opening = [picked.prompt, instruction].filter(Boolean).join('\n\n') || undefined;
|
|
1584
1656
|
// A `host` naming something beyond this hub's own machine has to go up
|
|
1585
1657
|
// the link: an attached hub holds rows for nothing but itself, and only
|
|
@@ -1705,6 +1777,48 @@ export class Hub extends EventEmitter {
|
|
|
1705
1777
|
async deliverOpeningInstruction(sessionId, prompt) {
|
|
1706
1778
|
await this.typeWhenReady(sessionId, prompt);
|
|
1707
1779
|
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Record how a session opens, and hold every later delivery behind it.
|
|
1782
|
+
*
|
|
1783
|
+
* Registered whether or not there is anything to type: an agent started with
|
|
1784
|
+
* no instruction at all still has a CLI that is not reading yet, and that is
|
|
1785
|
+
* the case issue 27 was reported for. Not awaited by the caller - the window
|
|
1786
|
+
* appears now - but stored, so the first message to arrive waits for the
|
|
1787
|
+
* same moment the instruction would have.
|
|
1788
|
+
*/
|
|
1789
|
+
noteOpening(sessionId, opening) {
|
|
1790
|
+
const done = opening
|
|
1791
|
+
? this.deliverOpeningInstruction(sessionId, opening)
|
|
1792
|
+
: this.awaitReady(sessionId).then(() => { });
|
|
1793
|
+
// Failures are the gate opening, not the gate jamming: a session that died
|
|
1794
|
+
// on the way up must not hold its own message log hostage.
|
|
1795
|
+
this.opened.set(sessionId, done.catch(() => { }));
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Wait for a local session to be ready to be typed at, but not forever.
|
|
1799
|
+
*
|
|
1800
|
+
* Once resolved the promise stays resolved, so this costs a steady-state
|
|
1801
|
+
* delivery a microtask and nothing else.
|
|
1802
|
+
*/
|
|
1803
|
+
async awaitOpened(sessionId) {
|
|
1804
|
+
const gate = this.opened.get(sessionId);
|
|
1805
|
+
if (!gate)
|
|
1806
|
+
return;
|
|
1807
|
+
let timer;
|
|
1808
|
+
try {
|
|
1809
|
+
await Promise.race([
|
|
1810
|
+
gate,
|
|
1811
|
+
new Promise((res) => {
|
|
1812
|
+
timer = setTimeout(res, DELIVERY_WAIT_CAP_MS);
|
|
1813
|
+
timer.unref?.();
|
|
1814
|
+
}),
|
|
1815
|
+
]);
|
|
1816
|
+
}
|
|
1817
|
+
finally {
|
|
1818
|
+
if (timer)
|
|
1819
|
+
clearTimeout(timer);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1708
1822
|
/**
|
|
1709
1823
|
* Wait for the CLI to be up, then type. Written straight away the text lands
|
|
1710
1824
|
* before the program is reading it; the wait is for output to arrive and
|
|
@@ -1725,17 +1839,65 @@ export class Hub extends EventEmitter {
|
|
|
1725
1839
|
* taken as answers to it. Such a screen is waited out until it changes.
|
|
1726
1840
|
*/
|
|
1727
1841
|
async typeWhenReady(sessionId, text) {
|
|
1842
|
+
const t0 = Date.now();
|
|
1843
|
+
debug('deliver', `${sessionId} first instruction (${text.length} chars): waiting for the CLI`);
|
|
1844
|
+
const ready = await this.awaitReady(sessionId);
|
|
1728
1845
|
const pty = this.sessions.pty(sessionId);
|
|
1729
|
-
if (!pty)
|
|
1846
|
+
if (!ready || !pty?.running) {
|
|
1847
|
+
debug('deliver', `${sessionId} first instruction dropped after ${Date.now() - t0}ms: ` +
|
|
1848
|
+
(pty?.running ? 'the CLI never drew anything' : 'the CLI exited'));
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
const session = this.sessions.get(sessionId);
|
|
1852
|
+
if (!session)
|
|
1730
1853
|
return;
|
|
1854
|
+
const mode = this.profiles.get(session.profile)?.inject ?? 'bracketed';
|
|
1855
|
+
debug('deliver', `${sessionId} first instruction: CLI ready after ${Date.now() - t0}ms, typing it`);
|
|
1856
|
+
try {
|
|
1857
|
+
// Awaited, so that a caller holding this session's readiness gate is not
|
|
1858
|
+
// released until the Enter is in: a message injected between the paste
|
|
1859
|
+
// and its Enter is folded into the same prompt.
|
|
1860
|
+
await this.sessions.inject(sessionId, encodeInjection(text, mode), INJECT_SUBMIT);
|
|
1861
|
+
}
|
|
1862
|
+
catch {
|
|
1863
|
+
// The agent died between the readiness check and the write; the message
|
|
1864
|
+
// log already reflects that it never started.
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Wait until the CLI in a session is up and reading, or give up.
|
|
1869
|
+
*
|
|
1870
|
+
* Split out of `typeWhenReady` because typing is not the only thing that has
|
|
1871
|
+
* to wait for this. A message delivered to an agent whose CLI has not
|
|
1872
|
+
* started reading is written into a program that is not listening: the paste
|
|
1873
|
+
* lands in a composer that is not there yet and the Enter goes nowhere, and
|
|
1874
|
+
* what the sender sees is a delivery that was recorded and never acted on.
|
|
1875
|
+
* See `deliverFrom`.
|
|
1876
|
+
*/
|
|
1877
|
+
async awaitReady(sessionId) {
|
|
1878
|
+
const pty = this.sessions.pty(sessionId);
|
|
1879
|
+
if (!pty)
|
|
1880
|
+
return false;
|
|
1731
1881
|
const profile = this.profiles.get(this.sessions.get(sessionId)?.profile ?? '');
|
|
1732
1882
|
const hint = profile?.askingHint ? new RegExp(profile.askingHint, 'i') : null;
|
|
1733
1883
|
// A question on screen outlasts any cap: the human answers it when they
|
|
1734
1884
|
// get to the window, and the instruction is still wanted after that.
|
|
1735
1885
|
const asking = () => !!hint && hint.test(pty.tailLines(pty.rows));
|
|
1736
|
-
|
|
1886
|
+
/*
|
|
1887
|
+
* Quiet is not enough for an agent that reports through hooks. On kid7,
|
|
1888
|
+
* starting straight after an update, Claude Code had drawn its prompt and
|
|
1889
|
+
* gone quiet five seconds in - and was not reading yet: the instruction
|
|
1890
|
+
* typed then lost its Enter, and SessionStart only arrived after the paste.
|
|
1891
|
+
* Its first hook is the agent saying it is up, so for such an agent that
|
|
1892
|
+
* is what readiness waits for, on top of the quiet. The cap still applies,
|
|
1893
|
+
* so an agent whose hooks never run is not held forever.
|
|
1894
|
+
*/
|
|
1895
|
+
const wantsHook = !!profile?.mcp && profile.status === 'hooks';
|
|
1896
|
+
const hooked = () => !wantsHook || pty.hooksReported;
|
|
1897
|
+
return new Promise((res) => {
|
|
1737
1898
|
let settled = false;
|
|
1738
1899
|
let seen = false;
|
|
1900
|
+
let isQuiet = false;
|
|
1739
1901
|
let quiet = null;
|
|
1740
1902
|
const done = (v) => {
|
|
1741
1903
|
if (settled)
|
|
@@ -1746,40 +1908,54 @@ export class Hub extends EventEmitter {
|
|
|
1746
1908
|
clearTimeout(quiet);
|
|
1747
1909
|
pty.off('data', onData);
|
|
1748
1910
|
pty.off('exit', onExit);
|
|
1911
|
+
pty.off('hook', onHook);
|
|
1749
1912
|
res(v);
|
|
1750
1913
|
};
|
|
1914
|
+
const settle = () => {
|
|
1915
|
+
if (asking()) {
|
|
1916
|
+
debug('deliver', `${sessionId} quiet, but a question is on screen; still waiting`);
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
if (!hooked()) {
|
|
1920
|
+
debug('deliver', `${sessionId} quiet, but its hooks have not reported yet; still waiting`);
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
debug('deliver', `${sessionId} ready: no output for ${OPENING_QUIET_MS}ms, hooks reported`);
|
|
1924
|
+
done(true);
|
|
1925
|
+
};
|
|
1751
1926
|
const onData = () => {
|
|
1752
1927
|
seen = true;
|
|
1928
|
+
isQuiet = false;
|
|
1753
1929
|
if (quiet)
|
|
1754
1930
|
clearTimeout(quiet);
|
|
1755
1931
|
quiet = setTimeout(() => {
|
|
1756
1932
|
quiet = null;
|
|
1757
|
-
|
|
1758
|
-
|
|
1933
|
+
isQuiet = true;
|
|
1934
|
+
settle();
|
|
1759
1935
|
}, OPENING_QUIET_MS);
|
|
1760
1936
|
};
|
|
1937
|
+
// A hook that lands after the quiet is what the quiet was waiting for.
|
|
1938
|
+
const onHook = () => {
|
|
1939
|
+
if (isQuiet)
|
|
1940
|
+
settle();
|
|
1941
|
+
};
|
|
1761
1942
|
const onExit = () => done(false);
|
|
1762
1943
|
const cap = setTimeout(() => {
|
|
1763
|
-
if (
|
|
1764
|
-
|
|
1944
|
+
if (asking())
|
|
1945
|
+
return;
|
|
1946
|
+
debug('deliver', `${sessionId} ${OPENING_READY_CAP_MS}ms cap reached ` +
|
|
1947
|
+
(!seen
|
|
1948
|
+
? 'with no output at all'
|
|
1949
|
+
: hooked()
|
|
1950
|
+
? 'while still drawing; typing anyway'
|
|
1951
|
+
: 'and its hooks never reported; typing anyway'));
|
|
1952
|
+
done(seen);
|
|
1765
1953
|
}, OPENING_READY_CAP_MS);
|
|
1766
1954
|
cap.unref?.();
|
|
1767
1955
|
pty.on('data', onData);
|
|
1768
1956
|
pty.on('exit', onExit);
|
|
1957
|
+
pty.on('hook', onHook);
|
|
1769
1958
|
});
|
|
1770
|
-
if (!ready || !pty.running)
|
|
1771
|
-
return;
|
|
1772
|
-
const session = this.sessions.get(sessionId);
|
|
1773
|
-
if (!session)
|
|
1774
|
-
return;
|
|
1775
|
-
const mode = this.profiles.get(session.profile)?.inject ?? 'bracketed';
|
|
1776
|
-
try {
|
|
1777
|
-
this.sessions.inject(sessionId, encodeInjection(text, mode), INJECT_SUBMIT);
|
|
1778
|
-
}
|
|
1779
|
-
catch {
|
|
1780
|
-
// The agent died between the readiness check and the write; the message
|
|
1781
|
-
// log already reflects that it never started.
|
|
1782
|
-
}
|
|
1783
1959
|
}
|
|
1784
1960
|
async readScreen(sessionId, address, lines) {
|
|
1785
1961
|
const me = this.requireSession(sessionId);
|
|
@@ -1790,6 +1966,9 @@ export class Hub extends EventEmitter {
|
|
|
1790
1966
|
// readScreenAt: that path also serves peers asking on this hub's behalf
|
|
1791
1967
|
// for a caller it never authenticated, and the count belongs to the
|
|
1792
1968
|
// caller's own hub.
|
|
1969
|
+
// Not for a shell: watching its screen is how its output is read at all.
|
|
1970
|
+
if (this.isShellAt(address))
|
|
1971
|
+
return result;
|
|
1793
1972
|
const note = this.pollWatch.noteRead(me.address, address);
|
|
1794
1973
|
return note ? { ...result, note } : result;
|
|
1795
1974
|
}
|