@cspeach/cli 1.1.51 → 1.1.53
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/chat-prompt.js +4 -1
- package/dist/agent/conversation-prompt.js +31 -13
- package/dist/agent/inline-skill-block.js +341 -0
- package/dist/agent/loop.js +304 -49
- package/dist/agent/maybe-build-project-context.js +11 -2
- package/dist/agent/provider-shape.js +8 -4
- package/dist/agent/providers/ai-hub-provider.js +2 -2
- package/dist/agent/providers/azure-provider.js +2 -2
- package/dist/agent/providers/byok-provider.js +2 -2
- package/dist/agent/providers/local-provider.js +2 -2
- package/dist/agent/repair-partial.js +21 -0
- package/dist/agent/skill-block-key.js +134 -0
- package/dist/agent/steering-queue.js +50 -14
- package/dist/agent/system-prompt.js +22 -0
- package/dist/agent/tool-dispatch.js +25 -9
- package/dist/agent/turn-watchdog.js +1 -1
- package/dist/approvals/advisory-render.js +6 -3
- package/dist/approvals/approval-prompt.js +9 -4
- package/dist/approvals/op-labels.js +20 -3
- package/dist/approvals/plan-grant.js +8 -0
- package/dist/approvals/render.js +22 -10
- package/dist/approvals/risk-floor.js +32 -4
- package/dist/approvals/visible-changes.js +50 -0
- package/dist/cli.js +10 -2
- package/dist/commands/compact.js +6 -1
- package/dist/commands/config-set.js +13 -0
- package/dist/commands/config-show.js +16 -4
- package/dist/commands/export-conversation.js +7 -4
- package/dist/commands/finish-check-guard.js +124 -0
- package/dist/commands/plan-chain.js +2 -2
- package/dist/commands/plan-reground.js +27 -4
- package/dist/commands/plan-resume.js +113 -3
- package/dist/commands/plan-startup.js +3 -3
- package/dist/config/loader.js +5 -1
- package/dist/cost/cost-log.js +6 -0
- package/dist/cost/system-block.js +6 -0
- package/dist/docx/doc-meta.js +1 -0
- package/dist/docx/markdown-to-docx.js +2 -0
- package/dist/lane/app-chain.js +13 -6
- package/dist/models/server-config.js +1 -1
- package/dist/one-shot.js +1 -0
- package/dist/project-context/conventions.js +8 -1
- package/dist/project-context/index-files.js +97 -34
- package/dist/project-context/index.js +1 -1
- package/dist/project-context/render.js +38 -4
- package/dist/projects/answer-blockers.js +107 -16
- package/dist/projects/blocker-answers-note.js +89 -0
- package/dist/projects/change-options.js +70 -0
- package/dist/projects/document-save.js +86 -0
- package/dist/projects/extract-design.js +4 -0
- package/dist/projects/extract-estimate.js +4 -0
- package/dist/projects/extract-spec-gap.js +8 -0
- package/dist/projects/index.js +1 -1
- package/dist/projects/plan-objects.js +24 -0
- package/dist/projects/plan-run.js +12 -2
- package/dist/projects/save-command.js +50 -18
- package/dist/projects/yes-no.js +5 -1
- package/dist/renderer/change-card.js +17 -1
- package/dist/renderer/html-entities.js +25 -0
- package/dist/renderer/machine-blocks.js +54 -7
- package/dist/renderer/steering-echo.js +53 -8
- package/dist/renderer/tool-labels.js +5 -1
- package/dist/renderer/tool-widget.js +20 -5
- package/dist/repl/chat-lane.js +15 -3
- package/dist/repl/inline-mode.js +51 -0
- package/dist/repl/leftover.js +90 -0
- package/dist/repl/reroute-turn.js +7 -1
- package/dist/repl/turn-stop.js +64 -0
- package/dist/repl.js +155 -24
- package/dist/router/classifier.js +1 -1
- package/dist/sap/reachability-state.js +7 -0
- package/dist/sap/unreachable.js +2 -2
- package/dist/session/fresh-start.js +4 -0
- package/dist/session/queued-step.js +3 -1
- package/dist/session/user-prompt.js +12 -1
- package/dist/skills/manifest-client.js +3 -2
- package/dist/skills/preamble.js +6 -4
- package/dist/skills/source-authed.js +3 -2
- package/dist/tools/approval.js +49 -8
- package/dist/tools/dispatch-skill.js +1 -1
- package/dist/tools/hand-off.js +49 -7
- package/dist/tools/lane/app-lane-resolve.js +9 -0
- package/dist/tools/method-only-change.js +241 -0
- package/dist/tools/project/spec_answers_record.js +190 -0
- package/dist/tools/rap/skeleton-resolve.js +4 -0
- package/dist/tools/sap-write.js +78 -3
- package/dist/tools/snapshot.js +7 -2
- package/dist/tools/subagent/agent_run.js +14 -0
- package/dist/tools/tool-loading.js +18 -8
- package/dist/tools/transport-fit-check.js +70 -0
- package/dist/tools/transport.js +17 -8
- package/dist/tools/write-pipeline.js +46 -5
- package/dist/ui/app.js +112 -13
- package/dist/ui/approval-modal.js +13 -7
- package/dist/ui/ascii-chars.js +21 -3
- package/dist/ui/footer.js +128 -7
- package/dist/ui/interrupt.js +35 -1
- package/dist/ui/live-area.js +21 -4
- package/dist/ui/queue-box-emitter.js +14 -0
- package/dist/ui/rewind-panel.js +2 -1
- package/dist/ui/session-timeline.js +2 -1
- package/dist/ui/text-input.js +31 -2
- package/dist/ui/widgets/ask-card.js +171 -29
- package/dist/ui/widgets/bar-chart.js +2 -1
- package/dist/ui/widgets/confirm-card.js +2 -1
- package/dist/ui/widgets/dep-graph.js +2 -1
- package/dist/ui/widgets/diff-viewer.js +2 -1
- package/dist/ui/widgets/question-card.js +2 -1
- package/dist/ui/widgets/shortcuts-sheet.js +2 -2
- package/dist/ui/widgets/stack-frames.js +2 -1
- package/dist/util/connect-retry.js +65 -0
- package/package.json +2 -2
package/dist/agent/loop.js
CHANGED
|
@@ -9,7 +9,7 @@ import { loadConfig } from '../config/loader.js';
|
|
|
9
9
|
import { resolveModelRole, resolveEffort, modelAcceptsEffort, resolveAutoCompactThreshold, callModelFor } from '../models/resolve.js';
|
|
10
10
|
import { getCustomerModelConfig, managedProxyServesNewShapes, servedToolLoading } from '../models/server-config.js';
|
|
11
11
|
import { familyOf, familyAcceptsEffort } from '../models/model-identity.js';
|
|
12
|
-
import { buildToolList, buildSkillToolAddition, placeSystemMessagesInPlace, resolveToolLoading, } from '../tools/tool-loading.js';
|
|
12
|
+
import { buildToolList, buildSkillToolAddition, buildToolAdditionMessage, deferredNames, surfacedToolNames, placeSystemMessagesInPlace, resolveToolLoading, } from '../tools/tool-loading.js';
|
|
13
13
|
import { settleSessionToolLoading } from './tool-loading-pin.js';
|
|
14
14
|
import { resolveSkillAlias } from '../skill-catalog.js';
|
|
15
15
|
import { retryWithBackoff } from './retry.js';
|
|
@@ -33,7 +33,9 @@ import { turnStatusEmitter } from '../ui/turn-status-emitter.js';
|
|
|
33
33
|
import { buildRetryCapPausePayload, buildSkippedSiblingResults } from './retry-cap.js';
|
|
34
34
|
import { reportCallUsage } from '../usage/usage-reporter.js';
|
|
35
35
|
import { appendCostLine, buildEntry as buildCostEntry, perCallTokens, summariseInputTransformations } from '../cost/cost-log.js';
|
|
36
|
-
import { noteSystemBlock, systemBlockKey } from '../cost/system-block.js';
|
|
36
|
+
import { INLINE_SYSTEM_BLOCK_SKILL, noteSystemBlock, systemBlockKey } from '../cost/system-block.js';
|
|
37
|
+
import { escapeInjectedTags, historyHoldsInline, neutraliseForgedSkillTags, planInlineTurn, playbookLineSystem, playbookLineSystemChat, } from './inline-skill-block.js';
|
|
38
|
+
import { SKILL_INLINE_HEADER } from './system-prompt.js';
|
|
37
39
|
import { sessionMaxTokens, clampMaxTokens, sendsBlockBinding } from './provider-shape.js';
|
|
38
40
|
import { profileOf, sendsEffortFor } from './connection-profile.js';
|
|
39
41
|
import { computeSessionCost } from '../cost/session-cost.js';
|
|
@@ -44,6 +46,7 @@ import { enrichUserMessage } from '../router/intent-extractor.js';
|
|
|
44
46
|
import { resetRule8State, shouldGateRule8 } from '../repl/rule8-detector.js';
|
|
45
47
|
import { consumeTodoSetThisTurn } from '../ui/todo-emitter.js';
|
|
46
48
|
import { runSaveCommand, runPromoteCommand, detectArtifactSkill, offerAnswerBlockers } from '../projects/index.js';
|
|
49
|
+
import { blockerAnswersNote, SPEC_ANSWERS_RECORD_TOOL } from '../projects/blocker-answers-note.js';
|
|
47
50
|
import { planLaneBlockForTurn } from '../projects/plan-lane.js';
|
|
48
51
|
import { DOCUMENT_SKILLS, WORD_AFTER_ARTIFACT_SAVE } from '../projects/document-skills.js';
|
|
49
52
|
import { metaFromMarkdown } from '../docx/doc-meta.js';
|
|
@@ -68,8 +71,9 @@ import { renderStandaloneContextBlock } from '../sap/standalone-profile.js';
|
|
|
68
71
|
import { renderStandardsBlock } from '../standards/standards-file.js';
|
|
69
72
|
import { composeSessionContextBlock } from './session-context.js';
|
|
70
73
|
import { objectKeyFromInput } from './retry-key.js';
|
|
71
|
-
import { repairPartialBlocks, stripPreFallbackBlocks, healSessionMessagesInPlace, healOrphanToolUses, isPartialJsonApiError, stripUnsignedThinking, isThinkingOnly, dropTrailingThinkingOnly } from './repair-partial.js';
|
|
72
|
-
import {
|
|
74
|
+
import { repairPartialBlocks, stripPreFallbackBlocks, healSessionMessagesInPlace, healOrphanToolUses, isPartialJsonApiError, stripUnsignedThinking, isThinkingOnly, dropTrailingThinkingOnly, closedOffStepsLine } from './repair-partial.js';
|
|
75
|
+
import { drainSteeringEntries, enqueueSteering } from './steering-queue.js';
|
|
76
|
+
import { steeringReadLine } from '../renderer/steering-echo.js';
|
|
73
77
|
import { TurnStreamWriter } from './turn-stream.js';
|
|
74
78
|
import { applyToolResultCheckpoint } from './skill-checkpoint.js';
|
|
75
79
|
import { loadWatchdogConfig, evaluateWatchdog } from './turn-watchdog.js';
|
|
@@ -84,6 +88,7 @@ import { CacheKeepalive, snapshotParams, recordLastRequest, isCacheablePrompt, n
|
|
|
84
88
|
import { maybeColdPrune } from './cold-prune.js';
|
|
85
89
|
import { createAdtLock, serializeAdt } from '../tools/subagent/adt-serial.js';
|
|
86
90
|
import { hasReaderReadTools, INTERRUPTED_LINE } from '../tools/subagent/reader-prompt.js';
|
|
91
|
+
import { settleHandOffNote } from '../tools/hand-off.js';
|
|
87
92
|
import { openStatusPage } from '../util/open-in-browser.js';
|
|
88
93
|
import { isChatSkill } from './chat-prompt.js';
|
|
89
94
|
import { getAssistantStyle } from './style-state.js';
|
|
@@ -135,6 +140,12 @@ const SAVE_HOOK_SKILLS = new Set([
|
|
|
135
140
|
// revision path in commands/plan-resume.ts persists instead.
|
|
136
141
|
'abap-plan',
|
|
137
142
|
]);
|
|
143
|
+
/** Stage 1A — the inline branch's debug channel (CSPEACH_DEBUG=1 → stderr). Never user output. */
|
|
144
|
+
function inlineDebug(line) {
|
|
145
|
+
if (process.env['CSPEACH_DEBUG'] !== '1')
|
|
146
|
+
return;
|
|
147
|
+
process.stderr.write(`[inline] ${line}\n`);
|
|
148
|
+
}
|
|
138
149
|
/**
|
|
139
150
|
* True when any user message in the session already carries the rendered
|
|
140
151
|
* <project_context> block. Phase 2 uses this to inject the block exactly
|
|
@@ -438,9 +449,51 @@ async function offerWordAfterArtifactSave(p, saveSkill, savedPath, confirm) {
|
|
|
438
449
|
p.emit(formatInfo(`Word copy not written: ${e.message}`).trimEnd());
|
|
439
450
|
}
|
|
440
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Change options (2026-10-23) — an answer carrying the change-options marker
|
|
454
|
+
* pair gets its marked part saved (every complete pair, in order: a level
|
|
455
|
+
* picked in session adds the decided document), whatever the skill and even when the turn
|
|
456
|
+
* ends on the level question. Never on a sub-agent turn or inside a guided
|
|
457
|
+
* flow. Returns true when the marker check handled the turn (the question was
|
|
458
|
+
* asked or the document saved), so maybeOfferSave runs nothing else. An answer
|
|
459
|
+
* with no complete marker pair returns false and takes today's path unchanged.
|
|
460
|
+
*/
|
|
461
|
+
async function maybeOfferChangeOptions(p, confirm) {
|
|
462
|
+
if (p.subagent || p.guided || p.assistantText.includes('<!-- guided-run:'))
|
|
463
|
+
return false;
|
|
464
|
+
if (!p.assistantText.includes('csforge:change-options'))
|
|
465
|
+
return false;
|
|
466
|
+
const { extractChangeOptions } = await import('../projects/change-options.js');
|
|
467
|
+
// Every complete pair, in order: a level picked in session shows the
|
|
468
|
+
// document again with the decision, and that one is saved too (last).
|
|
469
|
+
const docs = extractChangeOptions(p.assistantText).filter((d) => d.markdown.trim().length > 0);
|
|
470
|
+
if (docs.length === 0)
|
|
471
|
+
return false;
|
|
472
|
+
// Set before the save question, so a declined save still routes the level reply.
|
|
473
|
+
if (p.session && !p.subagent)
|
|
474
|
+
p.session.lastChangeOptions = { object: docs[docs.length - 1].object, skill: p.skill };
|
|
475
|
+
try {
|
|
476
|
+
const { offerChangeOptionsSave } = await import('../projects/document-save.js');
|
|
477
|
+
const { loadConfig, resolveWordCopy } = await import('../config/loader.js');
|
|
478
|
+
await offerChangeOptionsSave({
|
|
479
|
+
docs: docs.map((d) => ({ object: d.object, markdown: d.markdown })), cwd: process.cwd(), today: localToday(),
|
|
480
|
+
sapSystem: p.sapSystem ?? null,
|
|
481
|
+
...(p.sessionId ? { sessionId: p.sessionId } : {}),
|
|
482
|
+
...(p.operator ? { operator: p.operator } : {}),
|
|
483
|
+
headless: isHeadless(), wordCopy: resolveWordCopy(await loadConfig()), confirm, emit: p.emit,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
catch (e) {
|
|
487
|
+
p.emit(formatInfo(`Not saved. ${e.message}`).trimEnd());
|
|
488
|
+
}
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
441
491
|
export async function maybeOfferSave(p) {
|
|
442
492
|
if (p.assistantText.trim().length === 0)
|
|
443
493
|
return;
|
|
494
|
+
// Change options (2026-10-23): the marker check runs first, for every skill.
|
|
495
|
+
if (await maybeOfferChangeOptions(p, p.confirm ?? inkAwareConfirm))
|
|
496
|
+
return;
|
|
444
497
|
// Word export (2026-10-19): reviews and handovers are prose documents, not
|
|
445
498
|
// envelopes. They get a Markdown save under cspeach-out and the Word
|
|
446
499
|
// question (projects/document-save.ts). Nothing below this branch changes
|
|
@@ -567,7 +620,12 @@ export async function maybeOfferSave(p) {
|
|
|
567
620
|
// business questions stay open for a consultant to answer in the viewer.
|
|
568
621
|
// Uses inkAwarePrompt directly (NOT the save hook's blanket-'y' headless
|
|
569
622
|
// prompt): in headless it returns '' → declined, so nothing is written.
|
|
623
|
+
// Task 16 (F-M): what the form recorded reaches the model as one note on
|
|
624
|
+
// its next call (session.blockerAnswersNote, sent by runTurn). Main turns
|
|
625
|
+
// with a session only; declined / not offered / not saved → no note.
|
|
570
626
|
if (savedPath && saveSkill === 'abap-spec-gap') {
|
|
627
|
+
const noteSession = p.session && !p.subagent ? p.session : undefined;
|
|
628
|
+
let outcomes;
|
|
571
629
|
await offerAnswerBlockers({
|
|
572
630
|
path: savedPath,
|
|
573
631
|
identity: getAuthorIdentity(),
|
|
@@ -575,7 +633,14 @@ export async function maybeOfferSave(p) {
|
|
|
575
633
|
confirm: inkAwareConfirm,
|
|
576
634
|
choose: inkAwareChoose,
|
|
577
635
|
log: (...lines) => lines.forEach((l) => p.emit(l)),
|
|
636
|
+
...(noteSession ? { onOutcomes: (o) => { outcomes = o; } } : {}),
|
|
578
637
|
});
|
|
638
|
+
if (noteSession && outcomes) {
|
|
639
|
+
// The path is shown against the read tool's base (ctx.cwd), so the model opens it with file_read.
|
|
640
|
+
const note = blockerAnswersNote({ path: savedPath, cwd: p.readCwd ?? process.cwd(), outcomes });
|
|
641
|
+
if (note)
|
|
642
|
+
noteSession.blockerAnswersNote = note;
|
|
643
|
+
}
|
|
579
644
|
}
|
|
580
645
|
// App lane routing (Task 7) — the harness-owned /abap-app chain. Built
|
|
581
646
|
// from the exact saved path, never from model text. Outside an app flow
|
|
@@ -614,6 +679,7 @@ const SILENT_STATUS = {
|
|
|
614
679
|
activity: (_label) => undefined,
|
|
615
680
|
pause: () => undefined,
|
|
616
681
|
resume: () => undefined,
|
|
682
|
+
pausedMs: () => 0,
|
|
617
683
|
};
|
|
618
684
|
/** Task 18 — at most this many read_agent calls run at the same time. */
|
|
619
685
|
export const MAX_PARALLEL_READERS = 4;
|
|
@@ -946,6 +1012,15 @@ ${imageExpansion.error}`));
|
|
|
946
1012
|
if (!readerChild) {
|
|
947
1013
|
userMessageForLLM = await expandTextFileAttachments(userMessageForLLM, params.ctx.cwd, (line) => emit(chalk.dim(line)));
|
|
948
1014
|
}
|
|
1015
|
+
// Stage 1A, Task 1A.2 (S5) — an interactive chat-lane process neutralises
|
|
1016
|
+
// <skill, <playbook and <session_context in what the person sends (typed
|
|
1017
|
+
// text and @file expansions), so no text of theirs can open a block the
|
|
1018
|
+
// CLI writes. A caller-enriched message keeps its leading <session_context>.
|
|
1019
|
+
// Absent flag (skills lane, piped, one-shot, children) ⇒ the text as it is.
|
|
1020
|
+
if (params.ctx.chatLaneProcess === true && !readerChild && !params.ctx.subagent) {
|
|
1021
|
+
const lead = userMessageForLLM.startsWith('<session_context>') ? '<session_context>' : '';
|
|
1022
|
+
userMessageForLLM = lead + escapeInjectedTags(userMessageForLLM.slice(lead.length));
|
|
1023
|
+
}
|
|
949
1024
|
// App lane routing (Task 6) — the /abap-plan creation turn inside an app
|
|
950
1025
|
// flow gets the confirmed <lane_decision> block (read with the app-flow sha;
|
|
951
1026
|
// a changed file is refused with a plain line). '' on every other turn, so
|
|
@@ -1012,7 +1087,48 @@ ${imageExpansion.error}`));
|
|
|
1012
1087
|
const switchNote = session.systemSwitchNote
|
|
1013
1088
|
? `<system_switch>${session.systemSwitchNote}</system_switch>`
|
|
1014
1089
|
: '';
|
|
1015
|
-
|
|
1090
|
+
// Stage 1A, Task 1A.2 — inline skills (only when the REPL decided so for
|
|
1091
|
+
// this turn; never a reader or agent_run child). The playbook rides in the
|
|
1092
|
+
// conversation as a <skill> block, once per body per history ("loaded" is
|
|
1093
|
+
// read from the history by content hash, inline-skill-block.ts), and a
|
|
1094
|
+
// code-driven <playbook> line says which block is in charge. The main
|
|
1095
|
+
// playbook failing to load ⇒ this turn runs in system mode (S2). A
|
|
1096
|
+
// system-mode turn whose history holds inline blocks gets the
|
|
1097
|
+
// source="system" line (S-B); in a process that never ran inline the scan
|
|
1098
|
+
// finds nothing and nothing changes.
|
|
1099
|
+
const mainTurn = !readerChild && !params.ctx.subagent;
|
|
1100
|
+
let inlineTurn = false;
|
|
1101
|
+
let playbookLine = '';
|
|
1102
|
+
let inlineBlocks = [];
|
|
1103
|
+
if (mainTurn && params.ctx.inlineSkills === true) {
|
|
1104
|
+
const neutralised = neutraliseForgedSkillTags(session.messages);
|
|
1105
|
+
if (neutralised > 0)
|
|
1106
|
+
inlineDebug(`neutralised a <skill> tag that is not a loaded playbook in ${neutralised} earlier message(s)`);
|
|
1107
|
+
const plan = await planInlineTurn({
|
|
1108
|
+
skill: params.skill,
|
|
1109
|
+
delegateSkill: params.delegateSkill,
|
|
1110
|
+
messages: session.messages,
|
|
1111
|
+
source: params.ctx.skillSource ?? params.provider.skillSource,
|
|
1112
|
+
debug: inlineDebug,
|
|
1113
|
+
});
|
|
1114
|
+
if (plan.inline) {
|
|
1115
|
+
inlineTurn = true;
|
|
1116
|
+
playbookLine = plan.line;
|
|
1117
|
+
inlineBlocks = plan.blocks;
|
|
1118
|
+
}
|
|
1119
|
+
else {
|
|
1120
|
+
inlineDebug(`playbook ${params.skill} could not be loaded, this turn runs in system mode: ${plan.reason}`);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
if (mainTurn && !inlineTurn && historyHoldsInline(session.messages)) {
|
|
1124
|
+
playbookLine = isChatSkill(params.skill) ? playbookLineSystemChat() : playbookLineSystem(params.skill);
|
|
1125
|
+
}
|
|
1126
|
+
// Task 16 (F-M) — what the blocker form recorded after the last turn: once,
|
|
1127
|
+
// on the next main turn (never a sub-agent or reader child), then dropped.
|
|
1128
|
+
const blockerNote = mainTurn && session.blockerAnswersNote
|
|
1129
|
+
? `<blocker_answers>\n${session.blockerAnswersNote}\n</blocker_answers>`
|
|
1130
|
+
: '';
|
|
1131
|
+
const sessionContextBlock = composeSessionContextBlock([sapSystemBlock, sapProfileBlock, standardsBlock, switchNote, blockerNote, playbookLine]);
|
|
1016
1132
|
// Deterministically extract structured facts from the user's prompt
|
|
1017
1133
|
// (package, transport, object names, environment hint, create intent)
|
|
1018
1134
|
// and prepend them as an XML `<session_context>` block. The model reads
|
|
@@ -1027,10 +1143,13 @@ ${imageExpansion.error}`));
|
|
|
1027
1143
|
// when extraction yields nothing (enrichUserMessage returns original).
|
|
1028
1144
|
// Final batch 1 — a caller-enriched message still gets the switch note,
|
|
1029
1145
|
// inside its own <session_context> block.
|
|
1146
|
+
// Stage 1A — the <playbook> line rides the same way ('' ⇒ unchanged).
|
|
1147
|
+
const callerNotes = composeSessionContextBlock([switchNote, blockerNote, playbookLine]);
|
|
1030
1148
|
const enriched = userMessageForLLM.startsWith('<session_context>')
|
|
1031
|
-
? (
|
|
1149
|
+
? (callerNotes ? userMessageForLLM.replace('<session_context>', `<session_context>\n ${callerNotes}`) : userMessageForLLM)
|
|
1032
1150
|
: enrichUserMessage(userMessageForLLM, sessionContextBlock);
|
|
1033
1151
|
const switchNoteUsed = switchNote !== '' && enriched.includes(switchNote);
|
|
1152
|
+
const blockerNoteUsed = blockerNote !== '' && enriched.includes(blockerNote);
|
|
1034
1153
|
// Phase 2 of Track B — opt-in project-context enrichment, gated by
|
|
1035
1154
|
// CSPEACH_PROJECT_CONTEXT=on. Default OFF: production behavior is
|
|
1036
1155
|
// identical to today. When on, the rendered <project_context> block
|
|
@@ -1058,11 +1177,17 @@ ${imageExpansion.error}`));
|
|
|
1058
1177
|
projectContextBlock = await maybeBuildProjectContext({
|
|
1059
1178
|
cwd: params.ctx.cwd,
|
|
1060
1179
|
sapConnection,
|
|
1180
|
+
emit: (line) => emit(chalk.dim(line)),
|
|
1061
1181
|
});
|
|
1062
1182
|
}
|
|
1063
|
-
const
|
|
1183
|
+
const contextualContent = projectContextBlock
|
|
1064
1184
|
? `${projectContextBlock}\n\n${enriched}`
|
|
1065
1185
|
: enriched;
|
|
1186
|
+
// Stage 1A — the bodies the history lacks end the user message, each as
|
|
1187
|
+
// `\n\n<skill …>\n<body>\n</skill>` (none on most turns ⇒ unchanged).
|
|
1188
|
+
const finalUserContent = inlineBlocks.length > 0
|
|
1189
|
+
? `${contextualContent}${inlineBlocks.map((b) => `\n\n${b.text}`).join('')}`
|
|
1190
|
+
: contextualContent;
|
|
1066
1191
|
// Chat lane (D11) — what a withdrawn `_chat` turn restores: the whole
|
|
1067
1192
|
// pre-turn message array (placeSystemMessagesInPlace may re-sort the tail
|
|
1068
1193
|
// after the last assistant message, so a length-based splice is not
|
|
@@ -1074,6 +1199,9 @@ ${imageExpansion.error}`));
|
|
|
1074
1199
|
lane: session.lane,
|
|
1075
1200
|
tool_set_skill: session.tool_set_skill,
|
|
1076
1201
|
turn_seq: session.turn_seq,
|
|
1202
|
+
// Task 16 fix round 1 (I-1, R17): the one-time notes this turn consumes.
|
|
1203
|
+
systemSwitchNote: session.systemSwitchNote,
|
|
1204
|
+
blockerAnswersNote: session.blockerAnswersNote,
|
|
1077
1205
|
};
|
|
1078
1206
|
let round0Steers = [];
|
|
1079
1207
|
// Images (if any) ride as blocks ahead of the text; with none this is the
|
|
@@ -1089,6 +1217,8 @@ ${imageExpansion.error}`));
|
|
|
1089
1217
|
// it is in a pushed user message.
|
|
1090
1218
|
if (switchNoteUsed)
|
|
1091
1219
|
delete session.systemSwitchNote;
|
|
1220
|
+
if (blockerNoteUsed)
|
|
1221
|
+
delete session.blockerAnswersNote;
|
|
1092
1222
|
if (imageExpansion.images.length > 0 && params.provider.name === 'local-openai-compat') {
|
|
1093
1223
|
// The local (OpenAI-compatible) provider keeps text blocks only.
|
|
1094
1224
|
emit(chalk.dim('(local model: the image is not sent — the model gets the text only)'));
|
|
@@ -1133,9 +1263,14 @@ ${imageExpansion.error}`));
|
|
|
1133
1263
|
const contextTools = withoutTools(toolsForContext(listTools(), params.ctx), hiddenSapTools);
|
|
1134
1264
|
// Task 18 fixes M4 / M5 — no read_agent for an agent_run child, and none
|
|
1135
1265
|
// when a reader would have no real read tool (standalone, file tools off).
|
|
1136
|
-
const
|
|
1266
|
+
const readerVisible = params.ctx.subagent || !hasReaderReadTools(new Set(contextTools.map((t) => t.name)))
|
|
1137
1267
|
? contextTools.filter((t) => t.name !== READ_AGENT)
|
|
1138
1268
|
: contextTools;
|
|
1269
|
+
// Task 18 fix round 1 (item 5) — an agent_run child never records spec
|
|
1270
|
+
// answers: it could race the blocker form on the same file.
|
|
1271
|
+
const visibleTools = params.ctx.subagent
|
|
1272
|
+
? readerVisible.filter((t) => t.name !== SPEC_ANSWERS_RECORD_TOOL)
|
|
1273
|
+
: readerVisible;
|
|
1139
1274
|
// Final review C1 (controller ruling) — deferred shapes only for a model on
|
|
1140
1275
|
// the probe-proven allow-list, decided by the model actually sent on the
|
|
1141
1276
|
// call (a plan-tier sonnet override, a haiku child or claude-opus-4-8 ⇒
|
|
@@ -1184,20 +1319,41 @@ ${imageExpansion.error}`));
|
|
|
1184
1319
|
* N1 (c) — built from the turn's list (the list every request of this turn
|
|
1185
1320
|
* carries) and never for a session pinned static.
|
|
1186
1321
|
*/
|
|
1322
|
+
// Stage 1A (rev 6 M-5) — on an inline turn, what each tool_addition
|
|
1323
|
+
// surfaced and the size of its definition in this turn's list, for the
|
|
1324
|
+
// cost line of the call that carries it. Never filled in system mode.
|
|
1325
|
+
let pendingSurfaced = [];
|
|
1326
|
+
// Stage 1A — skill_appended goes on the turn's first cost line (a call
|
|
1327
|
+
// retried after a prompt-too-long recovery logs no line of its own).
|
|
1328
|
+
let skillAppendedLogged = false;
|
|
1329
|
+
const pushToolAddition = (msg) => {
|
|
1330
|
+
if (msg)
|
|
1331
|
+
session.messages.push(msg);
|
|
1332
|
+
if (msg && inlineTurn) {
|
|
1333
|
+
for (const b of msg.content) {
|
|
1334
|
+
const def = turnTools.find((t) => t?.name === b.tool.name);
|
|
1335
|
+
pendingSurfaced.push({ name: b.tool.name, chars: JSON.stringify(def ?? null).length });
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1187
1339
|
const surfaceStarters = (skill) => {
|
|
1188
|
-
|
|
1340
|
+
pushToolAddition(buildSkillToolAddition({
|
|
1189
1341
|
skill,
|
|
1190
1342
|
toolLoading: session.tool_loading,
|
|
1191
1343
|
toolList: turnTools,
|
|
1192
1344
|
messages: session.messages,
|
|
1193
|
-
});
|
|
1194
|
-
if (msg)
|
|
1195
|
-
session.messages.push(msg);
|
|
1345
|
+
}));
|
|
1196
1346
|
};
|
|
1197
1347
|
// A turn on a different skill than the one the array was built for: the
|
|
1198
1348
|
// user message was just pushed, so the system message follows it.
|
|
1199
1349
|
if (params.skill !== session.tool_set_skill)
|
|
1200
1350
|
surfaceStarters(params.skill);
|
|
1351
|
+
// Task 18 fix round 1 (item 7) — the blocker note names spec_answers_record:
|
|
1352
|
+
// on the turn that carries it, surface the tool when this list defers it
|
|
1353
|
+
// (loaded already, static, or surfaced before: nothing is added).
|
|
1354
|
+
if (blockerNoteUsed && session.tool_loading !== 'static' && !surfacedToolNames(session.messages).has(SPEC_ANSWERS_RECORD_TOOL)) {
|
|
1355
|
+
pushToolAddition(buildToolAdditionMessage([SPEC_ANSWERS_RECORD_TOOL], deferredNames(turnTools)));
|
|
1356
|
+
}
|
|
1201
1357
|
/**
|
|
1202
1358
|
* key: `${tool_name}:${object}` — consecutive failures per (tool, target).
|
|
1203
1359
|
* For object-identifying tools (sap_set_source / sap_activate / etc.) the
|
|
@@ -1254,7 +1410,34 @@ ${imageExpansion.error}`));
|
|
|
1254
1410
|
// rounds so it doesn't spam the model. See agent/turn-watchdog.ts.
|
|
1255
1411
|
const watchdogConfig = loadWatchdogConfig();
|
|
1256
1412
|
const turnStartedAt = Date.now();
|
|
1413
|
+
// Card-wait time already paused on the status emitter before this turn; the
|
|
1414
|
+
// watchdog counts only what this turn adds on top of it as "not working".
|
|
1415
|
+
const turnPausedBase = status.pausedMs();
|
|
1257
1416
|
let watchdogWarned = false;
|
|
1417
|
+
// LT-3 (live re-check 2026-10-26) — set after a successful hand_off in a
|
|
1418
|
+
// turn that already said its line. The next round's text is HELD: shown at
|
|
1419
|
+
// once if the round calls a tool, shown at the end unless the round is only
|
|
1420
|
+
// a short closing line that ends the turn (the second announcement). Kept
|
|
1421
|
+
// in the session either way.
|
|
1422
|
+
let muteHandOffEcho = false;
|
|
1423
|
+
let heldEcho = null;
|
|
1424
|
+
const showText = (out) => {
|
|
1425
|
+
if (heldEcho) {
|
|
1426
|
+
heldEcho.push(out);
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
if (params.chunkEmitter)
|
|
1430
|
+
params.chunkEmitter.emit('chunk', out);
|
|
1431
|
+
else
|
|
1432
|
+
writeStdout(out);
|
|
1433
|
+
};
|
|
1434
|
+
const releaseHeldEcho = () => {
|
|
1435
|
+
const held = heldEcho;
|
|
1436
|
+
heldEcho = null;
|
|
1437
|
+
if (held)
|
|
1438
|
+
for (const out of held)
|
|
1439
|
+
showText(out);
|
|
1440
|
+
};
|
|
1258
1441
|
// 2026-06-08 — manifest-on-interrupt: track the last plan-manifest block we
|
|
1259
1442
|
// already handed to onPlanManifest, so we fire once per DISTINCT manifest
|
|
1260
1443
|
// (a continue-in-session run emits one per phase) and not once per round.
|
|
@@ -1288,15 +1471,28 @@ ${imageExpansion.error}`));
|
|
|
1288
1471
|
// during round N lands at the start of round N+1 (between rounds). The
|
|
1289
1472
|
// Anthropic API concatenates consecutive user messages, so injecting after a
|
|
1290
1473
|
// tool_result user message is valid.
|
|
1474
|
+
// LT-15 F1 — only the main turn reads the queue: a reader or an agent_run
|
|
1475
|
+
// child (ctx.subagent) never takes the parent's messages.
|
|
1476
|
+
// LT-15 F2 — no drain when the turn is already stopped: this round can
|
|
1477
|
+
// never run, so the message stays queued for a later turn.
|
|
1291
1478
|
{
|
|
1292
|
-
const
|
|
1293
|
-
if (
|
|
1294
|
-
const
|
|
1479
|
+
const entries = mainTurn && !params.signal?.aborted ? drainSteeringEntries() : [];
|
|
1480
|
+
if (entries.length > 0) {
|
|
1481
|
+
const steers = entries.map((e) => e.text);
|
|
1482
|
+
// Stage 1A fix round 1 (I-1) — a steer is typed text: in an
|
|
1483
|
+
// interactive chat-lane process it gets the same tag escape.
|
|
1484
|
+
const joined = steers.join('\n');
|
|
1485
|
+
const text = params.ctx.chatLaneProcess === true ? escapeInjectedTags(joined) : joined;
|
|
1295
1486
|
session.messages.push({ role: 'user', content: text });
|
|
1296
1487
|
markCliMessage(session, text, 'steer', '');
|
|
1297
1488
|
if (roundIndex === 0)
|
|
1298
1489
|
round0Steers = steers;
|
|
1299
|
-
|
|
1490
|
+
// LT-15 Feature 1 (D5, F7) — one dim, clipped, sanitised line per message.
|
|
1491
|
+
// LT-15 Task 4 — not again for a message the D11 fallback put back
|
|
1492
|
+
// after its line was already shown.
|
|
1493
|
+
for (const e of entries)
|
|
1494
|
+
if (!e.alreadyRead)
|
|
1495
|
+
emit(steeringReadLine(e.text));
|
|
1300
1496
|
}
|
|
1301
1497
|
}
|
|
1302
1498
|
let buffer = initialBufferState();
|
|
@@ -1330,12 +1526,21 @@ ${imageExpansion.error}`));
|
|
|
1330
1526
|
session.lane = preTurnState.lane;
|
|
1331
1527
|
session.tool_set_skill = preTurnState.tool_set_skill;
|
|
1332
1528
|
session.turn_seq = preTurnState.turn_seq;
|
|
1529
|
+
// The re-run turn still gets the one-time notes (they were dropped on push).
|
|
1530
|
+
if (preTurnState.systemSwitchNote !== undefined)
|
|
1531
|
+
session.systemSwitchNote = preTurnState.systemSwitchNote;
|
|
1532
|
+
if (preTurnState.blockerAnswersNote !== undefined)
|
|
1533
|
+
session.blockerAnswersNote = preTurnState.blockerAnswersNote;
|
|
1333
1534
|
// Steering drained in round 0 goes back to the queue, ahead of any
|
|
1334
1535
|
// typed since, so the re-run turn still sees it.
|
|
1536
|
+
// LT-15 Task 4 — the round-0 ones were shown as "↳ read": marked, so the
|
|
1537
|
+
// re-run prints no second line; the later ones keep their own mark.
|
|
1335
1538
|
if (round0Steers.length > 0) {
|
|
1336
|
-
const later =
|
|
1337
|
-
for (const t of
|
|
1338
|
-
enqueueSteering(t);
|
|
1539
|
+
const later = drainSteeringEntries();
|
|
1540
|
+
for (const t of round0Steers)
|
|
1541
|
+
enqueueSteering(t, { alreadyRead: true });
|
|
1542
|
+
for (const e of later)
|
|
1543
|
+
enqueueSteering(e.text, { alreadyRead: e.alreadyRead });
|
|
1339
1544
|
}
|
|
1340
1545
|
throw new ChatUnavailableError(st);
|
|
1341
1546
|
};
|
|
@@ -1371,6 +1576,9 @@ ${imageExpansion.error}`));
|
|
|
1371
1576
|
// Task 15 — the request actually sent by the attempt that succeeded.
|
|
1372
1577
|
let sentParams;
|
|
1373
1578
|
let sentHeaders;
|
|
1579
|
+
// Stage 1A — the tool_additions this call carries (inline turns only).
|
|
1580
|
+
const callSurfaced = pendingSurfaced;
|
|
1581
|
+
pendingSurfaced = [];
|
|
1374
1582
|
try {
|
|
1375
1583
|
stream = await retryWithBackoff(() => (async () => {
|
|
1376
1584
|
// W2.5 — apply optional tool filter (used by agent_run with
|
|
@@ -1428,7 +1636,7 @@ ${imageExpansion.error}`));
|
|
|
1428
1636
|
// that ended before the model answered the tool_addition.
|
|
1429
1637
|
placeSystemMessagesInPlace(session.messages);
|
|
1430
1638
|
if (healedOrphans > 0) {
|
|
1431
|
-
emit(chalk.dim(
|
|
1639
|
+
emit(chalk.dim(closedOffStepsLine(healedOrphans)));
|
|
1432
1640
|
}
|
|
1433
1641
|
// Task 4 — effort only from an explicit act (ruling F2); never to a
|
|
1434
1642
|
// model that rejects it (probe P1: claude-haiku-4-5* ⇒ 400).
|
|
@@ -1494,6 +1702,9 @@ ${imageExpansion.error}`));
|
|
|
1494
1702
|
...styleHeader(getAssistantStyle()),
|
|
1495
1703
|
// §10 D14 — developer sends nothing (byte-identical requests).
|
|
1496
1704
|
...levelHeader(getAssistantLevel()),
|
|
1705
|
+
// Stage 1A — an inline turn asks for the one constant system
|
|
1706
|
+
// block; every other call sends nothing (byte-identical requests).
|
|
1707
|
+
...(inlineTurn ? { [SKILL_INLINE_HEADER]: '1' } : {}),
|
|
1497
1708
|
};
|
|
1498
1709
|
sentParams = streamParams;
|
|
1499
1710
|
sentHeaders = headers;
|
|
@@ -1547,8 +1758,11 @@ ${imageExpansion.error}`));
|
|
|
1547
1758
|
// largest tool results, save, and run the same request once more (or
|
|
1548
1759
|
// show the card). The main turn only.
|
|
1549
1760
|
if (!readerChild && !params.ctx.subagent && isPromptTooLongError(err)) {
|
|
1550
|
-
if (await recoverFromPromptTooLong(err))
|
|
1761
|
+
if (await recoverFromPromptTooLong(err)) {
|
|
1762
|
+
// Stage 1A — the retried call carries the same tool_additions.
|
|
1763
|
+
pendingSurfaced = [...callSurfaced, ...pendingSurfaced];
|
|
1551
1764
|
continue;
|
|
1765
|
+
}
|
|
1552
1766
|
return;
|
|
1553
1767
|
}
|
|
1554
1768
|
// Proxy returns 409 upgrade_required when CLI is older than skill's min_cli_version.
|
|
@@ -1584,6 +1798,10 @@ ${imageExpansion.error}`));
|
|
|
1584
1798
|
// not in here was cut mid-stream and must never be persisted.
|
|
1585
1799
|
const completedBlocks = new WeakSet();
|
|
1586
1800
|
let sawEndTurn = false;
|
|
1801
|
+
if (muteHandOffEcho) {
|
|
1802
|
+
heldEcho = [];
|
|
1803
|
+
muteHandOffEcho = false;
|
|
1804
|
+
} // LT-3 — hold this round's text
|
|
1587
1805
|
let sawToolUse = false;
|
|
1588
1806
|
let sawMaxTokens = false;
|
|
1589
1807
|
let sawPauseTurn = false;
|
|
@@ -1642,13 +1860,10 @@ ${imageExpansion.error}`));
|
|
|
1642
1860
|
writingCode = false;
|
|
1643
1861
|
const block = event.content_block;
|
|
1644
1862
|
currentAssistantContent.push(block);
|
|
1645
|
-
if (block.type === '
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
else
|
|
1650
|
-
writeStdout(sep);
|
|
1651
|
-
}
|
|
1863
|
+
if (block.type === 'tool_use')
|
|
1864
|
+
releaseHeldEcho(); // LT-3 — the round does more than restate
|
|
1865
|
+
if (block.type === 'text')
|
|
1866
|
+
showText(chalk.gray('\n'));
|
|
1652
1867
|
}
|
|
1653
1868
|
else if (type === 'content_block_delta') {
|
|
1654
1869
|
const delta = event.delta;
|
|
@@ -1664,12 +1879,8 @@ ${imageExpansion.error}`));
|
|
|
1664
1879
|
writingCode = wa.writingCode;
|
|
1665
1880
|
if (wa.label)
|
|
1666
1881
|
status.activity(wa.label);
|
|
1667
|
-
if (output)
|
|
1668
|
-
|
|
1669
|
-
params.chunkEmitter.emit('chunk', output);
|
|
1670
|
-
else
|
|
1671
|
-
writeStdout(output);
|
|
1672
|
-
}
|
|
1882
|
+
if (output)
|
|
1883
|
+
showText(output);
|
|
1673
1884
|
if (last?.type === 'text')
|
|
1674
1885
|
last.text = (last.text ?? '') + delta.text;
|
|
1675
1886
|
// v0.6 — also write the raw model text to disk so the user can
|
|
@@ -1807,8 +2018,11 @@ ${imageExpansion.error}`));
|
|
|
1807
2018
|
// retry: the catch only decides; the card and the final save (with
|
|
1808
2019
|
// turnInterrupted cleared) come from the recovery after the persist block.
|
|
1809
2020
|
if (!readerChild && !params.ctx.subagent && !params.signal?.aborted && currentAssistantContent.length === 0 && isPromptTooLongError(err)) {
|
|
1810
|
-
if (await recoverFromPromptTooLong(err, { card: false }))
|
|
2021
|
+
if (await recoverFromPromptTooLong(err, { card: false })) {
|
|
2022
|
+
// Stage 1A — the retried call carries the same tool_additions.
|
|
2023
|
+
pendingSurfaced = [...callSurfaced, ...pendingSurfaced];
|
|
1811
2024
|
continue;
|
|
2025
|
+
}
|
|
1812
2026
|
}
|
|
1813
2027
|
}
|
|
1814
2028
|
// Final flush: emit any pending partial line after the stream ends.
|
|
@@ -1820,22 +2034,23 @@ ${imageExpansion.error}`));
|
|
|
1820
2034
|
if (heldProse) {
|
|
1821
2035
|
const { output, state } = renderChunk(heldProse, buffer);
|
|
1822
2036
|
buffer = state;
|
|
1823
|
-
if (output)
|
|
1824
|
-
|
|
1825
|
-
params.chunkEmitter.emit('chunk', output);
|
|
1826
|
-
else
|
|
1827
|
-
writeStdout(output);
|
|
1828
|
-
}
|
|
2037
|
+
if (output)
|
|
2038
|
+
showText(output);
|
|
1829
2039
|
}
|
|
1830
2040
|
if (buffer.pending.length > 0) {
|
|
1831
|
-
|
|
1832
|
-
if (params.chunkEmitter)
|
|
1833
|
-
params.chunkEmitter.emit('chunk', flushed);
|
|
1834
|
-
else
|
|
1835
|
-
writeStdout(flushed);
|
|
2041
|
+
showText(render(buffer.pending));
|
|
1836
2042
|
buffer = initialBufferState();
|
|
1837
2043
|
writingCode = false;
|
|
1838
2044
|
}
|
|
2045
|
+
if (heldEcho) {
|
|
2046
|
+
// LT-3 — drop only a short text-only reply that ends the turn.
|
|
2047
|
+
const said = currentAssistantContent.filter((b) => b?.type === 'text').map((b) => String(b.text ?? '')).join('').trim();
|
|
2048
|
+
const restatement = sawEndTurn && !currentAssistantContent.some((b) => b?.type === 'tool_use') && said.length <= HAND_OFF_ECHO_MAX;
|
|
2049
|
+
if (restatement)
|
|
2050
|
+
heldEcho = null;
|
|
2051
|
+
else
|
|
2052
|
+
releaseHeldEcho();
|
|
2053
|
+
}
|
|
1839
2054
|
// Repair partial blocks before persistence. ALWAYS run, not just on
|
|
1840
2055
|
// interruption: on a clean tool-use round the model's tool_use blocks can
|
|
1841
2056
|
// still carry a leftover `partial_json` field, and persisting that poisons
|
|
@@ -1960,10 +2175,13 @@ ${imageExpansion.error}`));
|
|
|
1960
2175
|
// this call sent and whether its system block matched the session's
|
|
1961
2176
|
// previous main call (same four inputs as the headers above).
|
|
1962
2177
|
const mainLine = !readerChild;
|
|
2178
|
+
// Stage 1A (M2) — an inline call's system block is the one constant
|
|
2179
|
+
// block whatever the skill or delegate: the key says so, so a switch is
|
|
2180
|
+
// 'same' and a 'changed' line in an inline session is a real change.
|
|
1963
2181
|
const systemBlock = mainLine
|
|
1964
2182
|
? noteSystemBlock(session, systemBlockKey({
|
|
1965
|
-
skill: params.skill,
|
|
1966
|
-
delegateSkill: params.delegateSkill,
|
|
2183
|
+
skill: inlineTurn ? INLINE_SYSTEM_BLOCK_SKILL : params.skill,
|
|
2184
|
+
delegateSkill: inlineTurn ? undefined : params.delegateSkill,
|
|
1967
2185
|
style: getAssistantStyle(),
|
|
1968
2186
|
level: getAssistantLevel(),
|
|
1969
2187
|
}))
|
|
@@ -1996,7 +2214,15 @@ ${imageExpansion.error}`));
|
|
|
1996
2214
|
reader: params.ctx.readerSpend,
|
|
1997
2215
|
skill: mainLine ? params.skill : undefined,
|
|
1998
2216
|
system_block: systemBlock,
|
|
2217
|
+
// Stage 1A (S8, S-C, M-5) — inline lines only; absent otherwise.
|
|
2218
|
+
system_mode: mainLine && inlineTurn ? 'inline' : undefined,
|
|
2219
|
+
skill_appended: mainLine && inlineTurn && !skillAppendedLogged
|
|
2220
|
+
? inlineBlocks.map((b) => ({ name: b.name, sha256: b.sha256, chars: b.text.length }))
|
|
2221
|
+
: undefined,
|
|
2222
|
+
tools_surfaced: mainLine && inlineTurn ? callSurfaced : undefined,
|
|
1999
2223
|
});
|
|
2224
|
+
if (mainLine)
|
|
2225
|
+
skillAppendedLogged = true;
|
|
2000
2226
|
readerCallsLogged = params.ctx.readerSpend?.calls ?? 0;
|
|
2001
2227
|
void appendCostLine(session.id, entry);
|
|
2002
2228
|
// Security batch item 3 — usage counts for byok / ai-hub / local (the
|
|
@@ -2084,6 +2310,7 @@ ${imageExpansion.error}`));
|
|
|
2084
2310
|
const outputTokensThisTurn = Math.max(0, (session.usage?.output_tokens ?? 0) - turnStartOutputTokens);
|
|
2085
2311
|
const decision = evaluateWatchdog({
|
|
2086
2312
|
startedAt: turnStartedAt,
|
|
2313
|
+
pausedMs: status.pausedMs() - turnPausedBase,
|
|
2087
2314
|
outputTokensThisTurn,
|
|
2088
2315
|
config: watchdogConfig,
|
|
2089
2316
|
alreadyWarned: false,
|
|
@@ -2139,10 +2366,13 @@ ${imageExpansion.error}`));
|
|
|
2139
2366
|
model: params.modelOverride ?? resolveModelRole('session_default', cfg) ?? session.model ?? 'unknown',
|
|
2140
2367
|
emit,
|
|
2141
2368
|
sapSystem: session.sap_system,
|
|
2369
|
+
...(params.operator ? { operator: params.operator } : {}),
|
|
2370
|
+
sessionId: session.id,
|
|
2142
2371
|
...(params.ctx.subagent ? { subagent: true } : {}),
|
|
2143
2372
|
...(session.awaitingSkillAnswer === true ? { awaitingAnswer: true } : {}),
|
|
2144
2373
|
...(getJourneyFlow(session) || getAppFlow(session) ? { guided: true } : {}),
|
|
2145
2374
|
session,
|
|
2375
|
+
readCwd: params.ctx.cwd,
|
|
2146
2376
|
promotedFrom: promotedFromForSave,
|
|
2147
2377
|
...(promotedFromPathForSave ? { promotedFromPath: promotedFromPathForSave } : {}),
|
|
2148
2378
|
...(() => {
|
|
@@ -2617,10 +2847,17 @@ ${imageExpansion.error}`));
|
|
|
2617
2847
|
// immediately below handles both success and the not-yet-capped failure case.
|
|
2618
2848
|
// 2026-10-22 — the general guard: a result over TOOL_RESULT_MAX_CHARS
|
|
2619
2849
|
// is cut with a note; a result under it is the same value (byte-identical).
|
|
2850
|
+
// LT-3 (live check 2026-10-26) — text already went out in this turn:
|
|
2851
|
+
// the hand_off result tells the model plainly to add nothing more.
|
|
2852
|
+
const saidBefore = block.name === 'hand_off' && !result.is_error
|
|
2853
|
+
&& textBeforeInTurn(session.messages.slice(turnStartMessageCount), currentAssistantContent, block);
|
|
2854
|
+
const resultContent = saidBefore ? settleHandOffNote(result.content, true) : result.content;
|
|
2855
|
+
if (saidBefore && resultContent !== result.content)
|
|
2856
|
+
muteHandOffEcho = true;
|
|
2620
2857
|
toolResults.push({
|
|
2621
2858
|
type: 'tool_result',
|
|
2622
2859
|
tool_use_id: block.id,
|
|
2623
|
-
content: capToolResultContent(
|
|
2860
|
+
content: capToolResultContent(resultContent),
|
|
2624
2861
|
is_error: result.is_error,
|
|
2625
2862
|
});
|
|
2626
2863
|
}
|
|
@@ -2640,6 +2877,7 @@ ${imageExpansion.error}`));
|
|
|
2640
2877
|
const outputTokensThisTurn = Math.max(0, (session.usage?.output_tokens ?? 0) - turnStartOutputTokens);
|
|
2641
2878
|
const decision = evaluateWatchdog({
|
|
2642
2879
|
startedAt: turnStartedAt,
|
|
2880
|
+
pausedMs: status.pausedMs() - turnPausedBase,
|
|
2643
2881
|
outputTokensThisTurn,
|
|
2644
2882
|
config: watchdogConfig,
|
|
2645
2883
|
alreadyWarned: watchdogWarned,
|
|
@@ -2674,3 +2912,20 @@ function dispatchedSkills(assistantContent, toolResults) {
|
|
|
2674
2912
|
}
|
|
2675
2913
|
return out;
|
|
2676
2914
|
}
|
|
2915
|
+
/**
|
|
2916
|
+
* LT-3 — did the model already write text in this turn before `block`? Earlier
|
|
2917
|
+
* rounds' assistant messages, then this round's blocks ahead of the call.
|
|
2918
|
+
*/
|
|
2919
|
+
function textBeforeInTurn(turnMessages, roundContent, block) {
|
|
2920
|
+
const said = (b) => b?.type === 'text' && typeof b.text === 'string' && b.text.trim() !== '';
|
|
2921
|
+
for (const m of turnMessages) {
|
|
2922
|
+
if (m?.role !== 'assistant' || m.content === roundContent || !Array.isArray(m.content))
|
|
2923
|
+
continue;
|
|
2924
|
+
if (m.content.some(said))
|
|
2925
|
+
return true;
|
|
2926
|
+
}
|
|
2927
|
+
const at = roundContent.indexOf(block);
|
|
2928
|
+
return roundContent.slice(0, at < 0 ? roundContent.length : at).some(said);
|
|
2929
|
+
}
|
|
2930
|
+
/** LT-3 — the longest text-only closing reply after a hand-off that is treated as a second announcement. */
|
|
2931
|
+
const HAND_OFF_ECHO_MAX = 320;
|