@volter/supercode-teams 0.2.53 → 0.2.55
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/fleet.mjs +9 -2
- package/machine.mjs +17 -14
- package/package.json +1 -1
package/fleet.mjs
CHANGED
|
@@ -57,6 +57,9 @@ export async function fleetDiscover({ context = null, machines = null, params =
|
|
|
57
57
|
: listed;
|
|
58
58
|
const results = await Promise.all(targets.map(async (machine) => {
|
|
59
59
|
if (!machine.id) return { name: machine.name, error: 'not an accessible machine of this team' };
|
|
60
|
+
// A machine the server reports offline is not asked (asking only waits for a link that is not coming); it is
|
|
61
|
+
// named as offline, not as a failure, and only when it was named or seen in the last day.
|
|
62
|
+
if (machine.connection === 'offline') return { name: machine.name, offline: true, lastSeenAt: machine.last_seen_at ?? null };
|
|
60
63
|
let channel = null;
|
|
61
64
|
try {
|
|
62
65
|
channel = await openRemoteMachine({ origin: selected.origin, teamId: selected.team_id, machineId: machine.id, token });
|
|
@@ -70,9 +73,13 @@ export async function fleetDiscover({ context = null, machines = null, params =
|
|
|
70
73
|
}));
|
|
71
74
|
const sessions = [];
|
|
72
75
|
const unreachable = [];
|
|
76
|
+
const offline = [];
|
|
77
|
+
const named = new Set(machines ?? []);
|
|
73
78
|
for (const result of results) {
|
|
74
|
-
if (result.
|
|
79
|
+
if (result.offline) {
|
|
80
|
+
if (named.has(result.name) || (result.lastSeenAt && Date.now() - result.lastSeenAt < 86_400_000)) offline.push({ machine: result.name, last_seen_at: result.lastSeenAt });
|
|
81
|
+
} else if (result.error) unreachable.push({ machine: result.name, error: result.error });
|
|
75
82
|
else for (const session of result.sessions) sessions.push({ machine: result.name, ...session });
|
|
76
83
|
}
|
|
77
|
-
return { sessions, unreachable };
|
|
84
|
+
return { sessions, unreachable, ...(offline.length ? { offline } : {}) };
|
|
78
85
|
}
|
package/machine.mjs
CHANGED
|
@@ -144,9 +144,10 @@ export class TeamsMachine extends MachineCore {
|
|
|
144
144
|
// Each pane launched here opens a terminal window on this machine's screen (popupWindow).
|
|
145
145
|
this.popups = Boolean(popups);
|
|
146
146
|
this.launches = new Launches(this.p.launches);
|
|
147
|
-
// Windows has no tmux: its panes
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
// Windows has no tmux: its panes live in the machine's pane server, a process of their own that outlives this
|
|
148
|
+
// daemon (a restart or an upgrade never ends a pane), as tmux panes live in tmux's server.
|
|
149
|
+
if (process.platform === 'win32' && !terminal.PaneServerHost) throw new Error('panes on Windows need a newer @volter/supercode-terminal (one with PaneServerHost)');
|
|
150
|
+
this.host = process.platform === 'win32' ? new terminal.PaneServerHost({ pipe: `\\\\.\\pipe\\supercode-panes-${fingerprint(this.identity.publicKeyPem)}` }) : new TmuxTerminalHost({
|
|
150
151
|
// Stable owner id so panes this daemon launched stay "owned" across restarts.
|
|
151
152
|
ownerId: `supercode-teams-${fingerprint(this.identity.publicKeyPem)}`,
|
|
152
153
|
...(tmuxSocket ? { socket: tmuxSocket } : {}),
|
|
@@ -459,7 +460,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
459
460
|
let release;
|
|
460
461
|
try {
|
|
461
462
|
release = this.claimAttachment(principal);
|
|
462
|
-
grant = this.host.issueAttachment(session.id, { mode });
|
|
463
|
+
grant = await this.host.issueAttachment(session.id, { mode });
|
|
463
464
|
} catch (error) {
|
|
464
465
|
release?.();
|
|
465
466
|
this.log.append({ principal, cap, target: `pane:${pane}`, outcome: 'refused', detail: error.message });
|
|
@@ -571,9 +572,11 @@ export class TeamsMachine extends MachineCore {
|
|
|
571
572
|
} catch { /* no harness evidence this tick: the process tree still answers */ }
|
|
572
573
|
}
|
|
573
574
|
const live = new Set();
|
|
574
|
-
|
|
575
|
+
// Every pane's screen, read before the rows are built (a pane server answers asynchronously).
|
|
576
|
+
const captures = await Promise.all(sessions.map((session) => this.#safeCapture(session.id)));
|
|
577
|
+
const rows = sessions.map((session, index) => {
|
|
575
578
|
if (session.contextKey) live.add(session.contextKey);
|
|
576
|
-
const capture =
|
|
579
|
+
const capture = captures[index];
|
|
577
580
|
const requested = session.contextKey ? doc.requests[session.contextKey] ?? null : null;
|
|
578
581
|
const own = harnessLiveness(harness.get(session.rootPid));
|
|
579
582
|
const evidence = {
|
|
@@ -595,10 +598,10 @@ export class TeamsMachine extends MachineCore {
|
|
|
595
598
|
|
|
596
599
|
/** One capture, never fatal: a pane that died between the listing and the read
|
|
597
600
|
* is evidence of nothing, not a failed door. */
|
|
598
|
-
#safeCapture(sessionId) {
|
|
601
|
+
async #safeCapture(sessionId) {
|
|
599
602
|
if (!sessionId) return '';
|
|
600
603
|
try {
|
|
601
|
-
return this.host.capture(sessionId, { lines: 120 });
|
|
604
|
+
return await this.host.capture(sessionId, { lines: 120 });
|
|
602
605
|
} catch {
|
|
603
606
|
return '';
|
|
604
607
|
}
|
|
@@ -687,7 +690,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
687
690
|
const composerKind = typeof kind === 'string' ? kind.trim().toLowerCase() : '';
|
|
688
691
|
const until = Date.now() + INITIAL_INPUT_READY_DEADLINE_MS;
|
|
689
692
|
while (!signal.aborted && Date.now() < until) {
|
|
690
|
-
const capture = this.#safeCapture(session.id);
|
|
693
|
+
const capture = await this.#safeCapture(session.id);
|
|
691
694
|
const unchangedMs = this.#screenUnchangedMs(session.id, capture, Date.now());
|
|
692
695
|
const blank = capture.trim() === '';
|
|
693
696
|
// A blank screen is the one reading that can mean the pane is GONE rather
|
|
@@ -1119,7 +1122,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
1119
1122
|
need(operator ? 'control' : 'control', `pane:${params.pane}`);
|
|
1120
1123
|
const session = await this.#paneByContext(params.pane);
|
|
1121
1124
|
if (!session) throw new Error(`no pane with context ${params.pane}`);
|
|
1122
|
-
this.host.closeSession(session.id);
|
|
1125
|
+
await this.host.closeSession(session.id);
|
|
1123
1126
|
act('close', `pane:${params.pane}`);
|
|
1124
1127
|
return { closed: params.pane };
|
|
1125
1128
|
}
|
|
@@ -1170,10 +1173,10 @@ export class TeamsMachine extends MachineCore {
|
|
|
1170
1173
|
if (!session) throw Object.assign(new Error(`no pane with context ${params.pane}`), { code: 404 });
|
|
1171
1174
|
if (this.suspended.has(params.pane)) throw Object.assign(new Error(`pane ${params.pane} is suspended: remote input is refused until resume`), { code: 423 });
|
|
1172
1175
|
const kind = this.#paneKind(params.pane, session);
|
|
1173
|
-
const draftOf = () => composerHoldsDraft(kind, this.host.capture(session.id, { lines: 200, preserveEscapes: true }));
|
|
1174
|
-
const first = draftOf();
|
|
1176
|
+
const draftOf = async () => composerHoldsDraft(kind, await this.host.capture(session.id, { lines: 200, preserveEscapes: true }));
|
|
1177
|
+
const first = await draftOf();
|
|
1175
1178
|
await new Promise((resolve) => setTimeout(resolve, COMPOSER_RECHECK_MS));
|
|
1176
|
-
const second = draftOf();
|
|
1179
|
+
const second = await draftOf();
|
|
1177
1180
|
if (first !== false || second !== false) {
|
|
1178
1181
|
return { delivered: false, reason: first === true || second === true ? 'draft' : 'no-composer' };
|
|
1179
1182
|
}
|
|
@@ -1185,7 +1188,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
1185
1188
|
need('observe', `pane:${params.pane}`);
|
|
1186
1189
|
const session = await this.#paneByContext(params.pane);
|
|
1187
1190
|
if (!session) throw new Error(`no pane with context ${params.pane}`);
|
|
1188
|
-
return { text: this.host.capture(session.id, { lines: params.lines ?? 200 }) };
|
|
1191
|
+
return { text: await this.host.capture(session.id, { lines: params.lines ?? 200 }) };
|
|
1189
1192
|
}
|
|
1190
1193
|
case 'files.stat': case 'files.list': case 'files.read': case 'files.write': case 'files.mkdir': case 'files.rename': case 'files.delete': case 'files.search': {
|
|
1191
1194
|
// Authorize the REAL path: a symlink under a granted root must not reach outside it. Reading (stat, list,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.55",
|
|
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",
|