shadok-ai 0.17.38 → 0.17.39
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 +10 -2
- package/dist/auto-reload.d.ts +74 -0
- package/dist/auto-reload.js +65 -0
- package/dist/auto-reload.js.map +1 -0
- package/dist/server.js +94 -2
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/public/index.html +21 -7
package/README.md
CHANGED
|
@@ -368,8 +368,14 @@ Enter to skip; you can add it later from the web UI).
|
|
|
368
368
|
a profile edit that bumped no version still does. A `↻` lights up on the agent
|
|
369
369
|
(hover for the reasons) and the **version menu** shows *Reload N agents to
|
|
370
370
|
apply changes*; a reload respawns each one in place, keeping its history.
|
|
371
|
-
|
|
372
|
-
|
|
371
|
+
**You don't have to click it**: the cockpit applies it by itself as soon as the
|
|
372
|
+
agent has nothing to lose — no turn running, no question waiting, nothing in
|
|
373
|
+
its input box, no background shell or monitor, no child notification queued —
|
|
374
|
+
and has been quiet for two minutes (the ↻ tooltip then says so).
|
|
375
|
+
One agent at a time, no nudge afterwards, and at most once per 30 minutes per
|
|
376
|
+
agent so a change that does not stick cannot loop. A message you send while
|
|
377
|
+
the respawn runs waits a few seconds for it instead of being refused. Clicking
|
|
378
|
+
still reloads at once; `SHADOK_AUTO_RELOAD=0` turns the automatic part off.
|
|
373
379
|
- **Phone layout** — the cockpit is a fixed chassis that never pans sideways,
|
|
374
380
|
sized on the *visual* viewport so the composer stays clear of the browser's
|
|
375
381
|
own bar and, when the keyboard opens, sits right above it (the instrument bar
|
|
@@ -995,6 +1001,8 @@ machine, which silently shifts every daily prompt on a server running in UTC.
|
|
|
995
1001
|
| `SHADOK_ALLOWED_HOSTS` | host names the cockpit answers besides its own (DNS-rebinding guard); with a password, setting it makes the check strict. `*` disables it |
|
|
996
1002
|
| `SHADOK_TMUX=0` | force the node-pty transport instead of tmux |
|
|
997
1003
|
| `SHADOK_IDLE_MIN` | minutes with no client before a session is reclaimed (60) |
|
|
1004
|
+
| `SHADOK_AUTO_RELOAD=0` | don't apply a pending "reload to apply" by itself — leave it to the ↻ click |
|
|
1005
|
+
| `SHADOK_AUTO_RELOAD_IDLE_SEC` | how long an agent must be quiet before that automatic reload (120) |
|
|
998
1006
|
| `SHADOK_PERMISSION_MODE` | mode new agents start in (default `acceptEdits`) |
|
|
999
1007
|
| `SHADOK_AUTOUPDATE` | fallback only — the GUI setting wins once used |
|
|
1000
1008
|
| `SHADOK_PILOT_PROMPT=0` | don't inject the cockpit system prompt |
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-reload: apply a pending "reload to apply" by itself, at a moment that
|
|
3
|
+
* cuts nothing.
|
|
4
|
+
*
|
|
5
|
+
* The cockpit already KNOWS when a running agent is behind what a fresh spawn
|
|
6
|
+
* would give it (`stalenessReasons`: prompt/guardrails/skills/secrets changed,
|
|
7
|
+
* auth cookie past its week, Claude Code binary updated — plus a profile change
|
|
8
|
+
* stored "at next reload"). Until now it only painted a ↻ badge and waited for a
|
|
9
|
+
* human to click it, so a profile edit or a shadok update sat unapplied on every
|
|
10
|
+
* agent nobody happened to look at.
|
|
11
|
+
*
|
|
12
|
+
* A reload is a `--resume`: the conversation survives, but the PROCESS does not.
|
|
13
|
+
* Everything that lives only in that process dies with it — a turn in flight, a
|
|
14
|
+
* question on screen, text typed in the TUI's input box, a background shell or
|
|
15
|
+
* monitor, a parent notification waiting to be delivered. So the rule is not
|
|
16
|
+
* "reload when idle" but "reload when there is provably nothing to lose", and
|
|
17
|
+
* every condition below names one of those losses.
|
|
18
|
+
*
|
|
19
|
+
* The quiet window is the soft half: an agent that JUST finished is the one a
|
|
20
|
+
* human is most likely about to answer. Waiting a couple of minutes past the
|
|
21
|
+
* last activity keeps the reload out of a live exchange. (A prompt that arrives
|
|
22
|
+
* anyway while the respawn runs is held until it is ready — see the prompt
|
|
23
|
+
* handler — so the worst case is a few seconds of delay, never a lost message.)
|
|
24
|
+
*
|
|
25
|
+
* The backoff is the safety net: if the reasons survive a reload (a fingerprint
|
|
26
|
+
* that is not deterministic, a version probe that flaps), reloading again every
|
|
27
|
+
* two minutes would be a respawn loop. One automatic attempt per window, then
|
|
28
|
+
* the badge stays for a human to look at.
|
|
29
|
+
*/
|
|
30
|
+
export interface AutoReloadInput {
|
|
31
|
+
/** Is the feature on for this instance? */
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
/** How many reasons the agent is behind (staleness + a stored profile gap). */
|
|
34
|
+
pendingReasons: number;
|
|
35
|
+
/** A turn is running, or the server is driving one (`busy`). */
|
|
36
|
+
busy: boolean;
|
|
37
|
+
/** The pane itself shows work (spinner) — covers a turn nobody started from here. */
|
|
38
|
+
working: boolean;
|
|
39
|
+
/** A prompt is being typed into the TUI right now. */
|
|
40
|
+
submitting: boolean;
|
|
41
|
+
/** A restart is already swapping this agent's process. */
|
|
42
|
+
restarting: boolean;
|
|
43
|
+
/** The pilot's process has exited (nothing to reload; the resume path owns it). */
|
|
44
|
+
exited: boolean;
|
|
45
|
+
/** A question/permission dialog is on screen, waiting for an answer. */
|
|
46
|
+
dialogOpen: boolean;
|
|
47
|
+
/** What the TUI input box holds — text a respawn would discard. */
|
|
48
|
+
inputBox: string;
|
|
49
|
+
/** Background shells / monitors the pane reports running. */
|
|
50
|
+
shells: number;
|
|
51
|
+
monitors: number;
|
|
52
|
+
/** Child-agent notifications queued for this agent as a parent. */
|
|
53
|
+
inboxPending: number;
|
|
54
|
+
/** An auto-retry of a failed turn is scheduled. */
|
|
55
|
+
retryPending: boolean;
|
|
56
|
+
/** Milliseconds since the last thing that happened on this agent. */
|
|
57
|
+
idleMs: number;
|
|
58
|
+
/** How long it must have been quiet. */
|
|
59
|
+
quietMs: number;
|
|
60
|
+
/** Milliseconds since the last AUTOMATIC reload of this agent (Infinity if never). */
|
|
61
|
+
sinceLastAutoMs: number;
|
|
62
|
+
/** Minimum gap between two automatic reloads of the same agent. */
|
|
63
|
+
backoffMs: number;
|
|
64
|
+
}
|
|
65
|
+
export type AutoReloadVerdict = {
|
|
66
|
+
reload: true;
|
|
67
|
+
} | {
|
|
68
|
+
reload: false;
|
|
69
|
+
why: string;
|
|
70
|
+
};
|
|
71
|
+
/** Pure: may this agent be reloaded right now without cutting anything? */
|
|
72
|
+
export declare function autoReloadVerdict(i: AutoReloadInput): AutoReloadVerdict;
|
|
73
|
+
/** `SHADOK_AUTO_RELOAD=0` turns it off; anything else (or unset) leaves it on. */
|
|
74
|
+
export declare function autoReloadEnabled(env: string | undefined): boolean;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-reload: apply a pending "reload to apply" by itself, at a moment that
|
|
3
|
+
* cuts nothing.
|
|
4
|
+
*
|
|
5
|
+
* The cockpit already KNOWS when a running agent is behind what a fresh spawn
|
|
6
|
+
* would give it (`stalenessReasons`: prompt/guardrails/skills/secrets changed,
|
|
7
|
+
* auth cookie past its week, Claude Code binary updated — plus a profile change
|
|
8
|
+
* stored "at next reload"). Until now it only painted a ↻ badge and waited for a
|
|
9
|
+
* human to click it, so a profile edit or a shadok update sat unapplied on every
|
|
10
|
+
* agent nobody happened to look at.
|
|
11
|
+
*
|
|
12
|
+
* A reload is a `--resume`: the conversation survives, but the PROCESS does not.
|
|
13
|
+
* Everything that lives only in that process dies with it — a turn in flight, a
|
|
14
|
+
* question on screen, text typed in the TUI's input box, a background shell or
|
|
15
|
+
* monitor, a parent notification waiting to be delivered. So the rule is not
|
|
16
|
+
* "reload when idle" but "reload when there is provably nothing to lose", and
|
|
17
|
+
* every condition below names one of those losses.
|
|
18
|
+
*
|
|
19
|
+
* The quiet window is the soft half: an agent that JUST finished is the one a
|
|
20
|
+
* human is most likely about to answer. Waiting a couple of minutes past the
|
|
21
|
+
* last activity keeps the reload out of a live exchange. (A prompt that arrives
|
|
22
|
+
* anyway while the respawn runs is held until it is ready — see the prompt
|
|
23
|
+
* handler — so the worst case is a few seconds of delay, never a lost message.)
|
|
24
|
+
*
|
|
25
|
+
* The backoff is the safety net: if the reasons survive a reload (a fingerprint
|
|
26
|
+
* that is not deterministic, a version probe that flaps), reloading again every
|
|
27
|
+
* two minutes would be a respawn loop. One automatic attempt per window, then
|
|
28
|
+
* the badge stays for a human to look at.
|
|
29
|
+
*/
|
|
30
|
+
/** Pure: may this agent be reloaded right now without cutting anything? */
|
|
31
|
+
export function autoReloadVerdict(i) {
|
|
32
|
+
const no = (why) => ({ reload: false, why });
|
|
33
|
+
if (!i.enabled)
|
|
34
|
+
return no("disabled");
|
|
35
|
+
if (i.pendingReasons <= 0)
|
|
36
|
+
return no("up to date");
|
|
37
|
+
if (i.exited)
|
|
38
|
+
return no("process exited");
|
|
39
|
+
if (i.restarting)
|
|
40
|
+
return no("already restarting");
|
|
41
|
+
if (i.busy || i.working)
|
|
42
|
+
return no("working");
|
|
43
|
+
if (i.submitting)
|
|
44
|
+
return no("a prompt is being typed");
|
|
45
|
+
if (i.dialogOpen)
|
|
46
|
+
return no("waiting on a question");
|
|
47
|
+
if (i.inputBox.trim())
|
|
48
|
+
return no("text in the input box");
|
|
49
|
+
if (i.shells > 0 || i.monitors > 0)
|
|
50
|
+
return no("background task running");
|
|
51
|
+
if (i.inboxPending > 0)
|
|
52
|
+
return no("notifications queued");
|
|
53
|
+
if (i.retryPending)
|
|
54
|
+
return no("auto-retry scheduled");
|
|
55
|
+
if (i.idleMs < i.quietMs)
|
|
56
|
+
return no("recently active");
|
|
57
|
+
if (i.sinceLastAutoMs < i.backoffMs)
|
|
58
|
+
return no("auto-reloaded recently");
|
|
59
|
+
return { reload: true };
|
|
60
|
+
}
|
|
61
|
+
/** `SHADOK_AUTO_RELOAD=0` turns it off; anything else (or unset) leaves it on. */
|
|
62
|
+
export function autoReloadEnabled(env) {
|
|
63
|
+
return env === undefined || !/^(0|false|off|no)$/i.test(env.trim());
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=auto-reload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-reload.js","sourceRoot":"","sources":["../src/auto-reload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAwCH,2EAA2E;AAC3E,MAAM,UAAU,iBAAiB,CAAC,CAAkB;IAClD,MAAM,EAAE,GAAG,CAAC,GAAW,EAAqB,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IACxE,IAAI,CAAC,CAAC,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC,UAAU,CAAC,CAAC;IACtC,IAAI,CAAC,CAAC,cAAc,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC,YAAY,CAAC,CAAC;IACnD,IAAI,CAAC,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC,gBAAgB,CAAC,CAAC;IAC1C,IAAI,CAAC,CAAC,UAAU;QAAE,OAAO,EAAE,CAAC,oBAAoB,CAAC,CAAC;IAClD,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC,SAAS,CAAC,CAAC;IAC9C,IAAI,CAAC,CAAC,UAAU;QAAE,OAAO,EAAE,CAAC,yBAAyB,CAAC,CAAC;IACvD,IAAI,CAAC,CAAC,UAAU;QAAE,OAAO,EAAE,CAAC,uBAAuB,CAAC,CAAC;IACrD,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,CAAC,uBAAuB,CAAC,CAAC;IAC1D,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC,yBAAyB,CAAC,CAAC;IACzE,IAAI,CAAC,CAAC,YAAY,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC,sBAAsB,CAAC,CAAC;IAC1D,IAAI,CAAC,CAAC,YAAY;QAAE,OAAO,EAAE,CAAC,sBAAsB,CAAC,CAAC;IACtD,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC,iBAAiB,CAAC,CAAC;IACvD,IAAI,CAAC,CAAC,eAAe,GAAG,CAAC,CAAC,SAAS;QAAE,OAAO,EAAE,CAAC,wBAAwB,CAAC,CAAC;IACzE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAC1B,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,iBAAiB,CAAC,GAAuB;IACvD,OAAO,GAAG,KAAK,SAAS,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;AACtE,CAAC"}
|
package/dist/server.js
CHANGED
|
@@ -20,7 +20,8 @@ import { OFFER_TOOL, filesFileFor, loadOffers, saveOffers, offerVerdict, withOff
|
|
|
20
20
|
// the browser): one source for reading the in-flight text off the screen.
|
|
21
21
|
import { extractLiveText } from "../public/live-text.js";
|
|
22
22
|
import { findTransientErrors, newTransientErrors, RETRY_DELAYS_MS } from "./retry.js";
|
|
23
|
-
import { backgroundTasks, screenShowsWork, moveToOption } from "./detect.js";
|
|
23
|
+
import { backgroundTasks, screenShowsWork, moveToOption, boxText } from "./detect.js";
|
|
24
|
+
import { autoReloadVerdict, autoReloadEnabled } from "./auto-reload.js";
|
|
24
25
|
import { PtyPilot } from "./session.js";
|
|
25
26
|
import { ensureClaudeHome, ensureProjectTrusted, getDefaultModel, setDefaultModel, parseClaudeVersion, seedPlanForCwds, settingsPlan, } from "./claude-home.js";
|
|
26
27
|
import { authStatus, cancelLogin, invalidateAuthStatus, startLogin, submitLoginCode } from "./claude-auth.js";
|
|
@@ -6248,6 +6249,13 @@ function broadcast(s, msg, except) {
|
|
|
6248
6249
|
* Shared by the `restart` message and by `set-profile` with `restart: true`.
|
|
6249
6250
|
*/
|
|
6250
6251
|
async function restartSession(s, by = SYSTEM_SPAWN) {
|
|
6252
|
+
const done = restartSessionNow(s, by).finally(() => {
|
|
6253
|
+
s.lastActivityAt = Date.now();
|
|
6254
|
+
});
|
|
6255
|
+
s.restartDone = done.catch(() => { });
|
|
6256
|
+
return done;
|
|
6257
|
+
}
|
|
6258
|
+
async function restartSessionNow(s, by) {
|
|
6251
6259
|
// Fail-closed group check BEFORE any teardown. A restart re-derives the OS
|
|
6252
6260
|
// user from the channel's stored group; if that group no longer resolves,
|
|
6253
6261
|
// `resolveSpawnIsolation` would derive a null user and respawn a private agent
|
|
@@ -6355,6 +6363,7 @@ async function restartSession(s, by = SYSTEM_SPAWN) {
|
|
|
6355
6363
|
sessionId: s.id,
|
|
6356
6364
|
preprompt: prepromptById.get(s.id) ?? [],
|
|
6357
6365
|
stale: sessionStaleness(s),
|
|
6366
|
+
staleAuto: AUTO_RELOAD,
|
|
6358
6367
|
cwd: s.cwd,
|
|
6359
6368
|
branch: s.worktree?.branch ?? null,
|
|
6360
6369
|
lastTurnMs: s.lastTurnMs,
|
|
@@ -6447,7 +6456,70 @@ function sessionStaleness(s, channels) {
|
|
|
6447
6456
|
});
|
|
6448
6457
|
}
|
|
6449
6458
|
function staleMessage(s, channels) {
|
|
6450
|
-
|
|
6459
|
+
// `auto`: the cockpit will apply it by itself once the agent is idle, so the
|
|
6460
|
+
// badge reads "applies when idle" rather than asking for a click.
|
|
6461
|
+
return { type: "stale", sessionId: s.id, reasons: sessionStaleness(s, channels), auto: AUTO_RELOAD };
|
|
6462
|
+
}
|
|
6463
|
+
// ── Auto-reload: apply a pending reload at a moment that cuts nothing ────────
|
|
6464
|
+
// See src/auto-reload.ts for why each condition exists. One agent at a time —
|
|
6465
|
+
// the same no-herd rule as /restart-all (concurrent `--resume` race the OAuth
|
|
6466
|
+
// refresh) — and never a nudge afterwards: the agent was idle, so it stays idle.
|
|
6467
|
+
const AUTO_RELOAD = autoReloadEnabled(process.env.SHADOK_AUTO_RELOAD);
|
|
6468
|
+
const AUTO_RELOAD_QUIET_MS = Number(process.env.SHADOK_AUTO_RELOAD_IDLE_SEC ?? 120) * 1000;
|
|
6469
|
+
const AUTO_RELOAD_BACKOFF_MS = 30 * 60_000;
|
|
6470
|
+
let autoReloadRunning = false;
|
|
6471
|
+
/** Why `s` is behind, counting a profile stored "at next reload" too. */
|
|
6472
|
+
function pendingReloadReasons(s, channels) {
|
|
6473
|
+
const out = sessionStaleness(s, channels).map((r) => r.kind);
|
|
6474
|
+
if ((s.appliedProfile ?? null) !== (s.profile ?? null))
|
|
6475
|
+
out.push("profile");
|
|
6476
|
+
return out;
|
|
6477
|
+
}
|
|
6478
|
+
async function autoReloadSweep() {
|
|
6479
|
+
if (!AUTO_RELOAD || autoReloadRunning)
|
|
6480
|
+
return;
|
|
6481
|
+
const channels = loadChannels();
|
|
6482
|
+
const now = Date.now();
|
|
6483
|
+
for (const s of sessions.values()) {
|
|
6484
|
+
const reasons = pendingReloadReasons(s, channels);
|
|
6485
|
+
if (!reasons.length)
|
|
6486
|
+
continue;
|
|
6487
|
+
const screen = s.pilot.hasExited ? "" : s.pilot.screen();
|
|
6488
|
+
const verdict = autoReloadVerdict({
|
|
6489
|
+
enabled: AUTO_RELOAD,
|
|
6490
|
+
pendingReasons: reasons.length,
|
|
6491
|
+
busy: s.busy,
|
|
6492
|
+
working: !s.pilot.hasExited && s.pilot.isWorking(),
|
|
6493
|
+
submitting: !!s.submitting,
|
|
6494
|
+
restarting: !!s.restarting,
|
|
6495
|
+
exited: s.pilot.hasExited,
|
|
6496
|
+
dialogOpen: s.lastDialogKey !== null || !!detectDialog(screen),
|
|
6497
|
+
inputBox: boxText(screen),
|
|
6498
|
+
shells: s.shells,
|
|
6499
|
+
monitors: s.monitors,
|
|
6500
|
+
inboxPending: parentInbox.get(s.id)?.length ?? 0,
|
|
6501
|
+
retryPending: s.retryTimer !== null,
|
|
6502
|
+
idleMs: now - Math.max(s.lastActivityAt ?? 0, s.lastContentAt ?? 0),
|
|
6503
|
+
quietMs: AUTO_RELOAD_QUIET_MS,
|
|
6504
|
+
sinceLastAutoMs: s.lastAutoReloadAt ? now - s.lastAutoReloadAt : Infinity,
|
|
6505
|
+
backoffMs: AUTO_RELOAD_BACKOFF_MS,
|
|
6506
|
+
});
|
|
6507
|
+
if (!verdict.reload)
|
|
6508
|
+
continue;
|
|
6509
|
+
autoReloadRunning = true;
|
|
6510
|
+
s.lastAutoReloadAt = now;
|
|
6511
|
+
console.log(`auto-reload: ${s.id.slice(0, 8)} idle — respawning to apply ${reasons.join(", ")}`);
|
|
6512
|
+
try {
|
|
6513
|
+
await restartSession(s, { actor: "auto-reload", via: "system" });
|
|
6514
|
+
}
|
|
6515
|
+
catch {
|
|
6516
|
+
/* logged by restartSession; the backoff keeps it from looping */
|
|
6517
|
+
}
|
|
6518
|
+
finally {
|
|
6519
|
+
autoReloadRunning = false;
|
|
6520
|
+
}
|
|
6521
|
+
return; // one per sweep: the next one waits for the next tick
|
|
6522
|
+
}
|
|
6451
6523
|
}
|
|
6452
6524
|
/** Broadcast the staleness reasons, but only when they changed. */
|
|
6453
6525
|
function broadcastStaleness(s, channels) {
|
|
@@ -6650,6 +6722,9 @@ group = null) {
|
|
|
6650
6722
|
monitors: 0,
|
|
6651
6723
|
stopTail: null,
|
|
6652
6724
|
recentTexts: [],
|
|
6725
|
+
// A new Live (a spawn, an adoption after a server restart) counts as
|
|
6726
|
+
// activity: the auto-reload waits a quiet window before touching it.
|
|
6727
|
+
lastActivityAt: Date.now(),
|
|
6653
6728
|
lastTurnSilent: false,
|
|
6654
6729
|
worktree,
|
|
6655
6730
|
idleTimer: null,
|
|
@@ -7092,6 +7167,7 @@ async function finishTurn(s) {
|
|
|
7092
7167
|
finally {
|
|
7093
7168
|
s.busy = false;
|
|
7094
7169
|
s.interruptedAt = null;
|
|
7170
|
+
s.lastActivityAt = Date.now();
|
|
7095
7171
|
// Remember how long it took: a dialog suspends the turn and a completion
|
|
7096
7172
|
// ends it, but both freeze the client's timer, so both are worth keeping.
|
|
7097
7173
|
if (s.turnStartedAt)
|
|
@@ -7430,6 +7506,8 @@ wss.on("connection", (ws, req) => {
|
|
|
7430
7506
|
const current = sessions.get(session.id);
|
|
7431
7507
|
if (current && current !== session)
|
|
7432
7508
|
session = current;
|
|
7509
|
+
// Someone is interacting: hold the auto-reload's quiet window open.
|
|
7510
|
+
session.lastActivityAt = Date.now();
|
|
7433
7511
|
}
|
|
7434
7512
|
// Any user takeover cancels a pending auto-retry and ends the streak.
|
|
7435
7513
|
// "prompt" is settled inside its own case instead: a prompt refused on
|
|
@@ -7663,6 +7741,7 @@ wss.on("connection", (ws, req) => {
|
|
|
7663
7741
|
sessionId: id,
|
|
7664
7742
|
preprompt: prepromptById.get(id) ?? [],
|
|
7665
7743
|
stale: sessionStaleness(session),
|
|
7744
|
+
staleAuto: AUTO_RELOAD,
|
|
7666
7745
|
cwd: session.cwd,
|
|
7667
7746
|
lastTurnMs: session.lastTurnMs,
|
|
7668
7747
|
});
|
|
@@ -7843,6 +7922,7 @@ wss.on("connection", (ws, req) => {
|
|
|
7843
7922
|
sessionId: id,
|
|
7844
7923
|
preprompt: prepromptById.get(id) ?? [],
|
|
7845
7924
|
stale: sessionStaleness(session),
|
|
7925
|
+
staleAuto: AUTO_RELOAD,
|
|
7846
7926
|
cwd: effectiveCwd,
|
|
7847
7927
|
branch: worktree?.branch ?? null,
|
|
7848
7928
|
});
|
|
@@ -7961,6 +8041,14 @@ wss.on("connection", (ws, req) => {
|
|
|
7961
8041
|
if (session.busy)
|
|
7962
8042
|
return fail("a response is already in progress", "busy");
|
|
7963
8043
|
}
|
|
8044
|
+
// A respawn (an auto-reload, a reload click elsewhere) is swapping the
|
|
8045
|
+
// process: typing now would land in the one being torn down. Wait for
|
|
8046
|
+
// the new one — a few seconds — rather than refuse or lose the prompt.
|
|
8047
|
+
if (session.restarting) {
|
|
8048
|
+
await session.restartDone;
|
|
8049
|
+
if (session.busy)
|
|
8050
|
+
return fail("a response is already in progress", "busy");
|
|
8051
|
+
}
|
|
7964
8052
|
// Getting here means the prompt is really being sent — that is the
|
|
7965
8053
|
// takeover. A pending auto-retry (or pace pause) gives way to it.
|
|
7966
8054
|
clearRetry(session, true);
|
|
@@ -8832,6 +8920,10 @@ server.listen(port, HOST, () => {
|
|
|
8832
8920
|
broadcastStaleness(s, channels);
|
|
8833
8921
|
}, 60_000);
|
|
8834
8922
|
staleTimer.unref();
|
|
8923
|
+
// Auto-reload: twice a minute, respawn (at most) one idle agent that is
|
|
8924
|
+
// behind — the "reload to apply" badge applying itself. See autoReloadSweep.
|
|
8925
|
+
const autoReloadTimer = setInterval(() => void autoReloadSweep().catch(() => { }), 30_000);
|
|
8926
|
+
autoReloadTimer.unref();
|
|
8835
8927
|
// The lead agent, if this instance has none. Deferred a beat so it dials a
|
|
8836
8928
|
// server that is already accepting, and never awaited: a cockpit that starts
|
|
8837
8929
|
// without its lead agent is a far smaller problem than a boot that hangs.
|