@f5-sales-demo/xcsh 19.61.5 → 19.62.1
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/package.json +8 -8
- package/src/browser/capabilities.generated.ts +23 -3
- package/src/browser/capabilities.json +21 -1
- package/src/browser/chat-conformance.json +16 -2
- package/src/browser/chat-handler.ts +52 -6
- package/src/browser/chat-protocol.ts +20 -0
- package/src/commands/manager-core.ts +19 -0
- package/src/commands/manager.ts +33 -3
- package/src/internal-urls/build-info.generated.ts +8 -8
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "19.
|
|
4
|
+
"version": "19.62.1",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -56,13 +56,13 @@
|
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
58
58
|
"@mozilla/readability": "^0.6",
|
|
59
|
-
"@f5-sales-demo/xcsh-stats": "19.
|
|
60
|
-
"@f5-sales-demo/pi-agent-core": "19.
|
|
61
|
-
"@f5-sales-demo/pi-ai": "19.
|
|
62
|
-
"@f5-sales-demo/pi-natives": "19.
|
|
63
|
-
"@f5-sales-demo/pi-resource-management": "19.
|
|
64
|
-
"@f5-sales-demo/pi-tui": "19.
|
|
65
|
-
"@f5-sales-demo/pi-utils": "19.
|
|
59
|
+
"@f5-sales-demo/xcsh-stats": "19.62.1",
|
|
60
|
+
"@f5-sales-demo/pi-agent-core": "19.62.1",
|
|
61
|
+
"@f5-sales-demo/pi-ai": "19.62.1",
|
|
62
|
+
"@f5-sales-demo/pi-natives": "19.62.1",
|
|
63
|
+
"@f5-sales-demo/pi-resource-management": "19.62.1",
|
|
64
|
+
"@f5-sales-demo/pi-tui": "19.62.1",
|
|
65
|
+
"@f5-sales-demo/pi-utils": "19.62.1",
|
|
66
66
|
"@sinclair/typebox": "^0.34",
|
|
67
67
|
"@xterm/headless": "^6.0",
|
|
68
68
|
"ajv": "^8.20",
|
|
@@ -26,7 +26,7 @@ export interface ExtensionCapabilities {
|
|
|
26
26
|
|
|
27
27
|
export const EXTENSION_CAPABILITIES: ExtensionCapabilities = {
|
|
28
28
|
"version": "0.1.0",
|
|
29
|
-
"contractVersion": "1.
|
|
29
|
+
"contractVersion": "1.6.0",
|
|
30
30
|
"multiPortDiscovery": true,
|
|
31
31
|
"protocol": "tool_request/result",
|
|
32
32
|
"tools": [
|
|
@@ -625,6 +625,26 @@ export const EXTENSION_CAPABILITIES: ExtensionCapabilities = {
|
|
|
625
625
|
},
|
|
626
626
|
"flags": {}
|
|
627
627
|
},
|
|
628
|
+
{
|
|
629
|
+
"name": "diag_activation",
|
|
630
|
+
"summary": "Diagnostic: per-gate tab-activation readiness timings (bridge/worker/page), cold/warm.",
|
|
631
|
+
"category": "read",
|
|
632
|
+
"params": {
|
|
633
|
+
"type": "object",
|
|
634
|
+
"properties": {}
|
|
635
|
+
},
|
|
636
|
+
"flags": {}
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
"name": "diag_ttft",
|
|
640
|
+
"summary": "Diagnostic: init→first-token timeline (per-stage ms, total, dominant, cold/warm).",
|
|
641
|
+
"category": "read",
|
|
642
|
+
"params": {
|
|
643
|
+
"type": "object",
|
|
644
|
+
"properties": {}
|
|
645
|
+
},
|
|
646
|
+
"flags": {}
|
|
647
|
+
},
|
|
628
648
|
{
|
|
629
649
|
"name": "capture_login_flow",
|
|
630
650
|
"summary": "Diagnostic: captured login redirect chain annotated with tenant/env (Phase 0b).",
|
|
@@ -825,6 +845,6 @@ export const EXTENSION_CAPABILITIES: ExtensionCapabilities = {
|
|
|
825
845
|
}
|
|
826
846
|
};
|
|
827
847
|
|
|
828
|
-
export const EXTENSION_CONTRACT_VERSION = "1.
|
|
848
|
+
export const EXTENSION_CONTRACT_VERSION = "1.6.0";
|
|
829
849
|
|
|
830
|
-
export const EXTENSION_TOOL_NAMES: readonly string[] = ["ping","capabilities","reload","debug_exec","detach","set_bridge_port","navigate","login","scroll_to","resize_window","tabs_list","tabs_create","tabs_close","click","click_element","click_xy","type_text","form_input","key_press","select_option","label_select","file_upload","read_ax","get_page_text","query_dom","find","wait_for","assert_text","screenshot","read_console","read_network","diag_suspension","diag_bridges","capture_login_flow","wait_for_api_response","get_page_context","javascript_tool","browser_batch","set_explain_mode","annotate"];
|
|
850
|
+
export const EXTENSION_TOOL_NAMES: readonly string[] = ["ping","capabilities","reload","debug_exec","detach","set_bridge_port","navigate","login","scroll_to","resize_window","tabs_list","tabs_create","tabs_close","click","click_element","click_xy","type_text","form_input","key_press","select_option","label_select","file_upload","read_ax","get_page_text","query_dom","find","wait_for","assert_text","screenshot","read_console","read_network","diag_suspension","diag_bridges","diag_activation","diag_ttft","capture_login_flow","wait_for_api_response","get_page_context","javascript_tool","browser_batch","set_explain_mode","annotate"];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "0.1.0",
|
|
3
|
-
"contractVersion": "1.
|
|
3
|
+
"contractVersion": "1.6.0",
|
|
4
4
|
"multiPortDiscovery": true,
|
|
5
5
|
"protocol": "tool_request/result",
|
|
6
6
|
"tools": [
|
|
@@ -550,6 +550,26 @@
|
|
|
550
550
|
},
|
|
551
551
|
"flags": {}
|
|
552
552
|
},
|
|
553
|
+
{
|
|
554
|
+
"name": "diag_activation",
|
|
555
|
+
"summary": "Diagnostic: per-gate tab-activation readiness timings (bridge/worker/page), cold/warm.",
|
|
556
|
+
"category": "read",
|
|
557
|
+
"params": {
|
|
558
|
+
"type": "object",
|
|
559
|
+
"properties": {}
|
|
560
|
+
},
|
|
561
|
+
"flags": {}
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
"name": "diag_ttft",
|
|
565
|
+
"summary": "Diagnostic: init→first-token timeline (per-stage ms, total, dominant, cold/warm).",
|
|
566
|
+
"category": "read",
|
|
567
|
+
"params": {
|
|
568
|
+
"type": "object",
|
|
569
|
+
"properties": {}
|
|
570
|
+
},
|
|
571
|
+
"flags": {}
|
|
572
|
+
},
|
|
553
573
|
{
|
|
554
574
|
"name": "capture_login_flow",
|
|
555
575
|
"summary": "Diagnostic: captured login redirect chain annotated with tenant/env (Phase 0b).",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"contractVersion": "1.
|
|
2
|
+
"contractVersion": "1.6.0",
|
|
3
3
|
"schemas": {
|
|
4
4
|
"chat_request": {
|
|
5
5
|
"type": "object",
|
|
@@ -210,6 +210,19 @@
|
|
|
210
210
|
},
|
|
211
211
|
"error": {
|
|
212
212
|
"type": "string"
|
|
213
|
+
},
|
|
214
|
+
"reason": {
|
|
215
|
+
"anyOf": [
|
|
216
|
+
{ "const": "bridge-disconnected", "type": "string" },
|
|
217
|
+
{ "const": "bridge-unresponsive", "type": "string" },
|
|
218
|
+
{ "const": "no-worker", "type": "string" },
|
|
219
|
+
{ "const": "session-busy", "type": "string" },
|
|
220
|
+
{ "const": "session-disposed", "type": "string" },
|
|
221
|
+
{ "const": "token-expired", "type": "string" },
|
|
222
|
+
{ "const": "token-expiring", "type": "string" },
|
|
223
|
+
{ "const": "provider-4xx", "type": "string" },
|
|
224
|
+
{ "const": "provider-5xx", "type": "string" }
|
|
225
|
+
]
|
|
213
226
|
}
|
|
214
227
|
}
|
|
215
228
|
},
|
|
@@ -433,7 +446,8 @@
|
|
|
433
446
|
"chat_error": {
|
|
434
447
|
"type": "chat_error",
|
|
435
448
|
"id": "c-1111",
|
|
436
|
-
"error": "HTTP 403 forbidden"
|
|
449
|
+
"error": "HTTP 403 forbidden",
|
|
450
|
+
"reason": "provider-4xx"
|
|
437
451
|
},
|
|
438
452
|
"chat_tool_notice": {
|
|
439
453
|
"type": "chat_tool_notice",
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type ChatDelta,
|
|
5
5
|
type ChatDone,
|
|
6
6
|
type ChatError,
|
|
7
|
+
type ChatErrorReason,
|
|
7
8
|
type ChatReference,
|
|
8
9
|
type ChatRequest,
|
|
9
10
|
type InteractionMode,
|
|
@@ -52,7 +53,12 @@ export class ChatHandler {
|
|
|
52
53
|
|
|
53
54
|
this.#server.onDisconnected(() => {
|
|
54
55
|
for (const chat of this.#activeChats.values()) {
|
|
55
|
-
this.#sendTerminal(chat, {
|
|
56
|
+
this.#sendTerminal(chat, {
|
|
57
|
+
type: "chat_error",
|
|
58
|
+
id: chat.id,
|
|
59
|
+
error: "bridge disconnected",
|
|
60
|
+
reason: "bridge-disconnected",
|
|
61
|
+
});
|
|
56
62
|
chat.unsubscribe();
|
|
57
63
|
}
|
|
58
64
|
this.#activeChats.clear();
|
|
@@ -63,7 +69,12 @@ export class ChatHandler {
|
|
|
63
69
|
const { id } = req;
|
|
64
70
|
|
|
65
71
|
if (this.#session.isStreaming || this.#activeChats.size > 0) {
|
|
66
|
-
this.#server.send({
|
|
72
|
+
this.#server.send({
|
|
73
|
+
type: "chat_error",
|
|
74
|
+
id,
|
|
75
|
+
error: "session busy",
|
|
76
|
+
reason: "session-busy",
|
|
77
|
+
} satisfies ChatError);
|
|
67
78
|
return;
|
|
68
79
|
}
|
|
69
80
|
|
|
@@ -96,7 +107,7 @@ export class ChatHandler {
|
|
|
96
107
|
await this.#session.prompt(prompt, { expandPromptTemplates: false, synthetic: false });
|
|
97
108
|
} catch (err: unknown) {
|
|
98
109
|
const message = err instanceof Error ? err.message : "unknown error";
|
|
99
|
-
this.#sendTerminal(chat, { type: "chat_error", id, error: message });
|
|
110
|
+
this.#sendTerminal(chat, { type: "chat_error", id, error: message, reason: classifyChatErrorReason(message) });
|
|
100
111
|
} finally {
|
|
101
112
|
if (!chat.terminalSent) {
|
|
102
113
|
this.#sendTerminal(chat, { type: "chat_done", id });
|
|
@@ -153,15 +164,22 @@ export class ChatHandler {
|
|
|
153
164
|
}
|
|
154
165
|
} else if (ame.type === "error") {
|
|
155
166
|
const errorMsg = ame.error?.errorMessage ?? "LLM error";
|
|
156
|
-
this.#sendTerminal(chat, {
|
|
167
|
+
this.#sendTerminal(chat, {
|
|
168
|
+
type: "chat_error",
|
|
169
|
+
id: chat.id,
|
|
170
|
+
error: errorMsg,
|
|
171
|
+
reason: classifyChatErrorReason(errorMsg),
|
|
172
|
+
});
|
|
157
173
|
}
|
|
158
174
|
} else if (event.type === "message_end" && event.message.role === "assistant") {
|
|
159
175
|
const msg = event.message as AssistantMessage;
|
|
160
176
|
if (msg.stopReason === "error") {
|
|
177
|
+
const errorMsg = msg.errorMessage ?? "assistant error";
|
|
161
178
|
this.#sendTerminal(chat, {
|
|
162
179
|
type: "chat_error",
|
|
163
180
|
id: chat.id,
|
|
164
|
-
error:
|
|
181
|
+
error: errorMsg,
|
|
182
|
+
reason: classifyChatErrorReason(errorMsg),
|
|
165
183
|
});
|
|
166
184
|
} else {
|
|
167
185
|
const references = extractReferences(msg);
|
|
@@ -194,13 +212,41 @@ export class ChatHandler {
|
|
|
194
212
|
|
|
195
213
|
dispose(): void {
|
|
196
214
|
for (const chat of this.#activeChats.values()) {
|
|
197
|
-
this.#sendTerminal(chat, {
|
|
215
|
+
this.#sendTerminal(chat, {
|
|
216
|
+
type: "chat_error",
|
|
217
|
+
id: chat.id,
|
|
218
|
+
error: "session disposed",
|
|
219
|
+
reason: "session-disposed",
|
|
220
|
+
});
|
|
198
221
|
chat.unsubscribe();
|
|
199
222
|
}
|
|
200
223
|
this.#activeChats.clear();
|
|
201
224
|
}
|
|
202
225
|
}
|
|
203
226
|
|
|
227
|
+
/** Best-effort classification of an upstream/provider error message into a
|
|
228
|
+
* ChatErrorReason so the panel can pick a distinct, actionable message. Returns
|
|
229
|
+
* undefined for an unclassified error (the panel then shows the raw error text).
|
|
230
|
+
* Token checks run before the generic 4xx check (a 401 is more useful as an
|
|
231
|
+
* expired-token hint than a bare client error). */
|
|
232
|
+
export function classifyChatErrorReason(message: string): ChatErrorReason | undefined {
|
|
233
|
+
const m = message.toLowerCase();
|
|
234
|
+
if (/\btoken\b[^.]*\b(expir|invalid)|\b(expir|invalid)[^.]*\btoken\b|aws sso|\/context (create|validate)/.test(m)) {
|
|
235
|
+
return "token-expired";
|
|
236
|
+
}
|
|
237
|
+
if (
|
|
238
|
+
/\b5\d\d\b|internal server error|bad gateway|service unavailable|gateway timeout|econnreset|etimedout|socket hang up|network error/.test(
|
|
239
|
+
m,
|
|
240
|
+
)
|
|
241
|
+
) {
|
|
242
|
+
return "provider-5xx";
|
|
243
|
+
}
|
|
244
|
+
if (/\b4\d\d\b|forbidden|unauthorized|invalid model|bad request|not found|too many requests|rate limit/.test(m)) {
|
|
245
|
+
return "provider-4xx";
|
|
246
|
+
}
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
204
250
|
/**
|
|
205
251
|
* Chrome-extension self-awareness prompt. Injected when xcsh is serving a browser
|
|
206
252
|
* chat (not the CLI TUI). Tells the LLM it's in a Chrome side panel alongside the
|
|
@@ -80,10 +80,30 @@ export interface ChatDone {
|
|
|
80
80
|
references?: ChatReference[];
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/** Machine-readable cause of a terminal chat_error, so the panel can render a
|
|
84
|
+
* distinct, actionable message (and decide whether to auto-recover) instead of a
|
|
85
|
+
* generic failure. Additive/optional on the wire (contract 1.6.0); an omitted
|
|
86
|
+
* reason means an unclassified error (legacy behavior — show the error text).
|
|
87
|
+
* Shared vocabulary with the extension (keep both lists identical). */
|
|
88
|
+
export const CHAT_ERROR_REASONS = [
|
|
89
|
+
"bridge-disconnected", // the worker's bridge closed mid-turn
|
|
90
|
+
"bridge-unresponsive", // the socket looked open but the worker never answered
|
|
91
|
+
"no-worker", // no worker is running for this tab
|
|
92
|
+
"session-busy", // a turn is already in flight for this session
|
|
93
|
+
"session-disposed", // the worker session was torn down
|
|
94
|
+
"token-expired", // F5 XC API token expired
|
|
95
|
+
"token-expiring", // F5 XC API token is about to expire
|
|
96
|
+
"provider-4xx", // upstream provider rejected the request (client error)
|
|
97
|
+
"provider-5xx", // upstream provider failed (server error) — retryable
|
|
98
|
+
] as const;
|
|
99
|
+
|
|
100
|
+
export type ChatErrorReason = (typeof CHAT_ERROR_REASONS)[number];
|
|
101
|
+
|
|
83
102
|
export interface ChatError {
|
|
84
103
|
type: "chat_error";
|
|
85
104
|
id: string;
|
|
86
105
|
error: string;
|
|
106
|
+
reason?: ChatErrorReason;
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
// ---------------------------------------------------------------------------
|
|
@@ -131,6 +131,25 @@ export function touchLastSeen(reg: Registry, sessionId: string | undefined, now:
|
|
|
131
131
|
return true;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/** True iff this manager should self-recycle because it is running a STALE on-disk
|
|
135
|
+
* binary. Only for a COMPILED install: after `brew upgrade` + `brew cleanup`, the old
|
|
136
|
+
* versioned binary the manager was launched from is DELETED, yet the process lingers
|
|
137
|
+
* on the freed inode — and can no longer spawn workers/spares (spawn ENOENT), which
|
|
138
|
+
* surfaces as the extension's "xcsh didn't start". Detecting the deleted binary lets
|
|
139
|
+
* the manager step down so a fresh one (launched from the version-stable PATH wrapper)
|
|
140
|
+
* takes over. Dev (`bun src/cli.ts`) has a live interpreter execPath → never stale.
|
|
141
|
+
* Fail-closed: any error from `exists` → NOT stale, so uncertainty never triggers a
|
|
142
|
+
* recycle loop. Only the unambiguous deleted-binary signal is used (a version/realpath
|
|
143
|
+
* drift signal was deliberately NOT added — false positives would flap-recycle). */
|
|
144
|
+
export function binaryIsStale(opts: { compiled: boolean; execPath: string; exists: (p: string) => boolean }): boolean {
|
|
145
|
+
if (!opts.compiled) return false;
|
|
146
|
+
try {
|
|
147
|
+
return !opts.exists(opts.execPath);
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
134
153
|
/** The NDJSON keepalive an actively-chatting worker writes to the manager control
|
|
135
154
|
* socket to refresh its `lastSeen` (consumed by `touchLastSeen`). Null for the
|
|
136
155
|
* unbound `spare` sentinel or an empty id — a spare has no session to keep alive. */
|
package/src/commands/manager.ts
CHANGED
|
@@ -26,8 +26,11 @@ import { Command } from "@f5-sales-demo/pi-utils/cli";
|
|
|
26
26
|
// slows the manager's cold start — keep the daemon's module graph minimal).
|
|
27
27
|
import { VERSION } from "@f5-sales-demo/pi-utils/dirs";
|
|
28
28
|
import { portCandidates } from "../browser/extension-bridge";
|
|
29
|
+
// Lean standalone fn (compiled-runtime detection) — no heavy graph, safe for the daemon.
|
|
30
|
+
import { detectCompiledRuntime } from "../internal-urls/build-info-runtime";
|
|
29
31
|
import { removeManagerState, writeManagerState } from "../services/manager-state";
|
|
30
32
|
import {
|
|
33
|
+
binaryIsStale,
|
|
31
34
|
needsProvision,
|
|
32
35
|
parseControlMsg,
|
|
33
36
|
pickPort,
|
|
@@ -209,6 +212,17 @@ export default class Manager extends Command {
|
|
|
209
212
|
// (production always reports the real binary VERSION).
|
|
210
213
|
const selfVersion = process.env.XCSH_MANAGER_VERSION || VERSION;
|
|
211
214
|
|
|
215
|
+
// Durable-upgrade self-recycle (#upgrade-recycle): a compiled manager whose
|
|
216
|
+
// on-disk binary was removed by `brew upgrade`+`brew cleanup` lingers on the
|
|
217
|
+
// freed inode but can no longer spawn workers (ENOENT) → it must step down so a
|
|
218
|
+
// fresh manager (from the version-stable PATH wrapper) takes over. Dev is never
|
|
219
|
+
// compiled → never stale. `XCSH_FORCE_STALE=1` lets the dev-mode integration test
|
|
220
|
+
// exercise the recycle path (same env-override convention as XCSH_MANAGER_VERSION).
|
|
221
|
+
const compiled = detectCompiledRuntime(import.meta.url, Bun.env);
|
|
222
|
+
const isSelfStale = (): boolean =>
|
|
223
|
+
process.env.XCSH_FORCE_STALE === "1" ||
|
|
224
|
+
binaryIsStale({ compiled, execPath: process.execPath, exists: fs.existsSync });
|
|
225
|
+
|
|
212
226
|
// Lifecycle gate (#1874): once we begin graceful shutdown we stop accepting
|
|
213
227
|
// provisions (the host retries the successor manager) and never double-run
|
|
214
228
|
// the teardown.
|
|
@@ -430,13 +444,22 @@ export default class Manager extends Command {
|
|
|
430
444
|
const msg = parseControlMsg(raw);
|
|
431
445
|
if (!msg) return; // fail closed on unknown/invalid frames
|
|
432
446
|
if (msg.type === "provision") {
|
|
433
|
-
// Draining
|
|
434
|
-
|
|
447
|
+
// Draining, OR self-stale (our binary was removed by an upgrade): refuse
|
|
448
|
+
// this provision so the host retries the successor manager — spawning now
|
|
449
|
+
// would ENOENT on the deleted binary. If stale-but-still-accepting, step
|
|
450
|
+
// down immediately so the fresh manager takes over in seconds (not on the
|
|
451
|
+
// ≤60s sweep). Reuses the existing draining reply/contract.
|
|
452
|
+
const stale = accepting && isSelfStale();
|
|
453
|
+
if (!accepting || stale) {
|
|
435
454
|
try {
|
|
436
455
|
socket.write(`${JSON.stringify({ type: "draining" })}\n`);
|
|
437
456
|
} catch {
|
|
438
457
|
/* client hung up */
|
|
439
458
|
}
|
|
459
|
+
if (stale) {
|
|
460
|
+
console.error("[xcsh manager] binary is stale (upgraded/removed) — recycling on provision");
|
|
461
|
+
gracefulShutdown("updated");
|
|
462
|
+
}
|
|
440
463
|
return;
|
|
441
464
|
}
|
|
442
465
|
const provisionReceivedAt = Date.now(); // TTFT Phase 2: start of manager_provision
|
|
@@ -537,8 +560,15 @@ export default class Manager extends Command {
|
|
|
537
560
|
// Pre-warm the spare pool so provisions can adopt instead of cold-spawn.
|
|
538
561
|
if (poolTarget > 0) maintainPool();
|
|
539
562
|
|
|
540
|
-
// Idle sweep: reap workers untouched for longer than the TTL.
|
|
563
|
+
// Idle sweep: reap workers untouched for longer than the TTL. Also self-recycle
|
|
564
|
+
// if our own binary went stale (upgrade removed it) — a lingering deleted-binary
|
|
565
|
+
// manager can't spawn workers, so step down for a fresh one (#upgrade-recycle).
|
|
541
566
|
setInterval(() => {
|
|
567
|
+
if (isSelfStale()) {
|
|
568
|
+
console.error("[xcsh manager] binary is stale (upgraded/removed) — recycling on sweep");
|
|
569
|
+
gracefulShutdown("updated");
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
542
572
|
for (const key of staleKeys(reg, Date.now(), IDLE_MS)) reap(key);
|
|
543
573
|
}, SWEEP_MS);
|
|
544
574
|
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "19.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "19.62.1",
|
|
21
|
+
"commit": "914880a26dc07e8cbfe5f79dcaef4012e559779d",
|
|
22
|
+
"shortCommit": "914880a",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.
|
|
25
|
-
"commitDate": "2026-07-
|
|
26
|
-
"buildDate": "2026-07-
|
|
24
|
+
"tag": "v19.62.1",
|
|
25
|
+
"commitDate": "2026-07-08T16:48:22Z",
|
|
26
|
+
"buildDate": "2026-07-08T17:15:30.572Z",
|
|
27
27
|
"dirty": true,
|
|
28
28
|
"prNumber": "",
|
|
29
29
|
"repoUrl": "https://github.com/f5-sales-demo/xcsh",
|
|
30
30
|
"repoSlug": "f5-sales-demo/xcsh",
|
|
31
|
-
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/914880a26dc07e8cbfe5f79dcaef4012e559779d",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.62.1"
|
|
33
33
|
};
|