@aragon-agent/cli 0.6.45 → 0.6.47
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 +15 -0
- package/README.md +104 -6
- package/dist/agent/coalesce.d.ts +4 -0
- package/dist/agent/coalesce.js +13 -0
- package/dist/agent/controller.d.ts +21 -0
- package/dist/agent/controller.js +56 -3
- package/dist/agent/invocation-card.d.ts +77 -0
- package/dist/agent/invocation-card.js +169 -0
- package/dist/agent/prompt-options.d.ts +6 -0
- package/dist/agent/queued-messages.d.ts +3 -0
- package/dist/agent/queued-messages.js +4 -1
- package/dist/agent/reducer.d.ts +25 -0
- package/dist/agent/reducer.js +43 -5
- package/dist/agent/user-attention.d.ts +17 -0
- package/dist/agent/user-attention.js +32 -0
- package/dist/cli.js +2 -3
- package/dist/commands/builtins.js +12 -0
- package/dist/commands/governance-command.js +13 -1
- package/dist/commands/index-build.d.ts +8 -0
- package/dist/commands/index-build.js +11 -0
- package/dist/commands/registry.d.ts +23 -0
- package/dist/commands/registry.js +20 -7
- package/dist/commands/skills.js +36 -2
- package/dist/commands/subagent-command.js +3 -1
- package/dist/commands/team-command.d.ts +6 -0
- package/dist/commands/team-command.js +40 -3
- package/dist/config/load.js +4 -25
- package/dist/config/schema.d.ts +4 -4
- package/dist/config/schema.js +7 -1
- package/dist/proc/limits.d.ts +27 -13
- package/dist/proc/limits.js +27 -13
- package/dist/proc/progress.d.ts +7 -0
- package/dist/proc/progress.js +35 -7
- package/dist/proc/prompt.js +7 -4
- package/dist/proc/supervisor.js +7 -1
- package/dist/proc/types.d.ts +6 -0
- package/dist/proc/wait-plan.d.ts +39 -0
- package/dist/proc/wait-plan.js +77 -0
- package/dist/proc/wait-view.d.ts +65 -0
- package/dist/proc/wait-view.js +66 -0
- package/dist/proc/wait.d.ts +36 -3
- package/dist/proc/wait.js +67 -4
- package/dist/session/checkpoint.js +2 -0
- package/dist/session/persist.js +29 -2
- package/dist/session/validate-session.js +7 -2
- package/dist/team/forced-mode.d.ts +19 -0
- package/dist/team/forced-mode.js +42 -20
- package/dist/tools/job-tools.d.ts +13 -2
- package/dist/tools/job-tools.js +156 -33
- package/dist/ui/ActivityLine.js +5 -2
- package/dist/ui/App.js +59 -6
- package/dist/ui/StatusBar.d.ts +1 -0
- package/dist/ui/Transcript.js +11 -1
- package/dist/ui/entries/InvocationCard.d.ts +20 -0
- package/dist/ui/entries/InvocationCard.js +32 -0
- package/dist/ui/entries/ToolCard.js +11 -2
- package/dist/ui/entries/WaitCard.d.ts +37 -0
- package/dist/ui/entries/WaitCard.js +92 -0
- package/dist/ui/interaction-copy.d.ts +2 -0
- package/dist/ui/interaction-copy.js +3 -0
- package/dist/ui/layout/invocation-card.d.ts +41 -0
- package/dist/ui/layout/invocation-card.js +91 -0
- package/dist/ui/layout/status-layout.d.ts +1 -0
- package/dist/ui/layout/status-layout.js +10 -1
- package/dist/ui/layout/virtual-window.js +18 -2
- package/dist/ui/layout/wait-bar.d.ts +34 -0
- package/dist/ui/layout/wait-bar.js +48 -0
- package/dist/ui/run-status-row.d.ts +4 -2
- package/dist/ui/scrollback/live-preview.js +12 -0
- package/dist/ui/transcript-text.js +4 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,21 @@ were written.
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
|
+
- **Long waits: `job_wait` now waits up to 20 minutes, and the wait looks like it.**
|
|
12
|
+
`timeoutSeconds` takes any integer from 5 to 1200 (it used to stop at 60, and at 150 with the
|
|
13
|
+
default `toolTimeoutMs`, because the tool ceiling silently pulled it down). `job_wait` gets its own
|
|
14
|
+
executor ceiling (1230 s) and pauses the idle watchdog for the whole wait, so `toolTimeoutMs` no
|
|
15
|
+
longer limits it and `jobs.waitSeconds` accepts 5-1200. **Typing a message ends the wait at once**
|
|
16
|
+
(result line `[woke] ...`, the job keeps running, no `Esc` needed); reviewer notes and completion
|
|
17
|
+
notices do not. While waiting, the tool card is replaced by a fixed five-row card - a progress bar
|
|
18
|
+
(the job's own progress when it prints any, otherwise the wait window), both clocks, the job's
|
|
19
|
+
newest output with "N s ago", a calm "not a failure" note after `jobs.stallSeconds` of silence, and
|
|
20
|
+
a hint that a message ends the wait - and the status bar reads `Waiting on t3 4m12s/10m00s`. New:
|
|
21
|
+
`job: "any"` (return on the first job to end), a `[next]` suggestion with a `timeoutSeconds` (and the
|
|
22
|
+
job's ETA) so the model prefers one long wait to many short ones, and `[note]` when a request is
|
|
23
|
+
clamped to the ceiling. No new config keys; `jobs.enabled=false` is byte-identical to before.
|
|
24
|
+
Honest limit: while a wait runs the idle check is off, so a stuck wait is ended by its own 20-minute
|
|
25
|
+
bound, the executor backstop, or you.
|
|
11
26
|
- **Governance mode - a multi-agent "legion" collaboration posture.** `Shift+Tab` (and `Ctrl+N`)
|
|
12
27
|
now cycle `BUILD -> PLAN -> GOVERN -> PLAN+GOVERN -> BUILD`; `/gov` goes to any of them
|
|
13
28
|
directly. In `GOVERN` the main agent becomes an **Orchestrator** that cannot write files or run
|
package/README.md
CHANGED
|
@@ -2068,13 +2068,60 @@ longer reads "not ready yet, still coming up" for its first minute.
|
|
|
2068
2068
|
Two tools manage them:
|
|
2069
2069
|
|
|
2070
2070
|
- `job_wait({ job, timeoutSeconds?, since? })` — block up to a bound (default 30s,
|
|
2071
|
-
at most
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2071
|
+
**at most 20 minutes**: `timeoutSeconds` takes any integer from 5 to 1200) and
|
|
2072
|
+
return one status line, one progress line, at most six new output rows and a
|
|
2073
|
+
cursor. It returns immediately if the job already finished, and it also ends
|
|
2074
|
+
the moment **you send a message** (see below). `job` is an id, `"all"` (until
|
|
2075
|
+
every live job ends) or `"any"` (until the first one ends). This is the point
|
|
2076
|
+
of the feature: waiting 30 seconds used to cost six `bash_output` calls, six
|
|
2077
|
+
model turns and six 200-row tails; it now costs one call and about 150 tokens,
|
|
2078
|
+
and a 40-minute training run costs a handful of calls instead of dozens.
|
|
2076
2079
|
- `job_list()` — one line per job: id, kind, status, elapsed, progress.
|
|
2077
2080
|
|
|
2081
|
+
### Long waits
|
|
2082
|
+
|
|
2083
|
+
A wait can now last up to 20 minutes, so three things keep it safe and visible:
|
|
2084
|
+
|
|
2085
|
+
- **You can always interrupt it by typing.** Send a message while the agent is
|
|
2086
|
+
waiting and the wait ends at once; the result says `[woke] you sent a message...`,
|
|
2087
|
+
your message is delivered next, and the job keeps running. You do not need
|
|
2088
|
+
`Esc`, which would stop the whole run. (Automatic notes, such as a reviewer's
|
|
2089
|
+
or a completion notice, do not end a wait. If the agent had queued more tool
|
|
2090
|
+
calls in the same step, the engine skips them when your message arrives, as it
|
|
2091
|
+
does for any message sent mid-step.)
|
|
2092
|
+
- **The wait is drawn as a five-row card** instead of a bare "running":
|
|
2093
|
+
|
|
2094
|
+
```
|
|
2095
|
+
⠹ Waiting for t3 python train.py --epochs 10 4m12s of 10m00s
|
|
2096
|
+
│ ████████░░░░░░░░░░░░░░░░░░░░ 30% epoch 3/10 ETA 13m
|
|
2097
|
+
│ job running 42m04s · this wait ends in 5m48s, or sooner if the job finishes
|
|
2098
|
+
│ step 912/1000 loss 0.4098 3s ago
|
|
2099
|
+
│ Send a message to end the wait early · Esc x2 stops the whole run
|
|
2100
|
+
```
|
|
2101
|
+
|
|
2102
|
+
The bar shows the job's own progress when it prints any, and otherwise the
|
|
2103
|
+
wait window itself (the words say which). After `jobs.stallSeconds` of silence
|
|
2104
|
+
the output row becomes a calm
|
|
2105
|
+
`quiet for 6m02s - the job is still alive; this is not a failure`. The status
|
|
2106
|
+
bar and activity line carry the same clock (`Waiting on t3 4m12s/10m00s`).
|
|
2107
|
+
The card is always five rows, uses ASCII glyphs on terminals without Unicode,
|
|
2108
|
+
and keeps its numbers moving under reduced motion. It disappears when the wait
|
|
2109
|
+
ends and leaves the usual one-line history row.
|
|
2110
|
+
- **The idle watchdog is paused for the length of the wait**, and `toolTimeoutMs`
|
|
2111
|
+
no longer limits it: `job_wait` gets its own executor ceiling (20 minutes plus
|
|
2112
|
+
30 seconds). The honest cost is that a wait which is itself stuck is only
|
|
2113
|
+
caught by that ceiling and by you (`Esc x2`), not by the 3.5-minute idle check.
|
|
2114
|
+
|
|
2115
|
+
Results suggest the next wait: when the job reports an ETA or has been running for
|
|
2116
|
+
a while, the `[next]` line carries a `timeoutSeconds` worth using (for example
|
|
2117
|
+
`timeoutSeconds: 900 ... (ETA about 13m)`) so the agent prefers one long wait to
|
|
2118
|
+
many short ones. It is advice, never enforced. A request over 1200 seconds is
|
|
2119
|
+
clamped and says so (`[note] timeoutSeconds 3600 exceeds the 1200s ceiling`).
|
|
2120
|
+
|
|
2121
|
+
Cost note: a wait longer than your provider's prompt-cache lifetime (often five
|
|
2122
|
+
minutes) means the next request re-reads the context uncached. That is still far
|
|
2123
|
+
cheaper than the dozens of model turns short polling would have spent.
|
|
2124
|
+
|
|
2078
2125
|
Nothing else tells the model a job finished. When one ends while it is busy, a
|
|
2079
2126
|
`<job_notices>` block appears at the end of the next tool result:
|
|
2080
2127
|
|
|
@@ -2114,7 +2161,7 @@ to use it — `grep` and `read_file` already exist.
|
|
|
2114
2161
|
| Key | Default | Meaning |
|
|
2115
2162
|
|---|---|---|
|
|
2116
2163
|
| `jobs.enabled` | `true` | Register `job_wait`/`job_list`, splice the prompt block. `false` restores the pre-feature build byte for byte. |
|
|
2117
|
-
| `jobs.waitSeconds` | `30` | Default bound for one wait, 5–
|
|
2164
|
+
| `jobs.waitSeconds` | `30` | Default bound for one wait, 5–1200. No longer lowered by `toolTimeoutMs`. |
|
|
2118
2165
|
| `jobs.stallSeconds` | `300` | Silence before `[stalled]`, 30–3600. |
|
|
2119
2166
|
| `jobs.persistLog` | `true` | Mirror each job's output to disk. `false` creates no directory at all. |
|
|
2120
2167
|
| `jobs.noticeSteer` | `true` | Whether a completion notice may interrupt a running turn. `false` keeps the notice for the next tool result or the next user turn — it is **not** a notification off switch. |
|
|
@@ -3013,6 +3060,57 @@ declares no placeholder, the arguments are appended under an `## Arguments`
|
|
|
3013
3060
|
heading instead. Where a skill's name collides with a built-in command, the
|
|
3014
3061
|
**built-in wins** and the skill is reachable as `/skill:<name>`.
|
|
3015
3062
|
|
|
3063
|
+
### Invocation cards
|
|
3064
|
+
|
|
3065
|
+
A command that starts a task by submitting a long prompt on your behalf does not
|
|
3066
|
+
print that prompt. `/pdf-forms`, `/skill:<name>`, `/team <brief>`, `/todo continue`
|
|
3067
|
+
and `/gov resume` each leave one **card** in the transcript instead: what you
|
|
3068
|
+
started, a few facts, and your own words.
|
|
3069
|
+
|
|
3070
|
+
```
|
|
3071
|
+
> skill . pdf-forms v1.2.0 . user . 31 KB . 4 files
|
|
3072
|
+
Fill the W-9 form in ./tax/w9.pdf with the values from data.json
|
|
3073
|
+
|
|
3074
|
+
> team 4 subagents . read-only
|
|
3075
|
+
Audit the auth module for session fixation and add regression tests
|
|
3076
|
+
Asked for 12; team.maxSubagents is 8.
|
|
3077
|
+
|
|
3078
|
+
> todo . continue 3 of 7 steps left
|
|
3079
|
+
```
|
|
3080
|
+
|
|
3081
|
+
(`.` is `·` on a Unicode terminal.) The first row names the command and its
|
|
3082
|
+
facts and is shortened to the width of the window - facts go first, then the name.
|
|
3083
|
+
Your own words follow, capped at 8 lines / 480 characters with an honest
|
|
3084
|
+
`... +N chars not shown` tail. A line below it appears only when the command
|
|
3085
|
+
changed what you asked for (a fan-out above `team.maxSubagents`). `Ctrl+I` shows a
|
|
3086
|
+
`ctrl+i` fact and no input row: the sentence it sends the model is not something
|
|
3087
|
+
you said.
|
|
3088
|
+
|
|
3089
|
+
What changes and what does not:
|
|
3090
|
+
|
|
3091
|
+
- **The model receives exactly the bytes it did before.** Only the transcript
|
|
3092
|
+
entry is different; the skill body, the team directive and the continuation text
|
|
3093
|
+
are untouched.
|
|
3094
|
+
- The `Command:` echo and the old `Skill "x" loaded (...)` / `Forcing a fan-out...`
|
|
3095
|
+
notices are folded into the card, so one act is stated once.
|
|
3096
|
+
- Up-arrow history and the header task label hold the command you typed
|
|
3097
|
+
(`/skill:pdf-forms a.pdf`), not the prompt behind it. Canned turns the commands
|
|
3098
|
+
submit for you (`/team`, `/todo continue`, `/gov resume`) still stay out of
|
|
3099
|
+
history and do not trigger a task label, exactly as before.
|
|
3100
|
+
- A skill run now adds one history entry instead of two to the hint-fade counter.
|
|
3101
|
+
- Secrets in your arguments are redacted **before** anything is cut, so a key that
|
|
3102
|
+
straddles a cut cannot leave a readable prefix in the transcript, the session
|
|
3103
|
+
file or the exit replay.
|
|
3104
|
+
- The exit replay prints the command line plus one `[skill pdf-forms v1.2.0 - ...]`
|
|
3105
|
+
line. A very long brief is capped at 2000 characters there; the full text is in
|
|
3106
|
+
the up-arrow history and in the conversation itself.
|
|
3107
|
+
- Sessions written before cards existed load unchanged; a damaged card in a session
|
|
3108
|
+
file degrades to the plain command line instead of failing the load.
|
|
3109
|
+
|
|
3110
|
+
`ARAGON_NO_INVOCATION_CARDS=1` restores the previous behaviour completely (echo,
|
|
3111
|
+
notices and the full prompt in the transcript). It is an emergency switch, not a
|
|
3112
|
+
setting.
|
|
3113
|
+
|
|
3016
3114
|
### Where skills come from
|
|
3017
3115
|
|
|
3018
3116
|
Four scopes, later ones overriding earlier ones by name:
|
package/dist/agent/coalesce.d.ts
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
* concatenating, and a differing id starts a new pending action because merging
|
|
17
17
|
* across ids would paint one command's output onto another's card.
|
|
18
18
|
*
|
|
19
|
+
* A FOURTH ARRIVED WITH THE LONG WAIT (extended-job-wait RV-6): `waitUpdate`
|
|
20
|
+
* carries the WHOLE view, so it merges exactly like the tool tail - same
|
|
21
|
+
* `toolCallId` keeps the last, a different id or any other action flushes first.
|
|
22
|
+
*
|
|
19
23
|
* ONE PENDING SLOT, DISCRIMINATED, NEVER TWO (P1-5). A second, independent slot
|
|
20
24
|
* for the object-shaped payload would let a chunk overtake a token and reorder
|
|
21
25
|
* the two streams — exactly what I-L4-1 (`ui/render-governor.ts:14-19`) forbids:
|
package/dist/agent/coalesce.js
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
* concatenating, and a differing id starts a new pending action because merging
|
|
17
17
|
* across ids would paint one command's output onto another's card.
|
|
18
18
|
*
|
|
19
|
+
* A FOURTH ARRIVED WITH THE LONG WAIT (extended-job-wait RV-6): `waitUpdate`
|
|
20
|
+
* carries the WHOLE view, so it merges exactly like the tool tail - same
|
|
21
|
+
* `toolCallId` keeps the last, a different id or any other action flushes first.
|
|
22
|
+
*
|
|
19
23
|
* ONE PENDING SLOT, DISCRIMINATED, NEVER TWO (P1-5). A second, independent slot
|
|
20
24
|
* for the object-shaped payload would let a chunk overtake a token and reorder
|
|
21
25
|
* the two streams — exactly what I-L4-1 (`ui/render-governor.ts:14-19`) forbids:
|
|
@@ -28,6 +32,9 @@ function isTextDelta(action) {
|
|
|
28
32
|
function isToolOutputDelta(action) {
|
|
29
33
|
return action.type === 'toolOutputDelta';
|
|
30
34
|
}
|
|
35
|
+
function isWaitUpdate(action) {
|
|
36
|
+
return action.type === 'waitUpdate';
|
|
37
|
+
}
|
|
31
38
|
/** Merge consecutive same-kind deltas; flush on kind change or a non-delta. */
|
|
32
39
|
export function mergeDeltas(actions) {
|
|
33
40
|
const out = [];
|
|
@@ -70,6 +77,12 @@ export function mergeDeltas(actions) {
|
|
|
70
77
|
// increment, so merging means replacing (D-30).
|
|
71
78
|
pending = { kind: 'tool', action };
|
|
72
79
|
}
|
|
80
|
+
else if (isWaitUpdate(action)) {
|
|
81
|
+
const merges = prev !== null && prev.kind === 'wait' && prev.action.toolCallId === action.toolCallId;
|
|
82
|
+
if (!merges)
|
|
83
|
+
flush();
|
|
84
|
+
pending = { kind: 'wait', action };
|
|
85
|
+
}
|
|
73
86
|
else {
|
|
74
87
|
flush();
|
|
75
88
|
out.push(action);
|
|
@@ -28,6 +28,7 @@ import type { SubagentModelResolution } from '../team/model.js';
|
|
|
28
28
|
import type { ForcedTeamState } from '../team/forced-mode.js';
|
|
29
29
|
import type { TeamEvent, TeamSnapshot } from '../team/types.js';
|
|
30
30
|
import { ProcSupervisor } from '../proc/supervisor.js';
|
|
31
|
+
import type { WaitEvent } from '../proc/wait-view.js';
|
|
31
32
|
import type { ProcEventListener, ServiceSnapshot } from '../proc/types.js';
|
|
32
33
|
import { type FastStatus } from '../fast/wiring.js';
|
|
33
34
|
import type { FastEventListener, FastSnapshot } from '../fast/types.js';
|
|
@@ -350,6 +351,10 @@ export declare class AgentController {
|
|
|
350
351
|
*/
|
|
351
352
|
private readonly toolOutputs;
|
|
352
353
|
private readonly outputListeners;
|
|
354
|
+
/** `job_wait` live-view listeners (extended-job-wait); see `subscribeWait`. */
|
|
355
|
+
private readonly waitListeners;
|
|
356
|
+
/** "The user just spoke": ends a running `job_wait` early. */
|
|
357
|
+
private readonly userAttention;
|
|
353
358
|
private readonly fast;
|
|
354
359
|
private readonly compaction;
|
|
355
360
|
private settingsRevision;
|
|
@@ -768,6 +773,12 @@ export declare class AgentController {
|
|
|
768
773
|
* rather than a counter, so a nested call's `finally` re-arms the watchdog
|
|
769
774
|
* while the outer wait is still going. That is why a child's `--confirm`
|
|
770
775
|
* dialog does NOT route through this (see the constructor's `humanQueue`).
|
|
776
|
+
*
|
|
777
|
+
* `job_wait` ALSO DEPENDS ON THAT (extended-job-wait I-3): it holds the pause for
|
|
778
|
+
* up to 20 minutes and is safe only because the engine runs a batch's tools
|
|
779
|
+
* one at a time. If tool execution ever becomes parallel, make this a COUNTER
|
|
780
|
+
* first - otherwise the first tool to finish re-arms the watchdog under a
|
|
781
|
+
* still-waiting sibling, which then kills the run at the idle timeout.
|
|
771
782
|
*/
|
|
772
783
|
withPausedWatchdog<T>(fn: () => Promise<T>): Promise<T>;
|
|
773
784
|
/** @see withPausedWatchdog — the name the plan tools and `--confirm` use. */
|
|
@@ -1026,6 +1037,16 @@ export declare class AgentController {
|
|
|
1026
1037
|
* `warn` because a listener that throws once throws on every chunk.
|
|
1027
1038
|
*/
|
|
1028
1039
|
private emitToolOutput;
|
|
1040
|
+
/**
|
|
1041
|
+
* Subscribe to `job_wait`'s live view (`begin` / 1 Hz `update` / `end`).
|
|
1042
|
+
*
|
|
1043
|
+
* NOT GATED ON `liveToolOutput` (extended-job-wait D-9): that switch bounds a
|
|
1044
|
+
* high-frequency byte stream; this channel is capped at 1 Hz, and switching it
|
|
1045
|
+
* off would bring back the 20-minute black box.
|
|
1046
|
+
*/
|
|
1047
|
+
subscribeWait(listener: (event: WaitEvent) => void): () => void;
|
|
1048
|
+
/** Fan out one wait event; a throwing listener is logged and skipped (I-6). */
|
|
1049
|
+
private emitWait;
|
|
1029
1050
|
/**
|
|
1030
1051
|
* Subscribe to the CLI-local live tool-output stream (§3.1.3).
|
|
1031
1052
|
*
|
package/dist/agent/controller.js
CHANGED
|
@@ -27,7 +27,8 @@ import { buildForcedTeamBlock } from '../team/forced-prompt.js';
|
|
|
27
27
|
import { buildFastBlock } from '../fast/prompt.js';
|
|
28
28
|
import { ProcSupervisor } from '../proc/supervisor.js';
|
|
29
29
|
import { buildBackgroundJobsBlock, buildBackgroundServicesBlock } from '../proc/prompt.js';
|
|
30
|
-
import { PROC_LIMITS } from '../proc/limits.js';
|
|
30
|
+
import { JOB_WAIT_TOOL_CEILING_MS, PROC_LIMITS } from '../proc/limits.js';
|
|
31
|
+
import { createUserAttention } from './user-attention.js';
|
|
31
32
|
import { JobLogMirror } from '../proc/log-mirror.js';
|
|
32
33
|
import { JobNoticeQueue } from '../proc/notices.js';
|
|
33
34
|
import { makeBashKill, makeBashOutput } from '../tools/proc-tools.js';
|
|
@@ -261,6 +262,10 @@ export class AgentController {
|
|
|
261
262
|
*/
|
|
262
263
|
toolOutputs;
|
|
263
264
|
outputListeners = new Set();
|
|
265
|
+
/** `job_wait` live-view listeners (extended-job-wait); see `subscribeWait`. */
|
|
266
|
+
waitListeners = new Set();
|
|
267
|
+
/** "The user just spoke": ends a running `job_wait` early. */
|
|
268
|
+
userAttention = createUserAttention();
|
|
264
269
|
// -----------------------------------------------------------------------
|
|
265
270
|
// Fast model tier (fast-model-tier §3.1 / §3.3)
|
|
266
271
|
//
|
|
@@ -735,15 +740,19 @@ export class AgentController {
|
|
|
735
740
|
supervisor: this.procs,
|
|
736
741
|
waitSeconds: () => this.config.jobs.waitSeconds,
|
|
737
742
|
stallSeconds: () => this.config.jobs.stallSeconds,
|
|
738
|
-
toolTimeoutMs: () => this.config.toolTimeoutMs,
|
|
739
743
|
maxConsecutiveWaits: PROC_LIMITS.maxConsecutiveWaits,
|
|
740
744
|
terminalRecord: (id) => this.jobRelay?.terminalRecord(id),
|
|
745
|
+
// THE THREE LONG-WAIT PORTS (extended-job-wait). `holdWatchdog` wraps the
|
|
746
|
+
// WHOLE wait because tools run one at a time (I-3); `attention` is
|
|
747
|
+
// subscribe-only (RV-1); the view listener never throws back (I-6).
|
|
748
|
+
onWait: (event) => this.emitWait(event),
|
|
749
|
+
holdWatchdog: (fn) => this.withPausedWatchdog(fn),
|
|
750
|
+
attention: { subscribe: (listener) => this.userAttention.subscribe(listener) },
|
|
741
751
|
}),
|
|
742
752
|
makeJobList({
|
|
743
753
|
supervisor: this.procs,
|
|
744
754
|
waitSeconds: () => this.config.jobs.waitSeconds,
|
|
745
755
|
stallSeconds: () => this.config.jobs.stallSeconds,
|
|
746
|
-
toolTimeoutMs: () => this.config.toolTimeoutMs,
|
|
747
756
|
maxConsecutiveWaits: PROC_LIMITS.maxConsecutiveWaits,
|
|
748
757
|
terminalRecord: (id) => this.jobRelay?.terminalRecord(id),
|
|
749
758
|
}),
|
|
@@ -961,6 +970,13 @@ export class AgentController {
|
|
|
961
970
|
// outlived a mid-session `Settings > Team` switch to the supervisor
|
|
962
971
|
// and killed the dispatch the supervisor was meant to look after.
|
|
963
972
|
...(this.teamRegistered ? { task: 0 } : {}),
|
|
973
|
+
// THE EXECUTOR'S OWN CLOCK FOR A LONG WAIT (extended-job-wait I-2 / I-4').
|
|
974
|
+
// Without it `job_wait` falls onto `toolTimeoutMs` (180 s) and a 20-minute
|
|
975
|
+
// wait dies at three; with `0` the wait would have no independent backstop
|
|
976
|
+
// while the watchdog is paused. A finite value derived from the one
|
|
977
|
+
// authority, `PROC_LIMITS.waitMaxSeconds`, is both. `jobsRegistered` is
|
|
978
|
+
// assigned in the constructor before this table is built.
|
|
979
|
+
...(this.jobsRegistered ? { job_wait: JOB_WAIT_TOOL_CEILING_MS } : {}),
|
|
964
980
|
// P0 (governance-mode D-21 / RV-0): `gov_dispatch` runs a whole governed stage,
|
|
965
981
|
// routinely longer than `toolTimeoutMs`, and it listens to `ctx.signal` (it hands
|
|
966
982
|
// it to `TeamRuntime.dispatch`), which is what makes `0` safe. Registered in the SAME
|
|
@@ -1587,6 +1603,10 @@ export class AgentController {
|
|
|
1587
1603
|
this.pendingUserSteering.delete(id);
|
|
1588
1604
|
throw error;
|
|
1589
1605
|
}
|
|
1606
|
+
// AFTER a successful steer, and ONLY for user input (I-7): a reviewer or a job
|
|
1607
|
+
// notice must not cut short the wait the user chose.
|
|
1608
|
+
if (user)
|
|
1609
|
+
this.userAttention.notify();
|
|
1590
1610
|
return id;
|
|
1591
1611
|
}
|
|
1592
1612
|
followUp(text) {
|
|
@@ -1948,6 +1968,12 @@ export class AgentController {
|
|
|
1948
1968
|
* rather than a counter, so a nested call's `finally` re-arms the watchdog
|
|
1949
1969
|
* while the outer wait is still going. That is why a child's `--confirm`
|
|
1950
1970
|
* dialog does NOT route through this (see the constructor's `humanQueue`).
|
|
1971
|
+
*
|
|
1972
|
+
* `job_wait` ALSO DEPENDS ON THAT (extended-job-wait I-3): it holds the pause for
|
|
1973
|
+
* up to 20 minutes and is safe only because the engine runs a batch's tools
|
|
1974
|
+
* one at a time. If tool execution ever becomes parallel, make this a COUNTER
|
|
1975
|
+
* first - otherwise the first tool to finish re-arms the watchdog under a
|
|
1976
|
+
* still-waiting sibling, which then kills the run at the idle timeout.
|
|
1951
1977
|
*/
|
|
1952
1978
|
async withPausedWatchdog(fn) {
|
|
1953
1979
|
this.agent.pauseIdleWatchdog();
|
|
@@ -2501,6 +2527,33 @@ export class AgentController {
|
|
|
2501
2527
|
}
|
|
2502
2528
|
}
|
|
2503
2529
|
}
|
|
2530
|
+
/**
|
|
2531
|
+
* Subscribe to `job_wait`'s live view (`begin` / 1 Hz `update` / `end`).
|
|
2532
|
+
*
|
|
2533
|
+
* NOT GATED ON `liveToolOutput` (extended-job-wait D-9): that switch bounds a
|
|
2534
|
+
* high-frequency byte stream; this channel is capped at 1 Hz, and switching it
|
|
2535
|
+
* off would bring back the 20-minute black box.
|
|
2536
|
+
*/
|
|
2537
|
+
subscribeWait(listener) {
|
|
2538
|
+
this.waitListeners.add(listener);
|
|
2539
|
+
return () => {
|
|
2540
|
+
this.waitListeners.delete(listener);
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
/** Fan out one wait event; a throwing listener is logged and skipped (I-6). */
|
|
2544
|
+
emitWait(event) {
|
|
2545
|
+
for (const listener of this.waitListeners) {
|
|
2546
|
+
try {
|
|
2547
|
+
listener(event);
|
|
2548
|
+
}
|
|
2549
|
+
catch (err) {
|
|
2550
|
+
getLogger().debug('tool', 'wait_listener_threw', {
|
|
2551
|
+
toolCallId: event.toolCallId,
|
|
2552
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2504
2557
|
/**
|
|
2505
2558
|
* Subscribe to the CLI-local live tool-output stream (§3.1.3).
|
|
2506
2559
|
*
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invocation cards (tui-invocation-cards): what a transcript shows INSTEAD of the
|
|
3
|
+
* system prompt a command submitted on the user's behalf.
|
|
4
|
+
*
|
|
5
|
+
* Pure and total: no IO, no React, no Ink, nothing but `import type`. ASCII ONLY -
|
|
6
|
+
* `src/agent/**` new files sit inside the `glyphs.test.ts` scan scope.
|
|
7
|
+
*
|
|
8
|
+
* ONE CONSTRUCTOR (I-3). `buildInvocationCard` is the only function that turns raw
|
|
9
|
+
* command-layer material into a card, `App.submitMessage` is its only caller, and
|
|
10
|
+
* `sanitizeInvocationCard` (the session-load path) shares its normaliser so a card
|
|
11
|
+
* written today and a card read back tomorrow cannot drift apart.
|
|
12
|
+
*
|
|
13
|
+
* ORDER IS REDACT -> NORMALISE -> LIMIT, and it is the load-bearing part (I-6). Cut
|
|
14
|
+
* first and a secret that straddles the cut leaves a prefix shorter than the
|
|
15
|
+
* redaction patterns' minimum length: the half-key sails through into the
|
|
16
|
+
* transcript, the session file and the exit replay.
|
|
17
|
+
*/
|
|
18
|
+
export type InvocationVariant = 'skill' | 'team' | 'todo' | 'govern';
|
|
19
|
+
/** Everything one card needs; persisted, queued and rendered in this one shape. */
|
|
20
|
+
export interface InvocationCard {
|
|
21
|
+
readonly variant: InvocationVariant;
|
|
22
|
+
/** Skill name / 'continue' / 'resume'; '' for team. */
|
|
23
|
+
readonly name: string;
|
|
24
|
+
/** What the user typed (or its equivalent), redacted and capped; also `Entry.text`. */
|
|
25
|
+
readonly commandLine: string;
|
|
26
|
+
/** The user's own words (args / brief), redacted and trimmed; may be ''. */
|
|
27
|
+
readonly input: string;
|
|
28
|
+
/** Characters cut from `input`; 0 when nothing was cut. */
|
|
29
|
+
readonly inputHidden: number;
|
|
30
|
+
/** Fact fragments, each at most `factMaxChars`, at most `factsMax` of them. */
|
|
31
|
+
readonly facts: readonly string[];
|
|
32
|
+
/** One disclosure sentence. */
|
|
33
|
+
readonly note?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The renderer, the height estimate and the loader all read THESE numbers (I-4).
|
|
37
|
+
* A card's height is a function of its data and these limits, so it is bounded:
|
|
38
|
+
* header + `inputMaxLines` + hidden tail + note.
|
|
39
|
+
*/
|
|
40
|
+
export declare const INVOCATION_LIMITS: {
|
|
41
|
+
readonly commandLineMaxChars: 2000;
|
|
42
|
+
readonly inputMaxChars: 480;
|
|
43
|
+
readonly inputMaxLines: 8;
|
|
44
|
+
readonly nameMaxChars: 64;
|
|
45
|
+
readonly factsMax: 5;
|
|
46
|
+
readonly factMaxChars: 28;
|
|
47
|
+
readonly noteMaxChars: 140;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Raw material from the command layer: NOT redacted, NOT trimmed. Commands must not
|
|
51
|
+
* cut or scrub it themselves - both happen exactly once, in `buildInvocationCard`.
|
|
52
|
+
*/
|
|
53
|
+
export interface InvocationCardInput {
|
|
54
|
+
variant: InvocationVariant;
|
|
55
|
+
name?: string;
|
|
56
|
+
commandLine: string;
|
|
57
|
+
input?: string;
|
|
58
|
+
facts?: readonly string[];
|
|
59
|
+
note?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Total, never throws. `redact` is injected (the same closure `Command:` echo uses) so
|
|
63
|
+
* this module depends on nothing; the default is the identity for tests and the loader.
|
|
64
|
+
*/
|
|
65
|
+
export declare function buildInvocationCard(input: InvocationCardInput, redact?: (text: string) => string): InvocationCard;
|
|
66
|
+
/**
|
|
67
|
+
* Session-load path: `null` for anything that is not a plausible card, so the caller
|
|
68
|
+
* drops the field and the entry degrades to a plain user message (I-7). Re-derives
|
|
69
|
+
* `inputHidden` from nothing - it trusts the stored count only when it is a sane integer.
|
|
70
|
+
*/
|
|
71
|
+
export declare function sanitizeInvocationCard(value: unknown): InvocationCard | null;
|
|
72
|
+
/**
|
|
73
|
+
* Emergency kill switch (`ARAGON_NO_INVOCATION_CARDS=1`): every command and the App fall
|
|
74
|
+
* back to the pre-card behaviour. Read at call time, not import time, so a test (or a
|
|
75
|
+
* wrapper script) that sets the variable after load is honoured.
|
|
76
|
+
*/
|
|
77
|
+
export declare function invocationCardsEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invocation cards (tui-invocation-cards): what a transcript shows INSTEAD of the
|
|
3
|
+
* system prompt a command submitted on the user's behalf.
|
|
4
|
+
*
|
|
5
|
+
* Pure and total: no IO, no React, no Ink, nothing but `import type`. ASCII ONLY -
|
|
6
|
+
* `src/agent/**` new files sit inside the `glyphs.test.ts` scan scope.
|
|
7
|
+
*
|
|
8
|
+
* ONE CONSTRUCTOR (I-3). `buildInvocationCard` is the only function that turns raw
|
|
9
|
+
* command-layer material into a card, `App.submitMessage` is its only caller, and
|
|
10
|
+
* `sanitizeInvocationCard` (the session-load path) shares its normaliser so a card
|
|
11
|
+
* written today and a card read back tomorrow cannot drift apart.
|
|
12
|
+
*
|
|
13
|
+
* ORDER IS REDACT -> NORMALISE -> LIMIT, and it is the load-bearing part (I-6). Cut
|
|
14
|
+
* first and a secret that straddles the cut leaves a prefix shorter than the
|
|
15
|
+
* redaction patterns' minimum length: the half-key sails through into the
|
|
16
|
+
* transcript, the session file and the exit replay.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* The renderer, the height estimate and the loader all read THESE numbers (I-4).
|
|
20
|
+
* A card's height is a function of its data and these limits, so it is bounded:
|
|
21
|
+
* header + `inputMaxLines` + hidden tail + note.
|
|
22
|
+
*/
|
|
23
|
+
export const INVOCATION_LIMITS = {
|
|
24
|
+
commandLineMaxChars: 2000,
|
|
25
|
+
inputMaxChars: 480,
|
|
26
|
+
inputMaxLines: 8,
|
|
27
|
+
nameMaxChars: 64,
|
|
28
|
+
factsMax: 5,
|
|
29
|
+
factMaxChars: 28,
|
|
30
|
+
noteMaxChars: 140,
|
|
31
|
+
};
|
|
32
|
+
const VARIANTS = ['skill', 'team', 'todo', 'govern'];
|
|
33
|
+
// C0 except TAB (\x09) and LF (\x0a), then DEL and C1 (which includes the 8-bit CSI
|
|
34
|
+
// \x9b). Matches the range `QueueOverlay` strips, so the card is no more permissive.
|
|
35
|
+
// eslint-disable-next-line no-control-regex
|
|
36
|
+
const CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f-\x9f]/g;
|
|
37
|
+
const CAP_MARK = '...';
|
|
38
|
+
/** Cut at a code-point boundary: a surrogate pair is never split. */
|
|
39
|
+
function sliceCodePoints(text, count) {
|
|
40
|
+
if (count <= 0)
|
|
41
|
+
return '';
|
|
42
|
+
if (text.length <= count)
|
|
43
|
+
return text;
|
|
44
|
+
let end = count;
|
|
45
|
+
const last = text.charCodeAt(end - 1);
|
|
46
|
+
// A high surrogate at the cut would orphan its low half.
|
|
47
|
+
if (last >= 0xd800 && last <= 0xdbff)
|
|
48
|
+
end -= 1;
|
|
49
|
+
return text.slice(0, end);
|
|
50
|
+
}
|
|
51
|
+
function countCodePoints(text) {
|
|
52
|
+
let count = 0;
|
|
53
|
+
for (const _ of text)
|
|
54
|
+
count += 1;
|
|
55
|
+
return count;
|
|
56
|
+
}
|
|
57
|
+
/** CRLF and bare CR to LF (a bare CR returns Ink's cursor to column 0), then strip controls. */
|
|
58
|
+
function normalizeText(text) {
|
|
59
|
+
return text.replace(/\r\n?/g, '\n').replace(CONTROL_CHARS, '');
|
|
60
|
+
}
|
|
61
|
+
function capCommandLine(text) {
|
|
62
|
+
const max = INVOCATION_LIMITS.commandLineMaxChars;
|
|
63
|
+
if (text.length <= max)
|
|
64
|
+
return text;
|
|
65
|
+
return sliceCodePoints(text, max - CAP_MARK.length) + CAP_MARK;
|
|
66
|
+
}
|
|
67
|
+
function capInput(text) {
|
|
68
|
+
const trimmed = text.trim();
|
|
69
|
+
if (trimmed === '')
|
|
70
|
+
return { input: '', hidden: 0 };
|
|
71
|
+
const lines = trimmed.split('\n');
|
|
72
|
+
let kept = lines.length > INVOCATION_LIMITS.inputMaxLines
|
|
73
|
+
? lines.slice(0, INVOCATION_LIMITS.inputMaxLines).join('\n') : trimmed;
|
|
74
|
+
kept = sliceCodePoints(kept, INVOCATION_LIMITS.inputMaxChars).trimEnd();
|
|
75
|
+
return { input: kept, hidden: countCodePoints(trimmed) - countCodePoints(kept) };
|
|
76
|
+
}
|
|
77
|
+
function capSingleLine(text, max) {
|
|
78
|
+
const flat = text.replace(/\s+/g, ' ').trim();
|
|
79
|
+
if (countCodePoints(flat) <= max)
|
|
80
|
+
return flat;
|
|
81
|
+
return sliceCodePoints(flat, max - CAP_MARK.length) + CAP_MARK;
|
|
82
|
+
}
|
|
83
|
+
function capFacts(facts) {
|
|
84
|
+
return facts
|
|
85
|
+
.map((fact) => capSingleLine(fact, INVOCATION_LIMITS.factMaxChars))
|
|
86
|
+
.filter((fact) => fact !== '')
|
|
87
|
+
.slice(0, INVOCATION_LIMITS.factsMax);
|
|
88
|
+
}
|
|
89
|
+
function capNote(note) {
|
|
90
|
+
if (note === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
const capped = capSingleLine(note, INVOCATION_LIMITS.noteMaxChars);
|
|
93
|
+
return capped === '' ? undefined : capped;
|
|
94
|
+
}
|
|
95
|
+
/** Shared by the write path and the load path so the two cannot diverge. */
|
|
96
|
+
function assemble(parts) {
|
|
97
|
+
const { input, hidden } = capInput(normalizeText(parts.input));
|
|
98
|
+
const note = capNote(normalizeText(parts.note ?? ''));
|
|
99
|
+
return {
|
|
100
|
+
variant: parts.variant,
|
|
101
|
+
name: capSingleLine(normalizeText(parts.name), INVOCATION_LIMITS.nameMaxChars),
|
|
102
|
+
commandLine: capCommandLine(normalizeText(parts.commandLine).trim()),
|
|
103
|
+
input,
|
|
104
|
+
inputHidden: hidden,
|
|
105
|
+
facts: capFacts(parts.facts.map(normalizeText)),
|
|
106
|
+
...(note !== undefined ? { note } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Total, never throws. `redact` is injected (the same closure `Command:` echo uses) so
|
|
111
|
+
* this module depends on nothing; the default is the identity for tests and the loader.
|
|
112
|
+
*/
|
|
113
|
+
export function buildInvocationCard(input, redact = (text) => text) {
|
|
114
|
+
const scrub = (text) => {
|
|
115
|
+
if (text === undefined)
|
|
116
|
+
return '';
|
|
117
|
+
try {
|
|
118
|
+
return redact(text);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
return assemble({
|
|
125
|
+
variant: input.variant,
|
|
126
|
+
name: scrub(input.name),
|
|
127
|
+
commandLine: scrub(input.commandLine),
|
|
128
|
+
input: scrub(input.input),
|
|
129
|
+
facts: (input.facts ?? []).map((fact) => scrub(fact)),
|
|
130
|
+
note: input.note === undefined ? undefined : scrub(input.note),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function asString(value) {
|
|
134
|
+
return typeof value === 'string' ? value : null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Session-load path: `null` for anything that is not a plausible card, so the caller
|
|
138
|
+
* drops the field and the entry degrades to a plain user message (I-7). Re-derives
|
|
139
|
+
* `inputHidden` from nothing - it trusts the stored count only when it is a sane integer.
|
|
140
|
+
*/
|
|
141
|
+
export function sanitizeInvocationCard(value) {
|
|
142
|
+
if (typeof value !== 'object' || value === null)
|
|
143
|
+
return null;
|
|
144
|
+
const raw = value;
|
|
145
|
+
const variant = VARIANTS.find((candidate) => candidate === raw.variant);
|
|
146
|
+
const commandLine = asString(raw.commandLine);
|
|
147
|
+
if (variant === undefined || commandLine === null)
|
|
148
|
+
return null;
|
|
149
|
+
const name = asString(raw.name) ?? '';
|
|
150
|
+
const input = asString(raw.input) ?? '';
|
|
151
|
+
const facts = Array.isArray(raw.facts)
|
|
152
|
+
? raw.facts.filter((fact) => typeof fact === 'string') : [];
|
|
153
|
+
const note = asString(raw.note) ?? undefined;
|
|
154
|
+
const card = assemble({ variant, name, commandLine, input, facts, note });
|
|
155
|
+
// A stored card that was already capped must keep its recorded `inputHidden`; the
|
|
156
|
+
// re-cap above can only ever add to it.
|
|
157
|
+
const stored = raw.inputHidden;
|
|
158
|
+
const storedHidden = typeof stored === 'number' && Number.isInteger(stored) && stored > 0
|
|
159
|
+
? Math.min(stored, 10_000_000) : 0;
|
|
160
|
+
return { ...card, inputHidden: Math.max(card.inputHidden, storedHidden) };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Emergency kill switch (`ARAGON_NO_INVOCATION_CARDS=1`): every command and the App fall
|
|
164
|
+
* back to the pre-card behaviour. Read at call time, not import time, so a test (or a
|
|
165
|
+
* wrapper script) that sets the variable after load is honoured.
|
|
166
|
+
*/
|
|
167
|
+
export function invocationCardsEnabled(env = process.env) {
|
|
168
|
+
return env.ARAGON_NO_INVOCATION_CARDS !== '1';
|
|
169
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { InvocationCardInput } from './invocation-card.js';
|
|
1
2
|
/** Explicit TUI task boundary; omission retains legacy programmatic behavior. */
|
|
2
3
|
export interface PromptOptions {
|
|
3
4
|
todoPolicy?: 'new-task' | 'continue';
|
|
@@ -5,6 +6,11 @@ export interface PromptOptions {
|
|
|
5
6
|
/** History recording is independent of whether this message continues a plan. */
|
|
6
7
|
export interface SubmitMessageOptions extends PromptOptions {
|
|
7
8
|
userInitiated?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Raw material for the transcript card. `submitMessage` builds the card exactly once
|
|
11
|
+
* (redact -> normalise -> limit); the payload sent to the model is unaffected.
|
|
12
|
+
*/
|
|
13
|
+
invocation?: InvocationCardInput;
|
|
8
14
|
}
|
|
9
15
|
/** Startup outcome only; engine events still describe the result of the run. */
|
|
10
16
|
export type PromptOutcome = {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { Entry } from './reducer.js';
|
|
2
|
+
import type { InvocationCard } from './invocation-card.js';
|
|
2
3
|
/** Full user text retained until Core acknowledges its opaque ID. */
|
|
3
4
|
export interface PendingSteering {
|
|
4
5
|
readonly queueId: string;
|
|
5
6
|
readonly text: string;
|
|
7
|
+
/** Carried so the `queued -> user` rewrite keeps the card (tui-invocation-cards). */
|
|
8
|
+
readonly invocation?: InvocationCard;
|
|
6
9
|
}
|
|
7
10
|
/** Hide only active pending entries, using the same projection for measurement and paint. */
|
|
8
11
|
export declare function selectTranscriptEntries(entries: Entry[], pending: readonly PendingSteering[]): Entry[];
|
|
@@ -13,7 +13,10 @@ export function reconcileSteeringReceipt(options) {
|
|
|
13
13
|
if (!ids.size)
|
|
14
14
|
return { entries: options.entries, pending: [...options.pending], changed: false };
|
|
15
15
|
const entries = mergePendingEntries(options.entries, options.pending).map((entry) => entry.kind === 'queued' && ids.has(entry.queueId ?? '')
|
|
16
|
-
? {
|
|
16
|
+
? {
|
|
17
|
+
id: entry.id, kind: 'user', text: entry.text,
|
|
18
|
+
...(entry.invocation ? { invocation: entry.invocation } : {}),
|
|
19
|
+
} : entry);
|
|
17
20
|
return { entries, pending: options.pending.filter((item) => !ids.has(item.queueId)), changed: true };
|
|
18
21
|
}
|
|
19
22
|
/** Supplement trimmed/cleared history for save and exit without deduplicating text. */
|