@cspeach/cli 1.1.19 → 1.1.21
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/agent/anthropic-provider.js +30 -10
- package/dist/agent/cache-keepalive.js +162 -0
- package/dist/agent/cold-prune.js +116 -0
- package/dist/agent/loop.js +675 -148
- package/dist/agent/provider-shape.js +263 -0
- package/dist/agent/providers/ai-hub-provider.js +17 -2
- package/dist/agent/providers/byok-provider.js +33 -3
- package/dist/agent/providers/local-provider.js +8 -1
- package/dist/agent/repair-partial.js +66 -5
- package/dist/agent/summarise-via-provider.js +6 -1
- package/dist/agent/system-prompt.js +38 -0
- package/dist/agent/tool-dispatch.js +8 -0
- package/dist/agent/tool-loading-pin.js +100 -0
- package/dist/cli.js +11 -0
- package/dist/commands/auto-compact.js +33 -16
- package/dist/commands/compact.js +37 -2
- package/dist/commands/config-set.js +10 -1
- package/dist/commands/config-show.js +11 -0
- package/dist/commands/cost.js +14 -2
- package/dist/commands/plan-audit.js +1 -0
- package/dist/config/loader.js +55 -2
- package/dist/cost/cost-log.js +62 -2
- package/dist/cost/pricing.js +6 -2
- package/dist/lib/spill-labels.js +13 -0
- package/dist/models/resolve.js +93 -2
- package/dist/models/server-config.js +158 -3
- package/dist/one-shot.js +15 -5
- package/dist/projects/image-attachments.js +15 -2
- package/dist/renderer/footer-line.js +6 -2
- package/dist/renderer/startup-lines.js +5 -3
- package/dist/renderer/tool-labels.js +33 -2
- package/dist/renderer/ui-width.js +13 -0
- package/dist/repl/current-transport.js +13 -0
- package/dist/repl/post-turn-status.js +8 -1
- package/dist/repl.js +71 -10
- package/dist/session/repin-model.js +18 -0
- package/dist/session/store.js +16 -2
- package/dist/skills/bundled-skills.js +80 -80
- package/dist/skills/preamble.js +75 -0
- package/dist/skills/source-manifest.js +11 -1
- package/dist/tools/filesystem/file-read.js +11 -1
- package/dist/tools/result-spill.js +238 -0
- package/dist/tools/sap-read.js +58 -14
- package/dist/tools/shell/shell_exec.js +9 -0
- package/dist/tools/subagent/adt-serial.js +33 -0
- package/dist/tools/subagent/agent_run.js +2 -0
- package/dist/tools/subagent/read_agent.js +178 -0
- package/dist/tools/subagent/reader-prompt.js +48 -0
- package/dist/tools/todo.js +3 -1
- package/dist/tools/tool-loading.js +255 -0
- package/dist/tools/tool-output-read.js +117 -0
- package/dist/tools/transport.js +6 -1
- package/dist/ui/footer.js +5 -5
- package/dist/ui/sap-state-store.js +1 -1
- package/dist/ui/turn-status-emitter.js +37 -0
- package/dist/ui/turn-status.js +1 -1
- package/package.json +2 -1
package/dist/repl.js
CHANGED
|
@@ -36,7 +36,8 @@ import { AdtClient, snapshots } from '@cspeach/sap-client';
|
|
|
36
36
|
import { makeSessionId, saveSession, sessionSaved } from './session/store.js';
|
|
37
37
|
import { newSession } from './session/schema.js';
|
|
38
38
|
import { resolveModelRole } from './models/resolve.js';
|
|
39
|
-
import { loadServerModelConfig } from './models/server-config.js';
|
|
39
|
+
import { loadServerModelConfig, loadCustomerModelConfig, fetchToolLoadingMode } from './models/server-config.js';
|
|
40
|
+
import { repinResumedSessionModel } from './session/repin-model.js';
|
|
40
41
|
import { computeCost } from './cost/pricing.js';
|
|
41
42
|
import { formatSessionSpendValue } from './repl/session-spend-line.js';
|
|
42
43
|
import { formatGoodbye } from './renderer/goodbye.js';
|
|
@@ -45,6 +46,7 @@ import { initCredits } from './cost/credits-wire.js';
|
|
|
45
46
|
import { createProviderForMode } from './agent/providers/factory.js';
|
|
46
47
|
import { assertModeLicensed, CspeachLicenseError } from './agent/providers/license-gate.js';
|
|
47
48
|
import { runTurn } from './agent/loop.js';
|
|
49
|
+
import { IdleKeepalive } from './agent/cache-keepalive.js';
|
|
48
50
|
import { renderTurnError } from './agent/turn-error-ux.js';
|
|
49
51
|
import { runRerouteTurn } from './repl/reroute-turn.js';
|
|
50
52
|
// BUG-2b (2026-07-05 live smoke) — pure (no-Ink) modules, safe to import
|
|
@@ -105,7 +107,7 @@ function prefillReadline(rl, text) {
|
|
|
105
107
|
import { BracketedPasteDecoder, enableBracketedPaste, disableBracketedPaste, } from './repl/bracketed-paste.js';
|
|
106
108
|
import { InkStdinPasteGuard } from './repl/ink-stdin-guard.js';
|
|
107
109
|
import { printHelp } from './commands/help.js';
|
|
108
|
-
import { shouldUseInk, setOneShotOverride
|
|
110
|
+
import { shouldUseInk, setOneShotOverride } from './renderer/tty.js';
|
|
109
111
|
import { applyStartupRendering } from './renderer/rendering-mode.js';
|
|
110
112
|
import { createClassicOutputEmitter } from './renderer/notices.js';
|
|
111
113
|
import { saveConfig } from './config/loader.js';
|
|
@@ -137,6 +139,8 @@ import './tools/ask-question.js';
|
|
|
137
139
|
import './tools/dispatch-skill.js';
|
|
138
140
|
import './tools/todo.js';
|
|
139
141
|
import './tools/filesystem/file-read.js';
|
|
142
|
+
import './tools/tool-output-read.js';
|
|
143
|
+
import { pruneSpillFiles } from './tools/result-spill.js';
|
|
140
144
|
import './tools/filesystem/read-document.js';
|
|
141
145
|
import './tools/filesystem/file-edit.js';
|
|
142
146
|
import './tools/filesystem/file-write.js';
|
|
@@ -158,6 +162,7 @@ import './tools/subagent/background_run.js';
|
|
|
158
162
|
import './tools/subagent/monitor_emit.js';
|
|
159
163
|
import './tools/subagent/schedule_draft_create.js';
|
|
160
164
|
import './tools/subagent/agent_run.js';
|
|
165
|
+
import './tools/subagent/read_agent.js';
|
|
161
166
|
const HISTORY_SIZE = 500;
|
|
162
167
|
// Persistence (read + dedup-guarded append) lives in ./repl/history.ts —
|
|
163
168
|
// the single writer shared with the Ink Footer (Task 8, ux-wave1).
|
|
@@ -527,11 +532,22 @@ export async function runRepl(opts) {
|
|
|
527
532
|
cfg = await loadConfig();
|
|
528
533
|
}
|
|
529
534
|
}
|
|
530
|
-
// model-governance step 2d —
|
|
531
|
-
//
|
|
532
|
-
// fetch
|
|
533
|
-
//
|
|
534
|
-
|
|
535
|
+
// model-governance step 2d — fetch the served model-config (managed mode
|
|
536
|
+
// only). Task 3 (2026-09-27): AWAITED, but startup waits at most 1.5 s — a
|
|
537
|
+
// slower fetch aborts and falls back to a fresh disk cache, else the
|
|
538
|
+
// built-ins. The session model is resolved ONCE at newSession below and
|
|
539
|
+
// pinned, so call 0 and call 1 never run on different models (a switch
|
|
540
|
+
// forces a full cache rewrite). Precedence: env > local config > server >
|
|
541
|
+
// built-in. Never throws.
|
|
542
|
+
// Task 12 — the customer layer (/v1/me/model-config, every mode with a
|
|
543
|
+
// key) loads in parallel under the SAME 1.5 s cap: no extra wait. Final
|
|
544
|
+
// precedence: env > local > customer > server > built-in. Fails open.
|
|
545
|
+
await Promise.all([
|
|
546
|
+
loadServerModelConfig(cfg, { timeoutMs: 1500 }),
|
|
547
|
+
loadCustomerModelConfig(cfg, async () => apiKey ?? '', { timeoutMs: 1500 }),
|
|
548
|
+
// Task 16 (F13) — BYOK reads only the served tool_loading (signed in only).
|
|
549
|
+
fetchToolLoadingMode(cfg, async () => apiKey ?? '', { timeoutMs: 1500 }),
|
|
550
|
+
]);
|
|
535
551
|
// Credits (2026-09-16) — resolve ONCE, before any cost figure can render.
|
|
536
552
|
// A managed customer is billed in credits at a multiplier we do not
|
|
537
553
|
// disclose, so the dollar figures (our Anthropic COGS) must never reach
|
|
@@ -688,6 +704,10 @@ export async function runRepl(opts) {
|
|
|
688
704
|
// preserves messages[], usage, last_skill, awaitingSkillAnswer, and the
|
|
689
705
|
// turnInterrupted flag from the prior run. Skipping newSession() avoids
|
|
690
706
|
// overwriting the saved file with an empty fresh one.
|
|
707
|
+
// Task 3 fix I1 — except the model: a resumed session re-resolves it ONCE
|
|
708
|
+
// through the same role resolution new sessions use (after the awaited
|
|
709
|
+
// served config), marks it 'predicted', saves, then stays pinned. Every
|
|
710
|
+
// resume entry (picker, --resume <id>) lands here via session/resume.ts.
|
|
691
711
|
const session = opts?.resumedSession ?? newSession(makeSessionId(), selectedAlias, null, resolveModelRole('session_default', cfg));
|
|
692
712
|
// Round 6 item 4c — a resumed session keeps its skill for the first plain prompt.
|
|
693
713
|
let resumeKeepsSkill = Boolean(opts?.resumedSession?.skill);
|
|
@@ -710,9 +730,16 @@ export async function runRepl(opts) {
|
|
|
710
730
|
console.log(line.trimEnd());
|
|
711
731
|
},
|
|
712
732
|
});
|
|
733
|
+
// Merge (cost branch Task 3 + UI round 6) — a resumed session re-pins its
|
|
734
|
+
// model from the current config (after any --sap-alias switch above).
|
|
735
|
+
if (opts?.resumedSession) {
|
|
736
|
+
await repinResumedSessionModel(session, cfg);
|
|
737
|
+
}
|
|
713
738
|
// Live fix 2026-09-26 — notices cut to one screen line keep their full text here.
|
|
714
739
|
// Review M6: sanitised id; notice logs older than 14 days are collected.
|
|
715
740
|
pruneNoticeLogs(logsDir());
|
|
741
|
+
// Task 17 — saved large tool outputs (tool-*.txt) older than 14 days go too.
|
|
742
|
+
pruneSpillFiles();
|
|
716
743
|
setNoticeLogPath(noticeLogPathFor(logsDir(), session.id));
|
|
717
744
|
// License gate: non-managed modes (byok/local/ai-hub) bypass our proxy for
|
|
718
745
|
// inference, so without this anyone who never logged in could use the bundled
|
|
@@ -733,6 +760,13 @@ export async function runRepl(opts) {
|
|
|
733
760
|
throw new Error('No API key configured');
|
|
734
761
|
return key;
|
|
735
762
|
});
|
|
763
|
+
// Task 15 — keep the prompt cache warm while the user is idle between
|
|
764
|
+
// turns: armed after a turn, disarmed on the next submit and on exit;
|
|
765
|
+
// `[keepalive] idle_pings` per idle period (managed / BYOK only).
|
|
766
|
+
const idleKeepalive = new IdleKeepalive({
|
|
767
|
+
provider,
|
|
768
|
+
maxPings: () => (cfg.keepalive?.enabled === false ? 0 : (cfg.keepalive?.idle_pings ?? 4)),
|
|
769
|
+
});
|
|
736
770
|
// Panels status line — writeMode is the same EFFECTIVE (role-clamped) mode
|
|
737
771
|
// the legacy line prints.
|
|
738
772
|
const panelsStatus = {
|
|
@@ -941,7 +975,7 @@ export async function runRepl(opts) {
|
|
|
941
975
|
// new turn's content touched with no separator and looked like
|
|
942
976
|
// garbled overlap).
|
|
943
977
|
if (trimmed !== '/exit' && trimmed !== '/quit') {
|
|
944
|
-
chunkEmitter.emit('chunk', formatPromptEcho(trimmed,
|
|
978
|
+
chunkEmitter.emit('chunk', formatPromptEcho(trimmed, process.stdout.columns || 80));
|
|
945
979
|
}
|
|
946
980
|
if (trimmed === '/exit' || trimmed === '/quit') {
|
|
947
981
|
done = true;
|
|
@@ -1042,7 +1076,7 @@ export async function runRepl(opts) {
|
|
|
1042
1076
|
return await summariseViaProvider(provider, buildSummarisationPrompt(), serialiseForSummariser(toSummarise),
|
|
1043
1077
|
// model-governance step 2d — compact model resolves env > local >
|
|
1044
1078
|
// server > built-in (== COMPACTION_MODEL when nothing set).
|
|
1045
|
-
{ model: resolveModelRole('compact', cfg), maxTokens: 4000, headers: { 'X-CSForge-Skill': 'compact' } });
|
|
1079
|
+
{ model: resolveModelRole('compact', cfg), maxTokens: 4000, headers: { 'X-CSForge-Skill': 'compact', 'X-CSPeach-Model-Role': 'compact' } });
|
|
1046
1080
|
};
|
|
1047
1081
|
try {
|
|
1048
1082
|
chunkEmitter.emit('chunk', '\n' + formatInfo('Compacting older turns into a summary…'));
|
|
@@ -1123,6 +1157,7 @@ export async function runRepl(opts) {
|
|
|
1123
1157
|
ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: inkPreview, chunkEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor },
|
|
1124
1158
|
chunkEmitter,
|
|
1125
1159
|
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
1160
|
+
autoCompact: true, // Task 19 — top-level REPL turn: auto-compact gate at turn end
|
|
1126
1161
|
signal: rerouteAbort.signal,
|
|
1127
1162
|
}),
|
|
1128
1163
|
session,
|
|
@@ -1638,8 +1673,13 @@ export async function runRepl(opts) {
|
|
|
1638
1673
|
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
1639
1674
|
signal: currentTurnAbort.signal,
|
|
1640
1675
|
suppressSaveHook: planResume !== null || planContinue !== null,
|
|
1676
|
+
// Task 19 — auto-compact at turn end, except plan resume/continue
|
|
1677
|
+
// turns (their post-turn persist indexes messages from turn start).
|
|
1678
|
+
autoCompact: planResume === null && planContinue === null,
|
|
1641
1679
|
// A4 — undefined on every non-plan turn (and when tiering is off).
|
|
1642
1680
|
modelOverride: planModelOverride,
|
|
1681
|
+
// Task 9 — a tier-down, never the session role (proxy must not enforce it).
|
|
1682
|
+
modelRole: planModelOverride ? 'audit' : undefined,
|
|
1643
1683
|
// Persist the plan revision the instant its manifest is finalised
|
|
1644
1684
|
// (before the continuation modal) so a Ctrl+C can't lose the phase.
|
|
1645
1685
|
onPlanManifest: planResumeForHook
|
|
@@ -1948,6 +1988,7 @@ export async function runRepl(opts) {
|
|
|
1948
1988
|
}
|
|
1949
1989
|
return;
|
|
1950
1990
|
}
|
|
1991
|
+
idleKeepalive.disarm(); // Task 15 — a submit ends the idle period
|
|
1951
1992
|
turnStatusEmitter.reserve();
|
|
1952
1993
|
postTurn = false;
|
|
1953
1994
|
postTurnMayChain = false;
|
|
@@ -1972,6 +2013,11 @@ export async function runRepl(opts) {
|
|
|
1972
2013
|
if (resolveDone)
|
|
1973
2014
|
resolveDone();
|
|
1974
2015
|
}
|
|
2016
|
+
// Task 15 — back at the prompt: warm the cache while the user reads
|
|
2017
|
+
// (no-op unless this submit made a new cacheable request). Not when
|
|
2018
|
+
// the submit ended in an exit (merge: UI round-3 /exit mid-turn).
|
|
2019
|
+
if (!done)
|
|
2020
|
+
idleKeepalive.armAfterTurn(session);
|
|
1975
2021
|
}
|
|
1976
2022
|
};
|
|
1977
2023
|
// Fix round 3 (item 5) — `/exit` / `/quit` typed while a turn runs:
|
|
@@ -2160,6 +2206,7 @@ export async function runRepl(opts) {
|
|
|
2160
2206
|
const exitTail = await settleWithin(dispatchTail, EXIT_TAIL_CAP_MS);
|
|
2161
2207
|
unmount();
|
|
2162
2208
|
setTraceEmitter(undefined);
|
|
2209
|
+
idleKeepalive.disarm(); // Task 15 — no pings after /exit
|
|
2163
2210
|
// Bug 2 (wave1 live-fix) — undo the paste plumbing so the user's
|
|
2164
2211
|
// shell doesn't keep receiving marker-wrapped pastes after exit.
|
|
2165
2212
|
process.stdin.unpipe(inkStdin);
|
|
@@ -2514,8 +2561,12 @@ export async function runRepl(opts) {
|
|
|
2514
2561
|
}
|
|
2515
2562
|
else {
|
|
2516
2563
|
const effectivePrompt = pendingPrefill ? promptStr + pendingPrefill : promptStr;
|
|
2564
|
+
// Task 15 — idle at the prompt: warm the cache (no-op unless the last
|
|
2565
|
+
// turn made a new cacheable request); disarmed once the line arrives.
|
|
2566
|
+
idleKeepalive.armAfterTurn(session);
|
|
2517
2567
|
try {
|
|
2518
2568
|
line = await rl.question(effectivePrompt);
|
|
2569
|
+
idleKeepalive.disarm();
|
|
2519
2570
|
}
|
|
2520
2571
|
catch {
|
|
2521
2572
|
// rl.question rejects for two distinct reasons:
|
|
@@ -2529,6 +2580,7 @@ export async function runRepl(opts) {
|
|
|
2529
2580
|
// discriminates the two cases reliably.
|
|
2530
2581
|
const rlAny = rl;
|
|
2531
2582
|
if (rlAny.closed) {
|
|
2583
|
+
idleKeepalive.disarm();
|
|
2532
2584
|
// UX-5 #5 — piped stdin: a multi-line chunk can resolve the
|
|
2533
2585
|
// pending question with line 1 and QUEUE line 2+ before EOF
|
|
2534
2586
|
// closes the readline. Drain the queue before exiting.
|
|
@@ -2686,7 +2738,7 @@ export async function runRepl(opts) {
|
|
|
2686
2738
|
return await summariseViaProvider(provider, buildSummarisationPrompt(), serialiseForSummariser(toSummarise),
|
|
2687
2739
|
// model-governance step 2d — compact model resolves env > local >
|
|
2688
2740
|
// server > built-in (== COMPACTION_MODEL when nothing set).
|
|
2689
|
-
{ model: resolveModelRole('compact', cfg), maxTokens: 4000, headers: { 'X-CSForge-Skill': 'compact' } });
|
|
2741
|
+
{ model: resolveModelRole('compact', cfg), maxTokens: 4000, headers: { 'X-CSForge-Skill': 'compact', 'X-CSPeach-Model-Role': 'compact' } });
|
|
2690
2742
|
};
|
|
2691
2743
|
try {
|
|
2692
2744
|
console.log('\n' + formatInfo('Compacting older turns into a summary…').trimEnd());
|
|
@@ -2766,6 +2818,7 @@ export async function runRepl(opts) {
|
|
|
2766
2818
|
skill: rr.skill,
|
|
2767
2819
|
ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: replPreviewHook, chunkEmitter: classicOutputEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor },
|
|
2768
2820
|
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
2821
|
+
autoCompact: true, // Task 19 — top-level REPL turn: auto-compact gate at turn end
|
|
2769
2822
|
}),
|
|
2770
2823
|
session,
|
|
2771
2824
|
print: (line) => console.error(line),
|
|
@@ -3264,6 +3317,8 @@ export async function runRepl(opts) {
|
|
|
3264
3317
|
let planSubagentDispatches;
|
|
3265
3318
|
const planResumeForHook = planResume;
|
|
3266
3319
|
const turnStart = Date.now();
|
|
3320
|
+
// UI fix round (1.1.20) — card waits are excluded from the receipt's time.
|
|
3321
|
+
const turnStartPausedMs = turnStatusEmitter.pausedMs();
|
|
3267
3322
|
// H1 — `session.usage` is session-LIFETIME (initialised once in
|
|
3268
3323
|
// newSession, saved across turns). The footer's "this turn" label was
|
|
3269
3324
|
// misleading — it actually showed cumulative lifetime tokens. Snapshot
|
|
@@ -3346,8 +3401,13 @@ export async function runRepl(opts) {
|
|
|
3346
3401
|
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
3347
3402
|
signal: turnAbort.signal,
|
|
3348
3403
|
suppressSaveHook: planResume !== null || planContinue !== null,
|
|
3404
|
+
// Task 19 — auto-compact at turn end, except plan resume/continue
|
|
3405
|
+
// turns (their post-turn persist indexes messages from turn start).
|
|
3406
|
+
autoCompact: planResume === null && planContinue === null,
|
|
3349
3407
|
// A4 — undefined on every non-plan turn (and when tiering is off).
|
|
3350
3408
|
modelOverride: planModelOverride,
|
|
3409
|
+
// Task 9 — a tier-down, never the session role (proxy must not enforce it).
|
|
3410
|
+
modelRole: planModelOverride ? 'audit' : undefined,
|
|
3351
3411
|
onPlanManifest: planResumeForHook
|
|
3352
3412
|
? async (assistantText) => {
|
|
3353
3413
|
try {
|
|
@@ -3579,6 +3639,7 @@ export async function runRepl(opts) {
|
|
|
3579
3639
|
cacheRead: turnStartCostSnapshot.cacheRead,
|
|
3580
3640
|
cacheCreate: turnStartCostSnapshot.cacheCreate,
|
|
3581
3641
|
startedAt: turnStart,
|
|
3642
|
+
pausedMs: turnStartPausedMs,
|
|
3582
3643
|
},
|
|
3583
3644
|
// A4 — price the turn at the model that actually ran it (classic-path
|
|
3584
3645
|
// mirror of the Ink call site above).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task 3 fix round 1 (I1, 2026-09-27) — re-pin a resumed session's model.
|
|
3
|
+
*
|
|
4
|
+
* The agent loop sends the session's PINNED `session.model` on every call.
|
|
5
|
+
* A resumed session carries the id saved at creation (possibly retired, or
|
|
6
|
+
* an id the served config has since moved away from), so on resume the
|
|
7
|
+
* model is re-resolved ONCE through the same path new sessions use —
|
|
8
|
+
* resolveModelRole('session_default', cfg), after the awaited served config —
|
|
9
|
+
* marked 'predicted', and saved. It then stays pinned for the rest of the
|
|
10
|
+
* session (managed-mode adoption in the loop may still update it).
|
|
11
|
+
*/
|
|
12
|
+
import { resolveModelRole } from '../models/resolve.js';
|
|
13
|
+
import { saveSession } from './store.js';
|
|
14
|
+
export async function repinResumedSessionModel(session, cfg) {
|
|
15
|
+
session.model = resolveModelRole('session_default', cfg);
|
|
16
|
+
session.served_model_source = 'predicted';
|
|
17
|
+
await saveSession(session);
|
|
18
|
+
}
|
package/dist/session/store.js
CHANGED
|
@@ -87,7 +87,8 @@ function validateSchema(parsed) {
|
|
|
87
87
|
if (!Array.isArray(msg.content))
|
|
88
88
|
continue;
|
|
89
89
|
for (const block of msg.content) {
|
|
90
|
-
|
|
90
|
+
// Task 16 (F6): server_tool_use (tool search) streams input the same way.
|
|
91
|
+
if ((block?.type === 'tool_use' || block?.type === 'server_tool_use') && typeof block.partial_json === 'string') {
|
|
91
92
|
if (block.input === undefined) {
|
|
92
93
|
try {
|
|
93
94
|
block.input = JSON.parse(block.partial_json);
|
|
@@ -108,6 +109,14 @@ function validateSchema(parsed) {
|
|
|
108
109
|
dropTrailingThinkingOnly(parsed.messages);
|
|
109
110
|
return parsed;
|
|
110
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Task 18 — a reader subagent (tools/subagent/read_agent.ts) saves its own
|
|
114
|
+
* `reader-*` session file; it is an internal child of a turn, not something
|
|
115
|
+
* the user resumes.
|
|
116
|
+
*/
|
|
117
|
+
export function isReaderSessionId(id) {
|
|
118
|
+
return typeof id === 'string' && id.startsWith('reader-');
|
|
119
|
+
}
|
|
111
120
|
/** How many sessions `cspeach --resume` lists — the unfinished count uses the same window. */
|
|
112
121
|
export const RESUME_PICKER_LIMIT = 20;
|
|
113
122
|
/**
|
|
@@ -119,7 +128,10 @@ export const RESUME_PICKER_LIMIT = 20;
|
|
|
119
128
|
* full pass cost ~1.3 s at startup with 4,410 files).
|
|
120
129
|
*/
|
|
121
130
|
async function recentSessionFiles(dir, limit) {
|
|
122
|
-
|
|
131
|
+
// Merge note — Task 18 reader-* child sessions are skipped by NAME here, so a
|
|
132
|
+
// burst of reader files can never crowd real sessions out of the window.
|
|
133
|
+
const files = (await fs.readdir(dir).catch(() => []))
|
|
134
|
+
.filter((f) => f.endsWith('.json') && !f.includes('-reader-'));
|
|
123
135
|
const cap = Math.max(limit * 3, 50);
|
|
124
136
|
if (files.length <= cap)
|
|
125
137
|
return files;
|
|
@@ -146,6 +158,8 @@ export async function listSessions(limit = 10, opts = {}) {
|
|
|
146
158
|
try {
|
|
147
159
|
const raw = await fs.readFile(path.join(dir, f), 'utf-8');
|
|
148
160
|
const parsed = JSON.parse(raw);
|
|
161
|
+
if (isReaderSessionId(parsed.id))
|
|
162
|
+
continue; // Task 18 — internal child, never resumed
|
|
149
163
|
const messages = parsed.messages ?? [];
|
|
150
164
|
// First user message that's a plain string (not a tool_result array) — the
|
|
151
165
|
// most recent prompt text that initiated a turn. We pick the LAST such
|