@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 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');
@@ -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
- await (0, agent_home_workspace_1.applyAgentHomeWorkspaceDefault)(args.agentId, (agentId) => data_provider_1.dataService.getAgentById({ agent_id: agentId }));
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
- if (interrupts === 1) {
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; ` +
@@ -40,6 +40,12 @@ class AgentsEndpointEnable extends base_command_1.BaseCommand {
40
40
  }
41
41
  (0, agent_endpoints_1.printAgentEndpoint)(this, endpoint);
42
42
  this.log('');
43
+ if (!(0, agent_endpoints_1.endpointWillRun)(endpoint)) {
44
+ // On is not the same as runnable: say so rather than end on a connect
45
+ // line that leads to a refused first call (OSK-13521).
46
+ this.log(endpoint.notice ||
47
+ `The endpoint is on, but it will not run a turn until ${endpoint.blocked_by?.code ?? 'its blocker'} clears.`);
48
+ }
43
49
  this.log('Connect a client with: skrr agents endpoint connect ' + args.id + ' --space <space-id>');
44
50
  }
45
51
  }
@@ -66,6 +66,9 @@ class StoreAcquisitionsApprove extends base_command_1.BaseCommand {
66
66
  exit: 1,
67
67
  });
68
68
  }
69
+ if (request.invalidReason === 'buyer_terms_missing') {
70
+ this.error(`This request was recorded without buyer terms, so it cannot be approved. Decline it with \`skrr store acquisitions decline ${request.id}\`; the agent's next attempt opens a new request that carries the terms.`, { exit: 1 });
71
+ }
69
72
  if (!request.buyerTerms) {
70
73
  this.error('Buyer terms are not configured, so this purchase cannot be approved.', {
71
74
  exit: 1,
@@ -23,11 +23,14 @@ export interface AgentEndpoint {
23
23
  enabled_at?: string | null;
24
24
  switchable: boolean;
25
25
  ready: boolean;
26
- /** The gate that would refuse a turn right now (see docs/cli/SKRR_AGENT_ENDPOINTS.md). */
27
- blocked_by: {
28
- code: string;
29
- message: string;
30
- } | null;
26
+ /**
27
+ * The gate that would refuse a turn right now (see docs/cli/SKRR_AGENT_ENDPOINTS.md).
28
+ * Includes the agent-worker runtime admission endpoint turns run on — e.g.
29
+ * `RUNTIME_METERING_BACKLOG_BLOCKED` — with the next step in `remedy`.
30
+ */
31
+ blocked_by: AgentEndpointBlocker | null;
32
+ /** Set by `enable` when the endpoint is on but a turn would still be refused. */
33
+ notice?: string;
31
34
  release?: {
32
35
  id: string;
33
36
  version_label: string | null;
@@ -44,6 +47,12 @@ export interface AgentEndpoint {
44
47
  };
45
48
  workspace_id?: string | null;
46
49
  }
50
+ export interface AgentEndpointBlocker {
51
+ code: string;
52
+ message: string;
53
+ remedy?: string | null;
54
+ details?: Record<string, unknown>;
55
+ }
47
56
  export declare function getAgentEndpoint(agentId: string): Promise<AgentEndpoint>;
48
57
  /** Turn a Store instance's endpoint on or off. Owner only, enforced server-side. */
49
58
  export declare function setAgentEndpointEnabled(agentId: string, enabled: boolean): Promise<AgentEndpoint>;
@@ -56,6 +65,11 @@ export declare const AGENT_ENDPOINT_SCOPE = "agents:invoke";
56
65
  export declare function mcpClientCommand(endpoint: AgentEndpoint, secret: string): string;
57
66
  /** One human line for the state an endpoint is in. */
58
67
  export declare function describeEndpointState(endpoint: AgentEndpoint): string;
68
+ /**
69
+ * Whether the endpoint would run a turn now. `ready` alone is the answer; this
70
+ * exists so a caller never reads `enabled` as "it works" (OSK-13521).
71
+ */
72
+ export declare function endpointWillRun(endpoint: AgentEndpoint): boolean;
59
73
  /** The human rendering every `agents endpoint` command prints. */
60
74
  export declare function printAgentEndpoint(cmd: {
61
75
  log: (line: string) => void;
@@ -5,6 +5,7 @@ exports.getAgentEndpoint = getAgentEndpoint;
5
5
  exports.setAgentEndpointEnabled = setAgentEndpointEnabled;
6
6
  exports.mcpClientCommand = mcpClientCommand;
7
7
  exports.describeEndpointState = describeEndpointState;
8
+ exports.endpointWillRun = endpointWillRun;
8
9
  exports.printAgentEndpoint = printAgentEndpoint;
9
10
  const api_fetch_1 = require("./api-fetch");
10
11
  async function getAgentEndpoint(agentId) {
@@ -40,11 +41,21 @@ function describeEndpointState(endpoint) {
40
41
  }
41
42
  return 'on';
42
43
  }
44
+ /**
45
+ * Whether the endpoint would run a turn now. `ready` alone is the answer; this
46
+ * exists so a caller never reads `enabled` as "it works" (OSK-13521).
47
+ */
48
+ function endpointWillRun(endpoint) {
49
+ return endpoint.enabled && endpoint.ready && !endpoint.blocked_by;
50
+ }
43
51
  /** The human rendering every `agents endpoint` command prints. */
44
52
  function printAgentEndpoint(cmd, endpoint) {
45
53
  cmd.log(`Agent ${endpoint.agent_id}${endpoint.agent_name ? ` (${endpoint.agent_name})` : ''}`);
46
54
  cmd.log(`Kind ${endpoint.kind.replace(/_/g, ' ')}`);
47
55
  cmd.log(`Endpoint ${describeEndpointState(endpoint)}`);
56
+ if (endpoint.blocked_by?.remedy) {
57
+ cmd.log(`Next step ${endpoint.blocked_by.remedy}`);
58
+ }
48
59
  if (endpoint.url) {
49
60
  cmd.log(`URL ${endpoint.url} (MCP, ${endpoint.transport})`);
50
61
  cmd.log(`Key scope ${endpoint.scope}`);
@@ -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
  }