@aragon-agent/cli 0.6.2 → 0.6.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 +321 -0
- package/README.md +297 -2
- package/dist/agent/controller.d.ts +193 -1
- package/dist/agent/controller.js +475 -2
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +47 -0
- package/dist/agent/reducer.d.ts +189 -0
- package/dist/agent/reducer.js +301 -2
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +297 -16
- package/dist/commands/builtins.js +165 -24
- package/dist/commands/registry.d.ts +27 -0
- package/dist/compaction/archive.d.ts +175 -0
- package/dist/compaction/archive.js +267 -0
- package/dist/compaction/child.d.ts +97 -0
- package/dist/compaction/child.js +118 -0
- package/dist/compaction/command.d.ts +41 -0
- package/dist/compaction/command.js +355 -0
- package/dist/compaction/compactor.d.ts +302 -0
- package/dist/compaction/compactor.js +810 -0
- package/dist/compaction/digest.d.ts +62 -0
- package/dist/compaction/digest.js +164 -0
- package/dist/compaction/limits.d.ts +165 -0
- package/dist/compaction/limits.js +172 -0
- package/dist/compaction/pressure.d.ts +135 -0
- package/dist/compaction/pressure.js +167 -0
- package/dist/compaction/summarize-call.d.ts +70 -0
- package/dist/compaction/summarize-call.js +86 -0
- package/dist/compaction/summary-prompt.d.ts +85 -0
- package/dist/compaction/summary-prompt.js +197 -0
- package/dist/compaction/tail-budget.d.ts +52 -0
- package/dist/compaction/tail-budget.js +67 -0
- package/dist/compaction/types.d.ts +215 -0
- package/dist/compaction/types.js +22 -0
- package/dist/compaction/wiring.d.ts +264 -0
- package/dist/compaction/wiring.js +577 -0
- package/dist/config/app-paths.d.ts +14 -0
- package/dist/config/app-paths.js +16 -0
- package/dist/config/cli-commands.d.ts +41 -0
- package/dist/config/cli-commands.js +77 -1
- package/dist/config/env.d.ts +5 -2
- package/dist/config/env.js +140 -3
- package/dist/config/load.d.ts +42 -0
- package/dist/config/load.js +101 -1
- package/dist/config/schema.d.ts +244 -0
- package/dist/config/schema.js +170 -0
- package/dist/config/store.js +37 -1
- package/dist/config/ui-state.d.ts +28 -1
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +50 -1
- package/dist/exec/runner.js +42 -0
- package/dist/input/limits.d.ts +72 -0
- package/dist/input/limits.js +63 -0
- package/dist/input/mouse-events.d.ts +31 -5
- package/dist/input/mouse-events.js +86 -27
- package/dist/input/paste-parse.d.ts +71 -0
- package/dist/input/paste-parse.js +95 -0
- package/dist/input/stdin-filter.d.ts +81 -0
- package/dist/input/stdin-filter.js +379 -0
- package/dist/logging/install.d.ts +53 -1
- package/dist/logging/install.js +81 -1
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/classify.d.ts +52 -0
- package/dist/proc/classify.js +143 -0
- package/dist/proc/kill-tree.d.ts +42 -0
- package/dist/proc/kill-tree.js +119 -0
- package/dist/proc/limits.d.ts +84 -0
- package/dist/proc/limits.js +84 -0
- package/dist/proc/log-ring.d.ts +68 -0
- package/dist/proc/log-ring.js +102 -0
- package/dist/proc/prompt.d.ts +24 -0
- package/dist/proc/prompt.js +49 -0
- package/dist/proc/readiness.d.ts +102 -0
- package/dist/proc/readiness.js +219 -0
- package/dist/proc/supervisor.d.ts +103 -0
- package/dist/proc/supervisor.js +465 -0
- package/dist/proc/types.d.ts +175 -0
- package/dist/proc/types.js +15 -0
- package/dist/session/persist.js +55 -1
- package/dist/team/report.js +7 -1
- package/dist/team/runtime.d.ts +10 -0
- package/dist/team/runtime.js +7 -0
- package/dist/team/subagent.d.ts +31 -1
- package/dist/team/subagent.js +42 -15
- package/dist/team/types.d.ts +8 -0
- package/dist/tools/bash-tool.d.ts +51 -2
- package/dist/tools/bash-tool.js +321 -51
- package/dist/tools/fs-tools.d.ts +17 -0
- package/dist/tools/index.d.ts +55 -2
- package/dist/tools/index.js +39 -1
- package/dist/tools/proc-tools.d.ts +25 -0
- package/dist/tools/proc-tools.js +102 -0
- package/dist/ui/ActivityLine.d.ts +19 -1
- package/dist/ui/ActivityLine.js +10 -2
- package/dist/ui/App.d.ts +62 -2
- package/dist/ui/App.js +546 -16
- package/dist/ui/Composer.d.ts +66 -1
- package/dist/ui/Composer.js +50 -6
- package/dist/ui/PromptInput.d.ts +82 -3
- package/dist/ui/PromptInput.js +232 -49
- package/dist/ui/StatusBar.d.ts +59 -0
- package/dist/ui/StatusBar.js +16 -4
- package/dist/ui/TeamPanel.js +7 -1
- package/dist/ui/Transcript.d.ts +21 -1
- package/dist/ui/Transcript.js +61 -3
- package/dist/ui/clipboard.d.ts +52 -0
- package/dist/ui/clipboard.js +91 -0
- package/dist/ui/composer-limits.d.ts +31 -0
- package/dist/ui/composer-limits.js +50 -0
- package/dist/ui/composer-rows.d.ts +73 -0
- package/dist/ui/composer-rows.js +201 -0
- package/dist/ui/editor-reducer.d.ts +37 -0
- package/dist/ui/editor-reducer.js +88 -12
- package/dist/ui/entries/CompactionCard.d.ts +75 -0
- package/dist/ui/entries/CompactionCard.js +158 -0
- package/dist/ui/entries/ServiceCard.d.ts +66 -0
- package/dist/ui/entries/ServiceCard.js +116 -0
- package/dist/ui/entries/UserEntry.d.ts +17 -3
- package/dist/ui/entries/UserEntry.js +34 -5
- package/dist/ui/frame-differ.d.ts +46 -0
- package/dist/ui/frame-differ.js +94 -10
- package/dist/ui/gauge.d.ts +16 -1
- package/dist/ui/gauge.js +25 -6
- package/dist/ui/glyphs.d.ts +11 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/ScrollViewport.d.ts +43 -6
- package/dist/ui/layout/ScrollViewport.js +128 -21
- package/dist/ui/layout/budget.d.ts +44 -9
- package/dist/ui/layout/budget.js +42 -10
- package/dist/ui/layout/follow-state.d.ts +137 -0
- package/dist/ui/layout/follow-state.js +150 -0
- package/dist/ui/layout/virtual-window.d.ts +11 -0
- package/dist/ui/layout/virtual-window.js +85 -1
- package/dist/ui/overlays/HelpOverlay.js +9 -1
- package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
- package/dist/ui/overlays/QuestionOverlay.js +5 -1
- package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
- package/dist/ui/overlays/SettingsScreen.js +74 -2
- package/dist/ui/palettes.d.ts +16 -0
- package/dist/ui/palettes.js +6 -0
- package/dist/ui/paste-frames.d.ts +87 -0
- package/dist/ui/paste-frames.js +112 -0
- package/dist/ui/paste-tokens.d.ts +90 -0
- package/dist/ui/paste-tokens.js +147 -0
- package/dist/ui/screen.d.ts +38 -0
- package/dist/ui/screen.js +113 -8
- package/dist/ui/selection/highlight.d.ts +39 -0
- package/dist/ui/selection/highlight.js +64 -0
- package/dist/ui/selection/screen-mirror.d.ts +24 -0
- package/dist/ui/selection/screen-mirror.js +35 -0
- package/dist/ui/selection/selection-controller.d.ts +88 -0
- package/dist/ui/selection/selection-controller.js +282 -0
- package/dist/ui/selection/selection.d.ts +90 -0
- package/dist/ui/selection/selection.js +223 -0
- package/dist/ui/stdout-frame-writer.d.ts +33 -0
- package/dist/ui/stdout-frame-writer.js +15 -0
- package/dist/ui/theme.d.ts +11 -0
- package/dist/ui/theme.js +2 -0
- package/dist/ui/transcript-text.js +72 -0
- package/dist/ui/use-startup-notices.d.ts +27 -1
- package/dist/ui/use-startup-notices.js +43 -11
- package/dist/ui/use-wheel-routing.d.ts +1 -1
- package/dist/ui/use-wheel-routing.js +8 -0
- package/package.json +4 -2
- package/dist/input/stdin-mouse-filter.d.ts +0 -42
- package/dist/input/stdin-mouse-filter.js +0 -153
package/dist/agent/headless.js
CHANGED
|
@@ -167,6 +167,52 @@ export async function runHeadless(controller, prompt, options = {}) {
|
|
|
167
167
|
total.inputTokens += event.usage.inputTokens;
|
|
168
168
|
total.outputTokens += event.usage.outputTokens;
|
|
169
169
|
});
|
|
170
|
+
// Context compaction (context-auto-compaction §4.7).
|
|
171
|
+
//
|
|
172
|
+
// TWO STDERR LINES, AND UNLIKE THE FAST TIER THEY ARE WRITTEN (D-12's
|
|
173
|
+
// reasoning does not carry over). A review is an unrequested second opinion
|
|
174
|
+
// that changes nothing; a compaction PERMANENTLY DROPS MESSAGES from the
|
|
175
|
+
// conversation, and a `-p` run whose middle third vanished with no line
|
|
176
|
+
// anywhere saying so is not a compact run, it is an unexplained one — the same
|
|
177
|
+
// argument the retry lines above make for a three-minute pause.
|
|
178
|
+
//
|
|
179
|
+
// stderr, never stdout: stdout is the machine-readable answer and has to stay
|
|
180
|
+
// parseable by `aragon -p | jq`. Not `--quiet`-gated, for the reason the retry
|
|
181
|
+
// line is not.
|
|
182
|
+
//
|
|
183
|
+
// The summary text is NOT written (D-18): it can be tens of thousands of
|
|
184
|
+
// characters, and no consumer of `-p` asked for it.
|
|
185
|
+
const unsubscribeCompaction = controller.subscribeCompaction?.((event) => {
|
|
186
|
+
if (event.type === 'usage') {
|
|
187
|
+
total.inputTokens += event.usage.inputTokens;
|
|
188
|
+
total.outputTokens += event.usage.outputTokens;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (event.type !== 'compaction_end')
|
|
192
|
+
return;
|
|
193
|
+
const r = event.record;
|
|
194
|
+
if (!r.applied) {
|
|
195
|
+
err.write(`\n[compaction] skipped: ${r.reason ?? 'unknown'}\n`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const secs = (r.durationMs / 1000).toFixed(1);
|
|
199
|
+
const how = r.mode === 'truncated'
|
|
200
|
+
? ' WITHOUT a summary (summarize failed)'
|
|
201
|
+
: r.mode === 'relieved'
|
|
202
|
+
? ' by clipping the retained turns (nothing could be dropped)'
|
|
203
|
+
: '';
|
|
204
|
+
err.write(`\n[compaction] ${formatTokens(r.tokensBefore)} -> ${formatTokens(r.tokensAfter)} tokens, ` +
|
|
205
|
+
`${r.messagesBefore} -> ${r.messagesAfter} messages${how} (${secs}s)\n`);
|
|
206
|
+
// ONE MORE LINE, AND ONLY WHEN RELIEF FIRED (§4.5). A clip inside the turns
|
|
207
|
+
// the run is about to continue from is a real data loss, and a `-p` run whose
|
|
208
|
+
// recent tool output was silently shortened is an unexplained run - the same
|
|
209
|
+
// argument the compaction lines above make for a vanished middle third.
|
|
210
|
+
if (r.tailRelief) {
|
|
211
|
+
err.write(`[compaction] clipped ${r.tailRelief.messages} tool ` +
|
|
212
|
+
`${r.tailRelief.messages === 1 ? 'result' : 'results'} in the retained turns ` +
|
|
213
|
+
`(${formatTokens(r.tailRelief.charsRemoved)} chars)\n`);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
170
216
|
// --- The bounded continuation loop (todo-plan-followthrough §3.6). --------
|
|
171
217
|
//
|
|
172
218
|
// THE THREE `unsubscribe` CALLS STAY OUTSIDE IT (rule 4). The loop wraps the
|
|
@@ -227,6 +273,7 @@ export async function runHeadless(controller, prompt, options = {}) {
|
|
|
227
273
|
unsubscribeTeam?.();
|
|
228
274
|
unsubscribeTodos?.();
|
|
229
275
|
unsubscribeFast?.();
|
|
276
|
+
unsubscribeCompaction?.();
|
|
230
277
|
}
|
|
231
278
|
// Ensure a trailing newline so piped output is well-formed.
|
|
232
279
|
if (wroteText)
|
package/dist/agent/reducer.d.ts
CHANGED
|
@@ -14,7 +14,9 @@ import type { AgentEvent, ModelCost, ToolResult, TokenUsage } from '@aragon-agen
|
|
|
14
14
|
import type { AgentMode } from './agent-mode.js';
|
|
15
15
|
import type { DispatchOutcome, SubagentRun, SubagentSpec, TeamSnapshot } from '../team/types.js';
|
|
16
16
|
import type { FastReview, FastSnapshot } from '../fast/types.js';
|
|
17
|
+
import type { CompactionMode, CompactionRecord, CompactionSnapshot, CompactionUiTrigger } from '../compaction/types.js';
|
|
17
18
|
import type { TodoItem, TodoSnapshot } from '../todo/types.js';
|
|
19
|
+
import type { ServiceSnapshot, ServiceStatus } from '../proc/types.js';
|
|
18
20
|
import type { RetryPhase } from './retry-view.js';
|
|
19
21
|
import type { FilePatch } from '../tools/patch.js';
|
|
20
22
|
export type NoticeLevel = 'info' | 'warn' | 'error';
|
|
@@ -222,6 +224,107 @@ export type Entry = {
|
|
|
222
224
|
turn: number;
|
|
223
225
|
durationMs?: number;
|
|
224
226
|
live: boolean;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* One context compaction (context-auto-compaction §5.5). ONE CARD PER
|
|
230
|
+
* COMPACTION, rewritten once when the call returns — like the fast card, and
|
|
231
|
+
* unlike the todo card's per-turn checklist.
|
|
232
|
+
*
|
|
233
|
+
* JSON-SERIALIZABLE by construction, so `/save` and `/resume` need no format
|
|
234
|
+
* change.
|
|
235
|
+
*
|
|
236
|
+
* `live: true` is normalized away ON LOAD, in `session/persist.ts`, next to
|
|
237
|
+
* `team.active`, `todo.live`, the retry card's phase and `fast.live`, and for
|
|
238
|
+
* the identical reason (C-8): `Transcript`'s settled boundary is MONOTONIC, so
|
|
239
|
+
* an entry that never settles never reaches `<Static>` and is re-rendered on
|
|
240
|
+
* every frame for the rest of the session.
|
|
241
|
+
*
|
|
242
|
+
* EVERY STRING ON THIS CARD DESCRIBES THE HISTORY, NEVER THE SCREEN (P2-9).
|
|
243
|
+
* `/clear` empties the transcript view but not `messages`, so a compaction can
|
|
244
|
+
* legitimately fire against a visually empty transcript and report
|
|
245
|
+
* "112 -> 9 messages" with nine visible rows.
|
|
246
|
+
*/
|
|
247
|
+
| {
|
|
248
|
+
id: string;
|
|
249
|
+
kind: 'compaction';
|
|
250
|
+
/** 1-based within the session, so the card can name itself. */
|
|
251
|
+
index: number;
|
|
252
|
+
trigger: CompactionUiTrigger;
|
|
253
|
+
mode: CompactionMode;
|
|
254
|
+
applied: boolean;
|
|
255
|
+
reason?: string;
|
|
256
|
+
messagesBefore: number;
|
|
257
|
+
messagesAfter: number;
|
|
258
|
+
tokensBefore: number;
|
|
259
|
+
tokensAfter: number;
|
|
260
|
+
summary?: string;
|
|
261
|
+
model: string;
|
|
262
|
+
durationMs?: number;
|
|
263
|
+
/**
|
|
264
|
+
* When the live card opened, so the elapsed counter has an origin
|
|
265
|
+
* (context-auto-compaction-hardening §3.6 / W5).
|
|
266
|
+
*
|
|
267
|
+
* JSON-SERIALIZABLE like every other field here, and harmless on reload:
|
|
268
|
+
* `session/persist.ts` normalizes `live` to false, and a settled card
|
|
269
|
+
* renders `durationMs` rather than an elapsed figure.
|
|
270
|
+
*/
|
|
271
|
+
startedAt?: number;
|
|
272
|
+
/** Set when tail relief clipped the retained turns (W2). */
|
|
273
|
+
tailRelief?: {
|
|
274
|
+
messages: number;
|
|
275
|
+
charsRemoved: number;
|
|
276
|
+
};
|
|
277
|
+
live: boolean;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* One supervised background service (background-service-supervision §3.10).
|
|
281
|
+
*
|
|
282
|
+
* JSON-SERIALIZABLE by construction, so `/save` needs no format change.
|
|
283
|
+
*
|
|
284
|
+
* IT IS THE ONE LIVE CARD THAT DOES **NOT** BLOCK THE SETTLED BOUNDARY ONCE IT
|
|
285
|
+
* IS UP (D-11 / P0-4). Every other live clause in
|
|
286
|
+
* `Transcript.tsx::computeSettledCount` is bounded by an OPERATION — a
|
|
287
|
+
* dispatch, a turn, one LLM call, one countdown — and each carries a comment
|
|
288
|
+
* saying that an entry which never settles is re-rendered on every frame for
|
|
289
|
+
* the rest of the session. A service is bounded by the USER'S INTENT: a dev
|
|
290
|
+
* server left up for an hour is the normal case. So only `starting` blocks
|
|
291
|
+
* (bounded by `readyTimeoutMs`), a `ready`/`running` card commits its body to
|
|
292
|
+
* `<Static>`, and a later terminal transition appends a NEW one-row entry
|
|
293
|
+
* rather than trying to rewrite a card `<Static>` has already printed.
|
|
294
|
+
*
|
|
295
|
+
* `terminal: true` marks that second entry. `session/persist.ts` rewrites any
|
|
296
|
+
* restored non-terminal card to `stopped`, next to `team.active`, `todo.live`,
|
|
297
|
+
* the retry phase, `fast.live` and `compaction.live`, and for the identical
|
|
298
|
+
* reason.
|
|
299
|
+
*/
|
|
300
|
+
| {
|
|
301
|
+
id: string;
|
|
302
|
+
kind: 'service';
|
|
303
|
+
/** `s1`, `s2`, ... — the id the model quotes back. */
|
|
304
|
+
serviceId: string;
|
|
305
|
+
command: string;
|
|
306
|
+
status: ServiceStatus;
|
|
307
|
+
url?: string;
|
|
308
|
+
port?: number;
|
|
309
|
+
exitCode: number | null;
|
|
310
|
+
startedAt: number;
|
|
311
|
+
readyAt?: number;
|
|
312
|
+
endedAt?: number;
|
|
313
|
+
rows: readonly string[];
|
|
314
|
+
/**
|
|
315
|
+
* MONOTONIC row cursor — the revision term, NEVER `rows.length` (P0-3).
|
|
316
|
+
*
|
|
317
|
+
* The tail is a FIXED-SIZE RING: it evicts its oldest row while appending
|
|
318
|
+
* a new one, so the joined length is unchanged while the content is not.
|
|
319
|
+
* That is verbatim the non-append mutation the `tool` branch's `liveSeq`
|
|
320
|
+
* comment was written for, and a length term would go on matching while
|
|
321
|
+
* the card changed underneath it.
|
|
322
|
+
*/
|
|
323
|
+
rowsSeen: number;
|
|
324
|
+
/** The one-row entry a terminal transition appends (D-11). */
|
|
325
|
+
terminal?: boolean;
|
|
326
|
+
/** The stop reported the pid still alive afterwards (R-2 / P2-6). */
|
|
327
|
+
killIncomplete?: boolean;
|
|
225
328
|
};
|
|
226
329
|
export interface UsageTotal {
|
|
227
330
|
inputTokens: number;
|
|
@@ -296,6 +399,25 @@ export interface ViewState {
|
|
|
296
399
|
* what leaves an ordinary session's status bar byte-identical (AC-4).
|
|
297
400
|
*/
|
|
298
401
|
fast: FastSnapshot | null;
|
|
402
|
+
/**
|
|
403
|
+
* The live compaction projection, or `null` when the feature is off for this
|
|
404
|
+
* session (context-auto-compaction §5.5).
|
|
405
|
+
*
|
|
406
|
+
* NEVER PERSISTED — `entries` is the history and the settled cards are the
|
|
407
|
+
* record. It drives the status chip, the gauge's marks and `/compact status`'s
|
|
408
|
+
* totals. `null` is what leaves an ordinary session's status bar unchanged.
|
|
409
|
+
*/
|
|
410
|
+
compaction: CompactionSnapshot | null;
|
|
411
|
+
/**
|
|
412
|
+
* Whether `contextTokens` is currently a DERIVED figure rather than a measured
|
|
413
|
+
* one (§6.2 / P1-11).
|
|
414
|
+
*
|
|
415
|
+
* Set by `compactionEnd`, cleared by the next `turnEnd`. The status bar widens
|
|
416
|
+
* its leading `~` to cover it: today `~` means "the window is a guess", and
|
|
417
|
+
* this makes it also mean "the occupancy is a guess". Honesty about a guess is
|
|
418
|
+
* cheaper than a support thread about a wrong percentage.
|
|
419
|
+
*/
|
|
420
|
+
contextTokensEstimated: boolean;
|
|
299
421
|
seq: number;
|
|
300
422
|
/** The `kind: 'team'` entry the live dispatch is writing into. */
|
|
301
423
|
teamEntryId?: string;
|
|
@@ -305,6 +427,8 @@ export interface ViewState {
|
|
|
305
427
|
retryEntryId?: string;
|
|
306
428
|
/** The `kind: 'fast'` entry the in-flight review is writing into. */
|
|
307
429
|
fastEntryId?: string;
|
|
430
|
+
/** The `kind: 'compaction'` entry the in-flight compaction is writing into. */
|
|
431
|
+
compactionEntryId?: string;
|
|
308
432
|
streamingId?: string;
|
|
309
433
|
turnProduced: boolean;
|
|
310
434
|
errorNoticed: boolean;
|
|
@@ -497,6 +621,60 @@ export type ViewAction = {
|
|
|
497
621
|
} | {
|
|
498
622
|
type: 'fastTier';
|
|
499
623
|
snapshot: FastSnapshot;
|
|
624
|
+
} | {
|
|
625
|
+
type: 'compactionStart';
|
|
626
|
+
index: number;
|
|
627
|
+
trigger: CompactionUiTrigger;
|
|
628
|
+
model: string;
|
|
629
|
+
} | {
|
|
630
|
+
type: 'compactionEnd';
|
|
631
|
+
record: CompactionRecord;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Compaction spend.
|
|
635
|
+
*
|
|
636
|
+
* `costDelta` IS PRECOMPUTED BY THE CALLER, exactly as `teamUsage`'s and
|
|
637
|
+
* `fastUsage`'s are: the reducer stays pure and cost-table-free. It matters
|
|
638
|
+
* MORE here than for either of those, because the summarizer may be a
|
|
639
|
+
* different model from the lead AND from the fast tier, so a reducer that
|
|
640
|
+
* computed its own would have to know about three price tables.
|
|
641
|
+
*/
|
|
642
|
+
| {
|
|
643
|
+
type: 'compactionUsage';
|
|
644
|
+
usage: TokenUsage;
|
|
645
|
+
costDelta: number;
|
|
646
|
+
} | {
|
|
647
|
+
type: 'compactionSnapshot';
|
|
648
|
+
snapshot: CompactionSnapshot;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Overwrite the gauge with a DERIVED occupancy (§6.2).
|
|
652
|
+
*
|
|
653
|
+
* Separate from `compactionEnd` because the value is not always available at
|
|
654
|
+
* the same instant, and because the flag it sets — `contextTokensEstimated` —
|
|
655
|
+
* has to be clearable by the next `turnEnd` independently.
|
|
656
|
+
*/
|
|
657
|
+
| {
|
|
658
|
+
type: 'contextTokensEstimated';
|
|
659
|
+
tokens: number;
|
|
660
|
+
} | {
|
|
661
|
+
type: 'serviceStart';
|
|
662
|
+
service: ServiceSnapshot;
|
|
663
|
+
} | {
|
|
664
|
+
type: 'serviceUpdate';
|
|
665
|
+
service: ServiceSnapshot;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* A service reached a terminal state.
|
|
669
|
+
*
|
|
670
|
+
* IT APPENDS A NEW ONE-ROW ENTRY rather than rewriting the card (D-11). The
|
|
671
|
+
* card may already have been printed into `<Static>`, and `<Static>` cannot
|
|
672
|
+
* un-print — so any design that rewrites a printed card is wrong whatever it
|
|
673
|
+
* claims. A second event at a second time is also simply more honest.
|
|
674
|
+
*/
|
|
675
|
+
| {
|
|
676
|
+
type: 'serviceEnd';
|
|
677
|
+
service: ServiceSnapshot;
|
|
500
678
|
};
|
|
501
679
|
export declare const STORED_PREVIEW_CHARS = 8000;
|
|
502
680
|
export declare const STORED_PREVIEW_LINES = 200;
|
|
@@ -532,6 +710,17 @@ export interface PatchSource {
|
|
|
532
710
|
* it — every existing test, and headless mode — produces byte-identical actions.
|
|
533
711
|
*/
|
|
534
712
|
export declare function reduceEvent(event: AgentEvent, cost?: ModelCost, patches?: PatchSource): ViewAction[];
|
|
713
|
+
/**
|
|
714
|
+
* The opening words of the `context_overflow` banner.
|
|
715
|
+
*
|
|
716
|
+
* A SHARED CONSTANT RATHER THAN TWO LITERALS, because `compactionStart` matches
|
|
717
|
+
* on it to rewrite the notice a successful reactive recovery would otherwise
|
|
718
|
+
* leave behind (P1-4). Two spellings that must agree, with nothing checking that
|
|
719
|
+
* they do, is how that rewrite silently stops matching — at which point the red
|
|
720
|
+
* "start over" banner comes back and only on the recovery path, which is the one
|
|
721
|
+
* place nobody re-tests.
|
|
722
|
+
*/
|
|
723
|
+
export declare const CONTEXT_OVERFLOW_NOTICE_PREFIX = "Context length exceeded";
|
|
535
724
|
/** Turn a provider error into a friendly banner, mapping LLMError.errorType. */
|
|
536
725
|
export declare function formatStreamError(error: Error): string;
|
|
537
726
|
export declare function viewReducer(state: ViewState, action: ViewAction): ViewState;
|
package/dist/agent/reducer.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* synthesizes a generic error notice (a swallowed throw is never a no-op).
|
|
12
12
|
*/
|
|
13
13
|
import process from 'node:process';
|
|
14
|
+
import { occupiedTokens } from '../compaction/pressure.js';
|
|
14
15
|
import { computeCost } from './usage.js';
|
|
15
16
|
import { ENTRY_LIMITS, appendBounded, entryRetain, trimEntries } from './entry-limits.js';
|
|
16
17
|
/** Default auto-dismiss window for a toast. */
|
|
@@ -46,11 +47,14 @@ export function initialViewState(seed = {}) {
|
|
|
46
47
|
todos: null,
|
|
47
48
|
retry: null,
|
|
48
49
|
fast: null,
|
|
50
|
+
compaction: null,
|
|
51
|
+
contextTokensEstimated: false,
|
|
49
52
|
seq: 0,
|
|
50
53
|
teamEntryId: undefined,
|
|
51
54
|
todoEntryId: undefined,
|
|
52
55
|
retryEntryId: undefined,
|
|
53
56
|
fastEntryId: undefined,
|
|
57
|
+
compactionEntryId: undefined,
|
|
54
58
|
streamingId: undefined,
|
|
55
59
|
turnProduced: false,
|
|
56
60
|
errorNoticed: false,
|
|
@@ -223,6 +227,17 @@ function reduceStreamEvent(se) {
|
|
|
223
227
|
return [];
|
|
224
228
|
}
|
|
225
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* The opening words of the `context_overflow` banner.
|
|
232
|
+
*
|
|
233
|
+
* A SHARED CONSTANT RATHER THAN TWO LITERALS, because `compactionStart` matches
|
|
234
|
+
* on it to rewrite the notice a successful reactive recovery would otherwise
|
|
235
|
+
* leave behind (P1-4). Two spellings that must agree, with nothing checking that
|
|
236
|
+
* they do, is how that rewrite silently stops matching — at which point the red
|
|
237
|
+
* "start over" banner comes back and only on the recovery path, which is the one
|
|
238
|
+
* place nobody re-tests.
|
|
239
|
+
*/
|
|
240
|
+
export const CONTEXT_OVERFLOW_NOTICE_PREFIX = 'Context length exceeded';
|
|
226
241
|
/** Turn a provider error into a friendly banner, mapping LLMError.errorType. */
|
|
227
242
|
export function formatStreamError(error) {
|
|
228
243
|
const errorType = error.errorType;
|
|
@@ -235,7 +250,13 @@ export function formatStreamError(error) {
|
|
|
235
250
|
case 'overloaded':
|
|
236
251
|
return `Provider overloaded - please retry shortly. (${base})`;
|
|
237
252
|
case 'context_overflow':
|
|
238
|
-
|
|
253
|
+
// `/compact` IS NAMED FIRST, and adding it is the second half of P1-4's
|
|
254
|
+
// fix. This remedy list predates context compaction and was, until it
|
|
255
|
+
// existed, complete: throwing the conversation away really was the only
|
|
256
|
+
// exit. It is not any more, and a banner that offers only `/reset` now
|
|
257
|
+
// tells the user to discard a session the very next line of code may be
|
|
258
|
+
// about to save.
|
|
259
|
+
return `${CONTEXT_OVERFLOW_NOTICE_PREFIX} - try /compact, or start over with /reset. (${base})`;
|
|
239
260
|
case 'network_error':
|
|
240
261
|
return `Network error reaching the provider. (${base})`;
|
|
241
262
|
case 'timeout':
|
|
@@ -295,6 +316,11 @@ function appendEntry(state, entries, entry) {
|
|
|
295
316
|
// `fastEntryId` IS PART OF THIS LIST for the reason the note above gives:
|
|
296
317
|
// a fifth live card left out of it is a card I-L1-1 does not cover.
|
|
297
318
|
state.fastEntryId,
|
|
319
|
+
// And `compactionEntryId` is the sixth. Same reason, and it is the one
|
|
320
|
+
// whose absence would be hardest to notice: a compaction card is rare, so
|
|
321
|
+
// a `mapEntry` that silently stopped matching would be found by a user
|
|
322
|
+
// rather than by a test.
|
|
323
|
+
state.compactionEntryId,
|
|
298
324
|
].filter((id) => typeof id === 'string');
|
|
299
325
|
for (const id of live) {
|
|
300
326
|
if (!trimmed.entries.some((e) => e.id === id)) {
|
|
@@ -584,7 +610,17 @@ export function viewReducer(state, action) {
|
|
|
584
610
|
...state,
|
|
585
611
|
streamingId: undefined,
|
|
586
612
|
turnProduced: true,
|
|
587
|
-
|
|
613
|
+
// ONE FUNCTION, SHARED WITH THE COMPACTION TRIGGER (§3.4.1 / R-11 /
|
|
614
|
+
// AC-3). The old expression — `inputTokens + outputTokens` — silently
|
|
615
|
+
// UNDER-reports for anyone behind a caching gateway, because Anthropic
|
|
616
|
+
// reports `input_tokens` EXCLUDING cached tokens and puts them in two
|
|
617
|
+
// separate fields. That was a live defect in the number this gauge shows;
|
|
618
|
+
// it becomes a much worse one the moment a trigger fires on a different
|
|
619
|
+
// number from the one the user is watching.
|
|
620
|
+
contextTokens: occupiedTokens(action.usage),
|
|
621
|
+
// A MEASUREMENT SUPERSEDES A DERIVATION (§6.2). `compactionEnd` writes an
|
|
622
|
+
// estimate so the bar falls immediately; this is where it stops being one.
|
|
623
|
+
contextTokensEstimated: false,
|
|
588
624
|
usageTotal: {
|
|
589
625
|
inputTokens: state.usageTotal.inputTokens + action.usage.inputTokens,
|
|
590
626
|
outputTokens: state.usageTotal.outputTokens + action.usage.outputTokens,
|
|
@@ -713,6 +749,12 @@ export function viewReducer(state, action) {
|
|
|
713
749
|
// holds, it is a projection of an entry that has just been erased.
|
|
714
750
|
retryEntryId: undefined,
|
|
715
751
|
retry: null,
|
|
752
|
+
// Same reasoning again for the compaction card: the entry is gone, so the
|
|
753
|
+
// id must go with it or `compactionEnd` would `mapEntry` an id that no
|
|
754
|
+
// longer exists. `compaction` STAYS — unlike `retry` it is not a
|
|
755
|
+
// projection of an entry, it is the live state of a subsystem that is
|
|
756
|
+
// still running, and `/clear` clears the screen rather than the session.
|
|
757
|
+
compactionEntryId: undefined,
|
|
716
758
|
};
|
|
717
759
|
case 'resetConversation':
|
|
718
760
|
return {
|
|
@@ -731,6 +773,11 @@ export function viewReducer(state, action) {
|
|
|
731
773
|
todoEntryId: undefined,
|
|
732
774
|
retryEntryId: undefined,
|
|
733
775
|
retry: null,
|
|
776
|
+
compactionEntryId: undefined,
|
|
777
|
+
// `contextTokens` is zeroed above, so the flag that says it is a
|
|
778
|
+
// DERIVATION has to go with it — otherwise a fresh conversation renders
|
|
779
|
+
// `~0%` and the tilde is a lie about a number that is simply zero.
|
|
780
|
+
contextTokensEstimated: false,
|
|
734
781
|
};
|
|
735
782
|
case 'setOverlay':
|
|
736
783
|
return { ...state, overlay: action.overlay };
|
|
@@ -957,6 +1004,164 @@ export function viewReducer(state, action) {
|
|
|
957
1004
|
};
|
|
958
1005
|
case 'fastTier':
|
|
959
1006
|
return { ...state, fast: action.snapshot };
|
|
1007
|
+
// --- Context compaction (context-auto-compaction §5.5) ----------------
|
|
1008
|
+
// NOT A TOTAL FUNCTION OVER COMPACTION ATTEMPTS (quiet-noop §3.1). A
|
|
1009
|
+
// `pressure` compaction that declines before calling a model or changing the
|
|
1010
|
+
// history dispatches NOTHING - no `compactionStart`, no `compactionEnd`, no
|
|
1011
|
+
// entry - exactly as a checkpoint below the threshold dispatches nothing.
|
|
1012
|
+
// Counting cards here counts compactions that RAN, never checkpoints that
|
|
1013
|
+
// fired; `/compact status` and the diagnostic log are where the attempts are.
|
|
1014
|
+
case 'compactionStart': {
|
|
1015
|
+
const { id, seq } = nextId(state);
|
|
1016
|
+
let entries = state.entries;
|
|
1017
|
+
// RESPONSIBILITY 1 — SEAL ANY LIVE `streamingId` ENTRY, defensively.
|
|
1018
|
+
// C-8's failure mode, and reachable here: `provider.ts` records that
|
|
1019
|
+
// Anthropic can deliver an error IN-STREAM, after the connection is open
|
|
1020
|
+
// and therefore after text deltas have already been emitted. An entry that
|
|
1021
|
+
// never settles pins `Transcript`'s monotonic boundary and re-renders the
|
|
1022
|
+
// tail forever.
|
|
1023
|
+
if (state.streamingId) {
|
|
1024
|
+
entries = mapEntry(entries, state.streamingId, (e) => e.kind === 'assistant' ? { ...e, streaming: false, ...sealThinkingMs(e) } : e);
|
|
1025
|
+
}
|
|
1026
|
+
// RESPONSIBILITY 2 — REWRITE A TRAILING `context_overflow` ERROR NOTICE
|
|
1027
|
+
// (P1-4). `agent-loop.ts` emits `message_update` for EVERY stream event
|
|
1028
|
+
// before the `error` branch throws, and `reduceStreamEvent`'s `case 'error'`
|
|
1029
|
+
// turns that into a `level: 'error'` notice reading "Context length
|
|
1030
|
+
// exceeded". So the HAPPY PATH of the reactive recovery would otherwise end
|
|
1031
|
+
// with a red banner telling the user to throw away the session we are one
|
|
1032
|
+
// frame from saving.
|
|
1033
|
+
//
|
|
1034
|
+
// IT REWRITES RATHER THAN REMOVES, because `<Static>` cannot un-print an
|
|
1035
|
+
// entry it has already drawn — and it matches ONLY when that notice is the
|
|
1036
|
+
// TAIL entry, so it can never touch an older, genuine error. If the
|
|
1037
|
+
// recovery then fails, the ladder's own notice or the re-thrown 400 is what
|
|
1038
|
+
// the user is left with, so neither direction lies.
|
|
1039
|
+
if (action.trigger === 'overflow') {
|
|
1040
|
+
const tail = entries[entries.length - 1];
|
|
1041
|
+
if (tail &&
|
|
1042
|
+
tail.kind === 'notice' &&
|
|
1043
|
+
tail.level === 'error' &&
|
|
1044
|
+
tail.text.startsWith(CONTEXT_OVERFLOW_NOTICE_PREFIX)) {
|
|
1045
|
+
entries = mapEntry(entries, tail.id, (e) => e.kind === 'notice'
|
|
1046
|
+
? { ...e, level: 'info', text: 'Context window exceeded - compacting and retrying.' }
|
|
1047
|
+
: e);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
const entry = {
|
|
1051
|
+
id,
|
|
1052
|
+
kind: 'compaction',
|
|
1053
|
+
index: action.index,
|
|
1054
|
+
trigger: action.trigger,
|
|
1055
|
+
mode: 'none',
|
|
1056
|
+
applied: false,
|
|
1057
|
+
messagesBefore: 0,
|
|
1058
|
+
messagesAfter: 0,
|
|
1059
|
+
tokensBefore: 0,
|
|
1060
|
+
tokensAfter: 0,
|
|
1061
|
+
model: action.model,
|
|
1062
|
+
startedAt: Date.now(),
|
|
1063
|
+
live: true,
|
|
1064
|
+
};
|
|
1065
|
+
return {
|
|
1066
|
+
...state,
|
|
1067
|
+
seq,
|
|
1068
|
+
...appendEntry(state, entries, entry),
|
|
1069
|
+
compactionEntryId: id,
|
|
1070
|
+
streamingId: undefined,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
case 'compactionEnd': {
|
|
1074
|
+
const { record } = action;
|
|
1075
|
+
const settle = (e) => e.kind === 'compaction'
|
|
1076
|
+
? {
|
|
1077
|
+
...e,
|
|
1078
|
+
index: record.index,
|
|
1079
|
+
trigger: record.trigger,
|
|
1080
|
+
mode: record.mode,
|
|
1081
|
+
applied: record.applied,
|
|
1082
|
+
...(record.reason !== undefined ? { reason: record.reason } : {}),
|
|
1083
|
+
messagesBefore: record.messagesBefore,
|
|
1084
|
+
messagesAfter: record.messagesAfter,
|
|
1085
|
+
tokensBefore: record.tokensBefore,
|
|
1086
|
+
tokensAfter: record.tokensAfter,
|
|
1087
|
+
...(record.summary !== undefined ? { summary: record.summary } : {}),
|
|
1088
|
+
...(record.tailRelief ? { tailRelief: record.tailRelief } : {}),
|
|
1089
|
+
model: record.model,
|
|
1090
|
+
durationMs: record.durationMs,
|
|
1091
|
+
// SETTLED. `Transcript`'s boundary is monotonic, so a card that
|
|
1092
|
+
// never settles is re-rendered on every frame for the rest of the
|
|
1093
|
+
// session (C-8).
|
|
1094
|
+
live: false,
|
|
1095
|
+
}
|
|
1096
|
+
: e;
|
|
1097
|
+
// THE SECOND BRANCH IS NOT DEFENSIVE PADDING, exactly as `fastEnd`'s is
|
|
1098
|
+
// not: the IDLE `/compact` path settles a record for a card this session
|
|
1099
|
+
// may never have opened, and "it did nothing" must never be the observable
|
|
1100
|
+
// outcome (R-8 / §6.4).
|
|
1101
|
+
if (state.compactionEntryId) {
|
|
1102
|
+
return {
|
|
1103
|
+
...state,
|
|
1104
|
+
entries: mapEntry(state.entries, state.compactionEntryId, settle),
|
|
1105
|
+
compactionEntryId: undefined,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
const { id, seq } = nextId(state);
|
|
1109
|
+
const entry = {
|
|
1110
|
+
id,
|
|
1111
|
+
kind: 'compaction',
|
|
1112
|
+
index: record.index,
|
|
1113
|
+
trigger: record.trigger,
|
|
1114
|
+
mode: record.mode,
|
|
1115
|
+
applied: record.applied,
|
|
1116
|
+
...(record.reason !== undefined ? { reason: record.reason } : {}),
|
|
1117
|
+
messagesBefore: record.messagesBefore,
|
|
1118
|
+
messagesAfter: record.messagesAfter,
|
|
1119
|
+
tokensBefore: record.tokensBefore,
|
|
1120
|
+
tokensAfter: record.tokensAfter,
|
|
1121
|
+
...(record.summary !== undefined ? { summary: record.summary } : {}),
|
|
1122
|
+
...(record.tailRelief ? { tailRelief: record.tailRelief } : {}),
|
|
1123
|
+
model: record.model,
|
|
1124
|
+
durationMs: record.durationMs,
|
|
1125
|
+
live: false,
|
|
1126
|
+
};
|
|
1127
|
+
return { ...state, seq, ...appendEntry(state, state.entries, entry) };
|
|
1128
|
+
}
|
|
1129
|
+
case 'compactionUsage':
|
|
1130
|
+
// `usageTotal` AND NOTHING ELSE, exactly as `teamUsage` and `fastUsage`. In
|
|
1131
|
+
// particular NOT `contextTokens`: that gauge shows the LEAD's context
|
|
1132
|
+
// occupancy against the lead model's window, and a summarization is a
|
|
1133
|
+
// separate conversation with a possibly different model entirely.
|
|
1134
|
+
return {
|
|
1135
|
+
...state,
|
|
1136
|
+
usageTotal: {
|
|
1137
|
+
inputTokens: state.usageTotal.inputTokens + action.usage.inputTokens,
|
|
1138
|
+
outputTokens: state.usageTotal.outputTokens + action.usage.outputTokens,
|
|
1139
|
+
costUsd: state.usageTotal.costUsd + action.costDelta,
|
|
1140
|
+
},
|
|
1141
|
+
};
|
|
1142
|
+
case 'compactionSnapshot':
|
|
1143
|
+
return { ...state, compaction: action.snapshot };
|
|
1144
|
+
/**
|
|
1145
|
+
* THE GAUGE MUST FALL IMMEDIATELY (§6.2 / AC-5).
|
|
1146
|
+
*
|
|
1147
|
+
* `contextTokens` is otherwise only written at `turnEnd`, so a compaction
|
|
1148
|
+
* that took the session from 92 % to 24 % would leave the bar at 92 % until
|
|
1149
|
+
* the next turn completes — and the user would reasonably conclude that
|
|
1150
|
+
* nothing happened. This is a small change and it is the difference between
|
|
1151
|
+
* the feature feeling real and feeling broken.
|
|
1152
|
+
*
|
|
1153
|
+
* The value arrives ALREADY CALIBRATED from `App` (§6.2 / P1-11): it replaces
|
|
1154
|
+
* a MEASUREMENT that included the tool schemas, and core's raw estimator
|
|
1155
|
+
* never counts them, so writing the raw figure would make the bar fall too
|
|
1156
|
+
* far and then visibly climb again on the next `turn_end` for no reason the
|
|
1157
|
+
* user can see.
|
|
1158
|
+
*/
|
|
1159
|
+
case 'contextTokensEstimated':
|
|
1160
|
+
return {
|
|
1161
|
+
...state,
|
|
1162
|
+
contextTokens: Math.max(0, Math.round(action.tokens)),
|
|
1163
|
+
contextTokensEstimated: true,
|
|
1164
|
+
};
|
|
960
1165
|
// --- API retry (llm-api-retry-backoff §6.4) ---------------------------
|
|
961
1166
|
case 'retryScheduled': {
|
|
962
1167
|
const snapshot = {
|
|
@@ -1118,10 +1323,104 @@ export function viewReducer(state, action) {
|
|
|
1118
1323
|
fastEntryId: undefined,
|
|
1119
1324
|
};
|
|
1120
1325
|
}
|
|
1326
|
+
// --- Background services (§3.10 / D-11) -----------------------------
|
|
1327
|
+
case 'serviceStart': {
|
|
1328
|
+
const { id, seq } = nextId(state);
|
|
1329
|
+
const entry = { id, ...serviceFields(action.service) };
|
|
1330
|
+
const appended = appendEntry(state, state.entries, entry);
|
|
1331
|
+
return {
|
|
1332
|
+
...state,
|
|
1333
|
+
seq,
|
|
1334
|
+
entries: appended.entries,
|
|
1335
|
+
droppedEntries: appended.droppedEntries,
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Rewrite the LIVE card in place.
|
|
1340
|
+
*
|
|
1341
|
+
* SAFE ONLY BECAUSE OF D-11'S OTHER HALF: a `ready` / `running` card has
|
|
1342
|
+
* reached `<Static>` and cannot be re-printed, so `serviceEnd` appends a new
|
|
1343
|
+
* entry instead of routing through here. What this handles is the window in
|
|
1344
|
+
* which the card is still in the live region — `starting`, and the one
|
|
1345
|
+
* transition out of it — plus the output tail while it is there.
|
|
1346
|
+
*
|
|
1347
|
+
* The lookup is by `serviceId` and SKIPS terminal entries, so a stray update
|
|
1348
|
+
* cannot rewrite the one-row record `serviceEnd` appended.
|
|
1349
|
+
*/
|
|
1350
|
+
case 'serviceUpdate': {
|
|
1351
|
+
const target = findServiceEntryId(state, action.service.id);
|
|
1352
|
+
if (!target)
|
|
1353
|
+
return state;
|
|
1354
|
+
return {
|
|
1355
|
+
...state,
|
|
1356
|
+
entries: mapEntry(state.entries, target, (e) => e.kind === 'service' ? { ...e, ...serviceFields(action.service) } : e),
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* A service reached a terminal state.
|
|
1361
|
+
*
|
|
1362
|
+
* TWO WRITES, AND BOTH ARE REQUIRED. The live card is settled in place so
|
|
1363
|
+
* `computeSettledCount` can move past it and `estimateEntryRows` stops
|
|
1364
|
+
* charging for a tail nothing will add to; the NEW one-row entry is what the
|
|
1365
|
+
* user actually reads, because the card may already be printed into
|
|
1366
|
+
* `<Static>` and `<Static>` cannot un-print (D-11 / R-13).
|
|
1367
|
+
*/
|
|
1368
|
+
case 'serviceEnd': {
|
|
1369
|
+
const target = findServiceEntryId(state, action.service.id);
|
|
1370
|
+
const settled = target
|
|
1371
|
+
? mapEntry(state.entries, target, (e) => e.kind === 'service' ? { ...e, ...serviceFields(action.service) } : e)
|
|
1372
|
+
: state.entries;
|
|
1373
|
+
const { id, seq } = nextId(state);
|
|
1374
|
+
const appended = appendEntry(state, settled, {
|
|
1375
|
+
id,
|
|
1376
|
+
...serviceFields(action.service),
|
|
1377
|
+
terminal: true,
|
|
1378
|
+
});
|
|
1379
|
+
return {
|
|
1380
|
+
...state,
|
|
1381
|
+
seq,
|
|
1382
|
+
entries: appended.entries,
|
|
1383
|
+
droppedEntries: appended.droppedEntries,
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1121
1386
|
default:
|
|
1122
1387
|
return state;
|
|
1123
1388
|
}
|
|
1124
1389
|
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Project a supervisor snapshot onto the entry's own fields.
|
|
1392
|
+
*
|
|
1393
|
+
* ONE PROJECTION FOR ALL THREE ACTIONS, so a field that reaches the card through
|
|
1394
|
+
* `serviceStart` can never fail to reach it through `serviceUpdate` — the kind of
|
|
1395
|
+
* asymmetry that produces a card frozen at its first values with nothing
|
|
1396
|
+
* reporting it.
|
|
1397
|
+
*/
|
|
1398
|
+
function serviceFields(service) {
|
|
1399
|
+
return {
|
|
1400
|
+
kind: 'service',
|
|
1401
|
+
serviceId: service.id,
|
|
1402
|
+
command: service.command,
|
|
1403
|
+
status: service.status,
|
|
1404
|
+
...(service.url !== undefined ? { url: service.url } : {}),
|
|
1405
|
+
...(service.port !== undefined ? { port: service.port } : {}),
|
|
1406
|
+
exitCode: service.exitCode,
|
|
1407
|
+
startedAt: service.startedAt,
|
|
1408
|
+
...(service.readyAt !== undefined ? { readyAt: service.readyAt } : {}),
|
|
1409
|
+
...(service.endedAt !== undefined ? { endedAt: service.endedAt } : {}),
|
|
1410
|
+
rows: service.rows,
|
|
1411
|
+
rowsSeen: service.rowsSeen,
|
|
1412
|
+
...(service.killIncomplete ? { killIncomplete: true } : {}),
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
/** The LIVE card for a service id, newest first; terminal records are skipped. */
|
|
1416
|
+
function findServiceEntryId(state, serviceId) {
|
|
1417
|
+
for (let i = state.entries.length - 1; i >= 0; i -= 1) {
|
|
1418
|
+
const e = state.entries[i];
|
|
1419
|
+
if (e && e.kind === 'service' && e.serviceId === serviceId && !e.terminal)
|
|
1420
|
+
return e.id;
|
|
1421
|
+
}
|
|
1422
|
+
return undefined;
|
|
1423
|
+
}
|
|
1125
1424
|
function safeStringify(value) {
|
|
1126
1425
|
try {
|
|
1127
1426
|
return JSON.stringify(value);
|