@skrr-ai/cli 0.1.22 → 0.1.23
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 +83 -73
- package/bin/dev-fallback.js +221 -0
- package/bin/run.js +43 -0
- package/dist/base-command.d.ts +15 -0
- package/dist/base-command.js +71 -10
- package/dist/commands/agents/attach-harness.d.ts +37 -0
- package/dist/commands/agents/attach-harness.js +102 -3
- package/dist/commands/agents/chat.d.ts +69 -1
- package/dist/commands/agents/chat.js +202 -22
- package/dist/commands/agents/create.d.ts +38 -0
- package/dist/commands/agents/create.js +95 -2
- package/dist/commands/agents/schedule-trace.js +11 -0
- package/dist/commands/agents/show.js +29 -1
- package/dist/commands/api-keys/create.d.ts +24 -0
- package/dist/commands/api-keys/create.js +80 -0
- package/dist/commands/api-keys/list.d.ts +11 -0
- package/dist/commands/api-keys/list.js +42 -0
- package/dist/commands/api-keys/revoke.d.ts +14 -0
- package/dist/commands/api-keys/revoke.js +33 -0
- package/dist/commands/api-keys/rotate.d.ts +19 -0
- package/dist/commands/api-keys/rotate.js +42 -0
- package/dist/commands/balance/index.d.ts +18 -0
- package/dist/commands/balance/index.js +29 -0
- package/dist/commands/balance/show.d.ts +25 -0
- package/dist/commands/balance/show.js +60 -0
- package/dist/commands/balance/statement.d.ts +19 -0
- package/dist/commands/balance/statement.js +52 -0
- package/dist/commands/browser/doctor.d.ts +11 -0
- package/dist/commands/browser/doctor.js +11 -0
- package/dist/commands/code/jobs/approve.d.ts +25 -0
- package/dist/commands/code/jobs/approve.js +52 -0
- package/dist/commands/code/jobs/cancel.d.ts +12 -0
- package/dist/commands/code/jobs/cancel.js +39 -0
- package/dist/commands/code/jobs/index.d.ts +19 -0
- package/dist/commands/code/jobs/index.js +38 -0
- package/dist/commands/code/jobs/list.d.ts +12 -0
- package/dist/commands/code/jobs/list.js +87 -0
- package/dist/commands/code/jobs/run.d.ts +32 -0
- package/dist/commands/code/jobs/run.js +151 -0
- package/dist/commands/code/jobs/show.d.ts +12 -0
- package/dist/commands/code/jobs/show.js +86 -0
- package/dist/commands/commitments/preflight.js +18 -4
- package/dist/commands/commitments/receipt-metrics.d.ts +39 -0
- package/dist/commands/commitments/receipt-metrics.js +78 -0
- package/dist/commands/convos/search.js +5 -5
- package/dist/commands/harnesses/leases/list.d.ts +3 -0
- package/dist/commands/harnesses/leases/list.js +59 -12
- package/dist/commands/harnesses/leases/show.js +36 -0
- package/dist/commands/harnesses/list.d.ts +18 -0
- package/dist/commands/harnesses/list.js +30 -0
- package/dist/commands/harnesses/products.js +48 -3
- package/dist/commands/harnesses/quota.d.ts +16 -0
- package/dist/commands/harnesses/quota.js +26 -1
- package/dist/commands/harnesses/usage.js +17 -0
- package/dist/commands/inbox/index.d.ts +2 -1
- package/dist/commands/inbox/index.js +27 -4
- package/dist/commands/login.js +4 -1
- package/dist/commands/machines/list.d.ts +1 -0
- package/dist/commands/machines/list.js +33 -5
- package/dist/commands/memory/approvals/issue.d.ts +18 -0
- package/dist/commands/memory/approvals/issue.js +63 -0
- package/dist/commands/memory/commit.js +5 -2
- package/dist/commands/memory/context.d.ts +3 -2
- package/dist/commands/memory/context.js +9 -11
- package/dist/commands/memory/delete.d.ts +1 -1
- package/dist/commands/memory/list.d.ts +1 -1
- package/dist/commands/memory/list.js +1 -1
- package/dist/commands/memory/mounts/resolve.d.ts +10 -0
- package/dist/commands/memory/mounts/resolve.js +27 -0
- package/dist/commands/memory/proposals/commit.d.ts +18 -0
- package/dist/commands/memory/proposals/commit.js +32 -0
- package/dist/commands/memory/proposals/create.d.ts +18 -0
- package/dist/commands/memory/proposals/create.js +56 -0
- package/dist/commands/memory/proposals/list.d.ts +13 -0
- package/dist/commands/memory/proposals/list.js +31 -0
- package/dist/commands/memory/proposals/promote.d.ts +22 -0
- package/dist/commands/memory/proposals/promote.js +33 -0
- package/dist/commands/memory/proposals/reject.d.ts +16 -0
- package/dist/commands/memory/proposals/reject.js +28 -0
- package/dist/commands/memory/rebuild-index.d.ts +20 -0
- package/dist/commands/memory/rebuild-index.js +25 -0
- package/dist/commands/memory/save.d.ts +1 -1
- package/dist/commands/memory/save.js +1 -1
- package/dist/commands/memory/search.d.ts +6 -3
- package/dist/commands/memory/search.js +27 -3
- package/dist/commands/memory/show.d.ts +1 -1
- package/dist/commands/memory/stores/archive.d.ts +14 -0
- package/dist/commands/memory/stores/archive.js +31 -0
- package/dist/commands/memory/stores/create.d.ts +12 -0
- package/dist/commands/memory/stores/create.js +30 -0
- package/dist/commands/memory/stores/delete.d.ts +15 -0
- package/dist/commands/memory/stores/delete.js +38 -0
- package/dist/commands/memory/stores/list.d.ts +10 -0
- package/dist/commands/memory/stores/list.js +29 -0
- package/dist/commands/memory/stores/show.d.ts +12 -0
- package/dist/commands/memory/stores/show.js +14 -0
- package/dist/commands/memory/stores/update.d.ts +15 -0
- package/dist/commands/memory/stores/update.js +39 -0
- package/dist/commands/memory/sync/delete.d.ts +13 -0
- package/dist/commands/memory/sync/delete.js +35 -0
- package/dist/commands/memory/sync/pull.d.ts +19 -0
- package/dist/commands/memory/sync/pull.js +52 -0
- package/dist/commands/memory/sync/push.d.ts +18 -0
- package/dist/commands/memory/sync/push.js +115 -0
- package/dist/commands/memory/sync/resolve.d.ts +23 -0
- package/dist/commands/memory/sync/resolve.js +27 -0
- package/dist/commands/memory/sync/status.d.ts +9 -0
- package/dist/commands/memory/sync/status.js +22 -0
- package/dist/commands/memory/topics/archive.d.ts +20 -0
- package/dist/commands/memory/topics/archive.js +29 -0
- package/dist/commands/memory/topics/create.d.ts +23 -0
- package/dist/commands/memory/topics/create.js +46 -0
- package/dist/commands/memory/topics/delete.d.ts +22 -0
- package/dist/commands/memory/topics/delete.js +30 -0
- package/dist/commands/memory/topics/forget.d.ts +22 -0
- package/dist/commands/memory/topics/forget.js +29 -0
- package/dist/commands/memory/topics/list.d.ts +13 -0
- package/dist/commands/memory/topics/list.js +22 -0
- package/dist/commands/memory/topics/move.d.ts +22 -0
- package/dist/commands/memory/topics/move.js +26 -0
- package/dist/commands/memory/topics/show.d.ts +17 -0
- package/dist/commands/memory/topics/show.js +21 -0
- package/dist/commands/memory/topics/update.d.ts +23 -0
- package/dist/commands/memory/topics/update.js +27 -0
- package/dist/commands/memory/tree.d.ts +5 -2
- package/dist/commands/memory/tree.js +20 -2
- package/dist/commands/memory/update.d.ts +1 -1
- package/dist/commands/memory/versions/list.d.ts +16 -0
- package/dist/commands/memory/versions/list.js +22 -0
- package/dist/commands/memory/versions/redact.d.ts +21 -0
- package/dist/commands/memory/versions/redact.js +22 -0
- package/dist/commands/memory/versions/restore.d.ts +20 -0
- package/dist/commands/memory/versions/restore.js +16 -0
- package/dist/commands/memory/versions/show.d.ts +15 -0
- package/dist/commands/memory/versions/show.js +17 -0
- package/dist/commands/messages/list.js +25 -8
- package/dist/commands/messages/show.js +10 -0
- package/dist/commands/store/install.js +5 -1
- package/dist/commands/store/releases.js +1 -1
- package/dist/commands/store/update.js +5 -1
- package/dist/commands/subscriptions/cancel.js +5 -1
- package/dist/commands/subscriptions/health.js +5 -1
- package/dist/commands/subscriptions/status.js +5 -1
- package/dist/commands/subscriptions/subscribe.js +5 -1
- package/dist/commands/tasks/activity.d.ts +2 -3
- package/dist/commands/tasks/activity.js +15 -5
- package/dist/commands/tasks/complete.d.ts +18 -150
- package/dist/commands/tasks/complete.js +161 -600
- package/dist/commands/tasks/create.d.ts +5 -0
- package/dist/commands/tasks/create.js +107 -4
- package/dist/commands/tasks/deliverable/add.d.ts +8 -0
- package/dist/commands/tasks/deliverable/add.js +41 -8
- package/dist/commands/tasks/deliverable/confirm.d.ts +5 -5
- package/dist/commands/tasks/deliverable/confirm.js +9 -18
- package/dist/commands/tasks/deliverable/list.d.ts +12 -0
- package/dist/commands/tasks/deliverable/list.js +37 -0
- package/dist/commands/tasks/events/append.d.ts +2 -0
- package/dist/commands/tasks/events/append.js +11 -4
- package/dist/commands/tasks/events/list.d.ts +1 -0
- package/dist/commands/tasks/events/list.js +1 -0
- package/dist/commands/tasks/events/tail.d.ts +1 -0
- package/dist/commands/tasks/events/tail.js +1 -0
- package/dist/commands/tasks/expectations/assess.d.ts +21 -0
- package/dist/commands/tasks/expectations/assess.js +77 -0
- package/dist/commands/tasks/expectations.js +14 -18
- package/dist/commands/tasks/judge.d.ts +1 -0
- package/dist/commands/tasks/judge.js +2 -1
- package/dist/commands/tasks/output.d.ts +2 -11
- package/dist/commands/tasks/output.js +64 -145
- package/dist/commands/tasks/report.d.ts +3 -3
- package/dist/commands/tasks/report.js +64 -41
- package/dist/commands/tasks/resource/add.d.ts +23 -0
- package/dist/commands/tasks/resource/add.js +76 -0
- package/dist/commands/tasks/resource/list.d.ts +13 -0
- package/dist/commands/tasks/resource/list.js +41 -0
- package/dist/commands/tasks/result/show.d.ts +13 -0
- package/dist/commands/tasks/result/show.js +46 -0
- package/dist/commands/tasks/result/submit.d.ts +26 -0
- package/dist/commands/tasks/result/submit.js +134 -0
- package/dist/commands/tasks/resume/save.d.ts +16 -0
- package/dist/commands/tasks/resume/save.js +59 -0
- package/dist/commands/tasks/resume/show.d.ts +13 -0
- package/dist/commands/tasks/resume/show.js +35 -0
- package/dist/commands/tasks/review.d.ts +17 -0
- package/dist/commands/tasks/review.js +54 -0
- package/dist/commands/tasks/runs.js +5 -0
- package/dist/commands/tasks/timeline.d.ts +2 -2
- package/dist/commands/tasks/timeline.js +10 -23
- package/dist/commands/tasks/updates/add.d.ts +22 -0
- package/dist/commands/tasks/updates/add.js +101 -0
- package/dist/commands/tasks/updates/list.d.ts +15 -0
- package/dist/commands/tasks/updates/list.js +57 -0
- package/dist/commands/tasks/waive.d.ts +1 -0
- package/dist/commands/tasks/waive.js +2 -1
- package/dist/lib/agentic-stream.d.ts +257 -0
- package/dist/lib/agentic-stream.js +490 -33
- package/dist/lib/balance.d.ts +8 -0
- package/dist/lib/commitment-product.d.ts +10 -1
- package/dist/lib/commitment-product.js +66 -2
- package/dist/lib/commitments.d.ts +2 -1
- package/dist/lib/commitments.js +1 -0
- package/dist/lib/conversation-search.d.ts +3 -0
- package/dist/lib/conversation-search.js +75 -0
- package/dist/lib/execution-target.d.ts +21 -0
- package/dist/lib/execution-target.js +22 -6
- package/dist/lib/harnesses.d.ts +82 -6
- package/dist/lib/harnesses.js +30 -2
- package/dist/lib/hosted-machines.js +9 -0
- package/dist/lib/inbox.d.ts +2 -1
- package/dist/lib/machines.d.ts +17 -0
- package/dist/lib/machines.js +29 -0
- package/dist/lib/managed-agent-keys.d.ts +104 -0
- package/dist/lib/managed-agent-keys.js +50 -0
- package/dist/lib/memory-scope.d.ts +4 -3
- package/dist/lib/memory-scope.js +9 -9
- package/dist/lib/memory-sync.d.ts +114 -0
- package/dist/lib/memory-sync.js +730 -0
- package/dist/lib/message-provenance.d.ts +42 -0
- package/dist/lib/message-provenance.js +89 -0
- package/dist/lib/scoped-memory-command.d.ts +68 -0
- package/dist/lib/scoped-memory-command.js +183 -0
- package/dist/lib/session-task-endpoints.d.ts +1 -1
- package/dist/lib/session-task-endpoints.js +7 -0
- package/dist/lib/sky-code-broker.d.ts +47 -0
- package/dist/lib/sky-code-broker.js +50 -0
- package/dist/lib/sky-code-doctor.js +10 -1
- package/dist/lib/task-artifact-flags.d.ts +2 -0
- package/dist/lib/task-artifact-flags.js +7 -0
- package/dist/lib/task-execution.d.ts +4 -5
- package/dist/lib/task-execution.js +4 -5
- package/dist/lib/task-extras.d.ts +1 -5
- package/dist/lib/task-extras.js +1 -5
- package/dist/lib/task-transcript.d.ts +5 -7
- package/dist/lib/task-transcript.js +8 -16
- package/dist/lib/tasks.d.ts +7 -21
- package/dist/lib/tasks.js +17 -20
- package/dist/lib/triggers.d.ts +39 -1
- package/dist/lib/triggers.js +70 -2
- package/dist/lib/usage-discovery.d.ts +17 -1
- package/dist/lib/usage-discovery.js +17 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3733 -3618
- package/oclif.manifest.json +16201 -10377
- package/package.json +26 -5
|
@@ -1,15 +1,40 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AgenticStreamClient = void 0;
|
|
3
|
+
exports.AgenticStreamClient = exports.USER_CONNECTION_CAP_CODE = void 0;
|
|
4
|
+
exports.writeJsonLine = writeJsonLine;
|
|
4
5
|
exports.envelopesText = envelopesText;
|
|
5
6
|
exports.envelopeKey = envelopeKey;
|
|
6
7
|
exports.unseenEnvelopes = unseenEnvelopes;
|
|
8
|
+
exports.projectDaemonToolEvents = projectDaemonToolEvents;
|
|
9
|
+
exports.renderableDelta = renderableDelta;
|
|
10
|
+
exports.transportRefusal = transportRefusal;
|
|
11
|
+
exports.transportRefusalMessage = transportRefusalMessage;
|
|
12
|
+
exports.attachDeadlineFor = attachDeadlineFor;
|
|
13
|
+
exports.jitteredRetryDelay = jitteredRetryDelay;
|
|
14
|
+
exports.detectSeqGap = detectSeqGap;
|
|
15
|
+
exports.seqGapNotice = seqGapNotice;
|
|
7
16
|
exports.toWebSocketOrigin = toWebSocketOrigin;
|
|
8
17
|
const node_readline_1 = require("node:readline");
|
|
9
18
|
const socket_io_client_1 = require("socket.io-client");
|
|
10
19
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
11
20
|
const config_1 = require("./config");
|
|
12
21
|
const publicEndpoints_generated_1 = require("./publicEndpoints.generated");
|
|
22
|
+
/**
|
|
23
|
+
* Write exactly one complete JSON value on one line.
|
|
24
|
+
*
|
|
25
|
+
* `--jsonl` is the mode automation parses, and its whole contract is one JSON
|
|
26
|
+
* value per physical line. Two things break that if the line is composed by
|
|
27
|
+
* hand: a raw newline inside a string (a multi-line agent answer or tool
|
|
28
|
+
* output) splits one record across many unparseable lines, and routing the line
|
|
29
|
+
* through oclif's `this.log` puts it through `util.format`, which rewrites `%s`
|
|
30
|
+
* / `%d` / `%j` sequences that appear in ordinary shell output. `JSON.stringify`
|
|
31
|
+
* escapes the first and this helper avoids the second by writing to the stream
|
|
32
|
+
* directly, so every JSONL line in the CLI is produced in one place and cannot
|
|
33
|
+
* drift (OSK-7629).
|
|
34
|
+
*/
|
|
35
|
+
function writeJsonLine(output, value) {
|
|
36
|
+
output.write(`${JSON.stringify(value) ?? 'null'}\n`);
|
|
37
|
+
}
|
|
13
38
|
/**
|
|
14
39
|
* Small Node-side adapter for the existing Agentic Socket.IO contract.
|
|
15
40
|
* It deliberately normalizes only lifecycle/text/tool events; the server's
|
|
@@ -86,6 +111,162 @@ function unseenEnvelopes(event, seen) {
|
|
|
86
111
|
}
|
|
87
112
|
return fresh;
|
|
88
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Project daemon `tool-call-start` / `tool-call-end` envelopes into the same
|
|
116
|
+
* `tool.started` / `tool.completed` events a cloud run emits.
|
|
117
|
+
*
|
|
118
|
+
* The CLI used to emit tool events only for cloud-executed turns: a run on a
|
|
119
|
+
* local harness produced three shell commands and ZERO tool events, in both
|
|
120
|
+
* `--jsonl` and the human rendering, while the envelopes carrying them arrived
|
|
121
|
+
* intact inside `session:output` (OSK-7730). Automation counting tool calls
|
|
122
|
+
* concluded a local run used no tools; a human watching one saw nothing at all
|
|
123
|
+
* while commands ran on their own machine (OSK-7773 is the same absence on the
|
|
124
|
+
* web). The harness a turn lands on is a routing decision and must not change
|
|
125
|
+
* the event vocabulary.
|
|
126
|
+
*
|
|
127
|
+
* `tool-call-end` carries no tool name, so the name is correlated on `call`
|
|
128
|
+
* (the tool-use id) through `namesByCall`, which the caller keeps for the life
|
|
129
|
+
* of the run. The pairing is unambiguous within a turn.
|
|
130
|
+
*/
|
|
131
|
+
function projectDaemonToolEvents(envelopes, namesByCall) {
|
|
132
|
+
const out = [];
|
|
133
|
+
for (const envelope of envelopes) {
|
|
134
|
+
const ev = envelope?.ev;
|
|
135
|
+
if (!ev || typeof ev.t !== 'string')
|
|
136
|
+
continue;
|
|
137
|
+
const call = typeof ev.call === 'string' ? ev.call : undefined;
|
|
138
|
+
if (ev.t === 'tool-call-start') {
|
|
139
|
+
const name = typeof ev.name === 'string' ? ev.name : undefined;
|
|
140
|
+
if (call && name)
|
|
141
|
+
namesByCall.set(call, name);
|
|
142
|
+
out.push({
|
|
143
|
+
type: 'tool.started',
|
|
144
|
+
...(call ? { toolUseId: call } : {}),
|
|
145
|
+
...(name ? { toolName: name } : {}),
|
|
146
|
+
...(ev.args !== undefined ? { toolInput: ev.args } : {}),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else if (ev.t === 'tool-call-end') {
|
|
150
|
+
const name = call ? namesByCall.get(call) : undefined;
|
|
151
|
+
out.push({
|
|
152
|
+
type: 'tool.completed',
|
|
153
|
+
...(call ? { toolUseId: call } : {}),
|
|
154
|
+
...(name ? { toolName: name } : {}),
|
|
155
|
+
...(ev.output !== undefined ? { toolResponse: ev.output } : {}),
|
|
156
|
+
...(typeof ev.status === 'string' ? { status: ev.status } : {}),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The part of a cumulative snapshot that has not been shown yet.
|
|
164
|
+
*
|
|
165
|
+
* The stream carries CUMULATIVE text, and the plain renderer used to redraw
|
|
166
|
+
* each snapshot after a bare carriage return. `\r` returns the cursor to column
|
|
167
|
+
* 0 of the CURRENT line only — it cannot rewind past a newline — so the repaint
|
|
168
|
+
* was correct exactly while the answer was one line, and every snapshot after
|
|
169
|
+
* the model's first `\n` stranded the previous snapshot's earlier lines on
|
|
170
|
+
* screen. One two-line answer printed its first paragraph sixteen times
|
|
171
|
+
* (OSK-7731); another printed the answer twice and fused it into tokens the
|
|
172
|
+
* agent never wrote, `HOSTHOST=e2b.local` (OSK-7701).
|
|
173
|
+
*
|
|
174
|
+
* Printing only the new suffix needs no cursor control at all, which also makes
|
|
175
|
+
* it correct when stdout is a file or a pipe — where nothing consumes `\r` and
|
|
176
|
+
* the redirected capture looked worse than the screen.
|
|
177
|
+
*
|
|
178
|
+
* Returns the text to append, and the new "already shown" watermark. A snapshot
|
|
179
|
+
* that is not an extension of what was shown (a stream reset, or a server that
|
|
180
|
+
* rewrote the answer) is emitted whole on a fresh line rather than diffed.
|
|
181
|
+
*/
|
|
182
|
+
function renderableDelta(shown, snapshot) {
|
|
183
|
+
if (snapshot === shown)
|
|
184
|
+
return { append: '', shown };
|
|
185
|
+
if (snapshot.startsWith(shown))
|
|
186
|
+
return { append: snapshot.slice(shown.length), shown: snapshot };
|
|
187
|
+
return { append: `\n${snapshot}`, shown: snapshot };
|
|
188
|
+
}
|
|
189
|
+
/** The user-connection cap, as `userConnectionTracking.js` names it. */
|
|
190
|
+
exports.USER_CONNECTION_CAP_CODE = 'user_connection_cap';
|
|
191
|
+
function positiveInt(value) {
|
|
192
|
+
const n = Number(value);
|
|
193
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;
|
|
194
|
+
}
|
|
195
|
+
function transportRefusal(error) {
|
|
196
|
+
const data = error?.data;
|
|
197
|
+
if (!data || typeof data !== 'object')
|
|
198
|
+
return { retryable: false };
|
|
199
|
+
const code = typeof data.code === 'string' && data.code ? data.code : undefined;
|
|
200
|
+
const connectionClass = typeof data.connectionClass === 'string' && data.connectionClass
|
|
201
|
+
? data.connectionClass
|
|
202
|
+
: undefined;
|
|
203
|
+
return {
|
|
204
|
+
...(code ? { code } : {}),
|
|
205
|
+
...(connectionClass ? { connectionClass } : {}),
|
|
206
|
+
...(positiveInt(data.limit) !== undefined ? { limit: positiveInt(data.limit) } : {}),
|
|
207
|
+
...(positiveInt(data.retryAfterMs) !== undefined
|
|
208
|
+
? { retryAfterMs: positiveInt(data.retryAfterMs) }
|
|
209
|
+
: {}),
|
|
210
|
+
// Only an explicit `true`. An unknown refusal must not be retried forever.
|
|
211
|
+
retryable: data.retryable === true,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The sentence a person can act on, built from whatever the refusal carried.
|
|
216
|
+
*
|
|
217
|
+
* Degrades one clause at a time: an older server that sends no `data` still
|
|
218
|
+
* gets its own message back unchanged, which is exactly today's behaviour and
|
|
219
|
+
* no worse.
|
|
220
|
+
*/
|
|
221
|
+
function transportRefusalMessage(message, refusal) {
|
|
222
|
+
const parts = [String(message || 'The stream was refused').trim()];
|
|
223
|
+
if (refusal.limit !== undefined) {
|
|
224
|
+
parts.push(`The limit is ${refusal.limit} concurrent ${refusal.connectionClass ?? 'client'} connection${refusal.limit === 1 ? '' : 's'} per account, shared by every open \`skrr\` session and browser tab you have.`);
|
|
225
|
+
}
|
|
226
|
+
if (refusal.retryAfterMs !== undefined) {
|
|
227
|
+
parts.push(`Retrying in ${Math.round(refusal.retryAfterMs / 1000)}s.`);
|
|
228
|
+
}
|
|
229
|
+
else if (refusal.retryable) {
|
|
230
|
+
parts.push('Retrying.');
|
|
231
|
+
}
|
|
232
|
+
// The fact that matters most and was never said: the run is already accepted.
|
|
233
|
+
parts.push('The run was already accepted and keeps executing either way.');
|
|
234
|
+
return parts.join(' ');
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* How long a refusal is worth waiting out.
|
|
238
|
+
*
|
|
239
|
+
* The default 30s attach deadline is exactly the server's own
|
|
240
|
+
* `WS_USER_CAP_RETRY_AFTER_MS`, so a client honouring the retry contract on the
|
|
241
|
+
* default budget gives up in the same instant it is allowed to try again. The
|
|
242
|
+
* window is widened to cover the retry plus a connect, and only when the server
|
|
243
|
+
* named a delay.
|
|
244
|
+
*/
|
|
245
|
+
function attachDeadlineFor(timeoutMs, refusal) {
|
|
246
|
+
if (!refusal.retryable || refusal.retryAfterMs === undefined)
|
|
247
|
+
return timeoutMs;
|
|
248
|
+
return Math.max(timeoutMs, refusal.retryAfterMs + 15_000);
|
|
249
|
+
}
|
|
250
|
+
/** Jitter, so a fleet refused together does not return together. */
|
|
251
|
+
function jitteredRetryDelay(retryAfterMs, random = Math.random) {
|
|
252
|
+
return Math.round(retryAfterMs * (1 + random() * 0.25));
|
|
253
|
+
}
|
|
254
|
+
function detectSeqGap(highestApplied, incoming) {
|
|
255
|
+
if (!Number.isFinite(incoming) || incoming <= 0)
|
|
256
|
+
return null;
|
|
257
|
+
// The FIRST envelope is a gap when it is not seq 1 — that is the reported
|
|
258
|
+
// case, and a `> highestApplied + 1` test alone would miss it, because
|
|
259
|
+
// nothing has been applied yet.
|
|
260
|
+
if (incoming <= highestApplied + 1)
|
|
261
|
+
return null;
|
|
262
|
+
return { from: highestApplied + 1, to: incoming - 1 };
|
|
263
|
+
}
|
|
264
|
+
function seqGapNotice(gap) {
|
|
265
|
+
const count = gap.to - gap.from + 1;
|
|
266
|
+
return (`[${count} stream chunk${count === 1 ? '' : 's'} (seq ${gap.from}` +
|
|
267
|
+
`${count === 1 ? '' : `-${gap.to}`}) never arrived — the text above is incomplete. ` +
|
|
268
|
+
'The full answer is in the persisted message and in run.result.]');
|
|
269
|
+
}
|
|
89
270
|
class AgenticStreamClient {
|
|
90
271
|
options;
|
|
91
272
|
socket = null;
|
|
@@ -94,13 +275,48 @@ class AgenticStreamClient {
|
|
|
94
275
|
settled = false;
|
|
95
276
|
closed = false;
|
|
96
277
|
accumulated = '';
|
|
278
|
+
/** Highest envelope `__seq` folded into `accumulated`, for gap detection. */
|
|
279
|
+
highestSeq = 0;
|
|
280
|
+
reportedGap = false;
|
|
97
281
|
// Envelope keys already applied, so a re-delivered `session:output` frame is
|
|
98
282
|
// not appended twice (OSK-4834). Cleared on `execution:stream-reset`.
|
|
99
283
|
seenEnvelopes = new Set();
|
|
284
|
+
/**
|
|
285
|
+
* Lifecycle events already emitted, by their own identity (OSK-7632/7689/7709).
|
|
286
|
+
*
|
|
287
|
+
* `session:output` envelopes are deduped by `envelopeKey`, but the lifecycle
|
|
288
|
+
* events do not ride an envelope and were exempt: one turn delivered
|
|
289
|
+
* `run.started` four times byte-identically (76% of a 70KB stream), and one
|
|
290
|
+
* Bash execution arrived as four `tool.started` events sharing one
|
|
291
|
+
* `toolUseId`. Anything counting tool calls from the stream — a timeline, a
|
|
292
|
+
* usage display, an audit log — reported four sandbox commands where one ran.
|
|
293
|
+
* The server fans one broadcast to several rooms the client is in, so the
|
|
294
|
+
* repeat count varies run to run and cannot be inferred by a consumer.
|
|
295
|
+
*/
|
|
296
|
+
seenLifecycle = new Set();
|
|
297
|
+
/** `call` → tool name, so a daemon `tool-call-end` can be named (OSK-7730). */
|
|
298
|
+
toolNamesByCall = new Map();
|
|
299
|
+
/** Text already written to the terminal, for suffix-only rendering. */
|
|
300
|
+
renderedText = '';
|
|
301
|
+
idleTimer = null;
|
|
302
|
+
reconciling = false;
|
|
100
303
|
responseMessageId;
|
|
101
304
|
lastEvent;
|
|
305
|
+
/**
|
|
306
|
+
* The terminal result, once there is one.
|
|
307
|
+
*
|
|
308
|
+
* `run()`'s promise is the normal way to read it, but a caller that raced
|
|
309
|
+
* `run()` against its own detach has already discarded that promise by the
|
|
310
|
+
* time {@link probeTerminal} settles it. Keeping the value here is what lets
|
|
311
|
+
* a reconciled detach report the real outcome instead of the detach.
|
|
312
|
+
*/
|
|
313
|
+
terminalResult = null;
|
|
102
314
|
resolve;
|
|
103
315
|
reject;
|
|
316
|
+
/** The terminal result, or null while the run is still in flight. */
|
|
317
|
+
settledResult() {
|
|
318
|
+
return this.terminalResult;
|
|
319
|
+
}
|
|
104
320
|
constructor(options) {
|
|
105
321
|
this.options = options;
|
|
106
322
|
}
|
|
@@ -131,8 +347,71 @@ class AgenticStreamClient {
|
|
|
131
347
|
await this.waitForConnect();
|
|
132
348
|
this.socket.emit('session:join', { sessionId: this.options.sessionId });
|
|
133
349
|
this.emit({ type: 'session.joined', sessionId: this.options.sessionId });
|
|
350
|
+
this.armIdleProbe();
|
|
134
351
|
return result;
|
|
135
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* (Re)start the silence watchdog that asks the durable record what happened.
|
|
355
|
+
*
|
|
356
|
+
* Armed on join and reset by every event, so it only fires when the stream
|
|
357
|
+
* has actually gone quiet. A probe that finds the turn still running re-arms
|
|
358
|
+
* rather than giving up — a 200-second tool call is silence, not a failure —
|
|
359
|
+
* so the cost of being wrong is one cheap read, while the cost of not asking
|
|
360
|
+
* was an interactive session that never returned.
|
|
361
|
+
*/
|
|
362
|
+
armIdleProbe() {
|
|
363
|
+
if (!this.options.reconcileTerminal)
|
|
364
|
+
return;
|
|
365
|
+
if (this.settled || this.closed)
|
|
366
|
+
return;
|
|
367
|
+
if (this.idleTimer)
|
|
368
|
+
clearTimeout(this.idleTimer);
|
|
369
|
+
this.idleTimer = setTimeout(() => void this.probeTerminal('idle'), this.options.idleTerminalProbeMs ?? 20_000);
|
|
370
|
+
this.idleTimer.unref?.();
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Settle from the durable record, if it has reached a terminal state.
|
|
374
|
+
*
|
|
375
|
+
* Public so the caller can force one before declaring a detach: a run that
|
|
376
|
+
* finished 13 seconds ago must not be reported as "may still be executing"
|
|
377
|
+
* (OSK-7660).
|
|
378
|
+
*/
|
|
379
|
+
async probeTerminal(reason) {
|
|
380
|
+
const reconcile = this.options.reconcileTerminal;
|
|
381
|
+
if (!reconcile || this.settled || this.reconciling)
|
|
382
|
+
return false;
|
|
383
|
+
this.reconciling = true;
|
|
384
|
+
let terminal = null;
|
|
385
|
+
try {
|
|
386
|
+
terminal = await reconcile();
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// A failed probe is not evidence about the run. Keep waiting.
|
|
390
|
+
terminal = null;
|
|
391
|
+
}
|
|
392
|
+
finally {
|
|
393
|
+
this.reconciling = false;
|
|
394
|
+
}
|
|
395
|
+
if (this.settled)
|
|
396
|
+
return true;
|
|
397
|
+
if (!terminal) {
|
|
398
|
+
this.armIdleProbe();
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
// The record is the witness the stream refused to be, so say so: the
|
|
402
|
+
// recorded reason is exactly what "the surface shows nothing at all" cost
|
|
403
|
+
// the reader (OSK-7595 — the CLI reported a transport error while the run's
|
|
404
|
+
// own recorded failure said something else entirely).
|
|
405
|
+
this.finish(terminal.status === 'failed' ? 'failed' : 'completed', {
|
|
406
|
+
type: 'run.reconciled',
|
|
407
|
+
sessionId: this.options.sessionId,
|
|
408
|
+
reconciledFrom: 'durable_record',
|
|
409
|
+
reason,
|
|
410
|
+
...(terminal.error ? { message: terminal.error, error: terminal.error } : {}),
|
|
411
|
+
...(typeof terminal.text === 'string' ? { result: terminal.text } : {}),
|
|
412
|
+
});
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
136
415
|
cancel(reason = 'Interrupted by user') {
|
|
137
416
|
if (this.settled || !this.socket)
|
|
138
417
|
return;
|
|
@@ -148,6 +427,10 @@ class AgenticStreamClient {
|
|
|
148
427
|
if (this.closed)
|
|
149
428
|
return;
|
|
150
429
|
this.closed = true;
|
|
430
|
+
if (this.idleTimer) {
|
|
431
|
+
clearTimeout(this.idleTimer);
|
|
432
|
+
this.idleTimer = null;
|
|
433
|
+
}
|
|
151
434
|
const socket = this.socket;
|
|
152
435
|
this.socket = null;
|
|
153
436
|
// Stop dispatch BEFORE tearing anything down. socket.io can deliver a
|
|
@@ -167,6 +450,34 @@ class AgenticStreamClient {
|
|
|
167
450
|
this.readline?.close();
|
|
168
451
|
this.readline = null;
|
|
169
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* Report the first hole in the live envelope sequence, once.
|
|
455
|
+
*
|
|
456
|
+
* Once, not per gap: a stream that has already lost its beginning tends to
|
|
457
|
+
* lose more, and one honest sentence is the useful form of that. The terminal
|
|
458
|
+
* result is authoritative and reconciled separately, so this never changes
|
|
459
|
+
* what the run reports — only what the reader is told while it is happening.
|
|
460
|
+
*/
|
|
461
|
+
reportSeqGap(envelopes) {
|
|
462
|
+
for (const envelope of envelopes) {
|
|
463
|
+
const seq = Number(envelope?.__seq);
|
|
464
|
+
if (!Number.isFinite(seq) || seq <= 0)
|
|
465
|
+
continue;
|
|
466
|
+
const gap = detectSeqGap(this.highestSeq, seq);
|
|
467
|
+
if (gap && !this.reportedGap) {
|
|
468
|
+
this.reportedGap = true;
|
|
469
|
+
this.emit({
|
|
470
|
+
type: 'stream.gap',
|
|
471
|
+
sessionId: this.options.sessionId,
|
|
472
|
+
fromSeq: gap.from,
|
|
473
|
+
toSeq: gap.to,
|
|
474
|
+
message: seqGapNotice(gap),
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (seq > this.highestSeq)
|
|
478
|
+
this.highestSeq = seq;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
170
481
|
installHandlers() {
|
|
171
482
|
const socket = this.socket;
|
|
172
483
|
const match = (event) => !event.sessionId || event.sessionId === this.options.sessionId;
|
|
@@ -175,19 +486,35 @@ class AgenticStreamClient {
|
|
|
175
486
|
this.emit({ type: 'transport.connected', sessionId: this.options.sessionId });
|
|
176
487
|
});
|
|
177
488
|
socket.on('disconnect', (reason) => {
|
|
178
|
-
if (
|
|
179
|
-
|
|
489
|
+
if (this.settled)
|
|
490
|
+
return;
|
|
491
|
+
this.emit({ type: 'transport.disconnected', sessionId: this.options.sessionId, reason });
|
|
492
|
+
// Losing the view is not evidence the run stopped — but it IS the moment
|
|
493
|
+
// to ask the record, because a turn refused at admission publishes its
|
|
494
|
+
// reason there and nowhere else.
|
|
495
|
+
void this.probeTerminal('transport_disconnected');
|
|
180
496
|
});
|
|
181
497
|
socket.on('connect_error', (error) => {
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
498
|
+
if (this.settled)
|
|
499
|
+
return;
|
|
500
|
+
// Everything the server said, not just the sentence (OSK-7663).
|
|
501
|
+
const refusal = transportRefusal(error);
|
|
502
|
+
this.emit({
|
|
503
|
+
type: 'transport.error',
|
|
504
|
+
sessionId: this.options.sessionId,
|
|
505
|
+
message: transportRefusalMessage(error.message, refusal),
|
|
506
|
+
...(refusal.code ? { code: refusal.code } : {}),
|
|
507
|
+
...(refusal.limit !== undefined ? { limit: refusal.limit } : {}),
|
|
508
|
+
...(refusal.retryAfterMs !== undefined ? { retryAfterMs: refusal.retryAfterMs } : {}),
|
|
509
|
+
...(refusal.connectionClass ? { connectionClass: refusal.connectionClass } : {}),
|
|
510
|
+
retryable: refusal.retryable,
|
|
511
|
+
});
|
|
512
|
+
void this.probeTerminal('transport_error');
|
|
188
513
|
});
|
|
189
514
|
socket.on('session:init', (event) => {
|
|
190
|
-
|
|
515
|
+
// One run starts once. The server fans this to several rooms the client
|
|
516
|
+
// has joined, and the repeats are byte-identical (OSK-7689/7709).
|
|
517
|
+
if (match(event) && this.firstTimeSeen('run.started', this.options.sessionId))
|
|
191
518
|
this.emit({ ...event, type: 'run.started' });
|
|
192
519
|
});
|
|
193
520
|
socket.on('message:stream', (event) => {
|
|
@@ -228,11 +555,20 @@ class AgenticStreamClient {
|
|
|
228
555
|
if (fresh.length === 0)
|
|
229
556
|
return; // a pure re-delivery — drop it whole
|
|
230
557
|
const scoped = { ...event, envelopes: fresh };
|
|
558
|
+
// Say what never arrived, once, before folding this batch in
|
|
559
|
+
// (OSK-7631). Checked against the FRESH set: a re-delivery carries old
|
|
560
|
+
// seqs and would read as a gap that never happened.
|
|
561
|
+
this.reportSeqGap(fresh);
|
|
231
562
|
const envelopeText = envelopesText(scoped);
|
|
232
563
|
if (envelopeText) {
|
|
233
564
|
this.accumulated += envelopeText;
|
|
234
565
|
this.emit({ ...scoped, type: 'text.delta', text: this.accumulated });
|
|
235
566
|
}
|
|
567
|
+
// A daemon-routed turn carries its tool calls HERE and nowhere else.
|
|
568
|
+
// Projecting them gives both execution paths one vocabulary (OSK-7730).
|
|
569
|
+
for (const projected of projectDaemonToolEvents(fresh, this.toolNamesByCall)) {
|
|
570
|
+
this.emitToolEvent({ ...projected, sessionId: this.options.sessionId });
|
|
571
|
+
}
|
|
236
572
|
this.emit({ ...scoped, type: 'session.output' });
|
|
237
573
|
return;
|
|
238
574
|
}
|
|
@@ -240,11 +576,11 @@ class AgenticStreamClient {
|
|
|
240
576
|
});
|
|
241
577
|
socket.on('tool:start', (event) => {
|
|
242
578
|
if (match(event))
|
|
243
|
-
this.
|
|
579
|
+
this.emitToolEvent({ ...event, type: 'tool.started' });
|
|
244
580
|
});
|
|
245
581
|
socket.on('tool:end', (event) => {
|
|
246
582
|
if (match(event))
|
|
247
|
-
this.
|
|
583
|
+
this.emitToolEvent({ ...event, type: 'tool.completed' });
|
|
248
584
|
});
|
|
249
585
|
socket.on('execution:enqueue', (event) => {
|
|
250
586
|
if (match(event))
|
|
@@ -254,13 +590,23 @@ class AgenticStreamClient {
|
|
|
254
590
|
if (!match(event))
|
|
255
591
|
return;
|
|
256
592
|
this.accumulated = '';
|
|
593
|
+
this.renderedText = '';
|
|
594
|
+
// Seqs restart with the stream, so a fresh low seq is not a gap.
|
|
595
|
+
this.highestSeq = 0;
|
|
596
|
+
this.reportedGap = false;
|
|
257
597
|
// A reset restarts the stream, so envelope seqs may repeat; forget the
|
|
258
598
|
// seen keys or the fresh post-reset text would be deduped away (OSK-4834).
|
|
259
599
|
this.seenEnvelopes.clear();
|
|
600
|
+
this.seenLifecycle.clear();
|
|
260
601
|
this.emit({ ...event, type: 'text.reset' });
|
|
261
602
|
});
|
|
262
603
|
socket.on('message:stream-end', (event) => {
|
|
263
|
-
if (match(event))
|
|
604
|
+
if (!match(event))
|
|
605
|
+
return;
|
|
606
|
+
// Byte-identical repeats, including `endedAt` (OSK-7689). Key on the
|
|
607
|
+
// message the stream ended for, falling back to the session.
|
|
608
|
+
const key = String(event.messageId ?? event.endedAt ?? this.options.sessionId);
|
|
609
|
+
if (this.firstTimeSeen('text.completed', key))
|
|
264
610
|
this.emit({ ...event, type: 'text.completed' });
|
|
265
611
|
});
|
|
266
612
|
socket.on('execution:complete', (event) => {
|
|
@@ -349,32 +695,83 @@ class AgenticStreamClient {
|
|
|
349
695
|
}
|
|
350
696
|
return new Promise((resolve) => this.readline.question(prompt, resolve));
|
|
351
697
|
}
|
|
698
|
+
/**
|
|
699
|
+
* True the first time this (kind, identity) pair is seen; false afterwards.
|
|
700
|
+
* The identity is the event's OWN — a tool-use id, a message id — never a
|
|
701
|
+
* position in the stream, because the repeat count varies run to run.
|
|
702
|
+
*/
|
|
703
|
+
firstTimeSeen(kind, identity) {
|
|
704
|
+
if (!identity)
|
|
705
|
+
return true; // no identity to dedupe on — treat as new
|
|
706
|
+
const key = `${kind}|${identity}`;
|
|
707
|
+
if (this.seenLifecycle.has(key))
|
|
708
|
+
return false;
|
|
709
|
+
this.seenLifecycle.add(key);
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Emit a tool event once per tool invocation.
|
|
714
|
+
*
|
|
715
|
+
* Deduped on `toolUseId`, which both paths carry and which is the invocation's
|
|
716
|
+
* real identity: four byte-identical `tool.started` events for one Bash call
|
|
717
|
+
* all shared it (OSK-7632). An event with no id cannot be deduped and is
|
|
718
|
+
* passed through, preserving prior behaviour.
|
|
719
|
+
*/
|
|
720
|
+
emitToolEvent(event) {
|
|
721
|
+
const id = typeof event.toolUseId === 'string' ? event.toolUseId : undefined;
|
|
722
|
+
if (!this.firstTimeSeen(String(event.type), id))
|
|
723
|
+
return;
|
|
724
|
+
this.emit(event);
|
|
725
|
+
}
|
|
352
726
|
emit(event) {
|
|
353
727
|
this.lastEvent = event;
|
|
354
728
|
this.options.onEvent?.(event);
|
|
729
|
+
// Any event is proof the stream is alive; the watchdog only fires on real
|
|
730
|
+
// silence.
|
|
731
|
+
this.armIdleProbe();
|
|
355
732
|
// Accumulation and `onEvent` still run — only the incremental rendering is
|
|
356
733
|
// suppressed, so a quiet run is the same turn with a different display.
|
|
357
734
|
if (this.options.quiet)
|
|
358
735
|
return;
|
|
736
|
+
const output = this.options.output ?? process.stdout;
|
|
359
737
|
if (this.options.jsonl) {
|
|
360
|
-
(
|
|
738
|
+
writeJsonLine(output, event);
|
|
361
739
|
return;
|
|
362
740
|
}
|
|
363
|
-
const output = this.options.output ?? process.stdout;
|
|
364
741
|
if (event.type === 'text.delta' && typeof event.text === 'string') {
|
|
365
|
-
|
|
742
|
+
// Print what is NEW. See `renderableDelta` — the old `\r` repaint could
|
|
743
|
+
// not rewind past a newline, so a multi-line answer was reprinted once
|
|
744
|
+
// per snapshot and its blocks fused (OSK-7701, OSK-7731).
|
|
745
|
+
const { append, shown } = renderableDelta(this.renderedText, event.text);
|
|
746
|
+
this.renderedText = shown;
|
|
747
|
+
if (append)
|
|
748
|
+
output.write(append);
|
|
366
749
|
}
|
|
367
750
|
else if (event.type === 'tool.started') {
|
|
368
751
|
output.write(`\n[tool] ${String(event.toolName ?? 'unknown')}\n`);
|
|
752
|
+
// The banner moved the cursor to a fresh line, so nothing of the answer
|
|
753
|
+
// is on screen after it: the next snapshot must be printed from where the
|
|
754
|
+
// text left off, not from column 0 of a line that no longer holds it.
|
|
755
|
+
this.renderedText = this.accumulated;
|
|
369
756
|
}
|
|
370
757
|
else if (event.type === 'tool.completed') {
|
|
371
758
|
output.write(`[tool completed] ${String(event.toolName ?? 'unknown')}\n`);
|
|
759
|
+
this.renderedText = this.accumulated;
|
|
760
|
+
}
|
|
761
|
+
else if (event.type === 'stream.gap') {
|
|
762
|
+
// Said WHILE READING, which is the only moment it helps: a truncated
|
|
763
|
+
// live answer looks like a complete short answer, and by the terminal
|
|
764
|
+
// event the reader has already believed it (OSK-7631).
|
|
765
|
+
output.write(`\n${String(event.message ?? '')}\n`);
|
|
766
|
+
this.renderedText = this.accumulated;
|
|
372
767
|
}
|
|
373
768
|
else if (event.type === 'transport.disconnected') {
|
|
374
769
|
output.write(`\n[reconnecting] ${String(event.reason ?? '')}\n`);
|
|
770
|
+
this.renderedText = this.accumulated;
|
|
375
771
|
}
|
|
376
772
|
else if (event.type === 'permission.requested' || event.type === 'question.requested') {
|
|
377
773
|
output.write(`\n[${event.type}]\n`);
|
|
774
|
+
this.renderedText = this.accumulated;
|
|
378
775
|
}
|
|
379
776
|
}
|
|
380
777
|
finish(status, event) {
|
|
@@ -393,26 +790,41 @@ class AgenticStreamClient {
|
|
|
393
790
|
// agent's actual final message rather than however much of it arrived in
|
|
394
791
|
// time.
|
|
395
792
|
const finalText = typeof event.result === 'string' ? event.result : '';
|
|
396
|
-
|
|
397
|
-
|
|
793
|
+
// …EXCEPT when it is a strict suffix of what we accumulated.
|
|
794
|
+
//
|
|
795
|
+
// On the cloud path `result` carries only the LAST text block, not the fold
|
|
796
|
+
// of all of them: an answer that spoke, ran three tools, then spoke again
|
|
797
|
+
// arrived complete in the stream and in the database, and `run.result`
|
|
798
|
+
// dropped the opening block the user had explicitly asked for — reporting
|
|
799
|
+
// `status: completed` (OSK-7703). A single-part answer hides it entirely,
|
|
800
|
+
// because with one block the last block IS the answer. Taking the longer of
|
|
801
|
+
// the two when one contains the other is right for both defects: the
|
|
802
|
+
// truncated-stream case (result longer) and the last-block case
|
|
803
|
+
// (accumulation longer).
|
|
804
|
+
const accumulationIsSuperset = finalText.length > 0 &&
|
|
805
|
+
this.accumulated.length > finalText.length &&
|
|
806
|
+
this.accumulated.endsWith(finalText);
|
|
807
|
+
if (finalText && finalText !== this.accumulated && !accumulationIsSuperset) {
|
|
398
808
|
this.accumulated = finalText;
|
|
399
|
-
// Repaint the line so the terminal ends on the whole answer instead of
|
|
400
|
-
// the fragment the last delta happened to leave there.
|
|
401
|
-
if (!this.options.quiet && !this.options.jsonl && hadPartial) {
|
|
402
|
-
(this.options.output ?? process.stdout).write(`\r${finalText}`);
|
|
403
|
-
}
|
|
404
809
|
}
|
|
405
|
-
if (!this.options.quiet && !this.options.jsonl && this.accumulated)
|
|
406
|
-
|
|
810
|
+
if (!this.options.quiet && !this.options.jsonl && this.accumulated) {
|
|
811
|
+
const output = this.options.output ?? process.stdout;
|
|
812
|
+
const { append, shown } = renderableDelta(this.renderedText, this.accumulated);
|
|
813
|
+
this.renderedText = shown;
|
|
814
|
+
if (append)
|
|
815
|
+
output.write(append);
|
|
816
|
+
output.write('\n');
|
|
817
|
+
}
|
|
407
818
|
this.emit({ ...event, type: `run.${status}` });
|
|
408
819
|
this.close();
|
|
409
|
-
this.
|
|
820
|
+
this.terminalResult = {
|
|
410
821
|
sessionId: this.options.sessionId,
|
|
411
822
|
responseMessageId: this.responseMessageId,
|
|
412
823
|
text: this.accumulated,
|
|
413
824
|
status,
|
|
414
825
|
lastEvent: event,
|
|
415
|
-
}
|
|
826
|
+
};
|
|
827
|
+
this.resolve(this.terminalResult);
|
|
416
828
|
}
|
|
417
829
|
/**
|
|
418
830
|
* OSK-2944 — a transient connect failure must not end an ACCEPTED run.
|
|
@@ -436,21 +848,66 @@ class AgenticStreamClient {
|
|
|
436
848
|
return new Promise((resolve, reject) => {
|
|
437
849
|
const socket = this.socket;
|
|
438
850
|
let lastError = null;
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
851
|
+
let deadline = timeoutMs;
|
|
852
|
+
let retryTimer = null;
|
|
853
|
+
let retried = false;
|
|
854
|
+
let timer = armDeadline(deadline);
|
|
855
|
+
function armDeadline(ms) {
|
|
856
|
+
const handle = setTimeout(() => {
|
|
857
|
+
cleanup();
|
|
858
|
+
reject(Object.assign(new Error(`Could not attach to the run stream within ${Math.round(ms / 1000)}s` +
|
|
859
|
+
`${lastError ? `: ${lastError.message}` : ''}`), { code: 'STREAM_ATTACH_TIMEOUT', lastError }));
|
|
860
|
+
}, ms);
|
|
861
|
+
handle.unref?.();
|
|
862
|
+
return handle;
|
|
863
|
+
}
|
|
444
864
|
const onConnect = () => {
|
|
445
865
|
cleanup();
|
|
446
866
|
resolve();
|
|
447
867
|
};
|
|
448
|
-
// Report and keep waiting — socket.io is already retrying underneath.
|
|
449
868
|
const onError = (error) => {
|
|
450
869
|
lastError = error;
|
|
870
|
+
// ── HONOUR THE RETRY CONTRACT (OSK-7583/7588/7604/7663) ─────────────
|
|
871
|
+
//
|
|
872
|
+
// "socket.io is already retrying underneath" is true for a TRANSPORT
|
|
873
|
+
// failure and false for a MIDDLEWARE refusal, which is what the
|
|
874
|
+
// connection cap is: `userConnectionTracking.js` says so in its own
|
|
875
|
+
// words — Socket.IO destroys a socket that fails middleware and does
|
|
876
|
+
// not reconnect it, and every first-party client is expected to
|
|
877
|
+
// schedule one retry after the stated delay, jittered. The CLI was not
|
|
878
|
+
// one, so a cap refusal ended an accepted run and the user never saw an
|
|
879
|
+
// answer the server had already produced.
|
|
880
|
+
//
|
|
881
|
+
// ONE retry, on the server's own clock, and only when it said the
|
|
882
|
+
// refusal was retryable.
|
|
883
|
+
const refusal = transportRefusal(error);
|
|
884
|
+
if (!refusal.retryable || refusal.retryAfterMs === undefined || retried)
|
|
885
|
+
return;
|
|
886
|
+
retried = true;
|
|
887
|
+
// The default deadline IS the server's retry delay, so without widening
|
|
888
|
+
// it the client gives up in the same instant it is allowed to try.
|
|
889
|
+
const widened = attachDeadlineFor(deadline, refusal);
|
|
890
|
+
if (widened > deadline) {
|
|
891
|
+
clearTimeout(timer);
|
|
892
|
+
deadline = widened;
|
|
893
|
+
timer = armDeadline(deadline);
|
|
894
|
+
}
|
|
895
|
+
retryTimer = setTimeout(() => {
|
|
896
|
+
retryTimer = null;
|
|
897
|
+
try {
|
|
898
|
+
socket.connect();
|
|
899
|
+
}
|
|
900
|
+
catch {
|
|
901
|
+
// The deadline above is the backstop; a failed reconnect attempt
|
|
902
|
+
// must not reject here and turn a pause into a failure.
|
|
903
|
+
}
|
|
904
|
+
}, jitteredRetryDelay(refusal.retryAfterMs));
|
|
905
|
+
retryTimer.unref?.();
|
|
451
906
|
};
|
|
452
907
|
const cleanup = () => {
|
|
453
908
|
clearTimeout(timer);
|
|
909
|
+
if (retryTimer)
|
|
910
|
+
clearTimeout(retryTimer);
|
|
454
911
|
socket.off('connect', onConnect);
|
|
455
912
|
socket.off('connect_error', onError);
|
|
456
913
|
};
|