@skrr-ai/cli 0.1.71 → 0.1.73
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/dist/base-command.js +1 -0
- package/dist/commands/agents/chat.js +2 -0
- package/dist/commands/agents/releases/create.js +57 -11
- package/dist/commands/machines/dedicated/cp.js +3 -0
- package/dist/commands/machines/dedicated/exec.js +3 -0
- package/dist/commands/spaces/snapshot.d.ts +1 -0
- package/dist/commands/spaces/snapshot.js +7 -2
- package/dist/commands/store/installs.js +1 -2
- package/dist/commands/store/signals.js +3 -0
- package/dist/commands/store/uninstall.js +1 -1
- package/dist/commands/subscriptions/subscribe.js +2 -2
- package/dist/lib/agentic-stream.d.ts +23 -0
- package/dist/lib/agentic-stream.js +169 -49
- package/dist/node_modules/@skrr-ai/data-provider/index.js +28 -20
- package/oclif.manifest.json +27673 -27665
- package/package.json +1 -1
package/dist/base-command.js
CHANGED
|
@@ -1322,6 +1322,7 @@ class BaseCommand extends core_1.Command {
|
|
|
1322
1322
|
exit: 1,
|
|
1323
1323
|
retryable: isRetryableFailure(e.status, body),
|
|
1324
1324
|
details: body ?? undefined,
|
|
1325
|
+
requestId: stringProp(body, 'requestId') || e.requestId,
|
|
1325
1326
|
});
|
|
1326
1327
|
}
|
|
1327
1328
|
else if ((0, outbox_1.isOutageFailure)(e)) {
|
|
@@ -14,6 +14,7 @@ const node_fs_1 = require("node:fs");
|
|
|
14
14
|
const core_1 = require("@oclif/core");
|
|
15
15
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
16
16
|
const base_command_1 = require("../../base-command");
|
|
17
|
+
const api_fetch_1 = require("../../lib/api-fetch");
|
|
17
18
|
const agent_home_workspace_1 = require("../../lib/agent-home-workspace");
|
|
18
19
|
const agentic_stream_1 = require("../../lib/agentic-stream");
|
|
19
20
|
const web_url_1 = require("../../lib/web-url");
|
|
@@ -594,6 +595,7 @@ class AgentsChat extends base_command_1.BaseCommand {
|
|
|
594
595
|
quiet,
|
|
595
596
|
approveTools: flags['approve-tools'],
|
|
596
597
|
reconcileTerminal,
|
|
598
|
+
replayEnvelopes: (from) => (0, api_fetch_1.apiFetch)(`/api/agentic/sessions/${encodeURIComponent(started.sessionId)}/envelopes?from=${from}&limit=500`, { credential: this.resolvedCredential, signal: AbortSignal.timeout(5000) }),
|
|
597
599
|
});
|
|
598
600
|
// OSK-2938 — Ctrl-C must be bounded, visible, and honest about what it
|
|
599
601
|
// did NOT stop.
|
|
@@ -3,6 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const core_1 = require("@oclif/core");
|
|
4
4
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
5
5
|
const base_command_1 = require("../../../base-command");
|
|
6
|
+
function betaWriteMayHaveCommitted(error) {
|
|
7
|
+
const failure = error;
|
|
8
|
+
const status = failure?.status ?? failure?.response?.status;
|
|
9
|
+
return status === 408 || (typeof status === 'number' && status >= 500) ||
|
|
10
|
+
['ECONNRESET', 'ETIMEDOUT', 'EAI_AGAIN', 'ENOTFOUND'].includes(failure?.code ?? '') ||
|
|
11
|
+
/fetch failed|socket hang up|timed? ?out/i.test(failure?.message ?? '');
|
|
12
|
+
}
|
|
6
13
|
class AgentsReleasesCreate extends base_command_1.BaseCommand {
|
|
7
14
|
static description = 'Create a private agent release';
|
|
8
15
|
static args = { agentId: core_1.Args.string({ required: true, ignoreStdin: true }) };
|
|
@@ -27,24 +34,63 @@ class AgentsReleasesCreate extends base_command_1.BaseCommand {
|
|
|
27
34
|
exit: 2,
|
|
28
35
|
});
|
|
29
36
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
let result;
|
|
38
|
+
let recoveredAfterTransportFailure = false;
|
|
39
|
+
if (flags.beta) {
|
|
40
|
+
try {
|
|
41
|
+
result = await data_provider_1.dataService.createBetaAgentRelease({
|
|
42
|
+
agentId: args.agentId,
|
|
43
|
+
changelog: flags.changelog,
|
|
44
|
+
versionMajor: flags['version-major'],
|
|
45
|
+
versionMinor: flags['version-minor'],
|
|
46
|
+
releaseId: flags['release-id'],
|
|
47
|
+
offerVersionId: flags['offer-version-id'],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (!betaWriteMayHaveCommitted(error))
|
|
52
|
+
throw error;
|
|
53
|
+
const releaseId = flags['release-id'];
|
|
54
|
+
let verification = 'unavailable';
|
|
55
|
+
try {
|
|
56
|
+
const releases = await data_provider_1.dataService.getAgentReleases(args.agentId);
|
|
57
|
+
const found = releases.data?.find((release) => release.id === releaseId);
|
|
58
|
+
if (found) {
|
|
59
|
+
result = found;
|
|
60
|
+
recoveredAfterTransportFailure = true;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
verification = 'not_observed';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// A failed read cannot settle whether the write reached the server.
|
|
68
|
+
}
|
|
69
|
+
if (!result) {
|
|
70
|
+
const suggestion = `Run skrr agents releases list ${args.agentId} --json and check for ${releaseId} before retrying.`;
|
|
71
|
+
this.failWithCliError({
|
|
72
|
+
message: `The beta publish request ended before the server confirmed its outcome. ${suggestion}`,
|
|
73
|
+
code: 'BETA_RELEASE_OUTCOME_UNCONFIRMED',
|
|
74
|
+
exit: 1,
|
|
75
|
+
retryable: false,
|
|
76
|
+
details: { agentId: args.agentId, releaseId, verification, suggestion },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
result = await data_provider_1.dataService.createPrivateAgentRelease({
|
|
40
83
|
agentId: args.agentId,
|
|
41
84
|
changelog: flags.changelog,
|
|
42
85
|
versionMajor: flags['version-major'],
|
|
43
86
|
versionMinor: flags['version-minor'],
|
|
44
87
|
});
|
|
88
|
+
}
|
|
45
89
|
this.log(flags.json
|
|
46
90
|
? this.agentJson(args.agentId, result)
|
|
47
|
-
:
|
|
91
|
+
: recoveredAfterTransportFailure
|
|
92
|
+
? `Beta release ${flags['release-id']} exists for agent ${args.agentId}; the publish response was interrupted.`
|
|
93
|
+
: `Created release for agent ${args.agentId}`);
|
|
48
94
|
if (!flags.json)
|
|
49
95
|
this.printAgentUrl(args.agentId);
|
|
50
96
|
}
|
|
@@ -105,6 +105,9 @@ class DedicatedMachinesCp extends base_command_1.BaseCommand {
|
|
|
105
105
|
if (err instanceof dedicated_copy_1.DedicatedCopyError) {
|
|
106
106
|
this.error(err.message, { exit: 1, code: err.code });
|
|
107
107
|
}
|
|
108
|
+
// Keep the API status, request id and explicit retry decision together.
|
|
109
|
+
if ((0, base_command_1.apiRefusal)(err).body?.retryable === true)
|
|
110
|
+
this.handleApiError(err);
|
|
108
111
|
const message = (0, dedicated_machines_1.formatDedicatedRuntimeApiError)(err);
|
|
109
112
|
if (message)
|
|
110
113
|
this.error(message, { exit: 1, code: (0, dedicated_machines_1.dedicatedRuntimeApiErrorCode)(err) });
|
|
@@ -154,6 +154,9 @@ class DedicatedMachinesExec extends base_command_1.BaseCommand {
|
|
|
154
154
|
err instanceof dedicated_exec_1.DedicatedExecIdempotencyUnsupportedError) {
|
|
155
155
|
this.error(err.message, { exit: 1, code: err.code });
|
|
156
156
|
}
|
|
157
|
+
// Keep the API status, request id and explicit retry decision together.
|
|
158
|
+
if ((0, base_command_1.apiRefusal)(err).body?.retryable === true)
|
|
159
|
+
this.handleApiError(err);
|
|
157
160
|
const message = (0, dedicated_machines_1.formatDedicatedRuntimeApiError)(err);
|
|
158
161
|
if (message)
|
|
159
162
|
this.error(message, { exit: 1, code: (0, dedicated_machines_1.dedicatedRuntimeApiErrorCode)(err) });
|
|
@@ -8,6 +8,7 @@ export default class SpacesSnapshot extends BaseCommand {
|
|
|
8
8
|
static flags: {
|
|
9
9
|
json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
10
10
|
'from-json': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
11
|
+
'update-template': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
11
12
|
title: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
12
13
|
description: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
13
14
|
'short-description': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
@@ -21,12 +21,13 @@ const CATEGORY_OPTIONS = [
|
|
|
21
21
|
'other',
|
|
22
22
|
];
|
|
23
23
|
class SpacesSnapshot extends base_command_1.BaseCommand {
|
|
24
|
-
static description = 'Create a
|
|
24
|
+
static description = 'Create or recapture a template from a space';
|
|
25
25
|
static examples = [
|
|
26
26
|
'<%= config.bin %> spaces snapshot <space-id> --title "Q2 Launch Template"',
|
|
27
27
|
'<%= config.bin %> spaces snapshot <space-id> --title "Support" --category engineering --public',
|
|
28
28
|
'<%= config.bin %> spaces snapshot <space-id> --from-json snapshot.json',
|
|
29
29
|
'<%= config.bin %> spaces snapshot <space-id> --from-json snapshot.json --title "Override"',
|
|
30
|
+
'<%= config.bin %> spaces snapshot <source-space-id> --update-template <template-id> --title "Q2 Launch v2"',
|
|
30
31
|
];
|
|
31
32
|
static args = {
|
|
32
33
|
id: core_1.Args.string({ description: 'Space ID', required: true, ignoreStdin: true }),
|
|
@@ -36,6 +37,9 @@ class SpacesSnapshot extends base_command_1.BaseCommand {
|
|
|
36
37
|
'from-json': core_1.Flags.string({
|
|
37
38
|
description: 'Path to a JSON file (or "-" for stdin) to use as the request body',
|
|
38
39
|
}),
|
|
40
|
+
'update-template': core_1.Flags.string({
|
|
41
|
+
description: 'Recapture this existing template from its original Space',
|
|
42
|
+
}),
|
|
39
43
|
title: core_1.Flags.string({ description: 'Snapshot title (required unless set via --from-json)' }),
|
|
40
44
|
description: core_1.Flags.string({ description: 'Snapshot description' }),
|
|
41
45
|
'short-description': core_1.Flags.string({ description: 'Short description (one liner)' }),
|
|
@@ -58,6 +62,7 @@ class SpacesSnapshot extends base_command_1.BaseCommand {
|
|
|
58
62
|
this.error(err.message, { exit: 1 });
|
|
59
63
|
}
|
|
60
64
|
const payload = (0, prompt_1.mergeDefined)(body, {
|
|
65
|
+
templateId: flags['update-template'],
|
|
61
66
|
title: flags.title,
|
|
62
67
|
description: flags.description,
|
|
63
68
|
shortDescription: flags['short-description'],
|
|
@@ -83,7 +88,7 @@ class SpacesSnapshot extends base_command_1.BaseCommand {
|
|
|
83
88
|
return;
|
|
84
89
|
}
|
|
85
90
|
const s = result;
|
|
86
|
-
this.log(`Snapshot created: ${s.id ?? s._id ?? '?'} — ${String(payload.title)}`);
|
|
91
|
+
this.log(`Snapshot ${payload.templateId ? 'updated' : 'created'}: ${s.id ?? s._id ?? '?'} — ${String(payload.title)}`);
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
exports.default = SpacesSnapshot;
|
|
@@ -19,10 +19,9 @@ class StoreInstalls extends base_command_1.BaseCommand {
|
|
|
19
19
|
this.requireAuth();
|
|
20
20
|
const { flags } = await this.parse(StoreInstalls);
|
|
21
21
|
const workspaceId = await (0, workspaces_1.ambientWorkspaceId)();
|
|
22
|
-
(0, data_provider_1.setWorkspaceIdHeader)(workspaceId ?? null);
|
|
23
22
|
let response;
|
|
24
23
|
try {
|
|
25
|
-
response = await data_provider_1.dataService.getStoreInstalls();
|
|
24
|
+
response = await data_provider_1.dataService.getStoreInstalls(workspaceId);
|
|
26
25
|
}
|
|
27
26
|
catch (error) {
|
|
28
27
|
this.handleApiError(error);
|
|
@@ -34,6 +34,9 @@ class StoreSignals extends base_command_1.BaseCommand {
|
|
|
34
34
|
const fmt = (v) => `$${((v ?? 0) / 1_000_000).toFixed(4)}`;
|
|
35
35
|
this.log(`Run cost ${fmt(s.measuredRunCost.minCostMicros)}–${fmt(s.measuredRunCost.maxCostMicros)} per run (measured, ${s.measuredRunCost.cohorts} buyers, ${s.measuredRunCost.windowDays}d)`);
|
|
36
36
|
}
|
|
37
|
+
else {
|
|
38
|
+
this.log('Run cost Insufficient data (fewer than 5 independent buyers)');
|
|
39
|
+
}
|
|
37
40
|
}
|
|
38
41
|
catch (err) {
|
|
39
42
|
this.handleApiError(err);
|
|
@@ -42,7 +42,7 @@ function parseReceipt(body) {
|
|
|
42
42
|
: null;
|
|
43
43
|
}
|
|
44
44
|
class StoreUninstall extends base_command_1.BaseCommand {
|
|
45
|
-
static description = 'Uninstall
|
|
45
|
+
static description = 'Uninstall is reversible: reinstalling restores the Agent copy. --erase permanently removes its Memory and ends commitments; the Agent, purchase, and delivered tasks remain.';
|
|
46
46
|
static examples = [
|
|
47
47
|
'<%= config.bin %> store uninstall agent_abc123',
|
|
48
48
|
'<%= config.bin %> store uninstall agent_abc123 --erase',
|
|
@@ -5,7 +5,7 @@ const data_provider_1 = require("@skrr-ai/data-provider");
|
|
|
5
5
|
const base_command_1 = require("../../base-command");
|
|
6
6
|
const prompt_1 = require("../../lib/prompt");
|
|
7
7
|
class SubscriptionsSubscribe extends base_command_1.BaseCommand {
|
|
8
|
-
static description = 'Subscribe to a published Service
|
|
8
|
+
static description = 'Subscribe to a published Service. By default, this shows the first-period price before buying from your balance, installing a managed instance, and opening its delivery obligation. Use --trial for the Service\'s card-free trial instead.';
|
|
9
9
|
static examples = [
|
|
10
10
|
'<%= config.bin %> subscriptions subscribe agent_abc123',
|
|
11
11
|
'<%= config.bin %> subscriptions subscribe agent_abc123 --space space_xyz --yes --json',
|
|
@@ -29,7 +29,7 @@ class SubscriptionsSubscribe extends base_command_1.BaseCommand {
|
|
|
29
29
|
description: 'Activate a period that was already paid (its grant id) instead of buying one. Nothing is charged.',
|
|
30
30
|
}),
|
|
31
31
|
trial: core_1.Flags.boolean({
|
|
32
|
-
description: 'Start
|
|
32
|
+
description: 'Start this Service\'s card-free trial instead of buying. Its duration and compute terms are shown before confirmation; nothing is charged and it does not renew.',
|
|
33
33
|
}),
|
|
34
34
|
yes: core_1.Flags.boolean({
|
|
35
35
|
char: 'y',
|
|
@@ -60,6 +60,12 @@ export type ReconciledTerminal = {
|
|
|
60
60
|
* drift (OSK-7629).
|
|
61
61
|
*/
|
|
62
62
|
export declare function writeJsonLine(output: NodeJS.WritableStream, value: unknown): void;
|
|
63
|
+
export type EnvelopeReplayPage = {
|
|
64
|
+
envelopes: unknown[];
|
|
65
|
+
hasMore?: boolean;
|
|
66
|
+
nextSeq?: number;
|
|
67
|
+
gap?: boolean;
|
|
68
|
+
};
|
|
63
69
|
export type AgenticStreamOptions = {
|
|
64
70
|
baseURL: string;
|
|
65
71
|
token: string;
|
|
@@ -89,6 +95,10 @@ export type AgenticStreamOptions = {
|
|
|
89
95
|
* the stream alone.
|
|
90
96
|
*/
|
|
91
97
|
reconcileTerminal?: () => Promise<ReconciledTerminal | null>;
|
|
98
|
+
/** Owner-authorized canonical envelopes, including events emitted before room admission. */
|
|
99
|
+
replayEnvelopes?: (from: number) => Promise<EnvelopeReplayPage>;
|
|
100
|
+
/** Bounded live catch-up cadence while a run is active. Default 2s. */
|
|
101
|
+
envelopeReplayIntervalMs?: number;
|
|
92
102
|
/** Silence before the first durable-record probe. Default 20s. */
|
|
93
103
|
idleTerminalProbeMs?: number;
|
|
94
104
|
/** How long to keep trying to attach before reporting a detach. Default 30s. */
|
|
@@ -376,6 +386,12 @@ export declare class AgenticStreamClient {
|
|
|
376
386
|
private accumulated;
|
|
377
387
|
/** Highest envelope `__seq` folded into `accumulated`, for gap detection. */
|
|
378
388
|
private highestSeq;
|
|
389
|
+
private readonly pendingEnvelopes;
|
|
390
|
+
private replayTimer;
|
|
391
|
+
private replayFlight;
|
|
392
|
+
private replayGeneration;
|
|
393
|
+
private replayFailures;
|
|
394
|
+
private finishingReplay;
|
|
379
395
|
private reportedGap;
|
|
380
396
|
private readonly seenEnvelopes;
|
|
381
397
|
/**
|
|
@@ -487,6 +503,12 @@ export declare class AgenticStreamClient {
|
|
|
487
503
|
* what the run reports — only what the reader is told while it is happening.
|
|
488
504
|
*/
|
|
489
505
|
private reportSeqGap;
|
|
506
|
+
/** Keep later live frames behind missing earlier frames while canonical replay catches up. */
|
|
507
|
+
private receiveSessionOutput;
|
|
508
|
+
private flushPendingEnvelopes;
|
|
509
|
+
/** Poll even while a tool is quiet: a pre-join start has no later live event to reveal its gap. */
|
|
510
|
+
private catchUpEnvelopes;
|
|
511
|
+
private applySessionOutput;
|
|
490
512
|
private installHandlers;
|
|
491
513
|
private handlePermission;
|
|
492
514
|
private handleQuestion;
|
|
@@ -534,6 +556,7 @@ export declare class AgenticStreamClient {
|
|
|
534
556
|
private emitToolEvent;
|
|
535
557
|
private emit;
|
|
536
558
|
private finish;
|
|
559
|
+
private finishAfterReplay;
|
|
537
560
|
/**
|
|
538
561
|
* The answer as the durable record holds it, for a run whose stream had a
|
|
539
562
|
* hole. Bounded: the record is written before the terminal event is sent, so
|
|
@@ -628,6 +628,12 @@ class AgenticStreamClient {
|
|
|
628
628
|
accumulated = '';
|
|
629
629
|
/** Highest envelope `__seq` folded into `accumulated`, for gap detection. */
|
|
630
630
|
highestSeq = 0;
|
|
631
|
+
pendingEnvelopes = new Map();
|
|
632
|
+
replayTimer = null;
|
|
633
|
+
replayFlight = null;
|
|
634
|
+
replayGeneration = 0;
|
|
635
|
+
replayFailures = 0;
|
|
636
|
+
finishingReplay = false;
|
|
631
637
|
reportedGap = false;
|
|
632
638
|
// Envelope keys already applied, so a re-delivered `session:output` frame is
|
|
633
639
|
// not appended twice (OSK-4834). Cleared on `execution:stream-reset`.
|
|
@@ -726,6 +732,7 @@ class AgenticStreamClient {
|
|
|
726
732
|
this.socket.emit('session:join', this.sessionJoinPayload());
|
|
727
733
|
this.emit({ type: 'session.joined', sessionId: this.options.sessionId });
|
|
728
734
|
this.armIdleProbe();
|
|
735
|
+
void this.catchUpEnvelopes();
|
|
729
736
|
return result;
|
|
730
737
|
}
|
|
731
738
|
/**
|
|
@@ -835,6 +842,10 @@ class AgenticStreamClient {
|
|
|
835
842
|
clearTimeout(this.idleTimer);
|
|
836
843
|
this.idleTimer = null;
|
|
837
844
|
}
|
|
845
|
+
if (this.replayTimer)
|
|
846
|
+
clearTimeout(this.replayTimer);
|
|
847
|
+
this.replayTimer = null;
|
|
848
|
+
this.pendingEnvelopes.clear();
|
|
838
849
|
this.abandonConnectWait?.();
|
|
839
850
|
const socket = this.socket;
|
|
840
851
|
this.socket = null;
|
|
@@ -883,12 +894,145 @@ class AgenticStreamClient {
|
|
|
883
894
|
this.highestSeq = seq;
|
|
884
895
|
}
|
|
885
896
|
}
|
|
897
|
+
/** Keep later live frames behind missing earlier frames while canonical replay catches up. */
|
|
898
|
+
receiveSessionOutput(event) {
|
|
899
|
+
if (this.closed || this.settled)
|
|
900
|
+
return;
|
|
901
|
+
const envelopes = event.envelopes;
|
|
902
|
+
if (!this.options.replayEnvelopes || !Array.isArray(envelopes)) {
|
|
903
|
+
this.applySessionOutput(event);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
for (const envelope of envelopes) {
|
|
907
|
+
const seq = Number(envelope?.__seq);
|
|
908
|
+
if (!Number.isSafeInteger(seq) || seq <= 0) {
|
|
909
|
+
this.applySessionOutput({ ...event, envelopes: [envelope] });
|
|
910
|
+
}
|
|
911
|
+
else if (seq > this.highestSeq && !this.pendingEnvelopes.has(seq)) {
|
|
912
|
+
this.pendingEnvelopes.set(seq, { envelope, event });
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
this.flushPendingEnvelopes(false);
|
|
916
|
+
if (this.pendingEnvelopes.size > 2000)
|
|
917
|
+
this.flushPendingEnvelopes(true);
|
|
918
|
+
if (this.pendingEnvelopes.size)
|
|
919
|
+
void this.catchUpEnvelopes();
|
|
920
|
+
}
|
|
921
|
+
flushPendingEnvelopes(allowGap, through = Infinity) {
|
|
922
|
+
const seqs = [...this.pendingEnvelopes.keys()].sort((a, b) => a - b);
|
|
923
|
+
for (const seq of seqs) {
|
|
924
|
+
if (seq > through)
|
|
925
|
+
break;
|
|
926
|
+
if (!allowGap && seq !== this.highestSeq + 1)
|
|
927
|
+
break;
|
|
928
|
+
const item = this.pendingEnvelopes.get(seq);
|
|
929
|
+
this.pendingEnvelopes.delete(seq);
|
|
930
|
+
this.applySessionOutput({ ...item.event, envelopes: [item.envelope] });
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/** Poll even while a tool is quiet: a pre-join start has no later live event to reveal its gap. */
|
|
934
|
+
catchUpEnvelopes() {
|
|
935
|
+
if (!this.options.replayEnvelopes || this.closed || this.settled)
|
|
936
|
+
return Promise.resolve();
|
|
937
|
+
if (this.replayFlight)
|
|
938
|
+
return this.replayFlight;
|
|
939
|
+
if (this.replayTimer)
|
|
940
|
+
clearTimeout(this.replayTimer);
|
|
941
|
+
this.replayTimer = null;
|
|
942
|
+
const generation = this.replayGeneration;
|
|
943
|
+
this.replayFlight = (async () => {
|
|
944
|
+
try {
|
|
945
|
+
for (let page = 0; page < 4; page += 1) {
|
|
946
|
+
const from = this.highestSeq;
|
|
947
|
+
const result = await this.options.replayEnvelopes(from);
|
|
948
|
+
if (this.closed || this.settled || generation !== this.replayGeneration)
|
|
949
|
+
return;
|
|
950
|
+
this.replayFailures = 0;
|
|
951
|
+
this.receiveSessionOutput({ type: 'session.output', sessionId: this.options.sessionId, source: 'replay', envelopes: result.envelopes });
|
|
952
|
+
// The canonical endpoint explicitly identifies retention gaps. Only
|
|
953
|
+
// then may later frames pass with the existing honest gap notice.
|
|
954
|
+
if (result.gap) {
|
|
955
|
+
// A retention gap proves only this page, never a later live frame.
|
|
956
|
+
const pageSeqs = result.envelopes.map((e) => Number(e?.__seq)).filter(Number.isSafeInteger);
|
|
957
|
+
if (pageSeqs.length)
|
|
958
|
+
this.flushPendingEnvelopes(true, Math.max(...pageSeqs));
|
|
959
|
+
this.flushPendingEnvelopes(false);
|
|
960
|
+
}
|
|
961
|
+
if (!result.hasMore || this.highestSeq <= from)
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
catch {
|
|
966
|
+
// Recovery failure says nothing about the producer. After bounded
|
|
967
|
+
// retries retain the existing live/gap/final-record fallback.
|
|
968
|
+
this.replayFailures += 1;
|
|
969
|
+
if (this.replayFailures >= 3)
|
|
970
|
+
this.flushPendingEnvelopes(true);
|
|
971
|
+
}
|
|
972
|
+
})().finally(() => {
|
|
973
|
+
this.replayFlight = null;
|
|
974
|
+
if (!this.closed && !this.settled && !this.finishingReplay) {
|
|
975
|
+
this.replayTimer = setTimeout(() => void this.catchUpEnvelopes(), this.options.envelopeReplayIntervalMs ?? 2000);
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
return this.replayFlight;
|
|
979
|
+
}
|
|
980
|
+
applySessionOutput(event) {
|
|
981
|
+
// The agent's words arrive HERE for agentic runs, inside `envelopes`, and
|
|
982
|
+
// `event.text` is null. Normalising the frame without reading them meant
|
|
983
|
+
// `skrr agents chat` printed two header lines and exited having shown
|
|
984
|
+
// nothing, while the agent had answered perfectly — and `--no-stream
|
|
985
|
+
// --json` returned `status: completed` with `response: ""`.
|
|
986
|
+
//
|
|
987
|
+
// Only `ev.t === 'text'` counts. The same envelope stream carries tool
|
|
988
|
+
// and status frames, and folding those into the answer would put machine
|
|
989
|
+
// chatter in `response`.
|
|
990
|
+
// Dedupe re-delivered envelopes (session room + user room + join replay):
|
|
991
|
+
// append and reprint only the ones we have not applied yet (OSK-4834).
|
|
992
|
+
const rawEnvelopes = event.envelopes;
|
|
993
|
+
if (Array.isArray(rawEnvelopes) && rawEnvelopes.length > 0) {
|
|
994
|
+
const fresh = unseenEnvelopes(event, this.seenEnvelopes);
|
|
995
|
+
if (fresh.length === 0)
|
|
996
|
+
return; // a pure re-delivery — drop it whole
|
|
997
|
+
const scoped = { ...event, envelopes: fresh };
|
|
998
|
+
// Say what never arrived, once, before folding this batch in
|
|
999
|
+
// (OSK-7631). Checked against the FRESH set: a re-delivery carries old
|
|
1000
|
+
// seqs and would read as a gap that never happened.
|
|
1001
|
+
this.reportSeqGap(fresh);
|
|
1002
|
+
const envelopeText = envelopesText(scoped);
|
|
1003
|
+
if (envelopeText) {
|
|
1004
|
+
this.accumulated += envelopeText;
|
|
1005
|
+
this.emit({ ...scoped, type: 'text.delta', text: this.accumulated });
|
|
1006
|
+
}
|
|
1007
|
+
// A daemon-routed turn carries its tool calls HERE and nowhere else.
|
|
1008
|
+
// Projecting them gives both execution paths one vocabulary (OSK-7730).
|
|
1009
|
+
for (const projected of projectDaemonToolEvents(fresh, this.toolNamesByCall)) {
|
|
1010
|
+
this.emitToolEvent({ ...projected, sessionId: this.options.sessionId });
|
|
1011
|
+
}
|
|
1012
|
+
// Notices meant for the person — a local harness that could not resume
|
|
1013
|
+
// its previous session and started a fresh one (OSK-12131). A plain
|
|
1014
|
+
// `service` line is diagnostic chatter and stays in the envelope; one
|
|
1015
|
+
// marked `notice` is said out loud.
|
|
1016
|
+
for (const notice of projectSessionNotices(fresh)) {
|
|
1017
|
+
this.emit({ ...notice, sessionId: this.options.sessionId });
|
|
1018
|
+
}
|
|
1019
|
+
// Who decided each permission request — a person (and where), the
|
|
1020
|
+
// mode, a policy — said beside the decision (OSK-12113).
|
|
1021
|
+
for (const resolved of projectPermissionResolutions(fresh, this.toolNamesByPermission)) {
|
|
1022
|
+
this.emit({ ...resolved, sessionId: this.options.sessionId });
|
|
1023
|
+
}
|
|
1024
|
+
this.emit({ ...scoped, type: 'session.output' });
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
this.emit({ ...event, type: 'session.output' });
|
|
1028
|
+
}
|
|
886
1029
|
installHandlers() {
|
|
887
1030
|
const socket = this.socket;
|
|
888
1031
|
const match = (event) => !event.sessionId || event.sessionId === this.options.sessionId;
|
|
889
1032
|
socket.on('connect', () => {
|
|
890
1033
|
socket.emit('session:join', this.sessionJoinPayload());
|
|
891
1034
|
this.emit({ type: 'transport.connected', sessionId: this.options.sessionId });
|
|
1035
|
+
void this.catchUpEnvelopes();
|
|
892
1036
|
});
|
|
893
1037
|
socket.on('disconnect', (reason) => {
|
|
894
1038
|
this.transportDropped = true;
|
|
@@ -942,55 +1086,8 @@ class AgenticStreamClient {
|
|
|
942
1086
|
this.emit({ ...event, type: 'text.delta' });
|
|
943
1087
|
});
|
|
944
1088
|
socket.on('session:output', (event) => {
|
|
945
|
-
if (
|
|
946
|
-
|
|
947
|
-
// The agent's words arrive HERE for agentic runs, inside `envelopes`, and
|
|
948
|
-
// `event.text` is null. Normalising the frame without reading them meant
|
|
949
|
-
// `skrr agents chat` printed two header lines and exited having shown
|
|
950
|
-
// nothing, while the agent had answered perfectly — and `--no-stream
|
|
951
|
-
// --json` returned `status: completed` with `response: ""`.
|
|
952
|
-
//
|
|
953
|
-
// Only `ev.t === 'text'` counts. The same envelope stream carries tool
|
|
954
|
-
// and status frames, and folding those into the answer would put machine
|
|
955
|
-
// chatter in `response`.
|
|
956
|
-
// Dedupe re-delivered envelopes (session room + user room + join replay):
|
|
957
|
-
// append and reprint only the ones we have not applied yet (OSK-4834).
|
|
958
|
-
const rawEnvelopes = event.envelopes;
|
|
959
|
-
if (Array.isArray(rawEnvelopes) && rawEnvelopes.length > 0) {
|
|
960
|
-
const fresh = unseenEnvelopes(event, this.seenEnvelopes);
|
|
961
|
-
if (fresh.length === 0)
|
|
962
|
-
return; // a pure re-delivery — drop it whole
|
|
963
|
-
const scoped = { ...event, envelopes: fresh };
|
|
964
|
-
// Say what never arrived, once, before folding this batch in
|
|
965
|
-
// (OSK-7631). Checked against the FRESH set: a re-delivery carries old
|
|
966
|
-
// seqs and would read as a gap that never happened.
|
|
967
|
-
this.reportSeqGap(fresh);
|
|
968
|
-
const envelopeText = envelopesText(scoped);
|
|
969
|
-
if (envelopeText) {
|
|
970
|
-
this.accumulated += envelopeText;
|
|
971
|
-
this.emit({ ...scoped, type: 'text.delta', text: this.accumulated });
|
|
972
|
-
}
|
|
973
|
-
// A daemon-routed turn carries its tool calls HERE and nowhere else.
|
|
974
|
-
// Projecting them gives both execution paths one vocabulary (OSK-7730).
|
|
975
|
-
for (const projected of projectDaemonToolEvents(fresh, this.toolNamesByCall)) {
|
|
976
|
-
this.emitToolEvent({ ...projected, sessionId: this.options.sessionId });
|
|
977
|
-
}
|
|
978
|
-
// Notices meant for the person — a local harness that could not resume
|
|
979
|
-
// its previous session and started a fresh one (OSK-12131). A plain
|
|
980
|
-
// `service` line is diagnostic chatter and stays in the envelope; one
|
|
981
|
-
// marked `notice` is said out loud.
|
|
982
|
-
for (const notice of projectSessionNotices(fresh)) {
|
|
983
|
-
this.emit({ ...notice, sessionId: this.options.sessionId });
|
|
984
|
-
}
|
|
985
|
-
// Who decided each permission request — a person (and where), the
|
|
986
|
-
// mode, a policy — said beside the decision (OSK-12113).
|
|
987
|
-
for (const resolved of projectPermissionResolutions(fresh, this.toolNamesByPermission)) {
|
|
988
|
-
this.emit({ ...resolved, sessionId: this.options.sessionId });
|
|
989
|
-
}
|
|
990
|
-
this.emit({ ...scoped, type: 'session.output' });
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
this.emit({ ...event, type: 'session.output' });
|
|
1089
|
+
if (match(event))
|
|
1090
|
+
this.receiveSessionOutput(event);
|
|
994
1091
|
});
|
|
995
1092
|
socket.on('tool:start', (event) => {
|
|
996
1093
|
if (match(event))
|
|
@@ -1011,6 +1108,8 @@ class AgenticStreamClient {
|
|
|
1011
1108
|
this.renderedText = '';
|
|
1012
1109
|
// Seqs restart with the stream, so a fresh low seq is not a gap.
|
|
1013
1110
|
this.highestSeq = 0;
|
|
1111
|
+
this.replayGeneration += 1;
|
|
1112
|
+
this.pendingEnvelopes.clear();
|
|
1014
1113
|
this.reportedGap = false;
|
|
1015
1114
|
// A reset restarts the stream, so envelope seqs may repeat; forget the
|
|
1016
1115
|
// seen keys or the fresh post-reset text would be deduped away (OSK-4834).
|
|
@@ -1429,6 +1528,27 @@ class AgenticStreamClient {
|
|
|
1429
1528
|
}
|
|
1430
1529
|
}
|
|
1431
1530
|
finish(status, event) {
|
|
1531
|
+
if (this.settled || this.finishingReplay)
|
|
1532
|
+
return;
|
|
1533
|
+
if (this.options.replayEnvelopes) {
|
|
1534
|
+
this.finishingReplay = true;
|
|
1535
|
+
const activeReplay = this.replayFlight;
|
|
1536
|
+
void (async () => {
|
|
1537
|
+
if (activeReplay)
|
|
1538
|
+
await activeReplay;
|
|
1539
|
+
// A request already in flight may have read before the terminal event.
|
|
1540
|
+
await this.catchUpEnvelopes();
|
|
1541
|
+
})().finally(() => {
|
|
1542
|
+
if (this.closed || this.settled)
|
|
1543
|
+
return;
|
|
1544
|
+
this.flushPendingEnvelopes(true);
|
|
1545
|
+
this.finishAfterReplay(status, event);
|
|
1546
|
+
});
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
this.finishAfterReplay(status, event);
|
|
1550
|
+
}
|
|
1551
|
+
finishAfterReplay(status, event) {
|
|
1432
1552
|
if (this.settled)
|
|
1433
1553
|
return;
|
|
1434
1554
|
this.settled = true;
|