@bridge4dev/runner 0.59.1 → 0.61.0
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/dist/adapters/claude.js +197 -21
- package/dist/adapters/codex.js +22 -2
- package/dist/adapters/questions.js +1 -0
- package/dist/adapters/types.d.ts +42 -1
- package/dist/agent-binary.d.ts +45 -4
- package/dist/agent-binary.js +53 -4
- package/dist/attachments.d.ts +6 -0
- package/dist/attachments.js +162 -11
- package/dist/commit-message.js +8 -1
- package/dist/index.js +59 -2
- package/dist/journal.d.ts +40 -0
- package/dist/journal.js +82 -0
- package/dist/protocol.d.ts +52 -0
- package/dist/protocol.js +14 -0
- package/dist/self-update.d.ts +22 -0
- package/dist/self-update.js +22 -0
- package/dist/supervisor.d.ts +123 -2
- package/dist/supervisor.js +598 -91
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/adapters/claude.js
CHANGED
|
@@ -11,7 +11,7 @@ import { evaluateToolUse, maskSecrets, maskString, } from '../policy.js';
|
|
|
11
11
|
import { availableModes, cardDescription, DIRECT_BRANCH_RULE, MODE_REFUSED_TEXT, MODE_WITHDRAWN_TEXT, policyContextFor, DEVBRIDGE_MCP_SERVER_NAME, } from './types.js';
|
|
12
12
|
import { percentFromUtilization, RATE_WINDOW_MINUTES, rateWindowKey } from './rate-limits.js';
|
|
13
13
|
import { applyUsagePercentages, lastUsageRows, readUsageRows } from './claude-usage.js';
|
|
14
|
-
import { claudeExecutableOption, sessionClaudePath } from '../agent-binary.js';
|
|
14
|
+
import { assertClaudeInstalled, claudeExecutableOption, sessionClaudePath, } from '../agent-binary.js';
|
|
15
15
|
/** Same 2KB the SDK keeps: enough for the CLI's last words, not a log sink. */
|
|
16
16
|
const STDERR_TAIL_LIMIT = 2048;
|
|
17
17
|
import { answerSummary, answerValue, discussMessage, invalidationMessage, mirrorOptions, newAskId, MAX_OPTIONS, MAX_QUESTIONS, OPTION_TEXT_LIMIT, QUESTION_TEXT_LIMIT, } from './questions.js';
|
|
@@ -340,10 +340,21 @@ class ClaudeSession {
|
|
|
340
340
|
* has no such field. Without stashing it, the supervisor would be left with a
|
|
341
341
|
* sentence — and a decision made from a sentence can be made by prose (#252).
|
|
342
342
|
*
|
|
343
|
-
* Reset
|
|
343
|
+
* Reset when a turn BEGINS — `beginTurnFacts`, reached from `resumingTurn`
|
|
344
|
+
* for every turn this runner starts and from the boundary guard in the read
|
|
345
|
+
* loop for one the CLI started by itself (#406). The docblock used to say
|
|
346
|
+
* `endTaskTurn` owns the epoch; it never has, and reading it that way is what
|
|
347
|
+
* made «a code from this turn» and «a code from some turn» look identical.
|
|
344
348
|
*/
|
|
345
349
|
turnFailureCode = null;
|
|
346
350
|
turnFailureStatus = null;
|
|
351
|
+
/**
|
|
352
|
+
* A `result` has gone out and no new message has arrived since (#406).
|
|
353
|
+
*
|
|
354
|
+
* The marker the read loop uses to tell a turn the CLI opened by itself from
|
|
355
|
+
* a second `result` for the turn that just ended.
|
|
356
|
+
*/
|
|
357
|
+
turnClosed = false;
|
|
347
358
|
/**
|
|
348
359
|
* Did this turn put ANYTHING on the wire — text, thinking, or a tool call?
|
|
349
360
|
*
|
|
@@ -2169,6 +2180,65 @@ class ClaudeSession {
|
|
|
2169
2180
|
conversationAnchor() {
|
|
2170
2181
|
return this.lastMessageUuid;
|
|
2171
2182
|
}
|
|
2183
|
+
/** A compaction the CLI has announced and not yet ended (#348). */
|
|
2184
|
+
compacting = false;
|
|
2185
|
+
/**
|
|
2186
|
+
* `system:status` — the CLI's own word on what it is doing (#348).
|
|
2187
|
+
*
|
|
2188
|
+
* Read the way the CLI's own remote client reads it (`noteInbound`):
|
|
2189
|
+
* «compacting» opens a compaction, `requesting` says nothing about one (the
|
|
2190
|
+
* summary itself is a model request, and this status arrives DURING a
|
|
2191
|
+
* compaction), and any other status closes it — with `compact_result` when
|
|
2192
|
+
* the CLI says how it went, and as «skipped» when it does not: a PreCompact
|
|
2193
|
+
* hook that said no, or a reactive compaction that found nothing to fold,
|
|
2194
|
+
* both end on a bare `status: null`. Left unclosed, that start line would
|
|
2195
|
+
* keep the dashboard saying «compacting» until the turn ended, minutes later.
|
|
2196
|
+
*
|
|
2197
|
+
* De-duplicated here as well as in the supervisor: the CLI re-says
|
|
2198
|
+
* «compacting» every thirty seconds while it waits on a precomputed summary.
|
|
2199
|
+
*
|
|
2200
|
+
* No `standalone` on this agent: `/compact` is a turn, and its `result`
|
|
2201
|
+
* settles the status the ordinary way.
|
|
2202
|
+
*/
|
|
2203
|
+
noteStatus(msg) {
|
|
2204
|
+
const status = msg.status;
|
|
2205
|
+
if (status === 'requesting')
|
|
2206
|
+
return;
|
|
2207
|
+
if (status === 'compacting') {
|
|
2208
|
+
if (this.compacting)
|
|
2209
|
+
return;
|
|
2210
|
+
this.compacting = true;
|
|
2211
|
+
this.emit({ type: 'compaction', phase: 'started' });
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
// An end is only an end of something that began: without a start on
|
|
2215
|
+
// record there is nothing to close, and the second of two endings (a
|
|
2216
|
+
// `compact_result` after a `compact_boundary`) must not open a second
|
|
2217
|
+
// pair. A mode change also travels on this message — `{status: null,
|
|
2218
|
+
// permissionMode}` on every `setPermissionMode` in CLI 2.1.271 — and says
|
|
2219
|
+
// nothing about a compaction at all.
|
|
2220
|
+
if (!this.compacting)
|
|
2221
|
+
return;
|
|
2222
|
+
const compactResult = msg.compact_result;
|
|
2223
|
+
if (compactResult === undefined && msg.permissionMode !== undefined)
|
|
2224
|
+
return;
|
|
2225
|
+
this.compacting = false;
|
|
2226
|
+
if (compactResult === 'failed') {
|
|
2227
|
+
this.emit({
|
|
2228
|
+
type: 'compaction',
|
|
2229
|
+
phase: 'finished',
|
|
2230
|
+
ok: false,
|
|
2231
|
+
error: maskString(String(msg.compact_error ?? 'unknown')).slice(0, 300),
|
|
2232
|
+
});
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
this.emit({
|
|
2236
|
+
type: 'compaction',
|
|
2237
|
+
phase: 'finished',
|
|
2238
|
+
ok: true,
|
|
2239
|
+
...(compactResult === 'success' ? {} : { skipped: true }),
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2172
2242
|
/**
|
|
2173
2243
|
* `/compact` — the CLI's own command, delivered as ordinary user input.
|
|
2174
2244
|
*
|
|
@@ -2226,6 +2296,25 @@ class ClaudeSession {
|
|
|
2226
2296
|
async consume() {
|
|
2227
2297
|
try {
|
|
2228
2298
|
for await (const msg of this.q) {
|
|
2299
|
+
/**
|
|
2300
|
+
* A turn the CLI started by itself begins here (#406).
|
|
2301
|
+
*
|
|
2302
|
+
* `beginTurnFacts` has exactly one other caller, `resumingTurn`, and it
|
|
2303
|
+
* covers every turn THIS runner opens. The CLI opens turns too — a
|
|
2304
|
+
* background subagent finishing wakes one, and its `result` carries
|
|
2305
|
+
* `origin.kind: 'task-notification'` — and those used to inherit the
|
|
2306
|
+
* previous turn's `turnFailureCode`, `turnProduced` and
|
|
2307
|
+
* `turnIrreversible`, which is how a long-healed stream cut could still
|
|
2308
|
+
* be the reason attached to a turn an hour later.
|
|
2309
|
+
*
|
|
2310
|
+
* The guard is «a message that is not another `result`», not «the turn
|
|
2311
|
+
* ended»: one Stop can produce two results (see the stop-cycle tests),
|
|
2312
|
+
* and the second one has to see the same facts as the first.
|
|
2313
|
+
*/
|
|
2314
|
+
if (this.turnClosed && msg.type !== 'result') {
|
|
2315
|
+
this.turnClosed = false;
|
|
2316
|
+
this.beginTurnFacts();
|
|
2317
|
+
}
|
|
2229
2318
|
// Ticket #126: where we are in the transcript, remembered BEFORE the
|
|
2230
2319
|
// message is interpreted.
|
|
2231
2320
|
//
|
|
@@ -2266,19 +2355,15 @@ class ClaudeSession {
|
|
|
2266
2355
|
this.refreshUsage();
|
|
2267
2356
|
}
|
|
2268
2357
|
else if (msg.subtype === 'status') {
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
text: compactResult === 'failed'
|
|
2279
|
-
? `Compaction failed: ${maskString(String(msg.compact_error ?? 'unknown')).slice(0, 300)}`
|
|
2280
|
-
: 'Conversation compacted',
|
|
2281
|
-
});
|
|
2358
|
+
this.noteStatus(msg);
|
|
2359
|
+
}
|
|
2360
|
+
else if (msg.subtype === 'compact_boundary') {
|
|
2361
|
+
// The summary is in place: this marker is the end of a
|
|
2362
|
+
// compaction whether or not a status said so first (#348) — the
|
|
2363
|
+
// CLI's own remote client reads it the same way.
|
|
2364
|
+
if (this.compacting) {
|
|
2365
|
+
this.compacting = false;
|
|
2366
|
+
this.emit({ type: 'compaction', phase: 'finished', ok: true });
|
|
2282
2367
|
}
|
|
2283
2368
|
}
|
|
2284
2369
|
else if (msg.subtype === 'api_retry') {
|
|
@@ -2410,6 +2495,10 @@ class ClaudeSession {
|
|
|
2410
2495
|
break;
|
|
2411
2496
|
}
|
|
2412
2497
|
case 'result': {
|
|
2498
|
+
// A turn's end is the end of any compaction it carried (#348):
|
|
2499
|
+
// the supervisor closes the pair on `turn_end`, and the flag here
|
|
2500
|
+
// has to agree, or the next «compacting» would be read as a repeat.
|
|
2501
|
+
this.compacting = false;
|
|
2413
2502
|
this.emit({
|
|
2414
2503
|
type: 'cost',
|
|
2415
2504
|
costUsd: msg.total_cost_usd,
|
|
@@ -2426,7 +2515,50 @@ class ClaudeSession {
|
|
|
2426
2515
|
// once, for exactly one result.
|
|
2427
2516
|
const aborted = this.aborting;
|
|
2428
2517
|
this.aborting = false;
|
|
2429
|
-
|
|
2518
|
+
/**
|
|
2519
|
+
* The CLI's own word for why it stopped — and the one signal that
|
|
2520
|
+
* it gave up (#406).
|
|
2521
|
+
*
|
|
2522
|
+
* `subtype` alone is not that signal: on 12.09.2026 Claude Code
|
|
2523
|
+
* wrote «API Error: Connection lost mid-response», ended the turn,
|
|
2524
|
+
* and stamped the result `subtype: "success"` with
|
|
2525
|
+
* `terminal_reason: "api_error"` beside it. The runner read the
|
|
2526
|
+
* first field, reported a clean turn, and the session stood for
|
|
2527
|
+
* 5 h 17 min with «your turn» on it.
|
|
2528
|
+
*
|
|
2529
|
+
* Deliberately NOT the accumulated `turnFailureCode`: Claude Code
|
|
2530
|
+
* survives most stream cuts by itself, and a code left over from
|
|
2531
|
+
* one it survived would fail three healthy turns an hour (the
|
|
2532
|
+
* correction on §492, ticket #406's second comment).
|
|
2533
|
+
*/
|
|
2534
|
+
const terminalReason = typeof msg.terminal_reason === 'string'
|
|
2535
|
+
? msg.terminal_reason
|
|
2536
|
+
: null;
|
|
2537
|
+
/**
|
|
2538
|
+
* The other half of the turn boundary above (#406).
|
|
2539
|
+
*
|
|
2540
|
+
* The guard in the read loop forgets the last turn on the first
|
|
2541
|
+
* message that is not a `result` — which a turn the CLI woke and
|
|
2542
|
+
* that said NOTHING never sends. Its only message is its own
|
|
2543
|
+
* closing `result`, and `origin.kind` is the field the SDK marks it
|
|
2544
|
+
* with; #373 already treats that field as the one thing that tells a
|
|
2545
|
+
* genuinely new internal turn from a second ending of the last one.
|
|
2546
|
+
* Without this, a silent internal turn would report the `produced`
|
|
2547
|
+
* and `irreversible` of whatever ran before it.
|
|
2548
|
+
*/
|
|
2549
|
+
const originKind = typeof msg.origin?.kind === 'string'
|
|
2550
|
+
? msg.origin.kind
|
|
2551
|
+
: null;
|
|
2552
|
+
if (this.turnClosed && originKind === 'task-notification') {
|
|
2553
|
+
this.turnClosed = false;
|
|
2554
|
+
this.beginTurnFacts();
|
|
2555
|
+
}
|
|
2556
|
+
const gaveUp = msg.subtype === 'success' && terminalReason === 'api_error';
|
|
2557
|
+
const failure = gaveUp
|
|
2558
|
+
? API_ERROR_TURN_MESSAGE
|
|
2559
|
+
: msg.subtype === 'success'
|
|
2560
|
+
? ''
|
|
2561
|
+
: classifyError(msg.subtype, msg.errors);
|
|
2430
2562
|
// #373, plan stage D. What the incident could not answer: the second
|
|
2431
2563
|
// result's origin was never recorded, so «is this the same turn
|
|
2432
2564
|
// twice or a turn the CLI started by itself» had no evidence either
|
|
@@ -2452,10 +2584,13 @@ class ClaudeSession {
|
|
|
2452
2584
|
code: 'rewind_failed',
|
|
2453
2585
|
});
|
|
2454
2586
|
}
|
|
2455
|
-
else if (msg.subtype === 'success'
|
|
2456
|
-
//
|
|
2457
|
-
//
|
|
2458
|
-
//
|
|
2587
|
+
else if (aborted || (msg.subtype === 'success' && !gaveUp)) {
|
|
2588
|
+
// `aborted` is tested FIRST, and stays ahead of `gaveUp`: a turn
|
|
2589
|
+
// the user stopped is not a failed turn whatever the CLI writes
|
|
2590
|
+
// beside it, and an abort can land on top of an API error.
|
|
2591
|
+
// Reporting a stop as a failure moved the session to FAILED,
|
|
2592
|
+
// which is terminal — pressing Stop cost people the session they
|
|
2593
|
+
// meant to keep.
|
|
2459
2594
|
this.emit({
|
|
2460
2595
|
type: 'turn_end',
|
|
2461
2596
|
ok: true,
|
|
@@ -2487,7 +2622,17 @@ class ClaudeSession {
|
|
|
2487
2622
|
// the closed enum rides on the assistant message, this `result`
|
|
2488
2623
|
// has no such field. Handed over so the supervisor can decide
|
|
2489
2624
|
// from a code rather than from the sentence in `errorMessage`.
|
|
2490
|
-
|
|
2625
|
+
//
|
|
2626
|
+
// #406: a turn the CLI gave up on always carries a code, even
|
|
2627
|
+
// when nothing in it named one — `server_error` is what «their
|
|
2628
|
+
// side, try again» is spelled as, and without it `classifyFailure`
|
|
2629
|
+
// would have only the sentence to go on, which is the thing #252
|
|
2630
|
+
// took away from it.
|
|
2631
|
+
...(this.turnFailureCode !== null
|
|
2632
|
+
? { failureCode: this.turnFailureCode }
|
|
2633
|
+
: gaveUp
|
|
2634
|
+
? { failureCode: 'server_error' }
|
|
2635
|
+
: {}),
|
|
2491
2636
|
...(this.turnFailureStatus !== null
|
|
2492
2637
|
? { failureStatus: this.turnFailureStatus }
|
|
2493
2638
|
: {}),
|
|
@@ -2496,6 +2641,11 @@ class ClaudeSession {
|
|
|
2496
2641
|
});
|
|
2497
2642
|
this.refreshUsage();
|
|
2498
2643
|
}
|
|
2644
|
+
// #406: the facts above have been reported, so the next message —
|
|
2645
|
+
// whoever starts the turn it belongs to — may forget them. Armed
|
|
2646
|
+
// here rather than cleared here, because a second `result` for this
|
|
2647
|
+
// same ending has to read exactly what the first one did.
|
|
2648
|
+
this.turnClosed = true;
|
|
2499
2649
|
break;
|
|
2500
2650
|
}
|
|
2501
2651
|
default:
|
|
@@ -2671,6 +2821,15 @@ function stringifyContent(content) {
|
|
|
2671
2821
|
}
|
|
2672
2822
|
return content === undefined ? '' : JSON.stringify(content);
|
|
2673
2823
|
}
|
|
2824
|
+
/**
|
|
2825
|
+
* What the feed says about a turn Claude Code gave up on (#406).
|
|
2826
|
+
*
|
|
2827
|
+
* Deliberately free of «mid-response» and «may be incomplete»: `refine()` in
|
|
2828
|
+
* `error-policy.ts` matches those two phrases and narrows a retry to a single
|
|
2829
|
+
* «carry on» attempt. Here the narrowing must come from `produced`, which is a
|
|
2830
|
+
* fact about the turn, and not from the wording of a sentence.
|
|
2831
|
+
*/
|
|
2832
|
+
const API_ERROR_TURN_MESSAGE = 'The connection to the model failed and Claude Code ended the turn on an API error';
|
|
2674
2833
|
function classifyError(subtype, errors) {
|
|
2675
2834
|
// Optional on purpose despite the SDK's type: a `result` without `errors`
|
|
2676
2835
|
// threw from inside the event pump, which the loop's catch turned into a
|
|
@@ -2745,6 +2904,23 @@ export class ClaudeAdapter {
|
|
|
2745
2904
|
this.queryFn = queryFn;
|
|
2746
2905
|
}
|
|
2747
2906
|
startSession(spec) {
|
|
2907
|
+
/**
|
|
2908
|
+
* «There is no Claude here» is answered once, and here (#395).
|
|
2909
|
+
*
|
|
2910
|
+
* BEFORE the session object exists, and that placement is the point: the
|
|
2911
|
+
* constructor writes the MCP config — a 0600 file holding this workspace's
|
|
2912
|
+
* live key — before it builds `Options`, and nothing removes that file for
|
|
2913
|
+
* a constructor that threw. The supervisor turns this throw into one red
|
|
2914
|
+
* line in the feed (`launchCrashed`) and a FAILED session, which is the
|
|
2915
|
+
* honest answer when the agent is not on the machine.
|
|
2916
|
+
*
|
|
2917
|
+
* Only for the real SDK. An adapter built on an injected `query` is a test
|
|
2918
|
+
* or a probe: it spawns nothing, so the fallback this guards against cannot
|
|
2919
|
+
* happen — and asking anyway would fail the whole suite on every machine
|
|
2920
|
+
* without the CLI, CI included. That is how the 0.56.0 release broke.
|
|
2921
|
+
*/
|
|
2922
|
+
if (this.queryFn === query)
|
|
2923
|
+
assertClaudeInstalled();
|
|
2748
2924
|
return new ClaudeSession(spec, this.queryFn);
|
|
2749
2925
|
}
|
|
2750
2926
|
}
|
package/dist/adapters/codex.js
CHANGED
|
@@ -876,6 +876,7 @@ class CodexSession {
|
|
|
876
876
|
return false;
|
|
877
877
|
try {
|
|
878
878
|
await this.client.request('thread/compact/start', { threadId: this.threadId });
|
|
879
|
+
this.requestedCompaction = true;
|
|
879
880
|
return true;
|
|
880
881
|
}
|
|
881
882
|
catch (error) {
|
|
@@ -883,6 +884,14 @@ class CodexSession {
|
|
|
883
884
|
return false;
|
|
884
885
|
}
|
|
885
886
|
}
|
|
887
|
+
/**
|
|
888
|
+
* A compaction THIS adapter asked for with `thread/compact/start` (#348) —
|
|
889
|
+
* the one kind that runs outside any turn and therefore has nothing to
|
|
890
|
+
* settle the session status behind it. The agent's own compaction inside a
|
|
891
|
+
* turn is not it, even when its item happens to complete before the
|
|
892
|
+
* `turn/started` that announces the turn.
|
|
893
|
+
*/
|
|
894
|
+
requestedCompaction = false;
|
|
886
895
|
stop(reason = 'session_stopped') {
|
|
887
896
|
if (this.stopped)
|
|
888
897
|
return;
|
|
@@ -1472,8 +1481,19 @@ class CodexSession {
|
|
|
1472
1481
|
return;
|
|
1473
1482
|
}
|
|
1474
1483
|
case 'contextCompaction': {
|
|
1475
|
-
|
|
1476
|
-
|
|
1484
|
+
// #348: an event, not a notice — see `AgentEvent['compaction']`. A
|
|
1485
|
+
// compaction started from the dashboard runs outside any turn on this
|
|
1486
|
+
// agent (`thread/compact/start` creates none), so nothing will settle
|
|
1487
|
+
// the status behind it; `standalone` tells the supervisor to.
|
|
1488
|
+
if (method === 'item/started') {
|
|
1489
|
+
this.emit({ type: 'compaction', phase: 'started' });
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
if (done) {
|
|
1493
|
+
const standalone = this.requestedCompaction && this.activeTurnId === null;
|
|
1494
|
+
this.requestedCompaction = false;
|
|
1495
|
+
this.emit({ type: 'compaction', phase: 'finished', ok: true, standalone });
|
|
1496
|
+
}
|
|
1477
1497
|
return;
|
|
1478
1498
|
}
|
|
1479
1499
|
case 'webSearch':
|
|
@@ -110,6 +110,7 @@ const INVALIDATION_PHRASES = {
|
|
|
110
110
|
turn_aborted: 'the turn was interrupted',
|
|
111
111
|
agent_cancelled: 'the agent withdrew it',
|
|
112
112
|
budget_spent: 'the session ran out of its allowed working time',
|
|
113
|
+
not_held: 'the agent no longer holds this question',
|
|
113
114
|
};
|
|
114
115
|
/** The question was taken away from the user — say why, never fake an answer. */
|
|
115
116
|
export function invalidationMessage(reason) {
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -366,7 +366,16 @@ export interface AgentQuestionAnswer {
|
|
|
366
366
|
notes?: string;
|
|
367
367
|
}
|
|
368
368
|
/** Why a question was taken away from the user without them answering it. */
|
|
369
|
-
export type QuestionInvalidationReason = 'session_stopped' | 'session_parked' | 'runner_restarted' | 'turn_aborted' | 'agent_cancelled' | 'budget_spent'
|
|
369
|
+
export type QuestionInvalidationReason = 'session_stopped' | 'session_parked' | 'runner_restarted' | 'turn_aborted' | 'agent_cancelled' | 'budget_spent'
|
|
370
|
+
/**
|
|
371
|
+
* An answer arrived for a card no live process holds (#392): the card
|
|
372
|
+
* outlived the process that asked, or the ask was withdrawn before the
|
|
373
|
+
* answer got here. The runner does not know which, and says only what it
|
|
374
|
+
* does know. Never handed to an agent — there is no agent holding the ask
|
|
375
|
+
* to hand it to — so its phrase in `INVALIDATION_PHRASES` is for the type
|
|
376
|
+
* checker; the words a person reads are the dashboard's.
|
|
377
|
+
*/
|
|
378
|
+
| 'not_held';
|
|
370
379
|
/** The dashboard's answer to one open ask. */
|
|
371
380
|
export interface QuestionReply {
|
|
372
381
|
askId: string;
|
|
@@ -502,6 +511,38 @@ export type AgentEvent = {
|
|
|
502
511
|
type: 'notice';
|
|
503
512
|
level: 'info' | 'warn';
|
|
504
513
|
text: string;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* The conversation is being folded into a summary, or just was (#348).
|
|
517
|
+
*
|
|
518
|
+
* Its own event and not a `notice` on purpose: adapter notices are
|
|
519
|
+
* de-duplicated per session on their text (gotcha §148), so «Conversation
|
|
520
|
+
* compacted» reached the feed once and every later compaction was silence.
|
|
521
|
+
* A compaction is a fact the feed has to hear every time — the dashboard's
|
|
522
|
+
* «compacting…» indicator is read off the feed, and a second compaction
|
|
523
|
+
* with no lines is a second compaction the person cannot see end.
|
|
524
|
+
*
|
|
525
|
+
* `standalone` on the ending: `true` when NO turn is going to close behind
|
|
526
|
+
* this compaction, so the supervisor has to settle the session status
|
|
527
|
+
* itself. Codex runs a compaction started from the dashboard outside any
|
|
528
|
+
* turn (`thread/compact/start`); Claude delivers `/compact` as a turn, and
|
|
529
|
+
* that turn's own ending settles the status the ordinary way.
|
|
530
|
+
*/
|
|
531
|
+
| {
|
|
532
|
+
type: 'compaction';
|
|
533
|
+
phase: 'started';
|
|
534
|
+
} | {
|
|
535
|
+
type: 'compaction';
|
|
536
|
+
phase: 'finished';
|
|
537
|
+
ok: boolean;
|
|
538
|
+
error?: string;
|
|
539
|
+
standalone?: boolean;
|
|
540
|
+
/**
|
|
541
|
+
* It ended without a summary: the CLI decided not to compact after all
|
|
542
|
+
* (a PreCompact hook said no, or it found nothing worth folding). Not a
|
|
543
|
+
* failure — nothing broke — and not a compaction either.
|
|
544
|
+
*/
|
|
545
|
+
skipped?: boolean;
|
|
505
546
|
} | {
|
|
506
547
|
type: 'cost';
|
|
507
548
|
costUsd: number;
|
package/dist/agent-binary.d.ts
CHANGED
|
@@ -62,13 +62,54 @@ export declare const USE_BUNDLED_CLAUDE = false;
|
|
|
62
62
|
* the tests also need the real behaviour of; production always takes the default.
|
|
63
63
|
*/
|
|
64
64
|
export declare function sessionClaudePath(useBundled?: boolean): string | null;
|
|
65
|
+
/**
|
|
66
|
+
* What every caller says when Claude Code is not on this machine (#395).
|
|
67
|
+
*
|
|
68
|
+
* Word for word the API's own refusal (`assertAgentAvailable` in
|
|
69
|
+
* `dev-sessions.service.ts`): the two halves answer the same question about the
|
|
70
|
+
* same binary, and a person who meets both — the API when it can see the
|
|
71
|
+
* measurement, this one when it cannot — should not have to work out whether
|
|
72
|
+
* they are being told two different things.
|
|
73
|
+
*/
|
|
74
|
+
export declare const CLAUDE_NOT_INSTALLED = "Claude Code is not installed on this server \u2014 install it there, or pick another agent";
|
|
75
|
+
/** Thrown where a run would otherwise start on a binary nobody chose. */
|
|
76
|
+
export declare class ClaudeNotInstalledError extends Error {
|
|
77
|
+
constructor();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Refuse, in words, when a real Claude run has nothing legitimate to start.
|
|
81
|
+
*
|
|
82
|
+
* ## The hole this closes (#395, decision D14)
|
|
83
|
+
*
|
|
84
|
+
* With the bundled binary switched off and no system `claude`,
|
|
85
|
+
* `claudeExecutableOption` has nothing to pin and returns `{}`. To the SDK an
|
|
86
|
+
* absent `pathToClaudeCodeExecutable` does not mean «there is no Claude» — it
|
|
87
|
+
* means «resolve your own». So the machine either ran sessions on a SECOND
|
|
88
|
+
* Claude of a different version while its card said «not installed» (five
|
|
89
|
+
* sessions on `vmi3024903`, one of them $93), or — on a machine installed the
|
|
90
|
+
* standard way, where `--omit=optional` means the bundled package is not there
|
|
91
|
+
* at all — died inside the SDK with `Native CLI binary for linux-x64 not found`
|
|
92
|
+
* before its first word.
|
|
93
|
+
*
|
|
94
|
+
* ## Why it is a separate call and not a throw from `claudeExecutableOption`
|
|
95
|
+
*
|
|
96
|
+
* Both places that build `Options` do so eagerly, including in tests, where the
|
|
97
|
+
* SDK is replaced by a fake `query` and no binary is ever spawned. A throw from
|
|
98
|
+
* the option builder would therefore fail the whole suite on any machine
|
|
99
|
+
* without the CLI — CI among them, which is exactly how the 0.56.0 release
|
|
100
|
+
* broke. The callers below ask this question only on the path that reaches the
|
|
101
|
+
* REAL SDK.
|
|
102
|
+
*/
|
|
103
|
+
export declare function assertClaudeInstalled(useBundled?: boolean,
|
|
104
|
+
/** Where the binary is looked up. Production always takes the default. */
|
|
105
|
+
resolve?: (useBundled: boolean) => string | null): void;
|
|
65
106
|
/**
|
|
66
107
|
* The SDK option that pins the executable, for both places that build `Options`.
|
|
67
108
|
*
|
|
68
|
-
* Empty while the bundled binary is in use: the SDK then resolves its own,
|
|
69
|
-
* is exactly
|
|
70
|
-
*
|
|
71
|
-
*
|
|
109
|
+
* Empty while the bundled binary is in use: the SDK then resolves its own,
|
|
110
|
+
* which is exactly what `USE_BUNDLED_CLAUDE = true` asks for. Empty also when
|
|
111
|
+
* there is no system binary — and that emptiness is no longer allowed to reach
|
|
112
|
+
* the real SDK: `assertClaudeInstalled` is asked first, on both paths.
|
|
72
113
|
*/
|
|
73
114
|
export declare function claudeExecutableOption(useBundled?: boolean): {
|
|
74
115
|
pathToClaudeCodeExecutable?: string;
|
package/dist/agent-binary.js
CHANGED
|
@@ -137,13 +137,62 @@ export const USE_BUNDLED_CLAUDE = false;
|
|
|
137
137
|
export function sessionClaudePath(useBundled = USE_BUNDLED_CLAUDE) {
|
|
138
138
|
return useBundled ? claudeCliPath() : whichExecutable(AGENT_RUNTIMES.claude.bin);
|
|
139
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* What every caller says when Claude Code is not on this machine (#395).
|
|
142
|
+
*
|
|
143
|
+
* Word for word the API's own refusal (`assertAgentAvailable` in
|
|
144
|
+
* `dev-sessions.service.ts`): the two halves answer the same question about the
|
|
145
|
+
* same binary, and a person who meets both — the API when it can see the
|
|
146
|
+
* measurement, this one when it cannot — should not have to work out whether
|
|
147
|
+
* they are being told two different things.
|
|
148
|
+
*/
|
|
149
|
+
export const CLAUDE_NOT_INSTALLED = 'Claude Code is not installed on this server — install it there, or pick another agent';
|
|
150
|
+
/** Thrown where a run would otherwise start on a binary nobody chose. */
|
|
151
|
+
export class ClaudeNotInstalledError extends Error {
|
|
152
|
+
constructor() {
|
|
153
|
+
super(CLAUDE_NOT_INSTALLED);
|
|
154
|
+
this.name = 'ClaudeNotInstalledError';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Refuse, in words, when a real Claude run has nothing legitimate to start.
|
|
159
|
+
*
|
|
160
|
+
* ## The hole this closes (#395, decision D14)
|
|
161
|
+
*
|
|
162
|
+
* With the bundled binary switched off and no system `claude`,
|
|
163
|
+
* `claudeExecutableOption` has nothing to pin and returns `{}`. To the SDK an
|
|
164
|
+
* absent `pathToClaudeCodeExecutable` does not mean «there is no Claude» — it
|
|
165
|
+
* means «resolve your own». So the machine either ran sessions on a SECOND
|
|
166
|
+
* Claude of a different version while its card said «not installed» (five
|
|
167
|
+
* sessions on `vmi3024903`, one of them $93), or — on a machine installed the
|
|
168
|
+
* standard way, where `--omit=optional` means the bundled package is not there
|
|
169
|
+
* at all — died inside the SDK with `Native CLI binary for linux-x64 not found`
|
|
170
|
+
* before its first word.
|
|
171
|
+
*
|
|
172
|
+
* ## Why it is a separate call and not a throw from `claudeExecutableOption`
|
|
173
|
+
*
|
|
174
|
+
* Both places that build `Options` do so eagerly, including in tests, where the
|
|
175
|
+
* SDK is replaced by a fake `query` and no binary is ever spawned. A throw from
|
|
176
|
+
* the option builder would therefore fail the whole suite on any machine
|
|
177
|
+
* without the CLI — CI among them, which is exactly how the 0.56.0 release
|
|
178
|
+
* broke. The callers below ask this question only on the path that reaches the
|
|
179
|
+
* REAL SDK.
|
|
180
|
+
*/
|
|
181
|
+
export function assertClaudeInstalled(useBundled = USE_BUNDLED_CLAUDE,
|
|
182
|
+
/** Where the binary is looked up. Production always takes the default. */
|
|
183
|
+
resolve = sessionClaudePath) {
|
|
184
|
+
if (useBundled)
|
|
185
|
+
return;
|
|
186
|
+
if (!resolve(useBundled))
|
|
187
|
+
throw new ClaudeNotInstalledError();
|
|
188
|
+
}
|
|
140
189
|
/**
|
|
141
190
|
* The SDK option that pins the executable, for both places that build `Options`.
|
|
142
191
|
*
|
|
143
|
-
* Empty while the bundled binary is in use: the SDK then resolves its own,
|
|
144
|
-
* is exactly
|
|
145
|
-
*
|
|
146
|
-
*
|
|
192
|
+
* Empty while the bundled binary is in use: the SDK then resolves its own,
|
|
193
|
+
* which is exactly what `USE_BUNDLED_CLAUDE = true` asks for. Empty also when
|
|
194
|
+
* there is no system binary — and that emptiness is no longer allowed to reach
|
|
195
|
+
* the real SDK: `assertClaudeInstalled` is asked first, on both paths.
|
|
147
196
|
*/
|
|
148
197
|
export function claudeExecutableOption(useBundled = USE_BUNDLED_CLAUDE) {
|
|
149
198
|
if (useBundled)
|
package/dist/attachments.d.ts
CHANGED
|
@@ -56,6 +56,10 @@ export declare function ensureGitExclude(worktreePath: string): Promise<void>;
|
|
|
56
56
|
* Returns what actually landed — a file that could not be fetched is reported
|
|
57
57
|
* and skipped rather than failing the whole message, because the text the user
|
|
58
58
|
* typed is usually still worth delivering.
|
|
59
|
+
*
|
|
60
|
+
* Each file gets up to `DOWNLOAD_ATTEMPTS` goes inside ONE `DOWNLOAD_TIMEOUT_MS`
|
|
61
|
+
* budget (#407): the ceiling on how long a person waits for their own message is
|
|
62
|
+
* unchanged, and the attempts share it rather than each getting their own.
|
|
59
63
|
*/
|
|
60
64
|
export declare function saveAttachments(input: {
|
|
61
65
|
worktreePath: string;
|
|
@@ -63,6 +67,8 @@ export declare function saveAttachments(input: {
|
|
|
63
67
|
token: string;
|
|
64
68
|
attachments: RemoteAttachment[];
|
|
65
69
|
fetchImpl?: typeof fetch;
|
|
70
|
+
/** Test seam: the pauses between attempts, so a suite does not sit through them. */
|
|
71
|
+
sleepImpl?: (ms: number) => Promise<void>;
|
|
66
72
|
}): Promise<{
|
|
67
73
|
saved: SavedAttachment[];
|
|
68
74
|
failed: string[];
|