@f5-sales-demo/xcsh 19.91.0 → 19.91.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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "19.91.
|
|
4
|
+
"version": "19.91.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": "1.3.0",
|
|
58
58
|
"@mozilla/readability": "^0.6",
|
|
59
|
-
"@f5-sales-demo/xcsh-stats": "19.91.
|
|
60
|
-
"@f5-sales-demo/pi-agent-core": "19.91.
|
|
61
|
-
"@f5-sales-demo/pi-ai": "19.91.
|
|
62
|
-
"@f5-sales-demo/pi-natives": "19.91.
|
|
63
|
-
"@f5-sales-demo/pi-resource-management": "19.91.
|
|
64
|
-
"@f5-sales-demo/pi-tui": "19.91.
|
|
65
|
-
"@f5-sales-demo/pi-utils": "19.91.
|
|
59
|
+
"@f5-sales-demo/xcsh-stats": "19.91.1",
|
|
60
|
+
"@f5-sales-demo/pi-agent-core": "19.91.1",
|
|
61
|
+
"@f5-sales-demo/pi-ai": "19.91.1",
|
|
62
|
+
"@f5-sales-demo/pi-natives": "19.91.1",
|
|
63
|
+
"@f5-sales-demo/pi-resource-management": "19.91.1",
|
|
64
|
+
"@f5-sales-demo/pi-tui": "19.91.1",
|
|
65
|
+
"@f5-sales-demo/pi-utils": "19.91.1",
|
|
66
66
|
"@sinclair/typebox": "^0.34",
|
|
67
67
|
"@xterm/headless": "^6.0",
|
|
68
68
|
"ajv": "^8.20",
|
|
@@ -29,6 +29,24 @@ import type { ExtensionAPI, ExtensionContext } from "@f5-sales-demo/xcsh";
|
|
|
29
29
|
* The extension is completely inert unless it is running inside a herdr pane
|
|
30
30
|
* (detected via `HERDR_PANE_ID`), so it has zero effect for users who do not run
|
|
31
31
|
* xcsh under herdr.
|
|
32
|
+
*
|
|
33
|
+
* Sequencing contract with herdr — do not regress. herdr keeps one monotonic
|
|
34
|
+
* `seq` per *source*, shared across every method, and silently discards any frame
|
|
35
|
+
* whose `seq` is not greater than the last it accepted for `herdr:xcsh`. Two
|
|
36
|
+
* consequences drive the design below, and both previously cost panes their
|
|
37
|
+
* resume reference:
|
|
38
|
+
*
|
|
39
|
+
* 1. `seq` is seeded from the wall clock, not 0, so a restarted xcsh always
|
|
40
|
+
* outranks its predecessor in the same pane. With a per-process counter from
|
|
41
|
+
* 0, every frame from the second xcsh in a pane looks stale and is dropped.
|
|
42
|
+
* 2. Frames go out one at a time through `enqueue`. They used to be
|
|
43
|
+
* fire-and-forget on independent sockets, so ordering was left to chance.
|
|
44
|
+
* 3. The state frame is always sent *before* the session frame. herdr discards a
|
|
45
|
+
* `pane.report_agent_session` for a pane whose agent it does not yet own, so
|
|
46
|
+
* the state frame has to establish `herdr:xcsh` as the pane's agent first.
|
|
47
|
+
* Verified against a live herdr: session-then-state (even with a correctly
|
|
48
|
+
* ascending `seq`) leaves `agent_session` null, while state-then-session
|
|
49
|
+
* records it.
|
|
32
50
|
*/
|
|
33
51
|
|
|
34
52
|
const HERDR_AGENT_LABEL = "xcsh";
|
|
@@ -42,26 +60,43 @@ const SOCKET_TIMEOUT_MS = 2000;
|
|
|
42
60
|
|
|
43
61
|
/**
|
|
44
62
|
* Write one newline-delimited JSON-RPC request to herdr's unix socket and close.
|
|
45
|
-
*
|
|
46
|
-
*
|
|
63
|
+
* The response is ignored and every failure path is reported via `onError`
|
|
64
|
+
* without throwing, so a dead socket never disturbs the agent.
|
|
65
|
+
*
|
|
66
|
+
* Resolves once the frame has been flushed (or the attempt failed or timed out),
|
|
67
|
+
* which is what lets callers order frames relative to one another.
|
|
47
68
|
*/
|
|
48
69
|
function sendToHerdrSocket(
|
|
49
70
|
socketPath: string,
|
|
50
71
|
method: string,
|
|
51
72
|
params: Record<string, unknown>,
|
|
52
73
|
onError: (err: unknown) => void,
|
|
53
|
-
): void {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
return new Promise<void>(resolve => {
|
|
76
|
+
let settled = false;
|
|
77
|
+
const settle = (): void => {
|
|
78
|
+
if (settled) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
settled = true;
|
|
82
|
+
resolve();
|
|
83
|
+
};
|
|
84
|
+
const conn = connect({ path: socketPath });
|
|
85
|
+
// Never let a report keep xcsh's event loop alive.
|
|
86
|
+
conn.unref();
|
|
87
|
+
conn.once("error", err => {
|
|
88
|
+
onError(err);
|
|
89
|
+
conn.destroy();
|
|
90
|
+
settle();
|
|
91
|
+
});
|
|
92
|
+
conn.once("connect", () => {
|
|
93
|
+
conn.end(`${JSON.stringify({ id: "xcsh:herdr-reporter", method, params })}\n`, settle);
|
|
94
|
+
});
|
|
95
|
+
conn.setTimeout(SOCKET_TIMEOUT_MS, () => {
|
|
96
|
+
conn.destroy();
|
|
97
|
+
settle();
|
|
98
|
+
});
|
|
63
99
|
});
|
|
64
|
-
conn.setTimeout(SOCKET_TIMEOUT_MS, () => conn.destroy());
|
|
65
100
|
}
|
|
66
101
|
|
|
67
102
|
/** Translate a JSON-RPC report/release into `herdr` CLI arguments (fallback). */
|
|
@@ -90,10 +125,19 @@ export default function herdrReporter(pi: ExtensionAPI): void {
|
|
|
90
125
|
return;
|
|
91
126
|
}
|
|
92
127
|
|
|
93
|
-
|
|
128
|
+
// Seeded from the wall clock at microsecond scale rather than 0 so a new xcsh
|
|
129
|
+
// process in a pane always starts above whatever the previous process reached.
|
|
130
|
+
// Matches the pi/omp reporters and stays well inside Number.MAX_SAFE_INTEGER.
|
|
131
|
+
let seq = Date.now() * 1000;
|
|
94
132
|
// Tracks whether an interactive prompt is currently awaiting the user, so an
|
|
95
133
|
// agent_end that fires while a prompt is open is reported as blocked, not idle.
|
|
96
134
|
let promptOpen = false;
|
|
135
|
+
// Last session ref already delivered, so repeated lifecycle events do not
|
|
136
|
+
// re-send an unchanged ref every turn.
|
|
137
|
+
let lastSessionRefKey: string | undefined;
|
|
138
|
+
// Serializes frames: herdr compares `seq` across all methods for this source,
|
|
139
|
+
// so frames must reach it in the order they were generated.
|
|
140
|
+
let queue: Promise<void> = Promise.resolve();
|
|
97
141
|
|
|
98
142
|
pi.setLabel(HERDR_AGENT_LABEL);
|
|
99
143
|
|
|
@@ -103,17 +147,27 @@ export default function herdrReporter(pi: ExtensionAPI): void {
|
|
|
103
147
|
});
|
|
104
148
|
};
|
|
105
149
|
|
|
106
|
-
|
|
150
|
+
/** Chain a frame onto the tail of the send queue. Never rejects. */
|
|
151
|
+
const enqueue = (task: () => Promise<void>): Promise<void> => {
|
|
152
|
+
queue = queue.then(task, task).catch(onError);
|
|
153
|
+
return queue;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const send = (method: string, params: Record<string, unknown>): Promise<void> => {
|
|
107
157
|
const socketPath = process.env.HERDR_SOCKET_PATH;
|
|
108
158
|
if (socketPath) {
|
|
109
|
-
sendToHerdrSocket(socketPath, method, params, onError);
|
|
110
|
-
return;
|
|
159
|
+
return enqueue(() => sendToHerdrSocket(socketPath, method, params, onError));
|
|
111
160
|
}
|
|
112
161
|
// Fallback for the rare case herdr did not inject a socket path.
|
|
113
|
-
|
|
162
|
+
return enqueue(() =>
|
|
163
|
+
pi
|
|
164
|
+
.exec("herdr", toCliArgs(method, params))
|
|
165
|
+
.then(() => undefined)
|
|
166
|
+
.catch(onError),
|
|
167
|
+
);
|
|
114
168
|
};
|
|
115
169
|
|
|
116
|
-
const report = (state: "idle" | "working" | "blocked"): void =>
|
|
170
|
+
const report = (state: "idle" | "working" | "blocked"): Promise<void> =>
|
|
117
171
|
send(REPORT_METHOD, {
|
|
118
172
|
pane_id: paneId,
|
|
119
173
|
source: HERDR_SOURCE,
|
|
@@ -121,7 +175,6 @@ export default function herdrReporter(pi: ExtensionAPI): void {
|
|
|
121
175
|
state,
|
|
122
176
|
seq: seq++,
|
|
123
177
|
});
|
|
124
|
-
};
|
|
125
178
|
|
|
126
179
|
// Report the current session's identity so herdr can resume this pane
|
|
127
180
|
// (`xcsh --resume=<session>`) after a server restart. This is sent only over
|
|
@@ -129,10 +182,16 @@ export default function herdrReporter(pi: ExtensionAPI): void {
|
|
|
129
182
|
// reports via the CLI fallback). Prefer the absolute session file path, which
|
|
130
183
|
// herdr resumes directly; fall back to the session id for non-persisted
|
|
131
184
|
// sessions (e.g. print/RPC mode, where getSessionFile() is undefined).
|
|
132
|
-
|
|
185
|
+
//
|
|
186
|
+
// Called from every lifecycle handler because xcsh creates the session file
|
|
187
|
+
// lazily: getSessionFile() can still be undefined at session_start and even at
|
|
188
|
+
// agent_start, and a ref missed there would otherwise be lost until the next
|
|
189
|
+
// turn — long enough for a restart to lose the pane. Unchanged refs are
|
|
190
|
+
// suppressed, so the extra call sites cost nothing on the wire.
|
|
191
|
+
const reportSession = (ctx: ExtensionContext): Promise<void> => {
|
|
133
192
|
const socketPath = process.env.HERDR_SOCKET_PATH;
|
|
134
193
|
if (!socketPath) {
|
|
135
|
-
return;
|
|
194
|
+
return Promise.resolve();
|
|
136
195
|
}
|
|
137
196
|
let sessionRef: Record<string, unknown> | undefined;
|
|
138
197
|
try {
|
|
@@ -147,58 +206,63 @@ export default function herdrReporter(pi: ExtensionAPI): void {
|
|
|
147
206
|
}
|
|
148
207
|
} catch (err) {
|
|
149
208
|
onError(err);
|
|
150
|
-
return;
|
|
209
|
+
return Promise.resolve();
|
|
151
210
|
}
|
|
152
211
|
if (!sessionRef) {
|
|
153
|
-
return;
|
|
212
|
+
return Promise.resolve();
|
|
154
213
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
214
|
+
const refKey = JSON.stringify(sessionRef);
|
|
215
|
+
if (refKey === lastSessionRefKey) {
|
|
216
|
+
return Promise.resolve();
|
|
217
|
+
}
|
|
218
|
+
lastSessionRefKey = refKey;
|
|
219
|
+
const frame = {
|
|
220
|
+
pane_id: paneId,
|
|
221
|
+
source: HERDR_SOURCE,
|
|
222
|
+
agent: HERDR_AGENT_LABEL,
|
|
223
|
+
seq: seq++,
|
|
224
|
+
...sessionRef,
|
|
225
|
+
};
|
|
226
|
+
return enqueue(() => sendToHerdrSocket(socketPath, SESSION_METHOD, frame, onError));
|
|
167
227
|
};
|
|
168
228
|
|
|
169
229
|
// Announce presence and session identity as soon as the session is initialized.
|
|
170
|
-
pi.on("session_start", (_event, ctx) => {
|
|
171
|
-
|
|
172
|
-
|
|
230
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
231
|
+
await report("idle");
|
|
232
|
+
await reportSession(ctx);
|
|
173
233
|
});
|
|
174
234
|
|
|
175
235
|
// Busy while the agent loop is streaming a response. Re-report session identity
|
|
176
236
|
// in case the active session file changed (e.g. after /new, /resume, or /fork).
|
|
177
|
-
pi.on("agent_start", (_event, ctx) => {
|
|
178
|
-
|
|
179
|
-
|
|
237
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
238
|
+
await report("working");
|
|
239
|
+
await reportSession(ctx);
|
|
180
240
|
});
|
|
181
241
|
|
|
182
242
|
// Back to idle when the loop ends — unless we are waiting on a user prompt.
|
|
183
|
-
|
|
184
|
-
|
|
243
|
+
// This is also the first point where a lazily-created session file is certain
|
|
244
|
+
// to exist, so it is the backstop for capturing the resume ref.
|
|
245
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
246
|
+
await report(promptOpen ? "blocked" : "idle");
|
|
247
|
+
await reportSession(ctx);
|
|
185
248
|
});
|
|
186
249
|
|
|
187
250
|
// An interactive prompt (permission gate, ask tool, confirm/input) is
|
|
188
251
|
// awaiting the user: that is herdr's "needs attention" (blocked) state.
|
|
189
252
|
pi.on("user_prompt_start", () => {
|
|
190
253
|
promptOpen = true;
|
|
191
|
-
report("blocked");
|
|
254
|
+
void report("blocked");
|
|
192
255
|
});
|
|
193
256
|
|
|
194
|
-
pi.on("user_prompt_end", (_event, ctx) => {
|
|
257
|
+
pi.on("user_prompt_end", async (_event, ctx) => {
|
|
195
258
|
promptOpen = false;
|
|
196
|
-
report(ctx.isIdle() ? "idle" : "working");
|
|
259
|
+
await report(ctx.isIdle() ? "idle" : "working");
|
|
260
|
+
await reportSession(ctx);
|
|
197
261
|
});
|
|
198
262
|
|
|
199
263
|
// Relinquish pane authority so herdr stops showing xcsh once we exit.
|
|
200
264
|
pi.on("session_shutdown", () => {
|
|
201
|
-
send(RELEASE_METHOD, {
|
|
265
|
+
void send(RELEASE_METHOD, {
|
|
202
266
|
pane_id: paneId,
|
|
203
267
|
source: HERDR_SOURCE,
|
|
204
268
|
agent: HERDR_AGENT_LABEL,
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "19.91.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "19.91.1",
|
|
21
|
+
"commit": "cb975aa015ff199ecdc38c20544471ebf32e3737",
|
|
22
|
+
"shortCommit": "cb975aa",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.91.
|
|
25
|
-
"commitDate": "2026-07-
|
|
26
|
-
"buildDate": "2026-07-
|
|
24
|
+
"tag": "v19.91.1",
|
|
25
|
+
"commitDate": "2026-07-25T17:10:56Z",
|
|
26
|
+
"buildDate": "2026-07-25T17:31:09.895Z",
|
|
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.91.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/cb975aa015ff199ecdc38c20544471ebf32e3737",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.91.1"
|
|
33
33
|
};
|