@zswarm/core 0.2.6 → 0.2.7
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/dist/harness.d.ts +27 -2
- package/dist/harness.js +68 -2
- package/dist/host/cli.js +5 -2
- package/dist/host/relay.d.ts +2 -0
- package/dist/host/relay.js +60 -9
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/ops/dispatch.d.ts +9 -0
- package/dist/ops/dispatch.js +25 -3
- package/dist/ops/restart.d.ts +57 -0
- package/dist/ops/restart.js +416 -0
- package/dist/ops/serve-install.d.ts +8 -1
- package/dist/ops/serve-install.js +160 -73
- package/dist/ops/serve.d.ts +28 -0
- package/dist/ops/serve.js +35 -0
- package/dist/policy.js +1 -0
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +31 -7
- package/package.json +1 -1
package/dist/harness.d.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
export type SubmitStrategy = "auto" | "double-enter";
|
|
2
|
+
/**
|
|
3
|
+
* How to ask a harness to quit its own UI. A slash command is typed and
|
|
4
|
+
* submitted; keys are sent raw. `null` means the restart op falls back to
|
|
5
|
+
* Ctrl+C instead of guessing.
|
|
6
|
+
*/
|
|
7
|
+
export type HarnessExitRecipe = {
|
|
8
|
+
/** A command line to type into the harness composer, e.g. "/exit". */
|
|
9
|
+
command?: string;
|
|
10
|
+
/** Raw keys to send, e.g. ["Ctrl c"]. */
|
|
11
|
+
keys?: readonly string[];
|
|
12
|
+
};
|
|
2
13
|
export type HarnessProfile = {
|
|
3
|
-
/** Stable id: "codex" | "cursor" | "opencode" | "gemini" | "pi" | "unknown" */
|
|
14
|
+
/** Stable id: "codex" | "cursor" | "opencode" | "gemini" | "pi" | "claude" | "unknown" */
|
|
4
15
|
name: string;
|
|
5
16
|
/** What `send` should do when the caller did not pass submit= explicitly. */
|
|
6
17
|
submit: SubmitStrategy;
|
|
@@ -19,6 +30,12 @@ export type HarnessProfile = {
|
|
|
19
30
|
* these against the trailing lines to avoid treating a busy pane as idle.
|
|
20
31
|
*/
|
|
21
32
|
busy: readonly RegExp[];
|
|
33
|
+
/** Program to type in a shell pane to relaunch this harness; null when unknown. */
|
|
34
|
+
launch: string | null;
|
|
35
|
+
/** How to ask the harness to quit; null falls back to Ctrl+C. */
|
|
36
|
+
exit: HarnessExitRecipe | null;
|
|
37
|
+
/** Trailing lines showing the harness finished starting and is ready for input. */
|
|
38
|
+
ready: readonly RegExp[];
|
|
22
39
|
};
|
|
23
40
|
/**
|
|
24
41
|
* Identify the harness from a pane's command and title, case-insensitively.
|
|
@@ -35,7 +52,15 @@ export declare function resolveHarness(pane: {
|
|
|
35
52
|
*/
|
|
36
53
|
export declare function queuedPrompt(screen: string, profile: HarnessProfile): boolean;
|
|
37
54
|
/**
|
|
38
|
-
*
|
|
55
|
+
* The busy marker regex a screen shows on its trailing lines, or null. Only
|
|
39
56
|
* the last few lines count: a quoted marker in scrollback is not a live turn.
|
|
40
57
|
*/
|
|
58
|
+
export declare function busyMarker(screen: string, profile: HarnessProfile): RegExp | null;
|
|
59
|
+
/** Does this screen's trailing lines show the harness actively working? */
|
|
41
60
|
export declare function busyPrompt(screen: string, profile: HarnessProfile): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Does this screen's trailing lines show the harness ready for input? An empty
|
|
63
|
+
* ready set is unknown, never a match: a profile that never declared a marker
|
|
64
|
+
* must not make the restart op wait forever.
|
|
65
|
+
*/
|
|
66
|
+
export declare function readyPrompt(screen: string, profile: HarnessProfile): boolean;
|
package/dist/harness.js
CHANGED
|
@@ -5,6 +5,7 @@ const PROFILES = [
|
|
|
5
5
|
{ name: "opencode", command: ["opencode"], title: ["agent-opencode"] },
|
|
6
6
|
{ name: "gemini", command: ["agy", "gemini"], title: ["agent-gemini"] },
|
|
7
7
|
{ name: "pi", command: ["pi"], title: ["agent-pi"] },
|
|
8
|
+
{ name: "claude", command: ["claude"], title: ["agent-claude"] },
|
|
8
9
|
];
|
|
9
10
|
/**
|
|
10
11
|
* Only codex needs a second Enter to submit: its composer keeps the pasted
|
|
@@ -17,6 +18,7 @@ const SUBMIT = {
|
|
|
17
18
|
opencode: "auto",
|
|
18
19
|
gemini: "auto",
|
|
19
20
|
pi: "auto",
|
|
21
|
+
claude: "auto",
|
|
20
22
|
};
|
|
21
23
|
/**
|
|
22
24
|
* Prompt shapes any CLI can show, so every profile carries these as a base.
|
|
@@ -50,6 +52,7 @@ const WAITING = {
|
|
|
50
52
|
/Accept this file edit\?/i,
|
|
51
53
|
],
|
|
52
54
|
pi: GENERIC_WAITING,
|
|
55
|
+
claude: [...GENERIC_WAITING, /Do you want to (?:proceed|continue)\?/i],
|
|
53
56
|
};
|
|
54
57
|
/**
|
|
55
58
|
* Queued-message hints seen live. Claude Code prints
|
|
@@ -68,6 +71,7 @@ const QUEUED = {
|
|
|
68
71
|
opencode: GENERIC_QUEUED,
|
|
69
72
|
gemini: GENERIC_QUEUED,
|
|
70
73
|
pi: GENERIC_QUEUED,
|
|
74
|
+
claude: GENERIC_QUEUED,
|
|
71
75
|
};
|
|
72
76
|
/**
|
|
73
77
|
* Busy (working) markers seen live. OpenCode draws a progress bar of filled
|
|
@@ -80,6 +84,47 @@ const BUSY = {
|
|
|
80
84
|
opencode: [/[■⬝]{4,}/],
|
|
81
85
|
gemini: [],
|
|
82
86
|
pi: [],
|
|
87
|
+
claude: [/esc to interrupt/i],
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Program to type in a shell pane to start this harness. The first entry of the
|
|
91
|
+
* harness's command needle is the executable a shell would accept.
|
|
92
|
+
*/
|
|
93
|
+
const LAUNCH = {
|
|
94
|
+
codex: "codex",
|
|
95
|
+
cursor: "cursor-agent",
|
|
96
|
+
opencode: "opencode",
|
|
97
|
+
gemini: "agy",
|
|
98
|
+
pi: "pi",
|
|
99
|
+
claude: "claude",
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* How to ask a harness to quit its own UI. `/exit` is documented for OpenCode
|
|
103
|
+
* and Claude Code; the rest have no equally reliable in-band quit, so they carry
|
|
104
|
+
* `null` and the restart op falls back to Ctrl+C (twice) after a bounded wait.
|
|
105
|
+
*/
|
|
106
|
+
const EXIT = {
|
|
107
|
+
codex: null,
|
|
108
|
+
cursor: null,
|
|
109
|
+
opencode: { command: "/exit" },
|
|
110
|
+
gemini: null,
|
|
111
|
+
pi: null,
|
|
112
|
+
claude: { command: "/exit" },
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* A trailing line that means the harness finished starting and accepts input.
|
|
116
|
+
* OpenCode's idle screen pins its composer placeholder ("Ask anything…") and
|
|
117
|
+
* footer ("ctrl+p commands"); Claude Code pins "? for shortcuts". The profiles
|
|
118
|
+
* whose composer is not known well enough carry `[]`, and the restart op then
|
|
119
|
+
* falls back to waiting for the screen to settle instead of guessing.
|
|
120
|
+
*/
|
|
121
|
+
const READY = {
|
|
122
|
+
codex: [],
|
|
123
|
+
cursor: [],
|
|
124
|
+
opencode: [/ask anything/i, /ctrl\+p commands/i],
|
|
125
|
+
gemini: [],
|
|
126
|
+
pi: [],
|
|
127
|
+
claude: [/\? for shortcuts/i, /bypass permissions on/i],
|
|
83
128
|
};
|
|
84
129
|
const UNKNOWN = {
|
|
85
130
|
name: "unknown",
|
|
@@ -87,6 +132,9 @@ const UNKNOWN = {
|
|
|
87
132
|
waiting: GENERIC_WAITING,
|
|
88
133
|
queued: GENERIC_QUEUED,
|
|
89
134
|
busy: [/esc to interrupt/i],
|
|
135
|
+
launch: null,
|
|
136
|
+
exit: null,
|
|
137
|
+
ready: [],
|
|
90
138
|
};
|
|
91
139
|
function escapeRegExp(text) {
|
|
92
140
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -137,6 +185,9 @@ export function resolveHarness(pane) {
|
|
|
137
185
|
waiting: WAITING[name],
|
|
138
186
|
queued: QUEUED[name],
|
|
139
187
|
busy: BUSY[name],
|
|
188
|
+
launch: LAUNCH[name],
|
|
189
|
+
exit: EXIT[name],
|
|
190
|
+
ready: READY[name],
|
|
140
191
|
};
|
|
141
192
|
}
|
|
142
193
|
/**
|
|
@@ -148,12 +199,27 @@ export function queuedPrompt(screen, profile) {
|
|
|
148
199
|
return profile.queued.some((re) => lines.some((line) => re.test(line)));
|
|
149
200
|
}
|
|
150
201
|
/**
|
|
151
|
-
*
|
|
202
|
+
* The busy marker regex a screen shows on its trailing lines, or null. Only
|
|
152
203
|
* the last few lines count: a quoted marker in scrollback is not a live turn.
|
|
153
204
|
*/
|
|
205
|
+
export function busyMarker(screen, profile) {
|
|
206
|
+
const lines = trailingLines(screen);
|
|
207
|
+
return profile.busy.find((re) => lines.some((line) => re.test(line))) ?? null;
|
|
208
|
+
}
|
|
209
|
+
/** Does this screen's trailing lines show the harness actively working? */
|
|
154
210
|
export function busyPrompt(screen, profile) {
|
|
211
|
+
return busyMarker(screen, profile) !== null;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Does this screen's trailing lines show the harness ready for input? An empty
|
|
215
|
+
* ready set is unknown, never a match: a profile that never declared a marker
|
|
216
|
+
* must not make the restart op wait forever.
|
|
217
|
+
*/
|
|
218
|
+
export function readyPrompt(screen, profile) {
|
|
219
|
+
if (profile.ready.length === 0)
|
|
220
|
+
return false;
|
|
155
221
|
const lines = trailingLines(screen);
|
|
156
|
-
return profile.
|
|
222
|
+
return profile.ready.some((re) => lines.some((line) => re.test(line)));
|
|
157
223
|
}
|
|
158
224
|
/** The six last non-empty, right-trimmed lines of a screen, CR stripped. */
|
|
159
225
|
function trailingLines(screen) {
|
package/dist/host/cli.js
CHANGED
|
@@ -114,7 +114,7 @@ function leaseTable(leases) {
|
|
|
114
114
|
return [header, ...body].map((row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd()).join("\n");
|
|
115
115
|
}
|
|
116
116
|
async function relay(argv) {
|
|
117
|
-
const { flags } = parseHostArgs(argv, ["--foreground", "--prune", "--list", "--leases", "--cancel-all", "--json", "--queue", "--force"]);
|
|
117
|
+
const { flags } = parseHostArgs(argv, ["--foreground", "--prune", "--list", "--leases", "--cancel-all", "--json", "--queue", "--force", "--if-idle"]);
|
|
118
118
|
const run = str(flags["--run"]);
|
|
119
119
|
if (run)
|
|
120
120
|
return runRelayDir(run);
|
|
@@ -182,6 +182,7 @@ async function relay(argv) {
|
|
|
182
182
|
foreground: flags["--foreground"] === true,
|
|
183
183
|
queue: flags["--queue"] === true,
|
|
184
184
|
force: flags["--force"] === true,
|
|
185
|
+
ifIdle: flags["--if-idle"] === true,
|
|
185
186
|
}));
|
|
186
187
|
}
|
|
187
188
|
async function host(argv) {
|
|
@@ -245,12 +246,14 @@ const COMMANDS = {
|
|
|
245
246
|
usage: [
|
|
246
247
|
"zswarm relay --to PANE --done REGEX --reply-to PANE [--serve TARGET] [--session S] [--body-file F]",
|
|
247
248
|
" [--reply-session S] [--from NAME] [--check SCRIPT] [--message TEMPLATE] [--id ID]",
|
|
248
|
-
" [--timeout-min 240] [--max-age MIN] [--queue] [--force] [--foreground]",
|
|
249
|
+
" [--timeout-min 240] [--max-age MIN] [--queue] [--force] [--foreground] [--if-idle]",
|
|
249
250
|
" send the body to PANE over serve (ZSWARM_SERVE / ZSWARM_SERVE_TOKEN), then a detached relay",
|
|
250
251
|
" waits for REGEX (and SCRIPT exit 0) and pastes a message into the local --reply-to pane;",
|
|
251
252
|
" --max-age (default --timeout-min + 60) is a hard end that always yields a final status",
|
|
252
253
|
" it leases its target pane until it ends; a second relay to a leased pane fails pane_leased",
|
|
253
254
|
" unless --queue waits for the lease to free or --force shares it",
|
|
255
|
+
" --if-idle applies send's busy check before the lease: a harness that shows it is working fails pane_busy",
|
|
256
|
+
" without sending; --queue then waits for the pane to go idle as well as for the lease, and --force skips the check",
|
|
254
257
|
"zswarm relay --list [--json] id, state, target, session, age, last check, job and lease per relay dir",
|
|
255
258
|
"zswarm relay --leases [--json] live pane leases and the relay that holds each",
|
|
256
259
|
"zswarm relay --cancel ID | --cancel-all --to PANE stop the job, mark it CANCELLED, paste nothing",
|
package/dist/host/relay.d.ts
CHANGED
|
@@ -108,6 +108,8 @@ export type RelayStartInput = {
|
|
|
108
108
|
queue?: boolean;
|
|
109
109
|
/** Send to a leased pane anyway; the lease is shared and noted. */
|
|
110
110
|
force?: boolean;
|
|
111
|
+
/** With --if-idle, check the target harness before the lease: a visibly working pane fails pane_busy. */
|
|
112
|
+
ifIdle?: boolean;
|
|
111
113
|
env?: NodeJS.ProcessEnv;
|
|
112
114
|
deps?: RelayDeps;
|
|
113
115
|
/** State store for the pane lease; injected in tests. */
|
package/dist/host/relay.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import { appendFileSync, chmodSync, copyFileSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, join } from "node:path";
|
|
4
4
|
import { ZellijError } from "../errors.js";
|
|
5
|
-
import { dispatchZswarm } from "../ops/dispatch.js";
|
|
5
|
+
import { dispatchZswarm, paneBusyMarker } from "../ops/dispatch.js";
|
|
6
6
|
import { resolveServeCliLaunch } from "../ops/serve-install.js";
|
|
7
7
|
import { createStateStore, defaultStateDir } from "../state.js";
|
|
8
8
|
import { findLease, leaseKey, releaseLeaseKey, takeLease } from "./lease.js";
|
|
@@ -404,15 +404,69 @@ export async function startRelay(input) {
|
|
|
404
404
|
let how;
|
|
405
405
|
const leaseNow = input.deps?.now ?? Date.now;
|
|
406
406
|
const leaseSleep = input.deps?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
407
|
+
const call = input.deps?.call ?? ((args, e) => dispatchZswarm(args, undefined, { env: e }));
|
|
408
|
+
const remote = serveEnv(cfg, env);
|
|
409
|
+
const target = (args) => cfg.session ? { ...args, session: cfg.session } : args;
|
|
407
410
|
const take = () => takeLease(store, { relayId: id, from: cfg.from, at: leaseNow(), dir, target: leaseTarget, force: input.force });
|
|
408
411
|
const leaseKeyValue = leaseKey(leaseTarget);
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
|
|
412
|
+
/** The pane --to names, with the command/title the harness check needs. */
|
|
413
|
+
const resolveRemotePane = async () => {
|
|
414
|
+
const args = { op: "list" };
|
|
415
|
+
if (cfg.session)
|
|
416
|
+
args.session = cfg.session;
|
|
417
|
+
const res = await call(args, remote).catch((err) => ({ ok: false, error: { code: "failed", message: String(err) } }));
|
|
418
|
+
const panes = res.ok
|
|
419
|
+
? res.data.panes ?? []
|
|
420
|
+
: [];
|
|
421
|
+
const pane = panes.find((p) => p.title === cfg.to || p.id === cfg.to);
|
|
422
|
+
return pane ? { command: pane.command, title: pane.title } : { title: cfg.to };
|
|
423
|
+
};
|
|
424
|
+
// --if-idle: the same busy check send --if-idle uses, one dump before the
|
|
425
|
+
// lease is taken. --force skips it, as it skips the lease.
|
|
426
|
+
const checkIdle = input.ifIdle === true && input.force !== true;
|
|
427
|
+
const idlePane = checkIdle ? await resolveRemotePane() : undefined;
|
|
428
|
+
/** The harness working marker one dump of the target shows, or null. */
|
|
429
|
+
const busyMarkerNow = async () => {
|
|
430
|
+
const res = await call(target({ op: "dump", to: cfg.to, max: 400 }), remote).catch((err) => ({ ok: false, error: { code: "failed", message: String(err) } }));
|
|
431
|
+
if (!res.ok)
|
|
432
|
+
return null; // cannot see the screen: the lease is the only guard
|
|
433
|
+
return paneBusyMarker(String(res.data?.text ?? ""), idlePane ?? { title: cfg.to });
|
|
434
|
+
};
|
|
435
|
+
const deadline = leaseNow() + cfg.timeoutMin * 60_000;
|
|
436
|
+
let busy = null;
|
|
437
|
+
let taken;
|
|
438
|
+
if (!input.queue) {
|
|
439
|
+
if (checkIdle) {
|
|
440
|
+
busy = await busyMarkerNow();
|
|
441
|
+
if (busy) {
|
|
442
|
+
rmSync(dir, { recursive: true, force: true });
|
|
443
|
+
throw new ZellijError("pane_busy", `pane ${cfg.to} is busy with a task (marker: ${busy}); retry when it is idle`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
taken = take();
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
// --queue polls the lease and, under --if-idle, the busy marker. The lease
|
|
450
|
+
// is only taken once the pane is not visibly working.
|
|
451
|
+
let attempt;
|
|
452
|
+
for (;;) {
|
|
453
|
+
if (checkIdle)
|
|
454
|
+
busy = await busyMarkerNow();
|
|
455
|
+
attempt = busy ? undefined : take();
|
|
456
|
+
if (attempt?.ok)
|
|
457
|
+
break;
|
|
458
|
+
if (leaseNow() >= deadline)
|
|
459
|
+
break;
|
|
413
460
|
await leaseSleep(Math.min(LEASE_QUEUE_POLL_MS, Math.max(1, deadline - leaseNow())));
|
|
414
|
-
taken = take();
|
|
415
461
|
}
|
|
462
|
+
if (attempt?.ok)
|
|
463
|
+
taken = attempt;
|
|
464
|
+
else if (busy) {
|
|
465
|
+
rmSync(dir, { recursive: true, force: true });
|
|
466
|
+
throw new ZellijError("pane_busy", `pane ${cfg.to} is busy with a task (marker: ${busy}) and did not free before --timeout-min`);
|
|
467
|
+
}
|
|
468
|
+
else
|
|
469
|
+
taken = attempt ?? take();
|
|
416
470
|
}
|
|
417
471
|
if (!taken.ok) {
|
|
418
472
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -423,9 +477,6 @@ export async function startRelay(input) {
|
|
|
423
477
|
if (taken.shared) {
|
|
424
478
|
warning = `pane ${cfg.to} is already leased by relay ${taken.lease.relayId}; --force shares its lease`;
|
|
425
479
|
}
|
|
426
|
-
const call = input.deps?.call ?? ((args, e) => dispatchZswarm(args, undefined, { env: e }));
|
|
427
|
-
const remote = serveEnv(cfg, env);
|
|
428
|
-
const target = (args) => cfg.session ? { ...args, session: cfg.session } : args;
|
|
429
480
|
try {
|
|
430
481
|
if (body !== undefined) {
|
|
431
482
|
const res = await call(target({ op: "send", to: cfg.to, body, from: cfg.from, settleMs: 800 }), remote);
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export { createZellijClient, type ZellijClient, type ZellijClientOptions, type Z
|
|
|
12
12
|
export { createStateStore, defaultStateDir, type LeaseRecord, type LogEntry, type SignalChannel, type StateStore, type StateStoreOptions, } from "./state.js";
|
|
13
13
|
export { findLease, leaseKey, leaseStale, listLeases, releaseLease, releaseLeaseKey, takeLease, type LeaseTakeInput, type LeaseTakeResult, type LeaseTarget, } from "./host/lease.js";
|
|
14
14
|
export { dispatchZswarm, resolveInvocationEnv } from "./ops/dispatch.js";
|
|
15
|
+
export { EXIT_WAIT_MS, HANDOFF_INLINE_MAX, HANDOFF_MARKER, HANDOFF_SELF_WAIT_MS, READY_SETTLE_MS, READY_STABLE_GAP_MS, READY_WAIT_MS, restartLaunchCommand, restartPane, restartPaneKind, type RestartPaneKind, type RestartStep, } from "./ops/restart.js";
|
|
16
|
+
export { busyPrompt, queuedPrompt, readyPrompt, resolveHarness, type HarnessExitRecipe, type HarnessProfile, type SubmitStrategy, } from "./harness.js";
|
|
15
17
|
export { assertOpAllowed, assertPaneAllowed, isWriteOp, loadPolicy, type Policy, } from "./policy.js";
|
|
16
18
|
export { buildSshRemoteCommand, createSshExec, quoteRemoteArg, shellQuote, type IpcDiscoveryState, type SshExecFn, type SshTarget, } from "./exec.js";
|
|
17
19
|
export { resolveSshTarget } from "./zellij/binary.js";
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,8 @@ export { createZellijClient, } from "./zellij/client.js";
|
|
|
12
12
|
export { createStateStore, defaultStateDir, } from "./state.js";
|
|
13
13
|
export { findLease, leaseKey, leaseStale, listLeases, releaseLease, releaseLeaseKey, takeLease, } from "./host/lease.js";
|
|
14
14
|
export { dispatchZswarm, resolveInvocationEnv } from "./ops/dispatch.js";
|
|
15
|
+
export { EXIT_WAIT_MS, HANDOFF_INLINE_MAX, HANDOFF_MARKER, HANDOFF_SELF_WAIT_MS, READY_SETTLE_MS, READY_STABLE_GAP_MS, READY_WAIT_MS, restartLaunchCommand, restartPane, restartPaneKind, } from "./ops/restart.js";
|
|
16
|
+
export { busyPrompt, queuedPrompt, readyPrompt, resolveHarness, } from "./harness.js";
|
|
15
17
|
export { assertOpAllowed, assertPaneAllowed, isWriteOp, loadPolicy, } from "./policy.js";
|
|
16
18
|
export { buildSshRemoteCommand, createSshExec, quoteRemoteArg, shellQuote, } from "./exec.js";
|
|
17
19
|
export { resolveSshTarget } from "./zellij/binary.js";
|
package/dist/ops/dispatch.d.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { type ZellijClient } from "../zellij/client.js";
|
|
2
2
|
import type { DispatchDeps, OpsResult } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The harness busy marker a dumped screen shows for `pane`, or null. The one
|
|
5
|
+
* busy check behind `send --if-idle` and `relay --if-idle`; the marker names
|
|
6
|
+
* itself (its regex source) in the `pane_busy` error.
|
|
7
|
+
*/
|
|
8
|
+
export declare function paneBusyMarker(screen: string, pane: {
|
|
9
|
+
command?: string | null;
|
|
10
|
+
title?: string | null;
|
|
11
|
+
}): string | null;
|
|
3
12
|
/**
|
|
4
13
|
* Per-invocation routing from `--local` / `--ssh`. `--local` clears both SSH
|
|
5
14
|
* and serve (and remote IPC). `--ssh` sets the destination for this call and
|
package/dist/ops/dispatch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ZellijError } from "../errors.js";
|
|
2
2
|
import { createGitClient } from "../git.js";
|
|
3
|
-
import {
|
|
3
|
+
import { busyMarker, resolveHarness } from "../harness.js";
|
|
4
4
|
import { findLease } from "../host/lease.js";
|
|
5
5
|
import { normalizeKeys } from "../keys.js";
|
|
6
6
|
import { createZellijClient } from "../zellij/client.js";
|
|
@@ -16,6 +16,7 @@ import { attachKnownSender, deliverTo, scaleBudgets, withSenderLabel, selfPaneTi
|
|
|
16
16
|
import { assertNotPlugin, assertNotSelf, assertExpectedScreen, assertWritable, } from "./guards.js";
|
|
17
17
|
import { readDeliveryLog } from "./log.js";
|
|
18
18
|
import { awaitSignal, listSignals, postSignal } from "./signals.js";
|
|
19
|
+
import { restartPane } from "./restart.js";
|
|
19
20
|
import { spawnPane } from "./spawn.js";
|
|
20
21
|
import { observationBudget } from "./observation.js";
|
|
21
22
|
import { invocationContext } from "./routing.js";
|
|
@@ -30,6 +31,14 @@ import { serveCallTimeout, } from "./serve.js";
|
|
|
30
31
|
import { doctorOp } from "./doctor.js";
|
|
31
32
|
import { forwardServe } from "./serve-tunnel.js";
|
|
32
33
|
import { installServeService, uninstallServeService } from "../host/serve-service.js";
|
|
34
|
+
/**
|
|
35
|
+
* The harness busy marker a dumped screen shows for `pane`, or null. The one
|
|
36
|
+
* busy check behind `send --if-idle` and `relay --if-idle`; the marker names
|
|
37
|
+
* itself (its regex source) in the `pane_busy` error.
|
|
38
|
+
*/
|
|
39
|
+
export function paneBusyMarker(screen, pane) {
|
|
40
|
+
return busyMarker(screen, resolveHarness(pane))?.source ?? null;
|
|
41
|
+
}
|
|
33
42
|
/**
|
|
34
43
|
* Per-invocation routing from `--local` / `--ssh`. `--local` clears both SSH
|
|
35
44
|
* and serve (and remote IPC). `--ssh` sets the destination for this call and
|
|
@@ -270,6 +279,9 @@ async function dispatchOperation(args, injected, deps, context, env) {
|
|
|
270
279
|
if (isTrue(args.clear)) {
|
|
271
280
|
const cleared = await uninstallServeLogon({
|
|
272
281
|
platform: deps.serveInstall?.platform ?? process.platform,
|
|
282
|
+
// --session names the task to clear: its own per-session task, or the
|
|
283
|
+
// legacy root task only when that task records the same session.
|
|
284
|
+
session: optionalString(args.session),
|
|
273
285
|
timeoutMs,
|
|
274
286
|
signal,
|
|
275
287
|
env,
|
|
@@ -399,8 +411,11 @@ async function dispatchOperation(args, injected, deps, context, env) {
|
|
|
399
411
|
throw new ZellijError("pane_leased", `pane ${to} is leased by relay ${held.relayId} (from ${held.from}, since ${new Date(held.at).toISOString()})`, { relayId: held.relayId, from: held.from, at: held.at, dir: held.dir });
|
|
400
412
|
}
|
|
401
413
|
const screen = await client.dumpPane({ session, paneId: pane.id }).then((res) => res.text, () => null);
|
|
402
|
-
if (screen !== null
|
|
403
|
-
|
|
414
|
+
if (screen !== null) {
|
|
415
|
+
const marker = paneBusyMarker(screen, pane);
|
|
416
|
+
if (marker) {
|
|
417
|
+
throw new ZellijError("pane_busy", `pane ${to} is busy with a task (marker: ${marker}); retry when it is idle`);
|
|
418
|
+
}
|
|
404
419
|
}
|
|
405
420
|
}
|
|
406
421
|
const labeled = withSenderLabel(args, {
|
|
@@ -652,6 +667,13 @@ async function dispatchOperation(args, injected, deps, context, env) {
|
|
|
652
667
|
return await peerCheckpoint(git(), args, clock);
|
|
653
668
|
case "spawn":
|
|
654
669
|
return await spawnPane(client, args, deps.git, clock, signal);
|
|
670
|
+
case "restart": {
|
|
671
|
+
const { session, pane, panes } = await resolveTarget(client, args, state(), clock, env, policy, op, signal);
|
|
672
|
+
return await restartPane(client, state(), args, { session, pane, panes }, clock, {
|
|
673
|
+
env,
|
|
674
|
+
signal,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
655
677
|
case "worktrees":
|
|
656
678
|
return await listPeerWorktrees(git(), client, args);
|
|
657
679
|
case "unworktree":
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type HarnessProfile } from "../harness.js";
|
|
2
|
+
import type { StateStore } from "../state.js";
|
|
3
|
+
import type { ZellijClient } from "../zellij/client.js";
|
|
4
|
+
import type { ZellijPane } from "../zellij/panes.js";
|
|
5
|
+
import type { Clock, OpsResult } from "./types.js";
|
|
6
|
+
/**
|
|
7
|
+
* Restart the agent in one pane, in place: same pane id, title and position.
|
|
8
|
+
*
|
|
9
|
+
* A Zellij command pane (a `command=` layout entry or `zellij run`) stays open
|
|
10
|
+
* when its command exits and offers ENTER to re-run the same command — that is
|
|
11
|
+
* the in-place relaunch. A shell pane has no such prompt, so the profile's
|
|
12
|
+
* launch command (or `--command`) is typed instead.
|
|
13
|
+
*/
|
|
14
|
+
/** The agent prints this once its handoff file is written. */
|
|
15
|
+
export declare const HANDOFF_MARKER = "ZSWARM-HANDOFF-DONE";
|
|
16
|
+
/** A handoff at or under this many chars is pasted; longer ones get a pointer. */
|
|
17
|
+
export declare const HANDOFF_INLINE_MAX = 2000;
|
|
18
|
+
/** Bound on waiting for a self-written handoff marker. */
|
|
19
|
+
export declare const HANDOFF_SELF_WAIT_MS = 60000;
|
|
20
|
+
/** Bound on waiting for the pane's command to exit before the Ctrl+C fallback. */
|
|
21
|
+
export declare const EXIT_WAIT_MS = 10000;
|
|
22
|
+
/** Bound on waiting for readiness after relaunch (marker or settled screen). */
|
|
23
|
+
export declare const READY_WAIT_MS = 45000;
|
|
24
|
+
/** Extra settle after a ready marker before the handoff is pasted. */
|
|
25
|
+
export declare const READY_SETTLE_MS = 500;
|
|
26
|
+
/** A screen that has not changed for this long counts as settled. */
|
|
27
|
+
export declare const READY_STABLE_GAP_MS = 1000;
|
|
28
|
+
/** Screen poll interval while a step waits. */
|
|
29
|
+
export declare const RESTART_POLL_MS = 250;
|
|
30
|
+
export type RestartPaneKind = "command" | "shell";
|
|
31
|
+
export type RestartStep = {
|
|
32
|
+
step: string;
|
|
33
|
+
ok: boolean;
|
|
34
|
+
detail: string;
|
|
35
|
+
elapsedMs: number;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* A command pane ran the agent itself, so ENTER re-runs it. A shell pane had the
|
|
39
|
+
* agent started inside a shell, so the launch command has to be typed again.
|
|
40
|
+
* A pane with no command information is treated as a shell: typing a launch
|
|
41
|
+
* command is recoverable, pressing ENTER into a live agent is not.
|
|
42
|
+
*/
|
|
43
|
+
export declare function restartPaneKind(pane: ZellijPane): RestartPaneKind;
|
|
44
|
+
/** The command line a shell pane gets to relaunch the agent. */
|
|
45
|
+
export declare function restartLaunchCommand(args: Record<string, unknown>, profile: HarnessProfile): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Restart one already-resolved pane. `target` comes from dispatch's
|
|
48
|
+
* `resolveTarget`, so policy and pane lookup are shared with the other ops.
|
|
49
|
+
*/
|
|
50
|
+
export declare function restartPane(client: ZellijClient, state: StateStore, args: Record<string, unknown>, target: {
|
|
51
|
+
session: string;
|
|
52
|
+
pane: ZellijPane;
|
|
53
|
+
panes: ZellijPane[];
|
|
54
|
+
}, clock: Clock, opts?: {
|
|
55
|
+
env?: NodeJS.ProcessEnv;
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
}): Promise<OpsResult>;
|