@zswarm/core 0.2.6 → 0.2.8

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.
@@ -1,3 +1,4 @@
1
+ import type { HarnessProfile } from "../harness.js";
1
2
  import type { StateStore } from "../state.js";
2
3
  import type { ZellijClient } from "../zellij/client.js";
3
4
  import type { ZellijPane } from "../zellij/panes.js";
@@ -20,19 +21,26 @@ export declare const SEND_BUDGETS: {
20
21
  readonly settleMs: 300;
21
22
  /** Pane-lookup retry window for an absent pane. */
22
23
  readonly observeMs: 1000;
24
+ /**
25
+ * Base window that polls the pane for positive evidence before any negative
26
+ * verdict; scaled up for slow panes and capped by `confirmMaxMs`.
27
+ */
28
+ readonly confirmMs: 1000;
23
29
  /** The scaled window never exceeds this multiple of the default. */
24
30
  readonly maxScale: 5;
25
31
  /** A first Zellij call at or under this many ms is not "slow". */
26
32
  readonly slowMs: 1000;
27
33
  readonly settleMaxMs: 5000;
28
34
  readonly observeMaxMs: 30000;
35
+ readonly confirmMaxMs: 15000;
29
36
  };
30
37
  /**
31
- * Scale the fixed settle/observe windows by how slow the send's first Zellij
32
- * call was. A call under `slowMs` keeps the defaults; five seconds or more pins
33
- * the 5x cap. Explicit `--settle-ms` / `--observe-ms` are returned as given,
34
- * and a scaled value is capped by the call's own timeout so it cannot outlast
35
- * the observation budget.
38
+ * Scale the fixed settle/observe/confirm windows by how slow the send's first
39
+ * Zellij call was. A call under `slowMs` keeps the defaults; five seconds or
40
+ * more pins the 5x cap. Explicit `--settle-ms` / `--observe-ms` are returned as
41
+ * given, and a scaled value is capped by the call's own timeout so it cannot
42
+ * outlast the observation budget. `confirmMs` is always scaled (no explicit
43
+ * flag) and capped by both `confirmMaxMs` and the call timeout.
36
44
  */
