@skrr-ai/cli 0.1.86 → 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/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/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/oclif.manifest.json +28966 -28958
- package/package.json +1 -1
package/bin/run.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// Reuse V8's compiled code for this CLI's modules across runs. Every command
|
|
4
|
+
// compiles oclif plus a 2.4MB vendored data-provider bundle before it can do
|
|
5
|
+
// anything; with the cache that compile step is read back instead (Node >= 22.1;
|
|
6
|
+
// a no-op on older Node and on Bun, and NODE_DISABLE_COMPILE_CACHE=1 turns it
|
|
7
|
+
// off). The cache is keyed by file content, so a stale entry is never used.
|
|
8
|
+
try {
|
|
9
|
+
require('node:module').enableCompileCache?.();
|
|
10
|
+
} catch {
|
|
11
|
+
/* an optimisation must never be the reason a command cannot start */
|
|
12
|
+
}
|
|
13
|
+
|
|
3
14
|
const path = require('node:path');
|
|
4
15
|
const { spawnSync } = require('node:child_process');
|
|
5
16
|
const { findAlternateCli, missingLocalCommandBuild } = require('./dev-fallback');
|
package/dist/base-command.js
CHANGED
|
@@ -512,6 +512,7 @@ class BaseCommand extends core_1.Command {
|
|
|
512
512
|
const fresh = await (0, refresh_1.ensureFreshCliCredential)({
|
|
513
513
|
credential: this.resolvedCredential,
|
|
514
514
|
bareMode: this.bareMode,
|
|
515
|
+
reuseResolvedRead: true,
|
|
515
516
|
});
|
|
516
517
|
if (fresh.credential.token) {
|
|
517
518
|
this.resolvedCredential = fresh.credential;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dataService } from '@skrr-ai/data-provider';
|
|
2
2
|
import { BaseCommand } from '../../base-command';
|
|
3
|
-
import { type AgenticStreamResult, type ReconciledTerminal } from '../../lib/agentic-stream';
|
|
3
|
+
import { type AgenticStreamResult, type HandoverReading, type ReconciledTerminal } from '../../lib/agentic-stream';
|
|
4
4
|
/**
|
|
5
5
|
* Exit code for a run the USER stopped watching, by pressing Ctrl-C.
|
|
6
6
|
*
|
|
@@ -73,6 +73,62 @@ export declare function interruptedDetachMessage(input: {
|
|
|
73
73
|
* because a long tool call is silence, not a failure.
|
|
74
74
|
*/
|
|
75
75
|
export declare function reconcileTerminalFromMessages(messages: ReadonlyArray<Record<string, unknown>>, responseMessageId?: string): ReconciledTerminal | null;
|
|
76
|
+
/**
|
|
77
|
+
* Where a handed-over turn stands, read off its response message (OSK-13543).
|
|
78
|
+
*
|
|
79
|
+
* The server converges such a row to exactly one of two states, and only
|
|
80
|
+
* those two end the wait:
|
|
81
|
+
*
|
|
82
|
+
* - REPAIRED — `error` cleared and `metadata.agentic.deliveryRepair` written,
|
|
83
|
+
* by `sessionMessagePersister.repairDeliveryFailedMessage` on the machine's
|
|
84
|
+
* completed turn-end. The repair record is required, not just `error:
|
|
85
|
+
* false`: before the failure is saved a row can read `unfinished: false,
|
|
86
|
+
* error: false` from a daemon flush that never writes `error`, and calling
|
|
87
|
+
* that a success is the lie `recordHandedOverTurn` exists to prevent.
|
|
88
|
+
* - SETTLED FAILED — still `error: true`, but no longer carrying the
|
|
89
|
+
* hand-over reason (`lateDeliveryRepair.settleHandedOverFailure` moves it to
|
|
90
|
+
* `worker_replaced_daemon_failed` with the machine's own reason).
|
|
91
|
+
*
|
|
92
|
+
* Everything else — still failed with the hand-over reason, not yet written,
|
|
93
|
+
* not found — is `pending`. Pure, so the mapping is testable without a socket.
|
|
94
|
+
*/
|
|
95
|
+
export declare function handoverStateFromMessages(messages: ReadonlyArray<Record<string, unknown>>, responseMessageId?: string): HandoverReading;
|
|
96
|
+
/**
|
|
97
|
+
* How long `agents chat` waits for a handed-over turn by default (OSK-13543).
|
|
98
|
+
*
|
|
99
|
+
* Bounded above by the server's own hold: it quarantines the conversation for
|
|
100
|
+
* the worker job timeout plus a margin (30m + 5m, `conversationAdmissionClaim`)
|
|
101
|
+
* and after that stops treating the turn as possibly running, so waiting longer
|
|
102
|
+
* cannot observe anything the server would still act on. The default is well
|
|
103
|
+
* below that, because the cost of expiring is small and recoverable — the
|
|
104
|
+
* repair still lands on the record and `skrr messages list` shows it — while
|
|
105
|
+
* the cost of holding a terminal or a script is paid every minute. Ten minutes
|
|
106
|
+
* covers the repair observed in production (~90s after the failure) many times
|
|
107
|
+
* over and the long tail of a turn that was mid tool-call when the deploy hit.
|
|
108
|
+
*/
|
|
109
|
+
export declare const DEFAULT_HANDOVER_WAIT_SECONDS = 600;
|
|
110
|
+
/** The server's hold for a handed-over turn: 30m job timeout + 5m lease margin. */
|
|
111
|
+
export declare const MAX_HANDOVER_WAIT_SECONDS: number;
|
|
112
|
+
/**
|
|
113
|
+
* What to say when Ctrl-C stops a hand-over wait. There is nothing to cancel
|
|
114
|
+
* through — the server relaying the turn is gone — so the honest statement is
|
|
115
|
+
* that the CLI stopped watching and the machine may still finish.
|
|
116
|
+
*/
|
|
117
|
+
export declare function handoverInterruptMessage(input: {
|
|
118
|
+
sessionId: string;
|
|
119
|
+
conversationId?: string | null;
|
|
120
|
+
}): string;
|
|
121
|
+
/**
|
|
122
|
+
* What one Ctrl-C does, by state. Exported so the decision is testable
|
|
123
|
+
* without delivering a signal:
|
|
124
|
+
* - waiting on a handed-over turn → stop waiting at once (no cancel exists);
|
|
125
|
+
* - first interrupt of a live run → request a cancel, wait the grace;
|
|
126
|
+
* - any later interrupt → detach now.
|
|
127
|
+
*/
|
|
128
|
+
export declare function interruptAction(input: {
|
|
129
|
+
interrupts: number;
|
|
130
|
+
awaitingHandover: boolean;
|
|
131
|
+
}): 'stop-handover-wait' | 'cancel' | 'detach';
|
|
76
132
|
/**
|
|
77
133
|
* How to look at a turn the CLI is no longer watching.
|
|
78
134
|
*
|
|
@@ -168,6 +224,7 @@ export default class AgentsChat extends BaseCommand {
|
|
|
168
224
|
quick: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
169
225
|
jsonl: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
170
226
|
json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
227
|
+
'repair-wait': import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
171
228
|
'approve-tools': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
172
229
|
'permission-mode': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
173
230
|
'local-skill': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.STREAM_LOST_EXIT_CODE = exports.DETACH_EXIT_CODE = void 0;
|
|
3
|
+
exports.MAX_HANDOVER_WAIT_SECONDS = exports.DEFAULT_HANDOVER_WAIT_SECONDS = exports.STREAM_LOST_EXIT_CODE = exports.DETACH_EXIT_CODE = void 0;
|
|
4
4
|
exports.streamedResultExitCode = streamedResultExitCode;
|
|
5
5
|
exports.interruptedDetachMessage = interruptedDetachMessage;
|
|
6
6
|
exports.reconcileTerminalFromMessages = reconcileTerminalFromMessages;
|
|
7
|
+
exports.handoverStateFromMessages = handoverStateFromMessages;
|
|
8
|
+
exports.handoverInterruptMessage = handoverInterruptMessage;
|
|
9
|
+
exports.interruptAction = interruptAction;
|
|
7
10
|
exports.inspectTurnCommand = inspectTurnCommand;
|
|
8
11
|
exports.noOutputFailureMessage = noOutputFailureMessage;
|
|
9
12
|
exports.noStreamJsonPayload = noStreamJsonPayload;
|
|
@@ -110,6 +113,9 @@ function reconcileTerminalFromMessages(messages, responseMessageId) {
|
|
|
110
113
|
// The reason exists on the row; it simply never reached the surface. Say
|
|
111
114
|
// it, rather than the transport error that masked it (OSK-7595).
|
|
112
115
|
error: failure.reason ?? 'The run failed. No reason was recorded on the message.',
|
|
116
|
+
// Provisional, not final: the server rewrites this row from the
|
|
117
|
+
// machine's own turn-end (OSK-13543).
|
|
118
|
+
...(agenticErrorSubtype(row) === agentic_stream_1.DAEMON_HANDOVER_REASON ? { handedOver: true } : {}),
|
|
113
119
|
};
|
|
114
120
|
}
|
|
115
121
|
const text = typeof row.text === 'string' ? row.text : '';
|
|
@@ -120,6 +126,101 @@ function reconcileTerminalFromMessages(messages, responseMessageId) {
|
|
|
120
126
|
return { status: 'completed', text };
|
|
121
127
|
return null;
|
|
122
128
|
}
|
|
129
|
+
function agenticRecord(row) {
|
|
130
|
+
const metadata = row.metadata;
|
|
131
|
+
if (!metadata || typeof metadata !== 'object')
|
|
132
|
+
return null;
|
|
133
|
+
const agentic = metadata.agentic;
|
|
134
|
+
return agentic && typeof agentic === 'object' ? agentic : null;
|
|
135
|
+
}
|
|
136
|
+
function agenticErrorSubtype(row) {
|
|
137
|
+
const subtype = agenticRecord(row)?.errorSubtype;
|
|
138
|
+
return typeof subtype === 'string' ? subtype : undefined;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Where a handed-over turn stands, read off its response message (OSK-13543).
|
|
142
|
+
*
|
|
143
|
+
* The server converges such a row to exactly one of two states, and only
|
|
144
|
+
* those two end the wait:
|
|
145
|
+
*
|
|
146
|
+
* - REPAIRED — `error` cleared and `metadata.agentic.deliveryRepair` written,
|
|
147
|
+
* by `sessionMessagePersister.repairDeliveryFailedMessage` on the machine's
|
|
148
|
+
* completed turn-end. The repair record is required, not just `error:
|
|
149
|
+
* false`: before the failure is saved a row can read `unfinished: false,
|
|
150
|
+
* error: false` from a daemon flush that never writes `error`, and calling
|
|
151
|
+
* that a success is the lie `recordHandedOverTurn` exists to prevent.
|
|
152
|
+
* - SETTLED FAILED — still `error: true`, but no longer carrying the
|
|
153
|
+
* hand-over reason (`lateDeliveryRepair.settleHandedOverFailure` moves it to
|
|
154
|
+
* `worker_replaced_daemon_failed` with the machine's own reason).
|
|
155
|
+
*
|
|
156
|
+
* Everything else — still failed with the hand-over reason, not yet written,
|
|
157
|
+
* not found — is `pending`. Pure, so the mapping is testable without a socket.
|
|
158
|
+
*/
|
|
159
|
+
function handoverStateFromMessages(messages, responseMessageId) {
|
|
160
|
+
const row = (responseMessageId && messages.find((m) => m?.messageId === responseMessageId)) ||
|
|
161
|
+
(!responseMessageId && [...messages].reverse().find((m) => m && m.isCreatedByUser === false));
|
|
162
|
+
if (!row)
|
|
163
|
+
return { state: 'pending' };
|
|
164
|
+
const failure = (0, data_provider_1.resolveMessageFailure)(row);
|
|
165
|
+
if (failure) {
|
|
166
|
+
if (agenticErrorSubtype(row) === agentic_stream_1.DAEMON_HANDOVER_REASON)
|
|
167
|
+
return { state: 'pending' };
|
|
168
|
+
return {
|
|
169
|
+
state: 'failed',
|
|
170
|
+
error: failure.reason ?? 'The run failed. No reason was recorded on the message.',
|
|
171
|
+
...(failure.answer ? { text: failure.answer } : {}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const repair = agenticRecord(row)?.deliveryRepair;
|
|
175
|
+
if (!repair || typeof repair !== 'object')
|
|
176
|
+
return { state: 'pending' };
|
|
177
|
+
// JSON over the wire makes this an ISO string; anything else is not shown.
|
|
178
|
+
const repairedAt = typeof repair.repairedAt === 'string' ? repair.repairedAt : undefined;
|
|
179
|
+
return {
|
|
180
|
+
state: 'repaired',
|
|
181
|
+
text: typeof row.text === 'string' ? row.text : '',
|
|
182
|
+
...(repairedAt ? { repairedAt } : {}),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* How long `agents chat` waits for a handed-over turn by default (OSK-13543).
|
|
187
|
+
*
|
|
188
|
+
* Bounded above by the server's own hold: it quarantines the conversation for
|
|
189
|
+
* the worker job timeout plus a margin (30m + 5m, `conversationAdmissionClaim`)
|
|
190
|
+
* and after that stops treating the turn as possibly running, so waiting longer
|
|
191
|
+
* cannot observe anything the server would still act on. The default is well
|
|
192
|
+
* below that, because the cost of expiring is small and recoverable — the
|
|
193
|
+
* repair still lands on the record and `skrr messages list` shows it — while
|
|
194
|
+
* the cost of holding a terminal or a script is paid every minute. Ten minutes
|
|
195
|
+
* covers the repair observed in production (~90s after the failure) many times
|
|
196
|
+
* over and the long tail of a turn that was mid tool-call when the deploy hit.
|
|
197
|
+
*/
|
|
198
|
+
exports.DEFAULT_HANDOVER_WAIT_SECONDS = 600;
|
|
199
|
+
/** The server's hold for a handed-over turn: 30m job timeout + 5m lease margin. */
|
|
200
|
+
exports.MAX_HANDOVER_WAIT_SECONDS = 35 * 60;
|
|
201
|
+
/**
|
|
202
|
+
* What to say when Ctrl-C stops a hand-over wait. There is nothing to cancel
|
|
203
|
+
* through — the server relaying the turn is gone — so the honest statement is
|
|
204
|
+
* that the CLI stopped watching and the machine may still finish.
|
|
205
|
+
*/
|
|
206
|
+
function handoverInterruptMessage(input) {
|
|
207
|
+
return (`Stopped waiting for run ${input.sessionId}. A platform update had restarted the server ` +
|
|
208
|
+
'relaying it, and your machine MAY STILL BE WORKING on it: if it finishes, its reply is ' +
|
|
209
|
+
'saved to the conversation. Nothing was cancelled. ' +
|
|
210
|
+
`Check with: ${inspectTurnCommand(input.conversationId)}`);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* What one Ctrl-C does, by state. Exported so the decision is testable
|
|
214
|
+
* without delivering a signal:
|
|
215
|
+
* - waiting on a handed-over turn → stop waiting at once (no cancel exists);
|
|
216
|
+
* - first interrupt of a live run → request a cancel, wait the grace;
|
|
217
|
+
* - any later interrupt → detach now.
|
|
218
|
+
*/
|
|
219
|
+
function interruptAction(input) {
|
|
220
|
+
if (input.awaitingHandover)
|
|
221
|
+
return 'stop-handover-wait';
|
|
222
|
+
return input.interrupts === 1 ? 'cancel' : 'detach';
|
|
223
|
+
}
|
|
123
224
|
/**
|
|
124
225
|
* How to look at a turn the CLI is no longer watching.
|
|
125
226
|
*
|
|
@@ -158,6 +259,9 @@ function noStreamJsonPayload(input) {
|
|
|
158
259
|
conversationId,
|
|
159
260
|
responseMessageId: result.responseMessageId,
|
|
160
261
|
status: result.status,
|
|
262
|
+
// The run first failed as handed over and the machine's reply was then
|
|
263
|
+
// repaired onto the record (OSK-13543).
|
|
264
|
+
...(result.repaired ? { repaired: true } : {}),
|
|
161
265
|
response: result.text,
|
|
162
266
|
// The model a local harness reported running (OSK-12111). Absent on a
|
|
163
267
|
// cloud turn and on a harness that reports none.
|
|
@@ -279,11 +383,30 @@ class AgentsChat extends base_command_1.BaseCommand {
|
|
|
279
383
|
'conversation; `message` says why. A `permission.resolved` says ' +
|
|
280
384
|
'how a local harness permission request was decided: `decidedBy` in words, and ' +
|
|
281
385
|
'`basis` — `person` (with `surface`), `mode` (with `mode`), `policy` or ' +
|
|
282
|
-
'`session-grant` — beside the wire `by`.'
|
|
386
|
+
'`session-grant` — beside the wire `by`. When a platform update restarts the server ' +
|
|
387
|
+
'relaying a turn your machine is still running, the run does not end at that failure: ' +
|
|
388
|
+
'`run.handed_over` (`code: "AGENT_WORKER_REPLACED"`, `reason: ' +
|
|
389
|
+
'"worker_replaced_daemon_running"`, `waitMs`, `deadlineAt`) says the CLI is waiting, ' +
|
|
390
|
+
'then exactly one terminal event follows — `run.completed` with `repaired: true` and ' +
|
|
391
|
+
'`result` (then `run.result` with `status: "completed"`, `repaired: true`, exit 0) when ' +
|
|
392
|
+
'the machine finished; `run.failed` with `handover.outcome: "machine_failed"` and the ' +
|
|
393
|
+
'machine\'s reason when it did not; or `run.failed` with `handover.outcome: "expired"` ' +
|
|
394
|
+
'and the original failure when --repair-wait ran out (exit 1 for both). Ctrl-C during ' +
|
|
395
|
+
'the wait emits `run.detached` with `handedOver: true` and exits 130.',
|
|
283
396
|
}),
|
|
284
397
|
json: core_1.Flags.boolean({
|
|
285
398
|
description: 'Emit the final one-shot response as JSON (use with --no-stream)',
|
|
286
399
|
}),
|
|
400
|
+
'repair-wait': core_1.Flags.integer({
|
|
401
|
+
description: 'Seconds to keep waiting when a platform update restarts the server relaying a turn ' +
|
|
402
|
+
'your machine is still running. The server repairs the reply onto the conversation ' +
|
|
403
|
+
'when the machine finishes, and the CLI prints it and exits 0; if the wait runs out ' +
|
|
404
|
+
'the original failure is reported. 0 ends at the failure immediately. At most the ' +
|
|
405
|
+
`server's own hold for such a turn (${exports.MAX_HANDOVER_WAIT_SECONDS}s).`,
|
|
406
|
+
default: exports.DEFAULT_HANDOVER_WAIT_SECONDS,
|
|
407
|
+
min: 0,
|
|
408
|
+
max: exports.MAX_HANDOVER_WAIT_SECONDS,
|
|
409
|
+
}),
|
|
287
410
|
'approve-tools': core_1.Flags.boolean({
|
|
288
411
|
description: 'Allow this turn to use permission-gated tools. In a terminal it asks per call; ' +
|
|
289
412
|
'with no terminal the flag itself is the approval for this turn, and each one is ' +
|
|
@@ -409,6 +532,15 @@ class AgentsChat extends base_command_1.BaseCommand {
|
|
|
409
532
|
if (flags.conversation && flags['thread-root']) {
|
|
410
533
|
this.error('--conversation and --thread-root are mutually exclusive.', { exit: 2 });
|
|
411
534
|
}
|
|
535
|
+
// The agent read that picks the turn's default workspace depends on nothing
|
|
536
|
+
// below, so it starts now and runs beside the `--thread-root`, `--task` and
|
|
537
|
+
// `--local-skill` reads instead of after them (each is a full round trip).
|
|
538
|
+
// Only the LOOKUP starts here: the default is committed just before the
|
|
539
|
+
// turn, so those reads still go out without a workspace header, as before.
|
|
540
|
+
// `--quick` never consulted it. Never rejects.
|
|
541
|
+
const homeWorkspace = flags.quick
|
|
542
|
+
? null
|
|
543
|
+
: (0, agent_home_workspace_1.lookupAgentHomeWorkspaceDefault)(args.agentId, (agentId) => data_provider_1.dataService.getAgentById({ agent_id: agentId }));
|
|
412
544
|
let threadConversationId = flags.conversation;
|
|
413
545
|
if (flags['thread-root']) {
|
|
414
546
|
try {
|
|
@@ -522,7 +654,7 @@ class AgentsChat extends base_command_1.BaseCommand {
|
|
|
522
654
|
const prompt = localSkills
|
|
523
655
|
? (0, local_skills_1.withSkillInvocations)(flags.prompt, localSkills.refs)
|
|
524
656
|
: flags.prompt;
|
|
525
|
-
|
|
657
|
+
(0, agent_home_workspace_1.commitAgentHomeWorkspaceDefault)(await homeWorkspace);
|
|
526
658
|
let started;
|
|
527
659
|
try {
|
|
528
660
|
started = await data_provider_1.dataService.startAgenticChat({
|
|
@@ -593,6 +725,21 @@ class AgentsChat extends base_command_1.BaseCommand {
|
|
|
593
725
|
return reconcileTerminalFromMessages(messages, started.responseMessageId);
|
|
594
726
|
}
|
|
595
727
|
: undefined;
|
|
728
|
+
// OSK-13543 — a turn handed over on a deploy is waited out against the same
|
|
729
|
+
// record. Without a conversation there is no record to read, so no wait.
|
|
730
|
+
const awaitHandover = conversationId
|
|
731
|
+
? {
|
|
732
|
+
read: async () => {
|
|
733
|
+
const messages = (await data_provider_1.dataService.getMessagesByConvoId(conversationId));
|
|
734
|
+
if (!Array.isArray(messages))
|
|
735
|
+
return null;
|
|
736
|
+
return handoverStateFromMessages(messages, started.responseMessageId);
|
|
737
|
+
},
|
|
738
|
+
responseMessageId: started.responseMessageId,
|
|
739
|
+
conversationId,
|
|
740
|
+
maxWaitMs: (flags['repair-wait'] ?? exports.DEFAULT_HANDOVER_WAIT_SECONDS) * 1000,
|
|
741
|
+
}
|
|
742
|
+
: undefined;
|
|
596
743
|
const client = new agentic_stream_1.AgenticStreamClient({
|
|
597
744
|
baseURL: this.cliConfig.baseURL,
|
|
598
745
|
token: this.resolvedCredential.token,
|
|
@@ -602,6 +749,7 @@ class AgentsChat extends base_command_1.BaseCommand {
|
|
|
602
749
|
quiet,
|
|
603
750
|
approveTools: flags['approve-tools'],
|
|
604
751
|
reconcileTerminal,
|
|
752
|
+
awaitHandover,
|
|
605
753
|
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) }),
|
|
606
754
|
});
|
|
607
755
|
// OSK-2938 — Ctrl-C must be bounded, visible, and honest about what it
|
|
@@ -636,7 +784,35 @@ class AgentsChat extends base_command_1.BaseCommand {
|
|
|
636
784
|
let graceTimer = null;
|
|
637
785
|
const onInterrupt = () => {
|
|
638
786
|
interrupts += 1;
|
|
639
|
-
|
|
787
|
+
const action = interruptAction({ interrupts, awaitingHandover: client.awaitingHandover() });
|
|
788
|
+
if (action === 'stop-handover-wait') {
|
|
789
|
+
// Nothing to cancel and nothing to grace-wait for: stop at once.
|
|
790
|
+
if (!forcedDetachAnnounced) {
|
|
791
|
+
forcedDetachAnnounced = true;
|
|
792
|
+
const message = handoverInterruptMessage({
|
|
793
|
+
sessionId: started.sessionId,
|
|
794
|
+
conversationId: started.conversationId,
|
|
795
|
+
});
|
|
796
|
+
if (flags.jsonl) {
|
|
797
|
+
(0, node_fs_1.writeSync)(process.stdout.fd, `${JSON.stringify({
|
|
798
|
+
type: 'run.detached',
|
|
799
|
+
sessionId: started.sessionId,
|
|
800
|
+
conversationId: started.conversationId,
|
|
801
|
+
interrupted: true,
|
|
802
|
+
handedOver: true,
|
|
803
|
+
message,
|
|
804
|
+
})}\n`);
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
(0, node_fs_1.writeSync)(process.stderr.fd, `\n${message}\n`);
|
|
808
|
+
}
|
|
809
|
+
process.exitCode = exports.DETACH_EXIT_CODE;
|
|
810
|
+
}
|
|
811
|
+
client.close();
|
|
812
|
+
detach?.();
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (action === 'cancel') {
|
|
640
816
|
client.cancel();
|
|
641
817
|
if (!flags.jsonl) {
|
|
642
818
|
this.log(`\nCancelling run ${started.sessionId}… (waiting up to ${CANCEL_GRACE_MS / 1000}s; ` +
|
|
@@ -17,3 +17,22 @@
|
|
|
17
17
|
export declare function applyAgentHomeWorkspaceDefault(agentId: string, lookup: (agentId: string) => Promise<{
|
|
18
18
|
workspaceId?: string | null;
|
|
19
19
|
} | null | undefined>, env?: NodeJS.ProcessEnv): Promise<string | null>;
|
|
20
|
+
/**
|
|
21
|
+
* The read half of `applyAgentHomeWorkspaceDefault`: the workspace a turn
|
|
22
|
+
* would default to, WITHOUT setting it.
|
|
23
|
+
*
|
|
24
|
+
* Split so `agents chat` can start the agent read at the same time as its
|
|
25
|
+
* other pre-flight reads (`--task`, `--thread-root`, `--local-skill`) instead
|
|
26
|
+
* of after them — each is a full round trip — while those reads still go out
|
|
27
|
+
* exactly as before, with no workspace header. The default is committed only
|
|
28
|
+
* right before the turn is started.
|
|
29
|
+
*/
|
|
30
|
+
export declare function lookupAgentHomeWorkspaceDefault(agentId: string, lookup: (agentId: string) => Promise<{
|
|
31
|
+
workspaceId?: string | null;
|
|
32
|
+
} | null | undefined>, env?: NodeJS.ProcessEnv): Promise<string | null>;
|
|
33
|
+
/**
|
|
34
|
+
* The write half: apply a looked-up default, unless something chose a
|
|
35
|
+
* workspace in the meantime (that choice still wins) or this is an autonomous
|
|
36
|
+
* Agent process.
|
|
37
|
+
*/
|
|
38
|
+
export declare function commitAgentHomeWorkspaceDefault(workspaceId: string | null, env?: NodeJS.ProcessEnv): string | null;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.applyAgentHomeWorkspaceDefault = applyAgentHomeWorkspaceDefault;
|
|
4
|
+
exports.lookupAgentHomeWorkspaceDefault = lookupAgentHomeWorkspaceDefault;
|
|
5
|
+
exports.commitAgentHomeWorkspaceDefault = commitAgentHomeWorkspaceDefault;
|
|
4
6
|
/**
|
|
5
7
|
* The workspace an `agents chat` turn runs in when the caller named none.
|
|
6
8
|
*
|
|
@@ -18,6 +20,19 @@ exports.applyAgentHomeWorkspaceDefault = applyAgentHomeWorkspaceDefault;
|
|
|
18
20
|
* daemon session.
|
|
19
21
|
*/
|
|
20
22
|
async function applyAgentHomeWorkspaceDefault(agentId, lookup, env = process.env) {
|
|
23
|
+
return commitAgentHomeWorkspaceDefault(await lookupAgentHomeWorkspaceDefault(agentId, lookup, env), env);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The read half of `applyAgentHomeWorkspaceDefault`: the workspace a turn
|
|
27
|
+
* would default to, WITHOUT setting it.
|
|
28
|
+
*
|
|
29
|
+
* Split so `agents chat` can start the agent read at the same time as its
|
|
30
|
+
* other pre-flight reads (`--task`, `--thread-root`, `--local-skill`) instead
|
|
31
|
+
* of after them — each is a full round trip — while those reads still go out
|
|
32
|
+
* exactly as before, with no workspace header. The default is committed only
|
|
33
|
+
* right before the turn is started.
|
|
34
|
+
*/
|
|
35
|
+
async function lookupAgentHomeWorkspaceDefault(agentId, lookup, env = process.env) {
|
|
21
36
|
if (env.OVERSKY_AUTONOMOUS_SESSION === '1')
|
|
22
37
|
return null;
|
|
23
38
|
if (env.OVERSKY_WORKSPACE_ID?.trim())
|
|
@@ -32,8 +47,20 @@ async function applyAgentHomeWorkspaceDefault(agentId, lookup, env = process.env
|
|
|
32
47
|
return null;
|
|
33
48
|
}
|
|
34
49
|
const workspaceId = typeof agent?.workspaceId === 'string' ? agent.workspaceId.trim() : '';
|
|
50
|
+
return workspaceId || null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The write half: apply a looked-up default, unless something chose a
|
|
54
|
+
* workspace in the meantime (that choice still wins) or this is an autonomous
|
|
55
|
+
* Agent process.
|
|
56
|
+
*/
|
|
57
|
+
function commitAgentHomeWorkspaceDefault(workspaceId, env = process.env) {
|
|
35
58
|
if (!workspaceId)
|
|
36
59
|
return null;
|
|
60
|
+
if (env.OVERSKY_AUTONOMOUS_SESSION === '1')
|
|
61
|
+
return null;
|
|
62
|
+
if (env.OVERSKY_WORKSPACE_ID?.trim())
|
|
63
|
+
return null;
|
|
37
64
|
env.OVERSKY_WORKSPACE_ID = workspaceId;
|
|
38
65
|
return workspaceId;
|
|
39
66
|
}
|
|
@@ -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
|
/**
|