@skrr-ai/cli 0.1.85 → 0.1.87
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/bin/run.js +11 -0
- package/dist/base-command.js +1 -0
- package/dist/commands/agents/chat.d.ts +58 -1
- package/dist/commands/agents/chat.js +180 -4
- package/dist/commands/agents/endpoint/enable.js +6 -0
- package/dist/commands/store/acquisitions/approve.js +3 -0
- package/dist/lib/agent-endpoints.d.ts +19 -5
- package/dist/lib/agent-endpoints.js +11 -0
- package/dist/lib/agent-home-workspace.d.ts +19 -0
- package/dist/lib/agent-home-workspace.js +27 -0
- package/dist/lib/agentic-stream.d.ts +135 -0
- package/dist/lib/agentic-stream.js +341 -6
- package/dist/lib/auth-storage.d.ts +4 -0
- package/dist/lib/auth-storage.js +33 -0
- package/dist/lib/keychain.d.ts +1 -0
- package/dist/lib/keychain.js +67 -14
- package/dist/lib/refresh.d.ts +7 -0
- package/dist/lib/refresh.js +2 -1
- package/dist/lib/store-acquisition.js +11 -3
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +5139 -5029
- package/oclif.manifest.json +42317 -42309
- package/package.json +5 -2
|
@@ -20,6 +20,12 @@ export type AgenticStreamResult = {
|
|
|
20
20
|
responseMessageId?: string;
|
|
21
21
|
text: string;
|
|
22
22
|
status: 'completed' | 'aborted' | 'failed';
|
|
23
|
+
/**
|
|
24
|
+
* The run first failed as handed over (a deploy replaced the relaying server)
|
|
25
|
+
* and the machine's completed reply was then repaired onto the record
|
|
26
|
+
* (OSK-13543). Absent on every ordinary run.
|
|
27
|
+
*/
|
|
28
|
+
repaired?: true;
|
|
23
29
|
lastEvent?: AgenticStreamEvent;
|
|
24
30
|
};
|
|
25
31
|
/**
|
|
@@ -45,7 +51,82 @@ export type ReconciledTerminal = {
|
|
|
45
51
|
text?: string;
|
|
46
52
|
/** The recorded failure reason — `metadata.agentic.error` on the message. */
|
|
47
53
|
error?: string;
|
|
54
|
+
/**
|
|
55
|
+
* The failure is a hand-over, not an ending (OSK-13543): the row is failed
|
|
56
|
+
* with {@link DAEMON_HANDOVER_REASON}, which the server itself treats as
|
|
57
|
+
* "outcome pending the machine's own turn-end".
|
|
58
|
+
*/
|
|
59
|
+
handedOver?: boolean;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* OSK-13517 / OSK-13543 — the one failure that is not an ending.
|
|
63
|
+
*
|
|
64
|
+
* A deploy replaced the server RELAYING a local-daemon turn while the daemon
|
|
65
|
+
* kept running it. The server saves the turn `error: true` with this reason
|
|
66
|
+
* (`metadata.agentic.errorSubtype`), keeps the conversation quarantined, and
|
|
67
|
+
* converges the row later from the machine's own turn-end: completed →
|
|
68
|
+
* repaired (`error: false`, `metadata.agentic.deliveryRepair`), anything else →
|
|
69
|
+
* settled failed (`errorSubtype: worker_replaced_daemon_failed`).
|
|
70
|
+
*
|
|
71
|
+
* Mirrors `WORKER_REPLACED_DAEMON_RUNNING_REASON` in
|
|
72
|
+
* `api/server/services/AgentWorker/workerReplacedNotice.js` and the code
|
|
73
|
+
* beside it in `daemonHandlers.js` / `conversationAdmissionClaim.js`; a parity
|
|
74
|
+
* spec reads those files so the two cannot drift.
|
|
75
|
+
*/
|
|
76
|
+
export declare const WORKER_REPLACED_CODE = "AGENT_WORKER_REPLACED";
|
|
77
|
+
export declare const DAEMON_HANDOVER_REASON = "worker_replaced_daemon_running";
|
|
78
|
+
/**
|
|
79
|
+
* Is this terminal event the hand-over failure, and nothing else?
|
|
80
|
+
*
|
|
81
|
+
* The server sends it on two paths with different shapes, so the reason is
|
|
82
|
+
* read wherever either puts it:
|
|
83
|
+
* - the QueueEvents backstop (`queue.js`): `{code: 'AGENT_WORKER_REPLACED',
|
|
84
|
+
* errorType: 'worker_replaced_daemon_running', errors}`;
|
|
85
|
+
* - the worker's own result (`buildExecutionErrorEvent`): `errorType` and
|
|
86
|
+
* `reason` both carry the daemon reason, `code` only when the result had one.
|
|
87
|
+
* The plain deploy failure shares the CODE (`errorType: 'worker_replaced'`, the
|
|
88
|
+
* turn really ended), so the code alone must never qualify, and a code that
|
|
89
|
+
* is present but different disqualifies.
|
|
90
|
+
*/
|
|
91
|
+
export declare function isDaemonHandoverFailure(event: Record<string, unknown> | null | undefined): boolean;
|
|
92
|
+
/** What the durable record says about a handed-over turn, read while waiting. */
|
|
93
|
+
export type HandoverReading =
|
|
94
|
+
/** Still failed with the hand-over reason, or not yet written: keep waiting. */
|
|
95
|
+
{
|
|
96
|
+
state: 'pending';
|
|
97
|
+
}
|
|
98
|
+
/** The machine finished and the server repaired the row. */
|
|
99
|
+
| {
|
|
100
|
+
state: 'repaired';
|
|
101
|
+
text: string;
|
|
102
|
+
repairedAt?: string;
|
|
103
|
+
}
|
|
104
|
+
/** The machine's turn ended without finishing, and the server settled it so. */
|
|
105
|
+
| {
|
|
106
|
+
state: 'failed';
|
|
107
|
+
error: string;
|
|
108
|
+
text?: string;
|
|
109
|
+
};
|
|
110
|
+
export type HandoverWaitOptions = {
|
|
111
|
+
/** Read the persisted response message. A throw or null is "no evidence". */
|
|
112
|
+
read: () => Promise<HandoverReading | null>;
|
|
113
|
+
/** The response message the repair notification names. */
|
|
114
|
+
responseMessageId?: string;
|
|
115
|
+
conversationId?: string;
|
|
116
|
+
/** How long to wait for the machine's reply. 0 disables the wait. */
|
|
117
|
+
maxWaitMs: number;
|
|
118
|
+
/** Fallback re-read cadence when no notification arrives. Default 15s. */
|
|
119
|
+
pollIntervalMs?: number;
|
|
120
|
+
/**
|
|
121
|
+
* Where the human status line goes when stdout is reserved for the final
|
|
122
|
+
* result (`--no-stream`). Default stderr.
|
|
123
|
+
*/
|
|
124
|
+
statusOutput?: NodeJS.WritableStream;
|
|
48
125
|
};
|
|
126
|
+
/** Default fallback re-read cadence while waiting on a handed-over turn. */
|
|
127
|
+
export declare const HANDOVER_POLL_INTERVAL_MS = 15000;
|
|
128
|
+
/** `600000` → `10m`, `90000` → `1m30s`, `45000` → `45s`. */
|
|
129
|
+
export declare function formatWaitDuration(ms: number): string;
|
|
49
130
|
/**
|
|
50
131
|
* Write exactly one complete JSON value on one line.
|
|
51
132
|
*
|
|
@@ -106,6 +187,12 @@ export type AgenticStreamOptions = {
|
|
|
106
187
|
* owns that stream and its contract does not include diagnostics.
|
|
107
188
|
*/
|
|
108
189
|
debug?: (line: string) => void;
|
|
190
|
+
/**
|
|
191
|
+
* OSK-13543 — wait for a handed-over local-daemon turn instead of ending on
|
|
192
|
+
* its failure. See {@link isDaemonHandoverFailure}. Omitted: today's
|
|
193
|
+
* behaviour, every `execution:error` settles the run at once.
|
|
194
|
+
*/
|
|
195
|
+
awaitHandover?: HandoverWaitOptions;
|
|
109
196
|
/** Silence before the first durable-record probe. Default 20s. */
|
|
110
197
|
idleTerminalProbeMs?: number;
|
|
111
198
|
/** How long to keep trying to attach before reporting a detach. Default 30s. */
|
|
@@ -493,6 +580,11 @@ export declare class AgenticStreamClient {
|
|
|
493
580
|
* a reconciled detach report the real outcome instead of the detach.
|
|
494
581
|
*/
|
|
495
582
|
private terminalResult;
|
|
583
|
+
/**
|
|
584
|
+
* A handed-over turn being waited on (OSK-13543): the failure event that
|
|
585
|
+
* started the wait, and the timers bounding it. Null otherwise.
|
|
586
|
+
*/
|
|
587
|
+
private handover;
|
|
496
588
|
private resolve;
|
|
497
589
|
private reject;
|
|
498
590
|
/** The terminal result, or null while the run is still in flight. */
|
|
@@ -542,6 +634,44 @@ export declare class AgenticStreamClient {
|
|
|
542
634
|
probeTerminal(reason: string): Promise<boolean>;
|
|
543
635
|
cancel(reason?: string): void;
|
|
544
636
|
close(): void;
|
|
637
|
+
/**
|
|
638
|
+
* True while a handed-over turn is being waited on (OSK-13543). A Ctrl-C in
|
|
639
|
+
* this state stops the wait; it cannot cancel the machine's turn.
|
|
640
|
+
*/
|
|
641
|
+
awaitingHandover(): boolean;
|
|
642
|
+
/**
|
|
643
|
+
* OSK-13543 — the run failed because a deploy replaced the server relaying a
|
|
644
|
+
* turn the machine is still running. That failure is the server's PROVISIONAL
|
|
645
|
+
* verdict: it keeps the conversation held and rewrites the row from the
|
|
646
|
+
* machine's own turn-end. Ending here told a CLI user "failed" about a turn
|
|
647
|
+
* whose answer the web showed ninety seconds later.
|
|
648
|
+
*
|
|
649
|
+
* So keep the stream, the user-room subscription and the envelope replay
|
|
650
|
+
* open, and settle on what the RECORD converges to: repaired → completed,
|
|
651
|
+
* settled failed → failed with the machine's reason, neither within the
|
|
652
|
+
* bound → the original failure. The record, not the notification, decides:
|
|
653
|
+
* a notification only says "read again", so a lost or duplicated one costs
|
|
654
|
+
* at most a poll interval.
|
|
655
|
+
*
|
|
656
|
+
* Returns false (and changes nothing) when the run cannot wait: no hand-over
|
|
657
|
+
* reader configured, a zero bound, or a run already settling.
|
|
658
|
+
*/
|
|
659
|
+
private beginHandoverWait;
|
|
660
|
+
/** Ask the record again; coalesces with a read already in flight. */
|
|
661
|
+
private requestHandoverRead;
|
|
662
|
+
/** One read of the record. True when it settled the run. */
|
|
663
|
+
private readHandover;
|
|
664
|
+
/** The bound ran out: one last look, then the original failure. */
|
|
665
|
+
private expireHandover;
|
|
666
|
+
private stopHandoverTimers;
|
|
667
|
+
private endHandover;
|
|
668
|
+
/**
|
|
669
|
+
* The server's "this row was rewritten" notice (`lateDeliveryRepair`
|
|
670
|
+
* `notifyViewersMessageRewritten`), delivered to every socket of the user
|
|
671
|
+
* through the `user:<id>` room this socket joins on connect. A hint to read,
|
|
672
|
+
* never a verdict.
|
|
673
|
+
*/
|
|
674
|
+
private onConversationUpdate;
|
|
545
675
|
private debug;
|
|
546
676
|
/**
|
|
547
677
|
* Record a hole being passed over — the moment replay has given up on it.
|
|
@@ -622,6 +752,11 @@ export declare class AgenticStreamClient {
|
|
|
622
752
|
*/
|
|
623
753
|
private emitToolEvent;
|
|
624
754
|
private emit;
|
|
755
|
+
/**
|
|
756
|
+
* Human-mode lines for the hand-over wait. Returns true when the event is
|
|
757
|
+
* fully rendered here; false lets the ordinary rendering continue.
|
|
758
|
+
*/
|
|
759
|
+
private renderHandoverStatus;
|
|
625
760
|
private finish;
|
|
626
761
|
private finishAfterReplay;
|
|
627
762
|
/**
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AgenticStreamClient = exports.USER_CONNECTION_CAP_CODE = exports.CODEX_DECLINED_TOOL_OUTPUTS = exports.TYPE_AHEAD_DISCARD_MS = exports.NON_INTERACTIVE_QUESTION_DISMISSAL = void 0;
|
|
3
|
+
exports.AgenticStreamClient = exports.USER_CONNECTION_CAP_CODE = exports.CODEX_DECLINED_TOOL_OUTPUTS = exports.TYPE_AHEAD_DISCARD_MS = exports.HANDOVER_POLL_INTERVAL_MS = exports.DAEMON_HANDOVER_REASON = exports.WORKER_REPLACED_CODE = exports.NON_INTERACTIVE_QUESTION_DISMISSAL = void 0;
|
|
4
|
+
exports.isDaemonHandoverFailure = isDaemonHandoverFailure;
|
|
5
|
+
exports.formatWaitDuration = formatWaitDuration;
|
|
4
6
|
exports.writeJsonLine = writeJsonLine;
|
|
5
7
|
exports.discardTypeAhead = discardTypeAhead;
|
|
6
8
|
exports.describePermissionTarget = describePermissionTarget;
|
|
@@ -41,6 +43,54 @@ const dpop_auth_1 = require("./dpop-auth");
|
|
|
41
43
|
*/
|
|
42
44
|
exports.NON_INTERACTIVE_QUESTION_DISMISSAL = 'No answer: this turn was started non-interactively (no terminal), so nobody can reply to ' +
|
|
43
45
|
'questions. Proceed with your best judgement, or finish by stating exactly what you need.';
|
|
46
|
+
/**
|
|
47
|
+
* OSK-13517 / OSK-13543 — the one failure that is not an ending.
|
|
48
|
+
*
|
|
49
|
+
* A deploy replaced the server RELAYING a local-daemon turn while the daemon
|
|
50
|
+
* kept running it. The server saves the turn `error: true` with this reason
|
|
51
|
+
* (`metadata.agentic.errorSubtype`), keeps the conversation quarantined, and
|
|
52
|
+
* converges the row later from the machine's own turn-end: completed →
|
|
53
|
+
* repaired (`error: false`, `metadata.agentic.deliveryRepair`), anything else →
|
|
54
|
+
* settled failed (`errorSubtype: worker_replaced_daemon_failed`).
|
|
55
|
+
*
|
|
56
|
+
* Mirrors `WORKER_REPLACED_DAEMON_RUNNING_REASON` in
|
|
57
|
+
* `api/server/services/AgentWorker/workerReplacedNotice.js` and the code
|
|
58
|
+
* beside it in `daemonHandlers.js` / `conversationAdmissionClaim.js`; a parity
|
|
59
|
+
* spec reads those files so the two cannot drift.
|
|
60
|
+
*/
|
|
61
|
+
exports.WORKER_REPLACED_CODE = 'AGENT_WORKER_REPLACED';
|
|
62
|
+
exports.DAEMON_HANDOVER_REASON = 'worker_replaced_daemon_running';
|
|
63
|
+
/**
|
|
64
|
+
* Is this terminal event the hand-over failure, and nothing else?
|
|
65
|
+
*
|
|
66
|
+
* The server sends it on two paths with different shapes, so the reason is
|
|
67
|
+
* read wherever either puts it:
|
|
68
|
+
* - the QueueEvents backstop (`queue.js`): `{code: 'AGENT_WORKER_REPLACED',
|
|
69
|
+
* errorType: 'worker_replaced_daemon_running', errors}`;
|
|
70
|
+
* - the worker's own result (`buildExecutionErrorEvent`): `errorType` and
|
|
71
|
+
* `reason` both carry the daemon reason, `code` only when the result had one.
|
|
72
|
+
* The plain deploy failure shares the CODE (`errorType: 'worker_replaced'`, the
|
|
73
|
+
* turn really ended), so the code alone must never qualify, and a code that
|
|
74
|
+
* is present but different disqualifies.
|
|
75
|
+
*/
|
|
76
|
+
function isDaemonHandoverFailure(event) {
|
|
77
|
+
if (!event)
|
|
78
|
+
return false;
|
|
79
|
+
if (event.code !== undefined && event.code !== exports.WORKER_REPLACED_CODE)
|
|
80
|
+
return false;
|
|
81
|
+
return [event.errorType, event.reason, event.daemonReason].includes(exports.DAEMON_HANDOVER_REASON);
|
|
82
|
+
}
|
|
83
|
+
/** Default fallback re-read cadence while waiting on a handed-over turn. */
|
|
84
|
+
exports.HANDOVER_POLL_INTERVAL_MS = 15_000;
|
|
85
|
+
/** `600000` → `10m`, `90000` → `1m30s`, `45000` → `45s`. */
|
|
86
|
+
function formatWaitDuration(ms) {
|
|
87
|
+
const total = Math.max(0, Math.round(ms / 1000));
|
|
88
|
+
const minutes = Math.floor(total / 60);
|
|
89
|
+
const seconds = total % 60;
|
|
90
|
+
if (!minutes)
|
|
91
|
+
return `${seconds}s`;
|
|
92
|
+
return seconds ? `${minutes}m${seconds}s` : `${minutes}m`;
|
|
93
|
+
}
|
|
44
94
|
/**
|
|
45
95
|
* Write exactly one complete JSON value on one line.
|
|
46
96
|
*
|
|
@@ -745,6 +795,11 @@ class AgenticStreamClient {
|
|
|
745
795
|
* a reconciled detach report the real outcome instead of the detach.
|
|
746
796
|
*/
|
|
747
797
|
terminalResult = null;
|
|
798
|
+
/**
|
|
799
|
+
* A handed-over turn being waited on (OSK-13543): the failure event that
|
|
800
|
+
* started the wait, and the timers bounding it. Null otherwise.
|
|
801
|
+
*/
|
|
802
|
+
handover = null;
|
|
748
803
|
resolve;
|
|
749
804
|
reject;
|
|
750
805
|
/** The terminal result, or null while the run is still in flight. */
|
|
@@ -833,6 +888,11 @@ class AgenticStreamClient {
|
|
|
833
888
|
return;
|
|
834
889
|
if (this.settled || this.closed)
|
|
835
890
|
return;
|
|
891
|
+
// While a handed-over turn is waited on, the record reads `error: true` BY
|
|
892
|
+
// DESIGN — it is the failure being waited out. The generic probe would
|
|
893
|
+
// settle on exactly that; the hand-over reader owns the record instead.
|
|
894
|
+
if (this.handover)
|
|
895
|
+
return;
|
|
836
896
|
if (this.idleTimer)
|
|
837
897
|
clearTimeout(this.idleTimer);
|
|
838
898
|
this.idleTimer = setTimeout(() => void this.probeTerminal('idle'), this.options.idleTerminalProbeMs ?? 20_000);
|
|
@@ -846,7 +906,7 @@ class AgenticStreamClient {
|
|
|
846
906
|
*/
|
|
847
907
|
async probeTerminal(reason) {
|
|
848
908
|
const reconcile = this.options.reconcileTerminal;
|
|
849
|
-
if (!reconcile || this.settled || this.reconciling)
|
|
909
|
+
if (!reconcile || this.settled || this.reconciling || this.handover)
|
|
850
910
|
return false;
|
|
851
911
|
this.reconciling = true;
|
|
852
912
|
let terminal = null;
|
|
@@ -866,6 +926,23 @@ class AgenticStreamClient {
|
|
|
866
926
|
this.armIdleProbe();
|
|
867
927
|
return false;
|
|
868
928
|
}
|
|
929
|
+
// The stream never delivered the hand-over failure (joined late, or the
|
|
930
|
+
// frame was lost), but the record carries it: wait the same way. Not on a
|
|
931
|
+
// detach — the caller has already stopped watching and asks only what the
|
|
932
|
+
// record says now.
|
|
933
|
+
if (terminal.handedOver && reason !== 'detach') {
|
|
934
|
+
const begun = this.beginHandoverWait({
|
|
935
|
+
type: 'execution:error',
|
|
936
|
+
sessionId: this.options.sessionId,
|
|
937
|
+
code: exports.WORKER_REPLACED_CODE,
|
|
938
|
+
errorType: exports.DAEMON_HANDOVER_REASON,
|
|
939
|
+
reason: exports.DAEMON_HANDOVER_REASON,
|
|
940
|
+
reconciledFrom: 'durable_record',
|
|
941
|
+
...(terminal.error ? { message: terminal.error, errors: [terminal.error] } : {}),
|
|
942
|
+
});
|
|
943
|
+
if (begun)
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
869
946
|
// The record is the witness the stream refused to be, so say so: the
|
|
870
947
|
// recorded reason is exactly what "the surface shows nothing at all" cost
|
|
871
948
|
// the reader (OSK-7595 — the CLI reported a transport error while the run's
|
|
@@ -883,6 +960,11 @@ class AgenticStreamClient {
|
|
|
883
960
|
cancel(reason = 'Interrupted by user') {
|
|
884
961
|
if (this.settled || !this.socket)
|
|
885
962
|
return;
|
|
963
|
+
// A handed-over turn has no relaying server left to carry an interrupt to
|
|
964
|
+
// the machine, so sending one would claim a cancel nothing performs. The
|
|
965
|
+
// caller stops WAITING instead ({@link awaitingHandover}).
|
|
966
|
+
if (this.handover)
|
|
967
|
+
return;
|
|
886
968
|
this.cancelled = true;
|
|
887
969
|
this.socket.emit('execution:interrupt', {
|
|
888
970
|
sessionId: this.options.sessionId,
|
|
@@ -902,6 +984,7 @@ class AgenticStreamClient {
|
|
|
902
984
|
if (this.replayTimer)
|
|
903
985
|
clearTimeout(this.replayTimer);
|
|
904
986
|
this.replayTimer = null;
|
|
987
|
+
this.stopHandoverTimers();
|
|
905
988
|
this.pendingEnvelopes.clear();
|
|
906
989
|
this.abandonConnectWait?.();
|
|
907
990
|
const socket = this.socket;
|
|
@@ -923,6 +1006,189 @@ class AgenticStreamClient {
|
|
|
923
1006
|
this.readline?.close();
|
|
924
1007
|
this.readline = null;
|
|
925
1008
|
}
|
|
1009
|
+
/**
|
|
1010
|
+
* True while a handed-over turn is being waited on (OSK-13543). A Ctrl-C in
|
|
1011
|
+
* this state stops the wait; it cannot cancel the machine's turn.
|
|
1012
|
+
*/
|
|
1013
|
+
awaitingHandover() {
|
|
1014
|
+
return this.handover !== null && !this.settled && !this.closed;
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* OSK-13543 — the run failed because a deploy replaced the server relaying a
|
|
1018
|
+
* turn the machine is still running. That failure is the server's PROVISIONAL
|
|
1019
|
+
* verdict: it keeps the conversation held and rewrites the row from the
|
|
1020
|
+
* machine's own turn-end. Ending here told a CLI user "failed" about a turn
|
|
1021
|
+
* whose answer the web showed ninety seconds later.
|
|
1022
|
+
*
|
|
1023
|
+
* So keep the stream, the user-room subscription and the envelope replay
|
|
1024
|
+
* open, and settle on what the RECORD converges to: repaired → completed,
|
|
1025
|
+
* settled failed → failed with the machine's reason, neither within the
|
|
1026
|
+
* bound → the original failure. The record, not the notification, decides:
|
|
1027
|
+
* a notification only says "read again", so a lost or duplicated one costs
|
|
1028
|
+
* at most a poll interval.
|
|
1029
|
+
*
|
|
1030
|
+
* Returns false (and changes nothing) when the run cannot wait: no hand-over
|
|
1031
|
+
* reader configured, a zero bound, or a run already settling.
|
|
1032
|
+
*/
|
|
1033
|
+
beginHandoverWait(event) {
|
|
1034
|
+
const opts = this.options.awaitHandover;
|
|
1035
|
+
if (!opts || !(opts.maxWaitMs > 0))
|
|
1036
|
+
return false;
|
|
1037
|
+
if (this.settled || this.closed || this.finishingReplay || this.cancelled)
|
|
1038
|
+
return false;
|
|
1039
|
+
if (this.handover)
|
|
1040
|
+
return true;
|
|
1041
|
+
if (this.idleTimer) {
|
|
1042
|
+
clearTimeout(this.idleTimer);
|
|
1043
|
+
this.idleTimer = null;
|
|
1044
|
+
}
|
|
1045
|
+
const startedAt = Date.now();
|
|
1046
|
+
// Deliberately NOT unref'd, for the reason `armIdleProbe` gives: an
|
|
1047
|
+
// unsettled run is a reason to stay alive (OSK-10061).
|
|
1048
|
+
const deadlineTimer = setTimeout(() => void this.expireHandover(), opts.maxWaitMs);
|
|
1049
|
+
const pollTimer = setInterval(() => this.requestHandoverRead(), opts.pollIntervalMs ?? exports.HANDOVER_POLL_INTERVAL_MS);
|
|
1050
|
+
this.handover = { event, startedAt, deadlineTimer, pollTimer, reading: null, readAgain: false };
|
|
1051
|
+
const firstError = Array.isArray(event.errors) && typeof event.errors[0] === 'string'
|
|
1052
|
+
? event.errors[0]
|
|
1053
|
+
: undefined;
|
|
1054
|
+
const message = typeof event.message === 'string' && event.message ? event.message : firstError;
|
|
1055
|
+
this.emit({
|
|
1056
|
+
type: 'run.handed_over',
|
|
1057
|
+
sessionId: this.options.sessionId,
|
|
1058
|
+
code: exports.WORKER_REPLACED_CODE,
|
|
1059
|
+
reason: exports.DAEMON_HANDOVER_REASON,
|
|
1060
|
+
...(message ? { message } : {}),
|
|
1061
|
+
...(opts.responseMessageId ? { responseMessageId: opts.responseMessageId } : {}),
|
|
1062
|
+
waitMs: opts.maxWaitMs,
|
|
1063
|
+
deadlineAt: new Date(startedAt + opts.maxWaitMs).toISOString(),
|
|
1064
|
+
});
|
|
1065
|
+
// The repair can land before the failure notice reaches us (the queue's
|
|
1066
|
+
// backstop records and settles in one pass), so look once right away.
|
|
1067
|
+
this.requestHandoverRead();
|
|
1068
|
+
return true;
|
|
1069
|
+
}
|
|
1070
|
+
/** Ask the record again; coalesces with a read already in flight. */
|
|
1071
|
+
requestHandoverRead() {
|
|
1072
|
+
const handover = this.handover;
|
|
1073
|
+
if (!handover || this.settled || this.closed)
|
|
1074
|
+
return;
|
|
1075
|
+
if (handover.reading) {
|
|
1076
|
+
handover.readAgain = true;
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
handover.reading = this.readHandover().finally(() => {
|
|
1080
|
+
if (this.handover !== handover)
|
|
1081
|
+
return;
|
|
1082
|
+
handover.reading = null;
|
|
1083
|
+
if (handover.readAgain) {
|
|
1084
|
+
handover.readAgain = false;
|
|
1085
|
+
this.requestHandoverRead();
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
/** One read of the record. True when it settled the run. */
|
|
1090
|
+
async readHandover() {
|
|
1091
|
+
const handover = this.handover;
|
|
1092
|
+
const read = this.options.awaitHandover?.read;
|
|
1093
|
+
if (!handover || !read)
|
|
1094
|
+
return false;
|
|
1095
|
+
let reading = null;
|
|
1096
|
+
try {
|
|
1097
|
+
reading = await read();
|
|
1098
|
+
}
|
|
1099
|
+
catch {
|
|
1100
|
+
// A failed read is no evidence about the turn; the next trigger retries.
|
|
1101
|
+
reading = null;
|
|
1102
|
+
}
|
|
1103
|
+
if (this.handover !== handover || this.settled || this.closed)
|
|
1104
|
+
return false;
|
|
1105
|
+
if (!reading || reading.state === 'pending')
|
|
1106
|
+
return false;
|
|
1107
|
+
const waitedMs = Date.now() - handover.startedAt;
|
|
1108
|
+
this.endHandover();
|
|
1109
|
+
if (reading.state === 'repaired') {
|
|
1110
|
+
this.finish('completed', {
|
|
1111
|
+
type: 'execution:complete',
|
|
1112
|
+
sessionId: this.options.sessionId,
|
|
1113
|
+
result: reading.text,
|
|
1114
|
+
repaired: true,
|
|
1115
|
+
repairedFrom: 'durable_record',
|
|
1116
|
+
overturned: { code: exports.WORKER_REPLACED_CODE, reason: exports.DAEMON_HANDOVER_REASON },
|
|
1117
|
+
waitedMs,
|
|
1118
|
+
...(reading.repairedAt ? { repairedAt: reading.repairedAt } : {}),
|
|
1119
|
+
});
|
|
1120
|
+
return true;
|
|
1121
|
+
}
|
|
1122
|
+
// The machine reported its own ending and the server settled the row on
|
|
1123
|
+
// it. That sentence replaces the provisional "still working" one, which is
|
|
1124
|
+
// now false; the original fields stay so a consumer keyed on them still
|
|
1125
|
+
// matches.
|
|
1126
|
+
this.finish('failed', {
|
|
1127
|
+
...handover.event,
|
|
1128
|
+
message: reading.error,
|
|
1129
|
+
errors: [reading.error],
|
|
1130
|
+
...(typeof reading.text === 'string' && reading.text ? { result: reading.text } : {}),
|
|
1131
|
+
handover: { outcome: 'machine_failed', waitedMs },
|
|
1132
|
+
});
|
|
1133
|
+
return true;
|
|
1134
|
+
}
|
|
1135
|
+
/** The bound ran out: one last look, then the original failure. */
|
|
1136
|
+
async expireHandover() {
|
|
1137
|
+
const handover = this.handover;
|
|
1138
|
+
if (!handover || this.settled || this.closed)
|
|
1139
|
+
return;
|
|
1140
|
+
if (handover.pollTimer)
|
|
1141
|
+
clearInterval(handover.pollTimer);
|
|
1142
|
+
handover.pollTimer = null;
|
|
1143
|
+
handover.deadlineTimer = null;
|
|
1144
|
+
if (handover.reading)
|
|
1145
|
+
await handover.reading;
|
|
1146
|
+
if (this.handover !== handover || this.settled || this.closed)
|
|
1147
|
+
return;
|
|
1148
|
+
if (await this.readHandover())
|
|
1149
|
+
return;
|
|
1150
|
+
if (this.handover !== handover || this.settled || this.closed)
|
|
1151
|
+
return;
|
|
1152
|
+
const waitedMs = Date.now() - handover.startedAt;
|
|
1153
|
+
this.endHandover();
|
|
1154
|
+
this.finish('failed', { ...handover.event, handover: { outcome: 'expired', waitedMs } });
|
|
1155
|
+
}
|
|
1156
|
+
stopHandoverTimers() {
|
|
1157
|
+
if (!this.handover)
|
|
1158
|
+
return;
|
|
1159
|
+
if (this.handover.deadlineTimer)
|
|
1160
|
+
clearTimeout(this.handover.deadlineTimer);
|
|
1161
|
+
if (this.handover.pollTimer)
|
|
1162
|
+
clearInterval(this.handover.pollTimer);
|
|
1163
|
+
this.handover.deadlineTimer = null;
|
|
1164
|
+
this.handover.pollTimer = null;
|
|
1165
|
+
}
|
|
1166
|
+
endHandover() {
|
|
1167
|
+
this.stopHandoverTimers();
|
|
1168
|
+
this.handover = null;
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* The server's "this row was rewritten" notice (`lateDeliveryRepair`
|
|
1172
|
+
* `notifyViewersMessageRewritten`), delivered to every socket of the user
|
|
1173
|
+
* through the `user:<id>` room this socket joins on connect. A hint to read,
|
|
1174
|
+
* never a verdict.
|
|
1175
|
+
*/
|
|
1176
|
+
onConversationUpdate(data) {
|
|
1177
|
+
if (!this.handover || !data || data.messagesRepaired !== true)
|
|
1178
|
+
return;
|
|
1179
|
+
const opts = this.options.awaitHandover;
|
|
1180
|
+
const messageId = typeof data.messageId === 'string' ? data.messageId : undefined;
|
|
1181
|
+
const conversationId = typeof data.conversationId === 'string' ? data.conversationId : undefined;
|
|
1182
|
+
// Narrowest identity both sides know. When neither is known, read anyway:
|
|
1183
|
+
// a spare read is cheap and the record decides.
|
|
1184
|
+
let ours = true;
|
|
1185
|
+
if (messageId && opts?.responseMessageId)
|
|
1186
|
+
ours = messageId === opts.responseMessageId;
|
|
1187
|
+
else if (conversationId && opts?.conversationId)
|
|
1188
|
+
ours = conversationId === opts.conversationId;
|
|
1189
|
+
if (ours)
|
|
1190
|
+
this.requestHandoverRead();
|
|
1191
|
+
}
|
|
926
1192
|
debug(line) {
|
|
927
1193
|
try {
|
|
928
1194
|
(this.options.debug ?? defaultStreamDebug)(`session ${this.options.sessionId}: ${line}`);
|
|
@@ -1164,6 +1430,12 @@ class AgenticStreamClient {
|
|
|
1164
1430
|
// seqs and would read as a gap that never happened.
|
|
1165
1431
|
this.noteSeqGap(fresh);
|
|
1166
1432
|
this.noteDaemonToolEnvelopes(fresh);
|
|
1433
|
+
// The machine's own turn-end is what the server repairs or settles the
|
|
1434
|
+
// row on; seeing it relayed is the moment the record is about to move.
|
|
1435
|
+
if (this.handover &&
|
|
1436
|
+
fresh.some((env) => env?.ev?.t === 'turn-end')) {
|
|
1437
|
+
this.requestHandoverRead();
|
|
1438
|
+
}
|
|
1167
1439
|
const envelopeText = envelopesText(scoped);
|
|
1168
1440
|
if (envelopeText) {
|
|
1169
1441
|
this.accumulated += envelopeText;
|
|
@@ -1198,6 +1470,9 @@ class AgenticStreamClient {
|
|
|
1198
1470
|
socket.emit('session:join', this.sessionJoinPayload());
|
|
1199
1471
|
this.emit({ type: 'transport.connected', sessionId: this.options.sessionId });
|
|
1200
1472
|
void this.catchUpEnvelopes();
|
|
1473
|
+
// A repair notice sent while the transport was down is gone for good.
|
|
1474
|
+
if (this.handover)
|
|
1475
|
+
this.requestHandoverRead();
|
|
1201
1476
|
});
|
|
1202
1477
|
socket.on('disconnect', (reason) => {
|
|
1203
1478
|
this.transportDropped = true;
|
|
@@ -1294,22 +1569,42 @@ class AgenticStreamClient {
|
|
|
1294
1569
|
if (this.firstTimeSeen('text.completed', key))
|
|
1295
1570
|
this.emit({ ...event, type: 'text.completed' });
|
|
1296
1571
|
});
|
|
1572
|
+
// While a handed-over turn is waited on, a terminal frame — a repeat of the
|
|
1573
|
+
// hand-over failure through another room, or anything a later writer sends
|
|
1574
|
+
// — is a reason to read the record, not a verdict of its own (OSK-13543).
|
|
1297
1575
|
socket.on('execution:complete', (event) => {
|
|
1298
|
-
if (match(event))
|
|
1576
|
+
if (!match(event))
|
|
1577
|
+
return;
|
|
1578
|
+
if (this.handover)
|
|
1579
|
+
this.requestHandoverRead();
|
|
1580
|
+
else
|
|
1299
1581
|
this.finish('completed', event);
|
|
1300
1582
|
});
|
|
1301
1583
|
socket.on('execution:aborted', (event) => {
|
|
1302
|
-
if (match(event))
|
|
1584
|
+
if (!match(event))
|
|
1585
|
+
return;
|
|
1586
|
+
if (this.handover)
|
|
1587
|
+
this.requestHandoverRead();
|
|
1588
|
+
else
|
|
1303
1589
|
this.finish('aborted', event);
|
|
1304
1590
|
});
|
|
1305
1591
|
socket.on('execution:cancelled', (event) => {
|
|
1306
|
-
if (match(event))
|
|
1592
|
+
if (!match(event))
|
|
1593
|
+
return;
|
|
1594
|
+
if (this.handover)
|
|
1595
|
+
this.requestHandoverRead();
|
|
1596
|
+
else
|
|
1307
1597
|
this.finish('aborted', event);
|
|
1308
1598
|
});
|
|
1309
1599
|
socket.on('execution:error', (event) => {
|
|
1310
|
-
if (match(event))
|
|
1600
|
+
if (!match(event))
|
|
1601
|
+
return;
|
|
1602
|
+
if (this.handover)
|
|
1603
|
+
this.requestHandoverRead();
|
|
1604
|
+
else if (!(isDaemonHandoverFailure(event) && this.beginHandoverWait(event)))
|
|
1311
1605
|
this.finish('failed', event);
|
|
1312
1606
|
});
|
|
1607
|
+
socket.on('conversation:update', (data) => this.onConversationUpdate(data));
|
|
1313
1608
|
socket.on('permission:request', (event) => {
|
|
1314
1609
|
if (!match(event))
|
|
1315
1610
|
return;
|
|
@@ -1598,6 +1893,11 @@ class AgenticStreamClient {
|
|
|
1598
1893
|
// Any event is proof the stream is alive; the watchdog only fires on real
|
|
1599
1894
|
// silence.
|
|
1600
1895
|
this.armIdleProbe();
|
|
1896
|
+
// A handed-over turn can hold the terminal for minutes, so a person is told
|
|
1897
|
+
// why — in every human mode. `--no-stream` reserves stdout for the final
|
|
1898
|
+
// result, so there the line goes to stderr (OSK-13543).
|
|
1899
|
+
if (!this.options.jsonl && this.renderHandoverStatus(event))
|
|
1900
|
+
return;
|
|
1601
1901
|
// Accumulation and `onEvent` still run — only the incremental rendering is
|
|
1602
1902
|
// suppressed, so a quiet run is the same turn with a different display.
|
|
1603
1903
|
if (this.options.quiet)
|
|
@@ -1695,6 +1995,40 @@ class AgenticStreamClient {
|
|
|
1695
1995
|
this.renderedText = this.accumulated;
|
|
1696
1996
|
}
|
|
1697
1997
|
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Human-mode lines for the hand-over wait. Returns true when the event is
|
|
2000
|
+
* fully rendered here; false lets the ordinary rendering continue.
|
|
2001
|
+
*/
|
|
2002
|
+
renderHandoverStatus(event) {
|
|
2003
|
+
const handover = event.handover;
|
|
2004
|
+
const isStatus = event.type === 'run.handed_over' ||
|
|
2005
|
+
(event.type === 'run.completed' && event.repaired === true) ||
|
|
2006
|
+
(event.type === 'run.failed' && !!handover);
|
|
2007
|
+
if (!isStatus)
|
|
2008
|
+
return false;
|
|
2009
|
+
const output = this.options.quiet
|
|
2010
|
+
? (this.options.awaitHandover?.statusOutput ?? process.stderr)
|
|
2011
|
+
: (this.options.output ?? process.stdout);
|
|
2012
|
+
if (event.type === 'run.handed_over') {
|
|
2013
|
+
output.write('\n[machine] A platform update restarted the server relaying this turn, but your ' +
|
|
2014
|
+
'machine is still working on it. Waiting up to ' +
|
|
2015
|
+
`${formatWaitDuration(Number(event.waitMs) || 0)} for its reply ` +
|
|
2016
|
+
'(Ctrl-C stops waiting; it does not stop your machine).\n');
|
|
2017
|
+
this.renderedText = this.accumulated;
|
|
2018
|
+
return true;
|
|
2019
|
+
}
|
|
2020
|
+
if (event.type === 'run.completed') {
|
|
2021
|
+
output.write('[machine] Your machine finished the turn, and its full reply was recovered.\n');
|
|
2022
|
+
return true;
|
|
2023
|
+
}
|
|
2024
|
+
if (handover?.outcome === 'expired') {
|
|
2025
|
+
output.write(`\n[machine] No reply from your machine within ${formatWaitDuration(Number(this.options.awaitHandover?.maxWaitMs) || 0)}. If it finishes later, the reply is saved to the conversation.\n`);
|
|
2026
|
+
}
|
|
2027
|
+
else {
|
|
2028
|
+
output.write('\n[machine] Your machine ended the turn without finishing it.\n');
|
|
2029
|
+
}
|
|
2030
|
+
return this.options.quiet === true;
|
|
2031
|
+
}
|
|
1698
2032
|
finish(status, event) {
|
|
1699
2033
|
if (this.settled || this.finishingReplay)
|
|
1700
2034
|
return;
|
|
@@ -1823,6 +2157,7 @@ class AgenticStreamClient {
|
|
|
1823
2157
|
responseMessageId: this.responseMessageId,
|
|
1824
2158
|
text: this.accumulated,
|
|
1825
2159
|
status,
|
|
2160
|
+
...(status === 'completed' && event.repaired === true ? { repaired: true } : {}),
|
|
1826
2161
|
lastEvent: event,
|
|
1827
2162
|
};
|
|
1828
2163
|
this.resolve(this.terminalResult);
|
|
@@ -136,6 +136,10 @@ export declare function writeToBackend(bundle: AuthBundle, options?: WriteOption
|
|
|
136
136
|
* auth-failure cleanup). Idempotent.
|
|
137
137
|
*/
|
|
138
138
|
export declare function clearAllBackends(): void;
|
|
139
|
+
export declare function takeResolvedKeychainRead(credential: {
|
|
140
|
+
token: string | null;
|
|
141
|
+
source: string;
|
|
142
|
+
}, expectedServerOrigin: string): ReadResult | null;
|
|
139
143
|
/**
|
|
140
144
|
* The low-level backends, exposed for the credential resolver's
|
|
141
145
|
* `readKeychainToken` / `readFileToken` slots. Internal callers should
|