@prohost/cli 0.12.2 → 0.12.4
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/CHANGELOG.md +24 -0
- package/README.md +3 -1
- package/dist/agent/api.js +1 -0
- package/dist/agent/claude.d.ts +36 -5
- package/dist/agent/claude.js +95 -15
- package/dist/agent/codex.d.ts +9 -0
- package/dist/agent/codex.js +21 -0
- package/dist/agent/pair.js +1 -0
- package/dist/agent/run.js +37 -3
- package/dist/usage/command.js +6 -6
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ Versions follow [semver](https://semver.org/). Publishing is automated: merging
|
|
|
4
4
|
a version bump to `main` triggers `.github/workflows/npm-publish-cli.yml`, which
|
|
5
5
|
builds via `prepack`, runs the suite, publishes, and tags `cli-v<version>`.
|
|
6
6
|
|
|
7
|
+
## 0.12.4
|
|
8
|
+
|
|
9
|
+
- Refuse redirects on credentialed API writes and pairing requests, so a validated endpoint cannot replay keys or pairing codes to another destination. Direct HTTPS and local-loopback requests keep working.
|
|
10
|
+
- Reuse an existing usage or paired-agent key before reading implicit stdin. Explicit `usage connect --key -` still replaces the key from stdin; an unconnected pipe still supplies a key.
|
|
11
|
+
|
|
12
|
+
## 0.12.3
|
|
13
|
+
|
|
14
|
+
- **A run stopped by the Claude usage limit says so.** It used to fail with
|
|
15
|
+
"agent command exited 1", the reason left in the stream the CLI never read.
|
|
16
|
+
It now reports `Claude usage limit reached on <machine>: You've hit your
|
|
17
|
+
weekly limit · resets Oct 16 at 11am (America/New_York)`, and the thread's
|
|
18
|
+
notice shows it.
|
|
19
|
+
- **That run keeps its conversation's session, and runs once.** A resumed run
|
|
20
|
+
that failed for any reason was taken for a session Claude Code could no
|
|
21
|
+
longer find, because every stream event names the session. The CLI then
|
|
22
|
+
dropped the conversation's session and ran the turn again from scratch, which
|
|
23
|
+
spent quota and, at a usage limit, failed a second time. A resume now counts
|
|
24
|
+
as rejected only when no session started. A run stopped by the usage limit
|
|
25
|
+
is not run again, and the next run after the reset continues the
|
|
26
|
+
conversation.
|
|
27
|
+
- **Any Claude Code or Codex run that exits non-zero reports its own error**
|
|
28
|
+
(an API error, a failed turn), not just the exit code. Only that message is
|
|
29
|
+
read from the stream; tool output stays on this machine.
|
|
30
|
+
|
|
7
31
|
## 0.12.2
|
|
8
32
|
|
|
9
33
|
- **Two agents on one computer no longer delete each other's account.** Every
|
package/README.md
CHANGED
|
@@ -233,7 +233,9 @@ and gets a reply that has forgotten asking. The CLI reads the `session_id` out
|
|
|
233
233
|
of Claude Code's JSON output, records it per conversation in
|
|
234
234
|
`~/.prohost/sessions.json`, and `--resume`s it next time. Sessions don't live
|
|
235
235
|
forever; when one is gone the run is retried once from scratch rather than
|
|
236
|
-
failing.
|
|
236
|
+
failing. A run stopped by your Claude usage limit is not retried: it fails
|
|
237
|
+
with Claude Code's own message, which says when the limit resets, and the
|
|
238
|
+
conversation keeps its session for the next run.
|
|
237
239
|
|
|
238
240
|
**It lets the agent actually do things.** Runs are invoked with
|
|
239
241
|
`--dangerously-skip-permissions`, so tool calls execute without stopping to ask.
|
package/dist/agent/api.js
CHANGED
|
@@ -65,6 +65,7 @@ export async function postJson(options, path, body, idempotencyKey, captureBody
|
|
|
65
65
|
try {
|
|
66
66
|
const response = await fetchImpl(`${options.baseUrl.replace(/\/+$/, '')}${path}`, {
|
|
67
67
|
method: 'POST',
|
|
68
|
+
redirect: 'error',
|
|
68
69
|
headers: {
|
|
69
70
|
'Content-Type': 'application/json',
|
|
70
71
|
'X-API-Key': options.apiKey,
|
package/dist/agent/claude.d.ts
CHANGED
|
@@ -177,6 +177,28 @@ export interface ClaudeStream {
|
|
|
177
177
|
* `rate_limit_event`. Fields the stream never mentioned are absent.
|
|
178
178
|
*/
|
|
179
179
|
observed(): RuntimeObservation;
|
|
180
|
+
/**
|
|
181
|
+
* Why the turn failed, in Claude Code's own words: the error a `result`
|
|
182
|
+
* event reported, or else the API error an `assistant` event carried in
|
|
183
|
+
* place of an answer. `undefined` when the stream said neither.
|
|
184
|
+
*
|
|
185
|
+
* Read for a run that exited non-zero, whose stdout is otherwise never
|
|
186
|
+
* looked at — without it such a run reports only "exited 1". Never the raw
|
|
187
|
+
* stream: that carries the output of every tool call, and this text is
|
|
188
|
+
* echoed into a workspace thread.
|
|
189
|
+
*/
|
|
190
|
+
error(): string | undefined;
|
|
191
|
+
/**
|
|
192
|
+
* Claude Code's message when the subscription's usage limit stopped the
|
|
193
|
+
* turn — "You've hit your weekly limit · resets Oct 16 at 11am
|
|
194
|
+
* (America/New_York)" — or `undefined` when something else did.
|
|
195
|
+
*
|
|
196
|
+
* Recognised by the `rate_limit` error on the `assistant` event Claude Code
|
|
197
|
+
* writes in place of an answer, or by a `rate_limit_event` whose status is
|
|
198
|
+
* `rejected` with no overage to fall back on. Running the turn again cannot
|
|
199
|
+
* help until the limit resets, so a caller must not retry it.
|
|
200
|
+
*/
|
|
201
|
+
usageLimit(): string | undefined;
|
|
180
202
|
/**
|
|
181
203
|
* The turn's outcome. ``stdout`` is the complete captured output, used only
|
|
182
204
|
* when the stream held no `result` event — a crashed binary, a login prompt,
|
|
@@ -218,11 +240,20 @@ export declare function readClaudeStream(): ClaudeStream;
|
|
|
218
240
|
* and the ledger has no way to know. A stale entry must not turn into a
|
|
219
241
|
* permanently broken surface, so the caller retries once without `--resume`.
|
|
220
242
|
*
|
|
221
|
-
* The observed failure is exit 1,
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* is
|
|
225
|
-
*
|
|
243
|
+
* The observed failure (Claude Code 2.1.296) is exit 1, `No conversation found
|
|
244
|
+
* with session ID: <uuid>` on stderr, and on stdout a lone `result` event with
|
|
245
|
+
* that message in `errors` — no `system`/`init` event, because no session ever
|
|
246
|
+
* started. Matching is broader than that string because the wording is not
|
|
247
|
+
* ours to depend on, and it is gated by the caller on the run having actually
|
|
248
|
+
* failed — so a *successful* run whose reply text happens to quote a session id
|
|
249
|
+
* is never mistaken for one.
|
|
250
|
+
*
|
|
251
|
+
* A stream that announced its session or produced an assistant message is a
|
|
252
|
+
* resume that **worked**, whatever went wrong after it. That guard is what
|
|
253
|
+
* keeps an ordinary failed turn from matching: every stream-json event carries
|
|
254
|
+
* `session_id`, so "the output mentions the session id" — which this used to
|
|
255
|
+
* accept — is true of every resumed run, and a resumed run that then hit a
|
|
256
|
+
* usage limit was retried from scratch and lost its thread's session.
|
|
226
257
|
*/
|
|
227
258
|
export declare function looksLikeResumeFailure(output: string, sessionId: string): boolean;
|
|
228
259
|
/** Absolute path of the generated MCP config. */
|
package/dist/agent/claude.js
CHANGED
|
@@ -209,6 +209,19 @@ function resultEnvelope(stdout) {
|
|
|
209
209
|
function text(value) {
|
|
210
210
|
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
211
211
|
}
|
|
212
|
+
function asRecord(value) {
|
|
213
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The `errors` an `error_during_execution` envelope carries instead of a
|
|
217
|
+
* `result` — e.g. `No conversation found with session ID: <uuid>` for a
|
|
218
|
+
* rejected `--resume` (Claude Code 2.1.296).
|
|
219
|
+
*/
|
|
220
|
+
function envelopeErrors(parsed) {
|
|
221
|
+
if (!Array.isArray(parsed.errors))
|
|
222
|
+
return undefined;
|
|
223
|
+
return text(parsed.errors.filter((entry) => typeof entry === 'string' && entry.length > 0).join('; '));
|
|
224
|
+
}
|
|
212
225
|
/**
|
|
213
226
|
* Turn a `result` envelope into the harness's view of the turn.
|
|
214
227
|
*
|
|
@@ -229,7 +242,7 @@ function outputFromEnvelope(parsed) {
|
|
|
229
242
|
ok: false,
|
|
230
243
|
sessionId,
|
|
231
244
|
denials,
|
|
232
|
-
error: result ?? `claude reported an error (${String(parsed.subtype ?? 'unknown')})`,
|
|
245
|
+
error: result ?? envelopeErrors(parsed) ?? `claude reported an error (${String(parsed.subtype ?? 'unknown')})`,
|
|
233
246
|
};
|
|
234
247
|
}
|
|
235
248
|
if (!result) {
|
|
@@ -452,6 +465,10 @@ export function readClaudeStream() {
|
|
|
452
465
|
let lastActivity;
|
|
453
466
|
let lastText;
|
|
454
467
|
let result;
|
|
468
|
+
/** The last `assistant` event that was an API error rather than an answer. */
|
|
469
|
+
let apiError;
|
|
470
|
+
/** When a `rate_limit_event` said the limit is hit: its reset, epoch seconds. */
|
|
471
|
+
let limitRejected;
|
|
455
472
|
let toolEvents = [];
|
|
456
473
|
// `tool_use.id` → tool name, so a result can be reported under the name of
|
|
457
474
|
// the call it answers. Deleted on the result: bounded by tools in flight.
|
|
@@ -534,6 +551,13 @@ export function readClaudeStream() {
|
|
|
534
551
|
const windows = fromClaudeRateLimitInfo(event.rate_limit_info);
|
|
535
552
|
if (windows.length > 0)
|
|
536
553
|
observed.quota_windows = windows;
|
|
554
|
+
const info = asRecord(event.rate_limit_info);
|
|
555
|
+
// `rejected` with overage still `allowed` is a turn that carries on, on
|
|
556
|
+
// overage; only a rejection with nothing to fall back on stops it.
|
|
557
|
+
const overage = info?.overageStatus;
|
|
558
|
+
if (info?.status === 'rejected' && overage !== 'allowed' && overage !== 'allowed_warning') {
|
|
559
|
+
limitRejected = { resetsAt: typeof info.resetsAt === 'number' ? info.resetsAt : undefined };
|
|
560
|
+
}
|
|
537
561
|
}
|
|
538
562
|
// Present on the `system`/`init` event, and repeated on most others. Taken
|
|
539
563
|
// from whichever arrives first — they all name the same session.
|
|
@@ -551,6 +575,15 @@ export function readClaudeStream() {
|
|
|
551
575
|
}
|
|
552
576
|
if (event.type !== 'assistant')
|
|
553
577
|
return;
|
|
578
|
+
// Claude Code writes an API failure — a usage limit, a billing or auth
|
|
579
|
+
// error — as an assistant message whose text is the explanation, marked by
|
|
580
|
+
// `error` (stream-json) or `isApiErrorMessage` (the transcript shape).
|
|
581
|
+
if (typeof event.error === 'string' || event.isApiErrorMessage === true) {
|
|
582
|
+
apiError = {
|
|
583
|
+
kind: event.apiError === 'usage_limit_reached' ? 'rate_limit' : text(event.error),
|
|
584
|
+
text: assistantContent(event).text?.trim() || undefined,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
554
587
|
collectToolUses(event);
|
|
555
588
|
const { text: written, tool } = assistantContent(event);
|
|
556
589
|
if (written)
|
|
@@ -562,6 +595,14 @@ export function readClaudeStream() {
|
|
|
562
595
|
else if (written)
|
|
563
596
|
lastActivity = written.replace(/\s+/g, ' ').trim().slice(0, MAX_ACTIVITY_CHARS);
|
|
564
597
|
};
|
|
598
|
+
/** A trailing event with no newline after it is still an event. */
|
|
599
|
+
const flushTail = () => {
|
|
600
|
+
const tail = buffer.trim();
|
|
601
|
+
if (tail) {
|
|
602
|
+
handle(tail);
|
|
603
|
+
buffer = '';
|
|
604
|
+
}
|
|
605
|
+
};
|
|
565
606
|
return {
|
|
566
607
|
push(chunk) {
|
|
567
608
|
buffer += chunk;
|
|
@@ -594,14 +635,30 @@ export function readClaudeStream() {
|
|
|
594
635
|
return undefined;
|
|
595
636
|
return trimmed.length > MAX_SALVAGE_CHARS ? trimmed.slice(-MAX_SALVAGE_CHARS) : trimmed;
|
|
596
637
|
},
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
buffer = '';
|
|
638
|
+
error: () => {
|
|
639
|
+
flushTail();
|
|
640
|
+
if (result) {
|
|
641
|
+
const outcome = outputFromEnvelope(result);
|
|
642
|
+
if (!outcome.ok)
|
|
643
|
+
return outcome.error;
|
|
604
644
|
}
|
|
645
|
+
return apiError?.text;
|
|
646
|
+
},
|
|
647
|
+
usageLimit: () => {
|
|
648
|
+
flushTail();
|
|
649
|
+
if (apiError?.kind === 'rate_limit')
|
|
650
|
+
return apiError.text ?? 'usage limit reached';
|
|
651
|
+
if (!limitRejected)
|
|
652
|
+
return undefined;
|
|
653
|
+
const { resetsAt } = limitRejected;
|
|
654
|
+
return resetsAt === undefined
|
|
655
|
+
? 'usage limit reached'
|
|
656
|
+
: `usage limit reached · resets ${new Date(resetsAt * 1000).toISOString()}`;
|
|
657
|
+
},
|
|
658
|
+
finish(stdout) {
|
|
659
|
+
// A binary that exits without flushing a final `\n` must not lose its
|
|
660
|
+
// result.
|
|
661
|
+
flushTail();
|
|
605
662
|
if (result)
|
|
606
663
|
return outputFromEnvelope(result);
|
|
607
664
|
// No `result` event: either the command isn't emitting the stream we
|
|
@@ -621,19 +678,42 @@ export function readClaudeStream() {
|
|
|
621
678
|
* and the ledger has no way to know. A stale entry must not turn into a
|
|
622
679
|
* permanently broken surface, so the caller retries once without `--resume`.
|
|
623
680
|
*
|
|
624
|
-
* The observed failure is exit 1,
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
* is
|
|
628
|
-
*
|
|
681
|
+
* The observed failure (Claude Code 2.1.296) is exit 1, `No conversation found
|
|
682
|
+
* with session ID: <uuid>` on stderr, and on stdout a lone `result` event with
|
|
683
|
+
* that message in `errors` — no `system`/`init` event, because no session ever
|
|
684
|
+
* started. Matching is broader than that string because the wording is not
|
|
685
|
+
* ours to depend on, and it is gated by the caller on the run having actually
|
|
686
|
+
* failed — so a *successful* run whose reply text happens to quote a session id
|
|
687
|
+
* is never mistaken for one.
|
|
688
|
+
*
|
|
689
|
+
* A stream that announced its session or produced an assistant message is a
|
|
690
|
+
* resume that **worked**, whatever went wrong after it. That guard is what
|
|
691
|
+
* keeps an ordinary failed turn from matching: every stream-json event carries
|
|
692
|
+
* `session_id`, so "the output mentions the session id" — which this used to
|
|
693
|
+
* accept — is true of every resumed run, and a resumed run that then hit a
|
|
694
|
+
* usage limit was retried from scratch and lost its thread's session.
|
|
629
695
|
*/
|
|
630
696
|
export function looksLikeResumeFailure(output, sessionId) {
|
|
631
697
|
if (!sessionId)
|
|
632
698
|
return false;
|
|
633
|
-
if (output
|
|
634
|
-
return
|
|
699
|
+
if (sessionStarted(output))
|
|
700
|
+
return false;
|
|
635
701
|
return /no (conversation|session)[^\n]{0,40}found|session[^\n]{0,40}not found|--resume|resume[^\n]{0,20}(failed|invalid|expired)/i.test(output);
|
|
636
702
|
}
|
|
703
|
+
/** Did this output include the `system`/`init` or an `assistant` event of a running session? */
|
|
704
|
+
function sessionStarted(output) {
|
|
705
|
+
for (const line of output.split('\n')) {
|
|
706
|
+
const trimmed = line.trim();
|
|
707
|
+
if (!trimmed.startsWith('{'))
|
|
708
|
+
continue;
|
|
709
|
+
const event = asObject(trimmed);
|
|
710
|
+
if (!event)
|
|
711
|
+
continue;
|
|
712
|
+
if ((event.type === 'system' && event.subtype === 'init') || event.type === 'assistant')
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
637
717
|
/** Absolute path of the generated MCP config. */
|
|
638
718
|
export function mcpConfigPath(env = process.env) {
|
|
639
719
|
return path.join(prohostHome(env), MCP_CONFIG_FILENAME);
|
package/dist/agent/codex.d.ts
CHANGED
|
@@ -518,6 +518,15 @@ export type CodexOutput = {
|
|
|
518
518
|
* the model (verified against 0.144.1, same convention as Claude Code).
|
|
519
519
|
*/
|
|
520
520
|
export declare function parseCodexOutput(stdout: string): CodexOutput;
|
|
521
|
+
/**
|
|
522
|
+
* The failure a Codex run reported about itself — the last `turn.failed` or
|
|
523
|
+
* `error` event's message — or `undefined` if it reported none.
|
|
524
|
+
*
|
|
525
|
+
* For a run that exited non-zero, whose stdout is otherwise never read: without
|
|
526
|
+
* this such a run reports only "exited 1". Only that message, never the stream
|
|
527
|
+
* around it, which carries command output and would be echoed into a thread.
|
|
528
|
+
*/
|
|
529
|
+
export declare function codexFailureMessage(stdout: string): string | undefined;
|
|
521
530
|
/**
|
|
522
531
|
* Does this look like the resume having been rejected, rather than the agent
|
|
523
532
|
* having failed at its actual work?
|
package/dist/agent/codex.js
CHANGED
|
@@ -1053,6 +1053,27 @@ export function parseCodexOutput(stdout) {
|
|
|
1053
1053
|
: 'codex returned no output',
|
|
1054
1054
|
};
|
|
1055
1055
|
}
|
|
1056
|
+
/**
|
|
1057
|
+
* The failure a Codex run reported about itself — the last `turn.failed` or
|
|
1058
|
+
* `error` event's message — or `undefined` if it reported none.
|
|
1059
|
+
*
|
|
1060
|
+
* For a run that exited non-zero, whose stdout is otherwise never read: without
|
|
1061
|
+
* this such a run reports only "exited 1". Only that message, never the stream
|
|
1062
|
+
* around it, which carries command output and would be echoed into a thread.
|
|
1063
|
+
*/
|
|
1064
|
+
export function codexFailureMessage(stdout) {
|
|
1065
|
+
let failure;
|
|
1066
|
+
for (const line of stdout.split('\n')) {
|
|
1067
|
+
const trimmed = line.trim();
|
|
1068
|
+
if (!trimmed.startsWith('{'))
|
|
1069
|
+
continue;
|
|
1070
|
+
const event = asObject(trimmed);
|
|
1071
|
+
if (event && (event.type === 'turn.failed' || event.type === 'error')) {
|
|
1072
|
+
failure = failureMessage(event) ?? failure;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
return failure;
|
|
1076
|
+
}
|
|
1056
1077
|
/**
|
|
1057
1078
|
* Name the tool this item was refused, or `undefined` if it wasn't refused.
|
|
1058
1079
|
*
|
package/dist/agent/pair.js
CHANGED
|
@@ -127,6 +127,7 @@ export async function runPair(options) {
|
|
|
127
127
|
try {
|
|
128
128
|
response = await fetchImpl(`${baseUrl}${bound ? REDEEM_BOUND_PATH : REDEEM_PATH}`, {
|
|
129
129
|
method: 'POST',
|
|
130
|
+
redirect: 'error',
|
|
130
131
|
headers: { 'Content-Type': 'application/json', 'User-Agent': USER_AGENT },
|
|
131
132
|
body: JSON.stringify({
|
|
132
133
|
code: options.code,
|
package/dist/agent/run.js
CHANGED
|
@@ -33,7 +33,7 @@ import { HarnessAccount } from './account_runtime.js';
|
|
|
33
33
|
import { EVENT_AGENT_CONFIG_UPDATED, machineId } from './accounts.js';
|
|
34
34
|
import { completeRun, describeFailure, isIdempotencyInFlight, postReply } from './api.js';
|
|
35
35
|
import { MAX_SALVAGE_CHARS, claudeCommand, isClaudeExec, looksLikeResumeFailure, readClaudeStream, writeMcpConfig, } from './claude.js';
|
|
36
|
-
import { APPS_FEATURE, MCP_API_KEY_ENV_VAR, PLUGINS_DISABLED_OVERRIDE, codexBinary, codexCommand, codexMcpInventoryFlag, codexProfileName, codexShellSyntax, extraMcpEntries, isCodexExec, isolateConfiguredServers, listMcpServers, mcpServersOverride, prohostMcpEntry, looksLikeCodexResumeFailure, parseCodexOutput, } from './codex.js';
|
|
36
|
+
import { APPS_FEATURE, MCP_API_KEY_ENV_VAR, PLUGINS_DISABLED_OVERRIDE, codexBinary, codexCommand, codexMcpInventoryFlag, codexProfileName, codexShellSyntax, extraMcpEntries, isCodexExec, isolateConfiguredServers, listMcpServers, mcpServersOverride, prohostMcpEntry, codexFailureMessage, looksLikeCodexResumeFailure, parseCodexOutput, } from './codex.js';
|
|
37
37
|
import { ToolEventQueue } from './events.js';
|
|
38
38
|
import { EXTRA_MCP_FILENAME, loadExtraMcpServers } from './extras.js';
|
|
39
39
|
import { MCP_SERVER_NAME, MCP_TOOL_PATTERN } from './mcp.js';
|
|
@@ -723,6 +723,18 @@ runEnv) {
|
|
|
723
723
|
if (mcp.error) {
|
|
724
724
|
return { ok: false, reason: 'MCP isolation could not be established', detail: mcp.error, durationMs: 0 };
|
|
725
725
|
}
|
|
726
|
+
/**
|
|
727
|
+
* A turn the Claude subscription's usage limit stopped, worded for the
|
|
728
|
+
* thread: whose limit (this machine's login) and, in Claude Code's own
|
|
729
|
+
* words, when it resets. The only fix is waiting, so the reason says that
|
|
730
|
+
* rather than "exited 1".
|
|
731
|
+
*/
|
|
732
|
+
const usageLimitFailure = (said, stderr = '') => ({
|
|
733
|
+
ok: false,
|
|
734
|
+
reason: `Claude usage limit reached on ${options.machine ?? os.hostname()}`,
|
|
735
|
+
detail: [said, stderr.includes(said) ? '' : stderr].filter((part) => part.length > 0).join('\n'),
|
|
736
|
+
durationMs: elapsed(),
|
|
737
|
+
});
|
|
726
738
|
const failure = (result) => {
|
|
727
739
|
// A killed run has no reply, but it usually has most of one. Reporting the
|
|
728
740
|
// tail of what the agent was writing turns "no output for 15 minutes" into
|
|
@@ -751,7 +763,20 @@ runEnv) {
|
|
|
751
763
|
// completion `error` is capped server-side, and of the two, what the agent
|
|
752
764
|
// was saying beats the last 500 characters of its logging.
|
|
753
765
|
const stderrTail = result.stderr.trim().slice(salvaged ? -200 : -500);
|
|
754
|
-
const
|
|
766
|
+
const limit = killed ? undefined : stream?.usageLimit();
|
|
767
|
+
if (limit)
|
|
768
|
+
return usageLimitFailure(limit, stderrTail);
|
|
769
|
+
// What the brain itself said went wrong — a shaped brain reports it on
|
|
770
|
+
// stdout, which a non-zero exit otherwise leaves unread. First, because the
|
|
771
|
+
// thread shows only the opening of this text.
|
|
772
|
+
const said = killed
|
|
773
|
+
? undefined
|
|
774
|
+
: (stream?.error() ?? (runtime.brain === 'codex' ? codexFailureMessage(result.stdout) : undefined))?.slice(0, MAX_SALVAGE_CHARS);
|
|
775
|
+
const detail = [
|
|
776
|
+
said ?? '',
|
|
777
|
+
said && stderrTail.includes(said) ? '' : stderrTail,
|
|
778
|
+
salvaged ? `unfinished answer: ${salvaged}` : '',
|
|
779
|
+
]
|
|
755
780
|
.filter((part) => part.length > 0)
|
|
756
781
|
.join('\n');
|
|
757
782
|
return {
|
|
@@ -794,6 +819,9 @@ runEnv) {
|
|
|
794
819
|
const resumeFailed = codex ? looksLikeCodexResumeFailure : looksLikeResumeFailure;
|
|
795
820
|
if (!result.ok &&
|
|
796
821
|
sessionId &&
|
|
822
|
+
// A usage limit fails a fresh session exactly as it failed this one, and
|
|
823
|
+
// the retry would cost the thread its session for nothing.
|
|
824
|
+
!stream?.usageLimit() &&
|
|
797
825
|
resumeFailed(`${result.stderr}\n${result.stdout}`, sessionId) &&
|
|
798
826
|
// Skipped rather than squeezed into a token budget: a retry with no time
|
|
799
827
|
// left would be killed on the spot and reported as a timeout, which is a
|
|
@@ -811,8 +839,11 @@ runEnv) {
|
|
|
811
839
|
// transcript is real, it holds however long the agent worked, and the next
|
|
812
840
|
// delivery on that surface can resume it. Only a turn that failed on its
|
|
813
841
|
// own — a crash, an unparseable answer — has no continuity worth keeping.
|
|
814
|
-
|
|
842
|
+
// A usage limit is not the turn failing on its own either: the session is
|
|
843
|
+
// the thread's, and the next turn after the reset should continue it.
|
|
844
|
+
if (!result.timedOut && !result.idleTimedOut && !result.cancelled && !stream?.usageLimit()) {
|
|
815
845
|
forgetRecordedSession();
|
|
846
|
+
}
|
|
816
847
|
return failure(result);
|
|
817
848
|
}
|
|
818
849
|
// A denied tool is not an error either brain reports — the turn succeeds and
|
|
@@ -852,6 +883,9 @@ runEnv) {
|
|
|
852
883
|
toolEvents?.push(stream.drainToolEvents());
|
|
853
884
|
reportDenials(parsed.denials);
|
|
854
885
|
if (!parsed.ok) {
|
|
886
|
+
const limit = stream?.usageLimit();
|
|
887
|
+
if (limit)
|
|
888
|
+
return usageLimitFailure(limit);
|
|
855
889
|
forgetRecordedSession();
|
|
856
890
|
return {
|
|
857
891
|
ok: false,
|
package/dist/usage/command.js
CHANGED
|
@@ -88,8 +88,8 @@ export function renderDiscovery(result, env, dryRun) {
|
|
|
88
88
|
/**
|
|
89
89
|
* The key `connect` should store, in order: `--key -` (stdin, or a hidden
|
|
90
90
|
* prompt at a terminal), a literal `--key <value>` (warned about), then
|
|
91
|
-
* `$PROHOST_API_KEY`, then
|
|
92
|
-
*
|
|
91
|
+
* `$PROHOST_API_KEY`, then an existing credential, then piped stdin or — at a
|
|
92
|
+
* terminal — a hidden prompt. `undefined` means "none given";
|
|
93
93
|
* `null` means a usage error that has already been printed.
|
|
94
94
|
*/
|
|
95
95
|
async function connectKey(flags, io, env) {
|
|
@@ -109,12 +109,12 @@ async function connectKey(flags, io, env) {
|
|
|
109
109
|
}
|
|
110
110
|
if (env.PROHOST_API_KEY)
|
|
111
111
|
return env.PROHOST_API_KEY.trim();
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
// At a terminal: prompt only when there is nothing to fall back on, so a
|
|
115
|
-
// connected machine (or one with a paired agent) is not asked again.
|
|
112
|
+
// Implicit stdin can be an open pipe without any key writer. An explicit
|
|
113
|
+
// --key - above still deliberately replaces a stored credential.
|
|
116
114
|
if (resolveUsageKey(env))
|
|
117
115
|
return undefined;
|
|
116
|
+
if (!stdin.isTTY)
|
|
117
|
+
return readPipedKey(stdin);
|
|
118
118
|
return prompt();
|
|
119
119
|
}
|
|
120
120
|
async function connectCommand(flags, io) {
|
package/dist/version.d.ts
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
* `package.json`, so a release bump that forgets this file fails the suite
|
|
8
8
|
* instead of shipping a `User-Agent` that lies about which build is calling.
|
|
9
9
|
*/
|
|
10
|
-
export declare const CLI_VERSION = "0.12.
|
|
10
|
+
export declare const CLI_VERSION = "0.12.4";
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
|
-
export declare const USER_AGENT = "prohost-cli/0.12.
|
|
12
|
+
export declare const USER_AGENT = "prohost-cli/0.12.4";
|
package/dist/version.js
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
* `package.json`, so a release bump that forgets this file fails the suite
|
|
8
8
|
* instead of shipping a `User-Agent` that lies about which build is calling.
|
|
9
9
|
*/
|
|
10
|
-
export const CLI_VERSION = '0.12.
|
|
10
|
+
export const CLI_VERSION = '0.12.4';
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
12
|
export const USER_AGENT = `prohost-cli/${CLI_VERSION}`;
|