37
45
  export declare function scaleBudgets(input: {
38
46
  firstCallMs: number;
@@ -42,6 +50,7 @@ export declare function scaleBudgets(input: {
42
50
  }): {
43
51
  settleMs: number;
44
52
  observeMs: number;
53
+ confirmMs: number;
45
54
  scale: number;
46
55
  };
47
56
  export type SenderSource = "arg" | "env" | "title" | "default";
@@ -78,7 +87,8 @@ export declare function attachKnownSender(args: Record<string, unknown>, env: No
78
87
  export declare function bodyText(client: ZellijClient, args: Record<string, unknown>, body: string): string;
79
88
  /**
80
89
  * Caller `submit=` always wins. With none given, the pane's harness picks
81
- * auto vs double-enter (codex needs two Enters; the rest submit on one).
90
+ * auto vs double-enter (codex needs two Enters; the rest submit on one). An
91
+ * explicit `--harness` overrides the pane-inferred profile.
82
92
  */
83
93
  export declare function resolveSubmitMode(args: Record<string, unknown>, pane: {
84
94
  command?: string | null;
@@ -107,6 +117,20 @@ export declare function composerHolds(screen: string, body: string): boolean;
107
117
  export declare function classifySubmit(before: string, after: string, body: string, opts?: {
108
118
  queued?: (screen: string) => boolean;
109
119
  }): Submitted;
120
+ /**
121
+ * Press Enter once and report whether the body submitted, without pasting.
122
+ * The restart op uses this to re-ask a still-held composer; a failed dump is
123
+ * unverified, never a negative verdict.
124
+ */
125
+ export declare function submitAfterEnter(client: ZellijClient, input: {
126
+ session: string;
127
+ paneId: string;
128
+ body: string;
129
+ before: string;
130
+ clock: Clock;
131
+ settleMs: number;
132
+ profile: HarnessProfile;
133
+ }): Promise<Submitted>;
110
134
  /** Paste a body into one pane and record the attempt. */
111
135
  export declare function deliverTo(client: ZellijClient, state: StateStore, args: Record<string, unknown>, input: {
112
136
  session: string;
@@ -1,24 +1,33 @@
1
- import { queuedPrompt, resolveHarness } from "../harness.js";
2
- import { isTrue, numberArg } from "./util.js";
1
+ import { busyMarker, queuedPrompt, resolveHarness } from "../harness.js";
2
+ import { isTrue, numberArg, optionalString } from "./util.js";
3
3
  /** Fixed send windows before adaptive scaling; explicit flags always win. */
4
4
  export const SEND_BUDGETS = {
5
5
  /** Pause before checking the paste landed. */
6
6
  settleMs: 300,
7
7
  /** Pane-lookup retry window for an absent pane. */
8
8
  observeMs: 1_000,
9
+ /**
10
+ * Base window that polls the pane for positive evidence before any negative
11
+ * verdict; scaled up for slow panes and capped by `confirmMaxMs`.
12
+ */
13
+ confirmMs: 1_000,
9
14
  /** The scaled window never exceeds this multiple of the default. */
10
15
  maxScale: 5,
11
16
  /** A first Zellij call at or under this many ms is not "slow". */
12
17
  slowMs: 1_000,
13
18
  settleMaxMs: 5_000,
14
19
  observeMaxMs: 30_000,
20
+ confirmMaxMs: 15_000,
15
21
  };
22
+ /** Upper bound on confirm polls, so a pinned deadline cannot spin forever. */
23
+ const MAX_CONFIRM_POLLS = 40;
16
24
  /**
17
- * Scale the fixed settle/observe windows by how slow the send's first Zellij
18
- * call was. A call under `slowMs` keeps the defaults; five seconds or more pins
19
- * the 5x cap. Explicit `--settle-ms` / `--observe-ms` are returned as given,
20
- * and a scaled value is capped by the call's own timeout so it cannot outlast
21
- * the observation budget.
25
+ * Scale the fixed settle/observe/confirm windows by how slow the send's first
26
+ * Zellij call was. A call under `slowMs` keeps the defaults; five seconds or
27
+ * more pins the 5x cap. Explicit `--settle-ms` / `--observe-ms` are returned as
28
+ * given, and a scaled value is capped by the call's own timeout so it cannot
29
+ * outlast the observation budget. `confirmMs` is always scaled (no explicit
30
+ * flag) and capped by both `confirmMaxMs` and the call timeout.
22
31
  */
23
32
  export function scaleBudgets(input) {
24
33
  const raw = input.firstCallMs > SEND_BUDGETS.slowMs ? input.firstCallMs / SEND_BUDGETS.slowMs : 1;
@@ -27,6 +36,7 @@ export function scaleBudgets(input) {
27
36
  return {
28
37
  settleMs: input.settleMs !== undefined ? Math.min(input.settleMs, SEND_BUDGETS.settleMaxMs) : scaled(SEND_BUDGETS.settleMs, SEND_BUDGETS.settleMaxMs),
29
38
  observeMs: input.observeMs !== undefined ? Math.min(input.observeMs, SEND_BUDGETS.observeMaxMs) : scaled(SEND_BUDGETS.observeMs, SEND_BUDGETS.observeMaxMs),
39
+ confirmMs: scaled(SEND_BUDGETS.confirmMs, SEND_BUDGETS.confirmMaxMs),
30
40
  scale,
31
41
  };
32
42
  }
@@ -91,7 +101,8 @@ export function bodyText(client, args, body) {
91
101
  }
92
102
  /**
93
103
  * Caller `submit=` always wins. With none given, the pane's harness picks
94
- * auto vs double-enter (codex needs two Enters; the rest submit on one).
104
+ * auto vs double-enter (codex needs two Enters; the rest submit on one). An
105
+ * explicit `--harness` overrides the pane-inferred profile.
95
106
  */
96
107
  export function resolveSubmitMode(args, pane) {
97
108
  const raw = args.submit;
@@ -99,7 +110,7 @@ export function resolveSubmitMode(args, pane) {
99
110
  return raw;
100
111
  if (raw !== undefined && raw !== null && raw !== "")
101
112
  return "auto";
102
- return resolveHarness(pane).submit;
113
+ return resolveHarness(pane, optionalString(args.harness)).submit;
103
114
  }
104
115
  function nonEmptyLines(screen) {
105
116
  return screen
@@ -205,7 +216,7 @@ async function dumpOrNull(client, session, paneId) {
205
216
  }
206
217
  }
207
218
  async function verifySubmit(client, input) {
208
- const { session, paneId, body, mode, settleMs, clock, before, queued } = input;
219
+ const { session, paneId, body, mode, settleMs, confirmMs, clock, before, profile, queued } = input;
209
220
  const classify = (base, screen) => classifySubmit(base, screen, body, { queued });
210
221
  if (mode === "none")
211
222
  return "unverified";
@@ -232,24 +243,58 @@ async function verifySubmit(client, input) {
232
243
  verdict = classify(before, rescued);
233
244
  if (verdict === true || verdict === "queued")
234
245
  return verdict;
235
- if (verdict === false)
236
- return false;
237
246
  }
238
- // Inconclusive or not (yet) on screen; TUIs often redraw after the first settle.
239
- await clock.sleep(Math.min(Math.max(settleMs * 2, 800), 5000));
240
- const later = await dumpOrNull(client, session, paneId);
241
- if (later === null)
242
- return verdict;
243
- if (composerHolds(after, body) && !composerHolds(later, body))
244
- return true;
245
- return classify(before, later);
247
+ // Nothing conclusive yet: a freshly restarted agent can redraw slowly, so poll
248
+ // for the whole scaled confirm window before any negative verdict. Positive
249
+ // evidence is new output, a queue hint, the harness's busy marker, or the body
250
+ // leaving a composer it was sitting in.
251
+ const pollMs = Math.min(Math.max(settleMs * 2, 500), 1_000);
252
+ const deadline = clock.now() + confirmMs;
253
+ for (let polls = 0; polls < MAX_CONFIRM_POLLS && clock.now() < deadline; polls++) {
254
+ await clock.sleep(Math.min(pollMs, Math.max(0, deadline - clock.now())));
255
+ const later = await dumpOrNull(client, session, paneId);
256
+ if (later === null)
257
+ continue; // unreadable pane: keep polling
258
+ const seen = classify(before, later);
259
+ if (seen === true || seen === "queued")
260
+ return seen;
261
+ if (busyMarker(later, profile))
262
+ return true;
263
+ if (composerHolds(after, body) && !composerHolds(later, body))
264
+ return true;
265
+ verdict = seen;
266
+ }
267
+ // An unknown harness has no reliable busy marker, so an unchanged screen is
268
+ // not proof the body never arrived; a false not-delivered would make the
269
+ // caller resend the whole message. A known harness's not-delivered is now
270
+ // backed by the whole window.
271
+ if (verdict === "not-delivered" && profile.name === "unknown")
272
+ return "unverified";
273
+ return verdict;
274
+ }
275
+ /**
276
+ * Press Enter once and report whether the body submitted, without pasting.
277
+ * The restart op uses this to re-ask a still-held composer; a failed dump is
278
+ * unverified, never a negative verdict.
279
+ */
280
+ export async function submitAfterEnter(client, input) {
281
+ const { session, paneId, body, before, clock, settleMs, profile } = input;
282
+ await client.sendKeys({ session, paneId, keys: ["Enter"] });
283
+ await clock.sleep(settleMs);
284
+ const screen = await dumpOrNull(client, session, paneId);
285
+ if (screen === null)
286
+ return "unverified";
287
+ return classifySubmit(before, screen, body, {
288
+ queued: (s) => queuedPrompt(s, profile),
289
+ });
246
290
  }
247
291
  /** Paste a body into one pane and record the attempt. */
248
292
  export async function deliverTo(client, state, args, input) {
249
293
  const { session, pane, body, op, at, clock, firstCallMs = 0, timeoutMs = 30_000 } = input;
250
294
  const from = senderLabel(args);
295
+ const override = optionalString(args.harness);
251
296
  const mode = resolveSubmitMode(args, pane);
252
- const profile = resolveHarness(pane);
297
+ const profile = resolveHarness(pane, override);
253
298
  const queued = (screen) => queuedPrompt(screen, profile);
254
299
  const explicitSettle = args.settleMs !== undefined && args.settleMs !== null && args.settleMs !== "";
255
300
  const budgets = scaleBudgets({
@@ -258,6 +303,7 @@ export async function deliverTo(client, state, args, input) {
258
303
  settleMs: explicitSettle ? numberArg(args, "settleMs", SEND_BUDGETS.settleMs, { min: 50, max: SEND_BUDGETS.settleMaxMs }) : undefined,
259
304
  });
260
305
  const settleMs = budgets.settleMs;
306
+ const confirmMs = budgets.confirmMs;
261
307
  const confirmOnce = isTrue(args.confirm);
262
308
  try {
263
309
  let before = null;
@@ -276,8 +322,10 @@ export async function deliverTo(client, state, args, input) {
276
322
  body,
277
323
  mode,
278
324
  settleMs,
325
+ confirmMs,
279
326
  clock,
280
327
  before,
328
+ profile,
281
329
  queued,
282
330
  });
283
331
  // Opt-in confirm. A second paste is only safe on positive evidence the
@@ -293,8 +341,10 @@ export async function deliverTo(client, state, args, input) {
293
341
  body,
294
342
  mode,
295
343
  settleMs,
344
+ confirmMs,
296
345
  clock,
297
346
  before: retryBefore,
347
+ profile,
298
348
  queued,
299
349
  });
300
350
  }
@@ -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
+ }, harness?: string | null): 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
@@ -1,6 +1,6 @@
1
1
  import { ZellijError } from "../errors.js";
2
2
  import { createGitClient } from "../git.js";
3
- import { busyPrompt, resolveHarness } from "../harness.js";
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, harness) {
40
+ return busyMarker(screen, resolveHarness(pane, harness))?.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 && busyPrompt(screen, resolveHarness(pane))) {
403
- throw new ZellijError("pane_busy", `pane ${to} is busy with a task; retry when it is idle`);
414
+ if (screen !== null) {
415
+ const marker = paneBusyMarker(screen, pane, optionalString(args.harness));
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,59 @@
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
+ /** Settle after the rescue Enter before re-reading the composer, matching the relay. */
31
+ export declare const SUBMIT_ENTER_SETTLE_MS = 800;
32
+ export type RestartPaneKind = "command" | "shell";
33
+ export type RestartStep = {
34
+ step: string;
35
+ ok: boolean;
36
+ detail: string;
37
+ elapsedMs: number;
38
+ };
39
+ /**
40
+ * A command pane ran the agent itself, so ENTER re-runs it. A shell pane had the
41
+ * agent started inside a shell, so the launch command has to be typed again.
42
+ * A pane with no command information is treated as a shell: typing a launch
43
+ * command is recoverable, pressing ENTER into a live agent is not.
44
+ */
45
+ export declare function restartPaneKind(pane: ZellijPane): RestartPaneKind;
46
+ /** The command line a shell pane gets to relaunch the agent. */
47
+ export declare function restartLaunchCommand(args: Record<string, unknown>, profile: HarnessProfile): string | null;
48
+ /**
49
+ * Restart one already-resolved pane. `target` comes from dispatch's
50
+ * `resolveTarget`, so policy and pane lookup are shared with the other ops.
51
+ */
52
+ export declare function restartPane(client: ZellijClient, state: StateStore, args: Record<string, unknown>, target: {
53
+ session: string;
54
+ pane: ZellijPane;
55
+ panes: ZellijPane[];
56
+ }, clock: Clock, opts?: {
57
+ env?: NodeJS.ProcessEnv;
58
+ signal?: AbortSignal;
59
+ }): Promise<OpsResult>;