@skrr-ai/cli 0.1.122 → 0.1.123

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.
Files changed (54) hide show
  1. package/dist/commands/agents/chat.d.ts +7 -0
  2. package/dist/commands/agents/chat.js +32 -6
  3. package/dist/commands/followups/act.js +3 -0
  4. package/dist/commands/followups/checkin.js +3 -0
  5. package/dist/commands/followups/remind.js +3 -0
  6. package/dist/commands/followups/show.js +1 -1
  7. package/dist/commands/followups/watch.js +3 -0
  8. package/dist/lib/agentic-stream.d.ts +46 -0
  9. package/dist/lib/agentic-stream.js +135 -14
  10. package/dist/lib/auth-failure.js +12 -0
  11. package/dist/lib/exec-runtime-binary.d.ts +9 -0
  12. package/dist/lib/exec-runtime-binary.js +15 -1
  13. package/dist/lib/followups.d.ts +13 -0
  14. package/dist/lib/followups.js +8 -0
  15. package/dist/lib/profile-machine-identity-guard.js +2 -1
  16. package/dist/lib/runtime-start-verification.js +6 -1
  17. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +58 -1
  18. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +2 -0
  19. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +60 -0
  20. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -2
  21. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +8 -0
  22. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.d.ts +1 -0
  23. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.js +7 -0
  24. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.js +3 -2
  25. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.js +3 -2
  26. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.d.ts +29 -0
  27. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.js +37 -0
  28. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.d.ts +1 -1
  29. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +1 -0
  30. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +12 -6
  31. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.d.ts +3 -0
  32. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.js +41 -3
  33. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/types.d.ts +7 -1
  34. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +109 -3
  35. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +2 -0
  36. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +78 -0
  37. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -2
  38. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -2
  39. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.d.ts +1 -0
  40. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.js +1 -0
  41. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.js +3 -2
  42. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.js +3 -2
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.d.ts +29 -0
  44. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.js +38 -0
  45. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.d.ts +1 -1
  46. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +1 -0
  47. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +31 -6
  48. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.d.ts +3 -0
  49. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.js +40 -3
  50. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/types.d.ts +7 -1
  51. package/dist/node_modules/@skrr-ai/auth-core/package.json +10 -0
  52. package/dist/node_modules/@skrr-ai/data-provider/index.js +7330 -7324
  53. package/oclif.manifest.json +11 -2
  54. package/package.json +1 -1
@@ -155,6 +155,12 @@ export declare function noOutputFailureMessage(input: {
155
155
  text?: string;
156
156
  conversationId?: string | null;
157
157
  }): string | null;
158
+ /**
159
+ * What a failed run says for itself. The reason is whatever the terminal event
160
+ * carries (see `terminalFailureMessage`); only a failure that carries none gets
161
+ * the generic sentence.
162
+ */
163
+ export declare function agentRunFailureMessage(result: Pick<AgenticStreamResult, 'lastEvent'>): string;
158
164
  export declare function noStreamJsonPayload(input: {
159
165
  result: AgenticStreamResult;
160
166
  conversationId?: string;
@@ -222,6 +228,7 @@ export default class AgentsChat extends BaseCommand {
222
228
  'thread-root': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
223
229
  'no-stream': import("@oclif/core/interfaces").BooleanFlag<boolean>;
224
230
  quick: import("@oclif/core/interfaces").BooleanFlag<boolean>;
231
+ incremental: import("@oclif/core/interfaces").BooleanFlag<boolean>;
225
232
  jsonl: import("@oclif/core/interfaces").BooleanFlag<boolean>;
226
233
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
227
234
  'repair-wait': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
@@ -9,6 +9,7 @@ exports.handoverInterruptMessage = handoverInterruptMessage;
9
9
  exports.interruptAction = interruptAction;
10
10
  exports.inspectTurnCommand = inspectTurnCommand;
11
11
  exports.noOutputFailureMessage = noOutputFailureMessage;
12
+ exports.agentRunFailureMessage = agentRunFailureMessage;
12
13
  exports.noStreamJsonPayload = noStreamJsonPayload;
13
14
  exports.chatStreamHeaderLines = chatStreamHeaderLines;
14
15
  exports.daemonPinFields = daemonPinFields;
@@ -259,6 +260,14 @@ function noOutputFailureMessage(input) {
259
260
  return (`The turn ended with no output (status: ${input.status}). ` +
260
261
  `It may have failed on the runtime. Inspect it with: ${inspectTurnCommand(input.conversationId)}`);
261
262
  }
263
+ /**
264
+ * What a failed run says for itself. The reason is whatever the terminal event
265
+ * carries (see `terminalFailureMessage`); only a failure that carries none gets
266
+ * the generic sentence.
267
+ */
268
+ function agentRunFailureMessage(result) {
269
+ return (0, agentic_stream_1.terminalFailureMessage)(result.lastEvent) ?? 'Agent run failed';
270
+ }
262
271
  function noStreamJsonPayload(input) {
263
272
  const { result, conversationId, agentId, baseURL, threadRootMessageId } = input;
264
273
  return {
@@ -281,9 +290,9 @@ function noStreamJsonPayload(input) {
281
290
  ? {
282
291
  failureReason: result.lastEvent?.reason ??
283
292
  result.lastEvent?.code ??
284
- result.lastEvent?.message ??
293
+ (0, agentic_stream_1.terminalFailureMessage)(result.lastEvent) ??
285
294
  'agent_run_failed',
286
- error: result.lastEvent?.message ?? 'Agent run failed',
295
+ error: agentRunFailureMessage(result),
287
296
  terminalEvent: result.lastEvent,
288
297
  }
289
298
  : {}),
@@ -370,10 +379,20 @@ class AgentsChat extends base_command_1.BaseCommand {
370
379
  'agent’s runtime, tools, session, or working directory and cannot perform work — ' +
371
380
  'it is for inline text assistance. Use --no-stream for a real turn.',
372
381
  }),
382
+ incremental: core_1.Flags.boolean({
383
+ description: 'With --jsonl, make `text.delta` a real delta: it carries only `delta`, the characters ' +
384
+ 'it adds, instead of the whole answer so far, and leaves out the envelopes that ' +
385
+ '`session.output` already carries. A rewrite that is not an append arrives as ' +
386
+ '`replace`, the new full text. `run.result` still holds the whole answer. Without it ' +
387
+ 'a long answer written as JSON lines grows with the square of its length.',
388
+ dependsOn: ['jsonl'],
389
+ }),
373
390
  jsonl: core_1.Flags.boolean({
374
391
  description: 'Emit one JSON event per line while streaming. Every line is a complete JSON value. ' +
375
392
  'Assistant text arrives as `text.delta` whose `text` is the CUMULATIVE answer so far, ' +
376
- 'not a delta — take the last one, or `run.result` at the end. The same text is ' +
393
+ 'not a delta — take the last one, or `run.result` at the end. Each `text.delta` also ' +
394
+ 'carries `delta`, the characters it adds, when it only appends to the one before; ' +
395
+ 'with --incremental it carries nothing else of the text (see that flag). The same text is ' +
377
396
  'mirrored inside `session.output` envelopes for consumers that want the raw stream, ' +
378
397
  'so folding both doubles the answer: pick ONE lane. A widget the agent wrote ' +
379
398
  '(a block of CSS, HTML and JavaScript the apps draw) is its text description in ' +
@@ -392,7 +411,11 @@ class AgentsChat extends base_command_1.BaseCommand {
392
411
  'and local-harness runs alike. A `session.notice` with `notice: ' +
393
412
  '"harness-session-reset"` means the local harness could not resume its previous ' +
394
413
  'session and started a new one, which does not see earlier turns of the ' +
395
- 'conversation; `message` says why. A `permission.resolved` says ' +
414
+ 'conversation; `message` says why. `notice: "model-request-retrying"` means the ' +
415
+ 'model rejected or could not answer a request and the harness is retrying it: ' +
416
+ '`message` names the cause (for example the model rejecting the credentials on ' +
417
+ 'this machine, HTTP 403) and the attempt, a few times per retry episode, so a ' +
418
+ 'run that looks quiet is not. A `permission.resolved` says ' +
396
419
  'how a local harness permission request was decided: `decidedBy` in words, and ' +
397
420
  '`basis` — `person` (with `surface`), `mode` (with `mode`), `policy` or ' +
398
421
  '`session-grant` — beside the wire `by`. When a platform update restarts the server ' +
@@ -404,7 +427,9 @@ class AgentsChat extends base_command_1.BaseCommand {
404
427
  'the machine finished; `run.failed` with `handover.outcome: "machine_failed"` and the ' +
405
428
  'machine\'s reason when it did not; or `run.failed` with `handover.outcome: "expired"` ' +
406
429
  'and the original failure when --repair-wait ran out (exit 1 for both). Ctrl-C during ' +
407
- 'the wait emits `run.detached` with `handedOver: true` and exits 130.',
430
+ 'the wait emits `run.detached` with `handedOver: true` and exits 130. Whenever a run ' +
431
+ 'fails, `run.failed` carries `message`, the reason in words as the platform gave it (the ' +
432
+ 'same sentence a terminal prints after `Error:`), beside the short `reason`.',
408
433
  }),
409
434
  json: core_1.Flags.boolean({
410
435
  description: 'Emit the final one-shot response as JSON (use with --no-stream)',
@@ -788,6 +813,7 @@ class AgentsChat extends base_command_1.BaseCommand {
788
813
  sessionId: started.sessionId,
789
814
  conversationId,
790
815
  jsonl: flags.jsonl,
816
+ incrementalText: flags.incremental,
791
817
  quiet,
792
818
  approveTools: flags['approve-tools'],
793
819
  reconcileTerminal,
@@ -966,7 +992,7 @@ class AgentsChat extends base_command_1.BaseCommand {
966
992
  process.exitCode = 1;
967
993
  }
968
994
  else if (result.status === 'failed') {
969
- this.error(String(result.lastEvent?.message ?? 'Agent run failed'), { exit: 1 });
995
+ this.error(agentRunFailureMessage(result), { exit: 1 });
970
996
  }
971
997
  else if (quiet) {
972
998
  // Non-streaming human output: the answer the turn produced.
@@ -100,6 +100,9 @@ class FollowupsAct extends base_command_1.BaseCommand {
100
100
  this.log(JSON.stringify(response, null, 2));
101
101
  return;
102
102
  }
103
+ const producerNotice = (0, followups_1.producerDowngradeNotice)(response);
104
+ if (producerNotice)
105
+ this.log(producerNotice);
103
106
  const row = response.followup;
104
107
  if (response.reused) {
105
108
  this.log(`Already armed: ${row?.id} — the same act exists and was not duplicated.`);
@@ -126,6 +126,9 @@ class FollowupsCheckin extends base_command_1.BaseCommand {
126
126
  this.log(JSON.stringify(response, null, 2));
127
127
  return;
128
128
  }
129
+ const producerNotice = (0, followups_1.producerDowngradeNotice)(response);
130
+ if (producerNotice)
131
+ this.log(producerNotice);
129
132
  const row = response.followup;
130
133
  if (response.reused) {
131
134
  this.log(reusedMessage(row, subject, flags.why));
@@ -102,6 +102,9 @@ class FollowupsRemind extends base_command_1.BaseCommand {
102
102
  this.log(JSON.stringify(response, null, 2));
103
103
  return;
104
104
  }
105
+ const producerNotice = (0, followups_1.producerDowngradeNotice)(response);
106
+ if (producerNotice)
107
+ this.log(producerNotice);
105
108
  const row = response.followup;
106
109
  if (response.reused && response.rescheduled) {
107
110
  this.log(`Moved: ${row?.id} now fires at ${row?.nextRunAt ?? at} — the same reminder already existed, so it was rescheduled rather than duplicated.`);
@@ -42,7 +42,7 @@ class FollowupsShow extends base_command_1.BaseCommand {
42
42
  ['says', row.content],
43
43
  ['why', row.why],
44
44
  ['intent', row.intentKey],
45
- ['producer', row.producer],
45
+ ['producer', row.producerLabel ? `${row.producer} (${row.producerLabel})` : row.producer],
46
46
  ['gate', row.gate ? (GATE_LABEL[row.gate] ?? row.gate) : undefined],
47
47
  ['terminal', row.terminal ? `${row.terminal} (${row.terminalReason ?? '—'})` : undefined],
48
48
  [
@@ -237,6 +237,9 @@ class FollowupsWatch extends base_command_1.BaseCommand {
237
237
  this.log(JSON.stringify(response, null, 2));
238
238
  return;
239
239
  }
240
+ const producerNotice = (0, followups_1.producerDowngradeNotice)(response);
241
+ if (producerNotice)
242
+ this.log(producerNotice);
240
243
  const row = response.followup;
241
244
  if (response.reused) {
242
245
  this.log(`Already armed: ${row?.id} — an identical watch exists and was not duplicated.`);
@@ -11,11 +11,31 @@ import { type PublicProjection, type AssistantTextJoiner } from '@skrr-ai/data-p
11
11
  * — consent nobody gave, on a form nobody saw (OSK-10274).
12
12
  */
13
13
  export declare const NON_INTERACTIVE_QUESTION_DISMISSAL: string;
14
+ /**
15
+ * A terminal that has printed the header and nothing else for several seconds looks hung. The
16
+ * first line comes after this long with no output at all, and then again at this interval
17
+ * (OSK-20427). Typical waits are 4 to 6 seconds before the first words, so a quick reply
18
+ * prints nothing extra.
19
+ */
20
+ export declare const WAITING_NOTICE_AFTER_MS = 2500;
21
+ export declare const WAITING_NOTICE_EVERY_MS = 10000;
14
22
  export type AgenticStreamEvent = {
15
23
  type: string;
16
24
  sessionId?: string;
17
25
  [key: string]: unknown;
18
26
  };
27
+ /**
28
+ * The sentence a failed run's terminal event gives for why it failed.
29
+ *
30
+ * The server's failure funnel (`handleSessionFailure`) sends `errors: [<sentence>]`
31
+ * and a short `reason`, never a `message`; other terminal events carry `message`,
32
+ * and a few an `error` string. Every reader used to look at `message` alone, so a
33
+ * run the platform refused ("no valid release package", "insufficient balance", a
34
+ * runtime that was not there) ended as `Error: Agent run failed` with the reason
35
+ * sitting unread in the same event (OSK-20410). One reader, in one order: the
36
+ * explicit `message`, then `error`, then the first entry of `errors`.
37
+ */
38
+ export declare function terminalFailureMessage(event: Record<string, unknown> | null | undefined): string | undefined;
19
39
  export type AgenticStreamResult = {
20
40
  sessionId: string;
21
41
  responseMessageId?: string;
@@ -161,6 +181,12 @@ export type AgenticStreamOptions = {
161
181
  /** Action-required notices when stdout is reserved for a final result. */
162
182
  statusOutput?: NodeJS.WritableStream;
163
183
  jsonl?: boolean;
184
+ /**
185
+ * With `jsonl`, a `text.delta` carries only the characters it adds (`delta`), not the whole
186
+ * answer so far, and not the envelopes `session.output` already carries. A rewrite that is not
187
+ * an append arrives as `replace`, the new full text. `run.result` still holds the whole answer.
188
+ */
189
+ incrementalText?: boolean;
164
190
  approveTools?: boolean;
165
191
  input?: NodeJS.ReadableStream;
166
192
  output?: NodeJS.WritableStream;
@@ -579,6 +605,12 @@ export declare class AgenticStreamClient {
579
605
  private readonly openQuestionPrompts;
580
606
  /** Text already written to the terminal, for suffix-only rendering. */
581
607
  private renderedText;
608
+ /** The "still waiting" line for a terminal run with no output yet (OSK-20427). */
609
+ private waitingTimer;
610
+ private waitingStartedAt;
611
+ private outputSeen;
612
+ /** The cumulative text of the last `text.delta` written as JSON, for `delta`/`replace`. */
613
+ private lastJsonlText;
582
614
  private idleTimer;
583
615
  /** Releases a connect wait still in flight; `close()` calls it. */
584
616
  private abandonConnectWait;
@@ -777,6 +809,20 @@ export declare class AgenticStreamClient {
777
809
  * passed through, preserving prior behaviour.
778
810
  */
779
811
  private emitToolEvent;
812
+ /**
813
+ * Say so when a terminal run has been quiet since it started. Human output only: a script
814
+ * reading --jsonl has the events, and --quiet asked for nothing but the answer.
815
+ */
816
+ private trackWaiting;
817
+ private stopWaiting;
818
+ /**
819
+ * A `text.delta` is named for a delta and carries the whole answer so far (`text`), so a long
820
+ * answer written as JSON lines grows with the square of its length: 207 events for a 2.4 KB
821
+ * answer came to about 250 KB (OSK-20428). The contract stays, since scripts take the last one;
822
+ * `delta` is added to it, and `incrementalText` turns the cumulative text and the duplicated
823
+ * envelopes off for a consumer that wants only what is new.
824
+ */
825
+ private jsonlView;
780
826
  private emit;
781
827
  /**
782
828
  * Human-mode lines for the hand-over wait. Returns true when the event is
@@ -1,6 +1,7 @@
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.HANDOVER_POLL_INTERVAL_MS = exports.DAEMON_HANDOVER_REASON = exports.WORKER_REPLACED_CODE = 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.WAITING_NOTICE_EVERY_MS = exports.WAITING_NOTICE_AFTER_MS = exports.NON_INTERACTIVE_QUESTION_DISMISSAL = void 0;
4
+ exports.terminalFailureMessage = terminalFailureMessage;
4
5
  exports.isDaemonHandoverFailure = isDaemonHandoverFailure;
5
6
  exports.formatWaitDuration = formatWaitDuration;
6
7
  exports.writeJsonLine = writeJsonLine;
@@ -46,6 +47,47 @@ const dpop_auth_1 = require("./dpop-auth");
46
47
  */
47
48
  exports.NON_INTERACTIVE_QUESTION_DISMISSAL = 'No answer: this turn was started non-interactively (no terminal), so nobody can reply to ' +
48
49
  'questions. Proceed with your best judgement, or finish by stating exactly what you need.';
50
+ /**
51
+ * A terminal that has printed the header and nothing else for several seconds looks hung. The
52
+ * first line comes after this long with no output at all, and then again at this interval
53
+ * (OSK-20427). Typical waits are 4 to 6 seconds before the first words, so a quick reply
54
+ * prints nothing extra.
55
+ */
56
+ exports.WAITING_NOTICE_AFTER_MS = 2500;
57
+ exports.WAITING_NOTICE_EVERY_MS = 10_000;
58
+ /** What counts as the run having said something, so the waiting line stops. */
59
+ const OUTPUT_EVENT_TYPES = new Set([
60
+ 'text.delta',
61
+ 'tool.started',
62
+ 'tool.completed',
63
+ 'session.notice',
64
+ 'permission.requested',
65
+ 'run.completed',
66
+ 'run.failed',
67
+ 'run.aborted',
68
+ 'run.handed_over',
69
+ ]);
70
+ /**
71
+ * The sentence a failed run's terminal event gives for why it failed.
72
+ *
73
+ * The server's failure funnel (`handleSessionFailure`) sends `errors: [<sentence>]`
74
+ * and a short `reason`, never a `message`; other terminal events carry `message`,
75
+ * and a few an `error` string. Every reader used to look at `message` alone, so a
76
+ * run the platform refused ("no valid release package", "insufficient balance", a
77
+ * runtime that was not there) ended as `Error: Agent run failed` with the reason
78
+ * sitting unread in the same event (OSK-20410). One reader, in one order: the
79
+ * explicit `message`, then `error`, then the first entry of `errors`.
80
+ */
81
+ function terminalFailureMessage(event) {
82
+ if (!event)
83
+ return undefined;
84
+ const sentence = (value) => typeof value === 'string' && value.trim() ? value : undefined;
85
+ const entryText = (entry) => sentence(entry) ?? sentence(entry?.message);
86
+ const fromList = Array.isArray(event.errors)
87
+ ? event.errors.map(entryText).find((entry) => entry !== undefined)
88
+ : undefined;
89
+ return sentence(event.message) ?? sentence(event.error) ?? fromList;
90
+ }
49
91
  /**
50
92
  * OSK-13517 / OSK-13543 — the one failure that is not an ending.
51
93
  *
@@ -807,6 +849,12 @@ class AgenticStreamClient {
807
849
  openQuestionPrompts = new Map();
808
850
  /** Text already written to the terminal, for suffix-only rendering. */
809
851
  renderedText = '';
852
+ /** The "still waiting" line for a terminal run with no output yet (OSK-20427). */
853
+ waitingTimer = null;
854
+ waitingStartedAt = 0;
855
+ outputSeen = false;
856
+ /** The cumulative text of the last `text.delta` written as JSON, for `delta`/`replace`. */
857
+ lastJsonlText = '';
810
858
  idleTimer = null;
811
859
  /** Releases a connect wait still in flight; `close()` calls it. */
812
860
  abandonConnectWait = null;
@@ -1012,6 +1060,7 @@ class AgenticStreamClient {
1012
1060
  if (this.replayTimer)
1013
1061
  clearTimeout(this.replayTimer);
1014
1062
  this.replayTimer = null;
1063
+ this.stopWaiting();
1015
1064
  this.stopHandoverTimers();
1016
1065
  this.pendingEnvelopes.clear();
1017
1066
  this.browserPermissionRequests.clear();
@@ -1079,10 +1128,7 @@ class AgenticStreamClient {
1079
1128
  const deadlineTimer = setTimeout(() => void this.expireHandover(), opts.maxWaitMs);
1080
1129
  const pollTimer = setInterval(() => this.requestHandoverRead(), opts.pollIntervalMs ?? exports.HANDOVER_POLL_INTERVAL_MS);
1081
1130
  this.handover = { event, startedAt, deadlineTimer, pollTimer, reading: null, readAgain: false };
1082
- const firstError = Array.isArray(event.errors) && typeof event.errors[0] === 'string'
1083
- ? event.errors[0]
1084
- : undefined;
1085
- const message = typeof event.message === 'string' && event.message ? event.message : firstError;
1131
+ const message = terminalFailureMessage(event);
1086
1132
  this.emit({
1087
1133
  type: 'run.handed_over',
1088
1134
  sessionId: this.options.sessionId,
@@ -1745,6 +1791,10 @@ class AgenticStreamClient {
1745
1791
  return;
1746
1792
  }
1747
1793
  if (!this.options.approveTools || !this.socket) {
1794
+ // This CLI makes the decision and nobody is asked, so it says so FIRST. The bound path used
1795
+ // to return before any event, and the only lines a person saw were the server's "The
1796
+ // response was admitted. Tool execution is not confirmed.", which read like an approval.
1797
+ this.emit({ ...event, type: 'permission.denied', reason: 'noninteractive_or_not_enabled' });
1748
1798
  if (event.toolUseId) {
1749
1799
  if (await this.sendBoundPermissionDecision(event, false, { reason: 'CLI was not started with --approve-tools', automatic: true }))
1750
1800
  return;
@@ -1759,7 +1809,6 @@ class AgenticStreamClient {
1759
1809
  automatic: true,
1760
1810
  });
1761
1811
  }
1762
- this.emit({ ...event, type: 'permission.denied', reason: 'noninteractive_or_not_enabled' });
1763
1812
  return;
1764
1813
  }
1765
1814
  /**
@@ -1782,6 +1831,14 @@ class AgenticStreamClient {
1782
1831
  */
1783
1832
  const interactive = this.options.input !== undefined || Boolean(process.stdin.isTTY);
1784
1833
  if (!interactive) {
1834
+ // Announced before it is sent, as the denial is: the bound path used to return first, so
1835
+ // "allowed by --approve-tools" never reached a person who used the flag.
1836
+ this.emit({
1837
+ ...event,
1838
+ type: 'permission.approved',
1839
+ auto: true,
1840
+ reason: 'approved by --approve-tools, which was given with no terminal to ask at',
1841
+ });
1785
1842
  if (await this.sendBoundPermissionDecision(event, true))
1786
1843
  return;
1787
1844
  this.socket.emit('permission:approve', {
@@ -1789,12 +1846,6 @@ class AgenticStreamClient {
1789
1846
  toolUseId: event.toolUseId,
1790
1847
  toolName: event.toolName,
1791
1848
  });
1792
- this.emit({
1793
- ...event,
1794
- type: 'permission.approved',
1795
- auto: true,
1796
- reason: 'approved by --approve-tools, which was given with no terminal to ask at',
1797
- });
1798
1849
  return;
1799
1850
  }
1800
1851
  const prompt = new AbortController();
@@ -2084,6 +2135,60 @@ class AgenticStreamClient {
2084
2135
  return;
2085
2136
  this.emit(event);
2086
2137
  }
2138
+ /**
2139
+ * Say so when a terminal run has been quiet since it started. Human output only: a script
2140
+ * reading --jsonl has the events, and --quiet asked for nothing but the answer.
2141
+ */
2142
+ trackWaiting(event, output) {
2143
+ if (OUTPUT_EVENT_TYPES.has(event.type)) {
2144
+ this.outputSeen = true;
2145
+ this.stopWaiting();
2146
+ return;
2147
+ }
2148
+ if (event.type !== 'run.started' || this.outputSeen || this.waitingTimer)
2149
+ return;
2150
+ this.waitingStartedAt = Date.now();
2151
+ const schedule = (first) => {
2152
+ this.waitingTimer = setTimeout(() => {
2153
+ this.waitingTimer = null;
2154
+ if (this.outputSeen || this.closed)
2155
+ return;
2156
+ const seconds = Math.floor((Date.now() - this.waitingStartedAt) / 1000);
2157
+ output.write(first
2158
+ ? '[run] waiting for the agent to start answering…\n'
2159
+ : `[run] still waiting for the agent (${seconds} s)…\n`);
2160
+ schedule(false);
2161
+ }, first ? exports.WAITING_NOTICE_AFTER_MS : exports.WAITING_NOTICE_EVERY_MS);
2162
+ // A reminder must never be the reason the process stays alive.
2163
+ this.waitingTimer.unref?.();
2164
+ };
2165
+ schedule(true);
2166
+ }
2167
+ stopWaiting() {
2168
+ if (this.waitingTimer)
2169
+ clearTimeout(this.waitingTimer);
2170
+ this.waitingTimer = null;
2171
+ }
2172
+ /**
2173
+ * A `text.delta` is named for a delta and carries the whole answer so far (`text`), so a long
2174
+ * answer written as JSON lines grows with the square of its length: 207 events for a 2.4 KB
2175
+ * answer came to about 250 KB (OSK-20428). The contract stays, since scripts take the last one;
2176
+ * `delta` is added to it, and `incrementalText` turns the cumulative text and the duplicated
2177
+ * envelopes off for a consumer that wants only what is new.
2178
+ */
2179
+ jsonlView(event) {
2180
+ if (event.type !== 'text.delta' || typeof event.text !== 'string')
2181
+ return event;
2182
+ const previous = this.lastJsonlText;
2183
+ const text = event.text;
2184
+ this.lastJsonlText = text;
2185
+ const appended = text.startsWith(previous) ? text.slice(previous.length) : undefined;
2186
+ if (!this.options.incrementalText) {
2187
+ return appended === undefined ? event : { ...event, delta: appended };
2188
+ }
2189
+ const { text: _text, envelopes: _envelopes, ...rest } = event;
2190
+ return appended === undefined ? { ...rest, replace: text } : { ...rest, delta: appended };
2191
+ }
2087
2192
  emit(event) {
2088
2193
  this.lastEvent = event;
2089
2194
  this.options.onEvent?.(event);
@@ -2108,9 +2213,10 @@ class AgenticStreamClient {
2108
2213
  return;
2109
2214
  const output = this.options.output ?? process.stdout;
2110
2215
  if (this.options.jsonl) {
2111
- writeJsonLine(output, event);
2216
+ writeJsonLine(output, this.jsonlView(event));
2112
2217
  return;
2113
2218
  }
2219
+ this.trackWaiting(event, output);
2114
2220
  if (event.type === 'run.started') {
2115
2221
  const permission = describeRunPermission(event);
2116
2222
  if (permission)
@@ -2192,6 +2298,12 @@ class AgenticStreamClient {
2192
2298
  output.write(`\n[question] ${why}\n`);
2193
2299
  this.renderedText = this.answerText();
2194
2300
  }
2301
+ else if (event.type === 'permission.denied' && event.reason === 'noninteractive_or_not_enabled') {
2302
+ // Said out loud, because nobody was asked and the agent will not say it plainly: this run
2303
+ // had no terminal to ask at and was not given --approve-tools, so the call was refused.
2304
+ output.write(`\n[permission] ${describePermission(event)} denied: nobody could be asked, and this run was not started with --approve-tools\n`);
2305
+ this.renderedText = this.answerText();
2306
+ }
2195
2307
  else if (event.type === 'permission.approved' && event.auto === true) {
2196
2308
  // Said out loud, because nobody was asked: the flag approved it, and a
2197
2309
  // reader of this log should see which tools that covered (OSK-10323).
@@ -2379,7 +2491,16 @@ class AgenticStreamClient {
2379
2491
  output.write(append);
2380
2492
  output.write('\n');
2381
2493
  }
2382
- this.emit({ ...event, type: `run.${status}` });
2494
+ // A failed run states why under the one key a script reads (`message`), whichever
2495
+ // shape the terminal event used. `lastEvent` below stays the event as received.
2496
+ const failureMessage = status === 'failed' && !(typeof event.message === 'string' && event.message.trim())
2497
+ ? terminalFailureMessage(event)
2498
+ : undefined;
2499
+ this.emit({
2500
+ ...event,
2501
+ ...(failureMessage ? { message: failureMessage } : {}),
2502
+ type: `run.${status}`,
2503
+ });
2383
2504
  this.close();
2384
2505
  this.terminalResult = {
2385
2506
  sessionId: this.options.sessionId,
@@ -98,6 +98,18 @@ function describeRefreshRefusal(refusal, opts) {
98
98
  `server treats that as possible theft and revokes the whole family. ${relogin}`,
99
99
  };
100
100
  }
101
+ // The server wanted a device proof and this machine had no key to sign one
102
+ // with. `skrr login` signs the CLI in on its own and never creates a device
103
+ // key, so naming it here sends the person round the same loop (OSK-20472): the
104
+ // daemon's interactive sign-in is what mints a key and re-pins the server to it.
105
+ if (code === 'DEVICE_KEY_UNAVAILABLE') {
106
+ return {
107
+ code,
108
+ message: `This sign-in could not be renewed${said}. This machine's device key is missing or ` +
109
+ `unreadable, so requests carry no device proof. \`${bin} login\` cannot create one; ` +
110
+ `sign the background service in again with \`${bin} daemon login\`.`,
111
+ };
112
+ }
101
113
  return {
102
114
  code,
103
115
  message: `This sign-in could not be renewed${said}. ${relogin}`,
@@ -65,6 +65,15 @@ export declare function normalizeRuntimeGlobalArgs(args: readonly string[]): str
65
65
  * part of a copy sweep.
66
66
  */
67
67
  export declare function notInstalledMessage(): string;
68
+ /**
69
+ * Marks the `skrrd` process that `skrr daemon …` launches, so the daemon can print its recovery
70
+ * commands in the spelling the person can run: `skrr daemon login`, not `skrrd login`. The
71
+ * daemon lives in `~/.skrr/bin`, which this wrapper finds by itself and nothing puts on PATH.
72
+ * Spelled the same in `daemon/src/auth/login-hint.ts`; `scripts/__tests__/invoked-via-parity.spec.js`
73
+ * holds the two together.
74
+ */
75
+ export declare const INVOKED_VIA_ENV = "SKRR_INVOKED_VIA";
76
+ export declare const INVOKED_VIA_CLI = "skrr-cli";
68
77
  /**
69
78
  * Spawn `oversky <args>` with stdio inherited. Resolves with the child's
70
79
  * exit code so the caller can `process.exit()` with the same code,
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.RUNTIME_BINARY_NAMES = void 0;
6
+ exports.INVOKED_VIA_CLI = exports.INVOKED_VIA_ENV = exports.RUNTIME_BINARY_NAMES = void 0;
7
7
  exports.forwardedFlagsNote = forwardedFlagsNote;
8
8
  exports.findRuntimeBinary = findRuntimeBinary;
9
9
  exports.isCurrentRuntimeBinary = isCurrentRuntimeBinary;
@@ -206,6 +206,18 @@ function notInstalledMessage() {
206
206
  'Once installed, re-run your command.',
207
207
  ].join('\n');
208
208
  }
209
+ /**
210
+ * Marks the `skrrd` process that `skrr daemon …` launches, so the daemon can print its recovery
211
+ * commands in the spelling the person can run: `skrr daemon login`, not `skrrd login`. The
212
+ * daemon lives in `~/.skrr/bin`, which this wrapper finds by itself and nothing puts on PATH.
213
+ * Spelled the same in `daemon/src/auth/login-hint.ts`; `scripts/__tests__/invoked-via-parity.spec.js`
214
+ * holds the two together.
215
+ */
216
+ exports.INVOKED_VIA_ENV = 'SKRR_INVOKED_VIA';
217
+ exports.INVOKED_VIA_CLI = 'skrr-cli';
218
+ function runtimeEnv() {
219
+ return { ...process.env, [exports.INVOKED_VIA_ENV]: exports.INVOKED_VIA_CLI };
220
+ }
209
221
  /**
210
222
  * Spawn `oversky <args>` with stdio inherited. Resolves with the child's
211
223
  * exit code so the caller can `process.exit()` with the same code,
@@ -230,6 +242,7 @@ function execRuntimeBinary(args, opts = {}) {
230
242
  const isWinShim = process.platform === 'win32' && /\.(cmd|bat)$/i.test(bin);
231
243
  const child = (0, node_child_process_1.spawn)(bin, normalizeRuntimeGlobalArgs(args), {
232
244
  stdio: 'inherit',
245
+ env: runtimeEnv(),
233
246
  ...(isWinShim ? { shell: true } : {}),
234
247
  });
235
248
  child.on('error', reject);
@@ -261,6 +274,7 @@ function execRuntimeBinaryCapture(args, opts = {}) {
261
274
  const isWinShim = process.platform === 'win32' && /\.(cmd|bat)$/i.test(bin);
262
275
  const child = (0, node_child_process_1.spawn)(bin, normalizeRuntimeGlobalArgs(args), {
263
276
  stdio: ['ignore', 'pipe', 'pipe'],
277
+ env: runtimeEnv(),
264
278
  ...(isWinShim ? { shell: true } : {}),
265
279
  });
266
280
  let stdout = '';
@@ -49,6 +49,8 @@ export interface FollowUpRow {
49
49
  kind?: string;
50
50
  form?: string | null;
51
51
  producer?: string;
52
+ /** The producer in words (`describeFollowUpProducer`), so no surface prints a bare enum. */
53
+ producerLabel?: string;
52
54
  why?: string;
53
55
  /** A reminder's own words. */
54
56
  content?: string | null;
@@ -86,7 +88,18 @@ export interface FollowUpCreateResponse {
86
88
  proposal?: boolean;
87
89
  budget?: Json;
88
90
  fallbackTriggerId?: string | null;
91
+ /**
92
+ * Set when the credential could not carry the producer the request claimed: the
93
+ * server recorded `recorded` instead (never a higher one) and says why.
94
+ */
95
+ producerDowngraded?: {
96
+ requested: string;
97
+ recorded: string;
98
+ note: string;
99
+ };
89
100
  }
101
+ /** One line telling a person the follow-up was recorded with weaker provenance than claimed. */
102
+ export declare function producerDowngradeNotice(response: FollowUpCreateResponse): string | undefined;
90
103
  export interface FollowUpObservability {
91
104
  generatedAt?: string;
92
105
  agents?: Array<{
@@ -4,6 +4,7 @@ exports.followupsApi = exports.FOLLOWUP_SUBJECT_FLAG_HELP = exports.FOLLOWUP_SUB
4
4
  exports.whenFlagProblem = whenFlagProblem;
5
5
  exports.settledLine = settledLine;
6
6
  exports.recipeWatchBoundsRefusal = recipeWatchBoundsRefusal;
7
+ exports.producerDowngradeNotice = producerDowngradeNotice;
7
8
  exports.resolveFollowUpAgent = resolveFollowUpAgent;
8
9
  exports.parseSubjectRef = parseSubjectRef;
9
10
  exports.followupSummaryLine = followupSummaryLine;
@@ -127,6 +128,13 @@ function recipeWatchBoundsRefusal(waitKind, passed) {
127
128
  : 'A recipe watch ends through its --wait.',
128
129
  ].join(' ');
129
130
  }
131
+ /** One line telling a person the follow-up was recorded with weaker provenance than claimed. */
132
+ function producerDowngradeNotice(response) {
133
+ const downgraded = response.producerDowngraded;
134
+ if (!downgraded)
135
+ return undefined;
136
+ return `Note: recorded as ${downgraded.recorded}, not ${downgraded.requested}. ${downgraded.note}`;
137
+ }
130
138
  /**
131
139
  * A trigger subject becomes a durable concurrency key, so it cannot be prose.
132
140
  * Keep this grammar aligned with TriggerEngine's subject validation and the
@@ -5,6 +5,7 @@ exports.describeLegacySharedProfile = describeLegacySharedProfile;
5
5
  const machine_uuid_1 = require("@skrr-ai/auth-core/machine-uuid");
6
6
  const data_provider_1 = require("@skrr-ai/data-provider");
7
7
  const keychain_1 = require("./keychain");
8
+ const daemon_command_hint_1 = require("./daemon-command-hint");
8
9
  /** An old profile may still share the default machine row and its device pin. */
9
10
  function legacyProfileSharesDefaultMachine() {
10
11
  const profile = (0, keychain_1.getActiveProfile)();
@@ -47,6 +48,6 @@ function describeLegacySharedProfile(profile) {
47
48
  `computer the same machine identity, and it still has it. ${why}Signing in here would replace the default ` +
48
49
  "profile's device-key pin and sign the default daemon out. Nothing was changed.",
49
50
  `Use a new profile name instead: skrr login --profile ${next}`,
50
- `(For the background service alone: skrrd --profile ${next} login)`,
51
+ `(For the background service alone: ${(0, daemon_command_hint_1.daemonCommandHint)('login', { profile: next })})`,
51
52
  ];
52
53
  }
@@ -49,7 +49,12 @@ function parseRuntimeStartStatus(value, fallbackProfile) {
49
49
  return {
50
50
  profile: asString(profile?.name) ?? fallbackProfile,
51
51
  serverUrl: asString(root.serverUrl),
52
- cwd: asString(root.cwd),
52
+ // `root.cwd` is where THIS `status` ran from (the person's shell), not where
53
+ // the daemon runs. The recovery command binds a login to its `--cwd`, and
54
+ // `skrrd status` itself prints `effectiveCwd` (live daemon, then installed
55
+ // service, then the stored binding) for exactly that. A runtime binary that
56
+ // predates `effectiveCwd` only has `cwd`.
57
+ cwd: asString(root.effectiveCwd) ?? asString(root.cwd),
53
58
  reauth: {
54
59
  needed: reauth?.needed === true,
55
60
  reason: asString(reauth?.reason),