@cspeach/cli 1.1.18 → 1.1.20
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/README.md +3 -4
- 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 +804 -159
- 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 +12 -2
- package/dist/agent/repair-partial.js +125 -7
- package/dist/agent/summarise-via-provider.js +6 -1
- package/dist/agent/system-prompt.js +38 -0
- package/dist/agent/tool-dispatch.js +66 -21
- package/dist/agent/tool-loading-pin.js +100 -0
- package/dist/agent/turn-error-ux.js +77 -26
- package/dist/agent/turn-stream.js +25 -3
- package/dist/approvals/adt-type.js +27 -0
- package/dist/approvals/advisory-prompt.js +48 -0
- package/dist/approvals/advisory-render.js +25 -11
- package/dist/approvals/approval-prompt.js +74 -24
- package/dist/approvals/jwt.js +2 -1
- package/dist/approvals/render.js +64 -36
- package/dist/approvals/risk-floor.js +53 -2
- package/dist/auth/api-key.js +30 -16
- package/dist/auth/keychain.js +0 -0
- package/dist/auth/me.js +25 -7
- package/dist/cli.js +36 -6
- package/dist/commands/auto-compact.js +33 -16
- package/dist/commands/compact.js +49 -11
- package/dist/commands/config-set.js +16 -3
- package/dist/commands/config-show.js +18 -5
- package/dist/commands/cost.js +54 -35
- package/dist/commands/help.js +88 -50
- package/dist/commands/login.js +28 -19
- package/dist/commands/logout.js +7 -3
- package/dist/commands/plan-audit.js +1 -0
- package/dist/commands/plan-chain.js +71 -77
- package/dist/commands/plan-continue.js +1 -0
- package/dist/commands/plan-resume.js +20 -12
- package/dist/commands/whoami.js +23 -9
- package/dist/config/loader.js +70 -5
- package/dist/cost/cost-log.js +62 -2
- package/dist/cost/pricing.js +11 -6
- package/dist/doctor/checks/forge-rules.js +13 -3
- package/dist/doctor/checks/keychain.js +6 -4
- package/dist/doctor/run.js +101 -20
- package/dist/index.js +5 -2
- package/dist/lib/graceful-exit.js +56 -0
- package/dist/lib/piped-prompt.js +65 -0
- package/dist/lib/spill-labels.js +13 -0
- package/dist/lock-contention.js +2 -2
- package/dist/models/resolve.js +93 -2
- package/dist/models/server-config.js +158 -3
- package/dist/one-shot.js +150 -45
- package/dist/projects/answer-blockers.js +6 -1
- package/dist/projects/handover-md.js +15 -14
- package/dist/projects/image-attachments.js +15 -2
- package/dist/projects/plan-run.js +97 -54
- package/dist/projects/promote-command.js +11 -2
- package/dist/projects/save-command.js +3 -2
- package/dist/projects/status.js +2 -1
- package/dist/projects/yes-no.js +12 -0
- package/dist/renderer/abap-inline.js +18 -14
- package/dist/renderer/answer-trim.js +69 -0
- package/dist/renderer/banners.js +8 -8
- package/dist/renderer/brand-settled.js +19 -0
- package/dist/renderer/change-card.js +156 -0
- package/dist/renderer/checklist-format.js +130 -0
- package/dist/renderer/color-mode.js +45 -0
- package/dist/renderer/error-detail.js +104 -0
- package/dist/renderer/fence-state.js +46 -0
- package/dist/renderer/footer-line.js +100 -0
- package/dist/renderer/glyphs.js +25 -0
- package/dist/renderer/goodbye.js +38 -0
- package/dist/renderer/legacy-palette.js +41 -0
- package/dist/renderer/look.js +30 -0
- package/dist/renderer/markdown.js +299 -92
- package/dist/renderer/notice-log.js +72 -0
- package/dist/renderer/notice-shape.js +48 -0
- package/dist/renderer/notices.js +40 -8
- package/dist/renderer/panel-rows.js +16 -0
- package/dist/renderer/pipeline.js +66 -0
- package/dist/renderer/progress-chatter.js +23 -17
- package/dist/renderer/rendering-mode.js +34 -0
- package/dist/renderer/routing-line.js +14 -0
- package/dist/renderer/sanitize.js +12 -0
- package/dist/renderer/severity.js +2 -7
- package/dist/renderer/startup-lines.js +154 -0
- package/dist/renderer/status-footer.js +52 -34
- package/dist/renderer/steering-echo.js +41 -0
- package/dist/renderer/syntax.js +31 -12
- package/dist/renderer/tables.js +5 -1
- package/dist/renderer/theme.js +44 -0
- package/dist/renderer/thinking-heartbeat.js +33 -2
- package/dist/renderer/todo-block.js +10 -49
- package/dist/renderer/tool-labels.js +269 -0
- package/dist/renderer/tool-widget.js +245 -51
- package/dist/renderer/trace.js +42 -0
- package/dist/renderer/transcript-flow.js +132 -0
- package/dist/renderer/tty.js +34 -1
- package/dist/renderer/ui-width.js +55 -0
- package/dist/renderer/verify-chain.js +4 -2
- package/dist/renderer/widget-fallback.js +58 -65
- package/dist/repl/bracketed-paste.js +7 -1
- package/dist/repl/builtin-commands.js +19 -8
- package/dist/repl/credential-handover-gate.js +28 -0
- package/dist/repl/current-transport.js +13 -0
- package/dist/repl/early-line-buffer.js +5 -2
- package/dist/repl/file-picker.js +54 -12
- package/dist/repl/ink-stdin-guard.js +66 -0
- package/dist/repl/inquirer-guard.js +59 -16
- package/dist/repl/inquirer-theme.js +27 -33
- package/dist/repl/paste-marker.js +19 -0
- package/dist/repl/plan-turn-end.js +20 -0
- package/dist/repl/post-turn-status.js +43 -11
- package/dist/repl/reroute-turn.js +21 -0
- package/dist/repl/reset-tty-stdin.js +44 -0
- package/dist/repl/restore-guard.js +22 -0
- package/dist/repl/resume-standalone.js +63 -0
- package/dist/repl/rule8-detector.js +11 -3
- package/dist/repl/safety-confirm.js +161 -93
- package/dist/repl/session-spend-line.js +8 -4
- package/dist/repl/themed-prompts.js +19 -0
- package/dist/repl/ui-look-command.js +75 -0
- package/dist/repl/update-method-preview-hook.js +48 -5
- package/dist/repl.js +863 -339
- package/dist/rewind/cli.js +3 -2
- package/dist/rewind/format.js +14 -9
- package/dist/rewind/restore.js +11 -0
- package/dist/router/classifier.js +47 -4
- package/dist/router/intent-extractor.js +24 -8
- package/dist/router/piped-routing.js +23 -0
- package/dist/router/resume-routing.js +16 -0
- package/dist/router/routing-failure.js +57 -0
- package/dist/sap/first-run-choice.js +1 -1
- package/dist/sap/onboarding.js +3 -2
- package/dist/sap/standalone-onboarding.js +1 -1
- package/dist/sap/system-info.js +4 -2
- package/dist/sap/unreachable.js +28 -0
- package/dist/sap-errors/clean-error-text.js +182 -0
- package/dist/session/interrupt-reason.js +39 -0
- package/dist/session/recap.js +36 -24
- package/dist/session/repin-model.js +18 -0
- package/dist/session/resume.js +104 -36
- package/dist/session/store.js +84 -28
- package/dist/session/user-prompt.js +21 -0
- package/dist/skill-catalog.js +7 -0
- package/dist/skills/bundled-skills.js +76 -69
- package/dist/skills/preamble.js +75 -0
- package/dist/skills/source-manifest.js +11 -1
- package/dist/standards/standards-init.js +1 -1
- package/dist/test-helpers/answer-any.js +46 -0
- package/dist/tools/approval.js +27 -11
- package/dist/tools/ask-question.js +47 -5
- package/dist/tools/filesystem/file-read.js +11 -1
- package/dist/tools/filesystem/file-write.js +19 -2
- package/dist/tools/fiori/fe-extend.js +16 -1
- package/dist/tools/fiori/fe-scaffold.js +105 -3
- package/dist/tools/fiori/html-escapes.js +28 -0
- package/dist/tools/fiori/metadata/audit.js +605 -0
- package/dist/tools/fiori/metadata/references.js +104 -0
- package/dist/tools/fiori/metadata/types.js +8 -0
- package/dist/tools/fiori/preview/app-guard.js +215 -0
- package/dist/tools/fiori/preview/env.js +193 -0
- package/dist/tools/fiori/preview/readiness.js +127 -0
- package/dist/tools/fiori/preview/registry.js +412 -0
- package/dist/tools/fiori/preview/start.js +469 -0
- package/dist/tools/fiori/samples/loader.js +20 -5
- package/dist/tools/fiori/scaffold.js +18 -6
- package/dist/tools/fiori/smoke/app-driver-page.js +372 -0
- package/dist/tools/fiori/smoke/app-driver.js +100 -0
- package/dist/tools/fiori/smoke/assertions.js +161 -24
- package/dist/tools/fiori/smoke/audit-columns.js +21 -0
- package/dist/tools/fiori/smoke/batch.js +273 -0
- package/dist/tools/fiori/smoke/draft-safety.js +241 -0
- package/dist/tools/fiori/smoke/draft-smoke.js +473 -0
- package/dist/tools/fiori/smoke/driver.js +171 -4
- package/dist/tools/fiori/smoke/evidence.js +35 -0
- package/dist/tools/fiori/smoke/labels-codes.js +207 -0
- package/dist/tools/fiori/smoke/nav-error.js +26 -0
- package/dist/tools/fiori/smoke/run-smoke.js +155 -12
- package/dist/tools/fiori/tools.js +651 -7
- package/dist/tools/fiori/ui5-version.js +16 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/local-build.js +6 -0
- package/dist/tools/result-spill.js +238 -0
- package/dist/tools/sap-read.js +74 -15
- package/dist/tools/sap-write.js +122 -10
- package/dist/tools/shell/shell_exec.js +39 -12
- 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/syntax-state.js +67 -0
- package/dist/tools/todo.js +26 -19
- package/dist/tools/tool-loading.js +255 -0
- package/dist/tools/tool-output-read.js +117 -0
- package/dist/tools/transport-fit.js +462 -0
- package/dist/tools/transport-resolution.js +3 -1
- package/dist/tools/transport.js +39 -8
- package/dist/tools/verify.js +184 -8
- package/dist/ui/app.js +284 -146
- package/dist/ui/approval-modal.js +41 -15
- package/dist/ui/ask-answer-text.js +17 -0
- package/dist/ui/body.js +25 -6
- package/dist/ui/card-slot.js +98 -0
- package/dist/ui/checklist.js +9 -0
- package/dist/ui/coaching-picker-classic.js +8 -5
- package/dist/ui/command-palette.js +6 -4
- package/dist/ui/confirm-request.js +18 -0
- package/dist/ui/context-grid.js +2 -1
- package/dist/ui/file-palette.js +6 -4
- package/dist/ui/files-card-emitter.js +19 -0
- package/dist/ui/footer-line.js +16 -0
- package/dist/ui/footer.js +105 -104
- package/dist/ui/input-wrap.js +122 -0
- package/dist/ui/interrupt.js +103 -0
- package/dist/ui/key-burst.js +117 -0
- package/dist/ui/line-resolution.js +4 -3
- package/dist/ui/live-area.js +71 -0
- package/dist/ui/login-banner.js +54 -43
- package/dist/ui/rewind-panel.js +76 -24
- package/dist/ui/role-props.js +47 -0
- package/dist/ui/sap-state-store.js +2 -1
- package/dist/ui/session-timeline.js +49 -3
- package/dist/ui/skill-picker.js +5 -47
- package/dist/ui/text-input.js +128 -24
- package/dist/ui/todo-emitter.js +19 -0
- package/dist/ui/turn-status-emitter.js +65 -2
- package/dist/ui/turn-status.js +16 -19
- package/dist/ui/use-card-request.js +44 -0
- package/dist/ui/widgets/ask-card.js +521 -0
- package/dist/ui/widgets/bar-chart.js +11 -5
- package/dist/ui/widgets/coaching-picker.js +50 -55
- package/dist/ui/widgets/confirm-card.js +193 -0
- package/dist/ui/widgets/dep-graph.js +3 -2
- package/dist/ui/widgets/diff-viewer.js +6 -5
- package/dist/ui/widgets/files-card.js +97 -0
- package/dist/ui/widgets/question-card.js +2 -1
- package/dist/ui/widgets/reclaim-raw-stdin.js +24 -0
- package/dist/ui/widgets/shortcuts-sheet.js +25 -0
- package/dist/ui/widgets/stack-frames.js +2 -1
- package/dist/upgrade-check.js +39 -8
- package/package.json +12 -2
- package/test-harness/fe-draft/app.ts +188 -0
- package/test-harness/fe-draft/fe-draft.e2e.test.ts +461 -0
- package/test-harness/fe-draft/manifest-enhancer.unit.test.ts +51 -0
- package/test-harness/fe-draft/routes.ts +224 -0
- package/test-harness/fe-draft/routes.unit.test.ts +45 -0
- package/test-harness/fe-draft/server.ts +225 -0
- package/test-harness/fe-draft/variants.ts +196 -0
- package/vitest.smoke-e2e.config.ts +20 -0
package/dist/cost/cost-log.js
CHANGED
|
@@ -15,6 +15,29 @@ import fs from 'node:fs/promises';
|
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import { sessionsDir } from '../config/paths.js';
|
|
17
17
|
import { computeCost } from './pricing.js';
|
|
18
|
+
/**
|
|
19
|
+
* Task 7 — summarise `message_start.message.input_transformations` (probe P4
|
|
20
|
+
* shape: `[{ type: 'thinking_dropped', path, reason }]`). Counts the
|
|
21
|
+
* `thinking_dropped` entries and lists each distinct reason once ('unknown'
|
|
22
|
+
* when an entry has none). Returns undefined for absent, empty or malformed
|
|
23
|
+
* input, and when nothing was dropped — so the cost-line key stays absent.
|
|
24
|
+
*/
|
|
25
|
+
export function summariseInputTransformations(raw) {
|
|
26
|
+
if (!Array.isArray(raw))
|
|
27
|
+
return undefined;
|
|
28
|
+
let dropped = 0;
|
|
29
|
+
const reasons = [];
|
|
30
|
+
for (const t of raw) {
|
|
31
|
+
if (!t || typeof t !== 'object' || t.type !== 'thinking_dropped')
|
|
32
|
+
continue;
|
|
33
|
+
dropped += 1;
|
|
34
|
+
const r = t.reason;
|
|
35
|
+
const reason = typeof r === 'string' && r ? r : 'unknown';
|
|
36
|
+
if (!reasons.includes(reason))
|
|
37
|
+
reasons.push(reason);
|
|
38
|
+
}
|
|
39
|
+
return dropped > 0 ? { dropped, reasons } : undefined;
|
|
40
|
+
}
|
|
18
41
|
/**
|
|
19
42
|
* Path to a session's cost log file. Co-located with the session JSON so
|
|
20
43
|
* deletion cleans up both atomically.
|
|
@@ -39,6 +62,20 @@ export function buildEntry(opts) {
|
|
|
39
62
|
entry.credits_charged = opts.credits.charged;
|
|
40
63
|
entry.balance_credits = opts.credits.balance;
|
|
41
64
|
}
|
|
65
|
+
if (opts.served_model !== undefined)
|
|
66
|
+
entry.served_model = opts.served_model;
|
|
67
|
+
if (opts.call_index !== undefined)
|
|
68
|
+
entry.call_index = opts.call_index;
|
|
69
|
+
if (opts.kind !== undefined)
|
|
70
|
+
entry.kind = opts.kind;
|
|
71
|
+
if (opts.refusal_category !== undefined)
|
|
72
|
+
entry.refusal_category = opts.refusal_category;
|
|
73
|
+
if (opts.input_transformations !== undefined)
|
|
74
|
+
entry.input_transformations = opts.input_transformations;
|
|
75
|
+
if (opts.reader && opts.reader.calls > 0) {
|
|
76
|
+
entry.reader_cost_usd = Math.round(opts.reader.cost * 10000) / 10000;
|
|
77
|
+
entry.reader_calls = opts.reader.calls;
|
|
78
|
+
}
|
|
42
79
|
return entry;
|
|
43
80
|
}
|
|
44
81
|
/**
|
|
@@ -90,7 +127,22 @@ export function summarise(entries) {
|
|
|
90
127
|
const totals = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
|
|
91
128
|
let total = 0;
|
|
92
129
|
const perModel = new Map();
|
|
130
|
+
const readerByTurn = new Map();
|
|
131
|
+
let lines = 0;
|
|
93
132
|
for (const e of entries) {
|
|
133
|
+
if (typeof e.reader_calls === 'number' && e.reader_calls > 0) {
|
|
134
|
+
const prev = readerByTurn.get(e.turn);
|
|
135
|
+
if (!prev || e.reader_calls > prev.calls || (e.reader_cost_usd ?? 0) > prev.cost) {
|
|
136
|
+
readerByTurn.set(e.turn, {
|
|
137
|
+
cost: Math.max(prev?.cost ?? 0, e.reader_cost_usd ?? 0),
|
|
138
|
+
calls: Math.max(prev?.calls ?? 0, e.reader_calls),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Fix M3 — a zero-token reader rollup line (interrupted turn) is not a model call.
|
|
143
|
+
if (e.kind === 'reader_rollup')
|
|
144
|
+
continue;
|
|
145
|
+
lines += 1;
|
|
94
146
|
total += e.cost;
|
|
95
147
|
totals.input += e.tokens.input;
|
|
96
148
|
totals.output += e.tokens.output;
|
|
@@ -109,10 +161,18 @@ export function summarise(entries) {
|
|
|
109
161
|
slot.tokens.cacheCreate += e.tokens.cacheCreate;
|
|
110
162
|
perModel.set(e.model, slot);
|
|
111
163
|
}
|
|
164
|
+
let readerCost = 0;
|
|
165
|
+
let readerCalls = 0;
|
|
166
|
+
for (const r of readerByTurn.values()) {
|
|
167
|
+
readerCost += r.cost;
|
|
168
|
+
readerCalls += r.calls;
|
|
169
|
+
}
|
|
112
170
|
return {
|
|
113
|
-
totalCost: Math.round(total * 10000) / 10000,
|
|
171
|
+
totalCost: Math.round((total + readerCost) * 10000) / 10000,
|
|
114
172
|
totalTokens: totals,
|
|
115
|
-
turns:
|
|
173
|
+
turns: lines,
|
|
116
174
|
byModel: [...perModel.entries()].map(([model, v]) => ({ model, ...v })),
|
|
175
|
+
readerCost: Math.round(readerCost * 10000) / 10000,
|
|
176
|
+
readerCalls,
|
|
117
177
|
};
|
|
118
178
|
}
|
package/dist/cost/pricing.js
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* BOTH this file AND the proxy's version. Verify on the public pricing page:
|
|
12
12
|
* https://www.anthropic.com/pricing
|
|
13
13
|
*
|
|
14
|
-
* Last verified 2026-
|
|
14
|
+
* Last verified 2026-09-27. Same as proxy. The claude-opus-5-5 row comes from
|
|
15
|
+
* the Opus 5.5 section of the model migration guide. The two tables are pinned
|
|
16
|
+
* in sync by src/cost/__tests__/pricing-drift-pin.test.ts.
|
|
15
17
|
*
|
|
16
18
|
* 2026-06-07 SYNC: e294cff switched the default model to claude-opus-4-8 AND
|
|
17
19
|
* applied the 3x Opus correction, but only to the proxy copy — this CLI copy
|
|
@@ -22,8 +24,12 @@
|
|
|
22
24
|
* Unknown models → returns null + records the model name to a process-level
|
|
23
25
|
* Set so we surface them as an obvious warning rather than silently zeroing.
|
|
24
26
|
*/
|
|
27
|
+
import { recordFullNotice } from '../renderer/notice-log.js';
|
|
25
28
|
import { getServerModelConfig } from '../models/server-config.js';
|
|
26
29
|
const PRICING = {
|
|
30
|
+
// Claude Opus 5.5 (2026-09-27, migration guide) — $4/$20, cache read $0.20
|
|
31
|
+
// (0.05x input, not 10%), 5-minute cache write $5 (1.25x input).
|
|
32
|
+
'claude-opus-5-5': { inputPer1M: 4.00, outputPer1M: 20.00, cacheReadPer1M: 0.20, cacheCreatePer1M: 5.00 },
|
|
27
33
|
// Claude 5 family (2026-09-15) — Opus 5 keeps the Opus 4.5+ rate ($5/$25);
|
|
28
34
|
// Sonnet 5 is $2/$10. Cache read = 10% of input, cache write = 1.25x input.
|
|
29
35
|
'claude-opus-5': { inputPer1M: 5.00, outputPer1M: 25.00, cacheReadPer1M: 0.50, cacheCreatePer1M: 6.25 },
|
|
@@ -90,11 +96,10 @@ export function getRate(model) {
|
|
|
90
96
|
return PRICING[stripDate];
|
|
91
97
|
if (!seenUnknownModels.has(normalized)) {
|
|
92
98
|
seenUnknownModels.add(normalized);
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
catch { /* noop */ }
|
|
99
|
+
// Diagnostics never reach the user's screen (fix round 3, item 1): a raw
|
|
100
|
+
// console.warn bypassed the Ink/classic writers and printed JSON into the
|
|
101
|
+
// chat, spinner, goodbye and one-shot stderr. Log file only.
|
|
102
|
+
recordFullNotice('debug', JSON.stringify({ msg: 'pricing_unknown_model', model: normalized }));
|
|
98
103
|
}
|
|
99
104
|
return null;
|
|
100
105
|
}
|
|
@@ -19,6 +19,7 @@ import { loadConfig } from '../../config/loader.js';
|
|
|
19
19
|
import { getConnection } from '../../sap/connection-manager.js';
|
|
20
20
|
import { AdtClient, snapshots } from '@cspeach/sap-client';
|
|
21
21
|
import { sapChecksSkipped, standaloneSkip } from './_standalone-skip.js';
|
|
22
|
+
import { verifySyntax } from '../../tools/verify.js';
|
|
22
23
|
const PROBE_NAME = 'Z_CSPEACH_DOCTOR_PROBE';
|
|
23
24
|
const PROBE_PACKAGE = '$TMP';
|
|
24
25
|
const PROBE_SOURCE = `REPORT ${PROBE_NAME}.\nWRITE / 'cspeach doctor probe — safe to delete'.`;
|
|
@@ -84,12 +85,21 @@ export async function checkForgeRules() {
|
|
|
84
85
|
};
|
|
85
86
|
}
|
|
86
87
|
// Step 4: Verify Rule 10 fired — syntax was checked. Re-run syntaxCheck and confirm clean.
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
// Re-review item 2 — through verifySyntax (fail closed): a check that did
|
|
89
|
+
// not run is ok:false with its reason, never a throw out of collectChecks.
|
|
90
|
+
const syntax = await verifySyntax(adt, PROBE_NAME, 'PROG');
|
|
91
|
+
if (syntax.didNotRun !== undefined) {
|
|
89
92
|
return {
|
|
90
93
|
name: 'forge-rules',
|
|
91
94
|
ok: false,
|
|
92
|
-
message: `Rule 10: syntax check
|
|
95
|
+
message: `Rule 10: syntax check did not run: ${syntax.didNotRun.replace(/^Syntax check did not run:?\s*/i, '')}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (!syntax.ok) {
|
|
99
|
+
return {
|
|
100
|
+
name: 'forge-rules',
|
|
101
|
+
ok: false,
|
|
102
|
+
message: `Rule 10: syntax check returned errors on probe: ${syntax.errors[0] ?? 'unknown'}`,
|
|
93
103
|
};
|
|
94
104
|
}
|
|
95
105
|
return {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import keychain from '../../auth/keychain.js';
|
|
1
|
+
import keychain, { newProbeAccount } from '../../auth/keychain.js';
|
|
2
2
|
export async function checkKeychain() {
|
|
3
3
|
try {
|
|
4
|
+
// Round 4 item 7 — per-process probe account (see newProbeAccount).
|
|
5
|
+
const account = newProbeAccount();
|
|
4
6
|
const probe = `probe-${Date.now()}`;
|
|
5
|
-
await keychain.setPassword('cspeach',
|
|
6
|
-
const ok = (await keychain.getPassword('cspeach',
|
|
7
|
-
await keychain.deletePassword('cspeach',
|
|
7
|
+
await keychain.setPassword('cspeach', account, probe);
|
|
8
|
+
const ok = (await keychain.getPassword('cspeach', account)) === probe;
|
|
9
|
+
await keychain.deletePassword('cspeach', account);
|
|
8
10
|
if (ok)
|
|
9
11
|
return { name: 'keychain', ok: true, message: 'OS keychain working' };
|
|
10
12
|
return { name: 'keychain', ok: false, message: 'probe write/read mismatch', hint: 'Per-session fallback mode will be used' };
|
package/dist/doctor/run.js
CHANGED
|
@@ -14,8 +14,93 @@ import { checkCredits } from './checks/credits.js';
|
|
|
14
14
|
import { checkForgeRules } from './checks/forge-rules.js';
|
|
15
15
|
import { checkSystemRoles } from './checks/system-roles.js';
|
|
16
16
|
import { checkStandards } from './checks/standards.js';
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
import { paint } from '../renderer/theme.js';
|
|
18
|
+
import { getGlyphs } from '../renderer/glyphs.js';
|
|
19
|
+
import { isNoColor } from '../renderer/color-mode.js';
|
|
20
|
+
import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
21
|
+
import { CLI_VERSION } from '../lib/version.js';
|
|
22
|
+
const NAME_WIDTH = 20;
|
|
23
|
+
/** Check messages can carry server text (HTTP bodies, SAP errors): one line, no control characters. */
|
|
24
|
+
const oneLine = (s) => sanitizeControlChars(s).replace(/\s*\r?\n\s*/g, ' ').trim();
|
|
25
|
+
/** Human name of the terminal CSPeach is running in (for the doctor title). */
|
|
26
|
+
export function terminalName(env = process.env, platform = process.platform) {
|
|
27
|
+
if (env['WT_SESSION'])
|
|
28
|
+
return 'Windows Terminal';
|
|
29
|
+
const tp = env['TERM_PROGRAM'];
|
|
30
|
+
if (tp === 'vscode')
|
|
31
|
+
return 'VS Code terminal';
|
|
32
|
+
if (tp === 'iTerm.app')
|
|
33
|
+
return 'iTerm2';
|
|
34
|
+
if (tp === 'Apple_Terminal')
|
|
35
|
+
return 'Terminal.app';
|
|
36
|
+
if (tp)
|
|
37
|
+
return oneLine(tp);
|
|
38
|
+
if (env['TERM'])
|
|
39
|
+
return oneLine(env['TERM']);
|
|
40
|
+
return platform === 'win32' ? 'Windows console' : 'unknown terminal';
|
|
41
|
+
}
|
|
42
|
+
export function colourLevelName(level, noColor = isNoColor()) {
|
|
43
|
+
if (noColor)
|
|
44
|
+
return 'no colour (NO_COLOR)';
|
|
45
|
+
if (level >= 3)
|
|
46
|
+
return 'truecolor';
|
|
47
|
+
if (level === 2)
|
|
48
|
+
return '256 colours';
|
|
49
|
+
if (level === 1)
|
|
50
|
+
return '16 colours';
|
|
51
|
+
return 'no colour';
|
|
52
|
+
}
|
|
53
|
+
/** 1 when any check failed; a skipped check (C.2 permission skip) is not a failure. */
|
|
54
|
+
export function doctorExitCode(checks) {
|
|
55
|
+
return checks.some((c) => !c.ok && !c.skipped) ? 1 : 0;
|
|
56
|
+
}
|
|
57
|
+
export function formatDoctorTitle(meta) {
|
|
58
|
+
return ['', `${chalk.bold('CSPeach doctor')} ${paint('dim', `${meta.version} · ${meta.terminal} · ${meta.colour}`)}`, ''];
|
|
59
|
+
}
|
|
60
|
+
export function formatDoctorRows(checks) {
|
|
61
|
+
const g = getGlyphs();
|
|
62
|
+
const lines = [];
|
|
63
|
+
let passed = 0;
|
|
64
|
+
let warnings = 0;
|
|
65
|
+
let failed = 0;
|
|
66
|
+
for (const c of checks) {
|
|
67
|
+
let glyph;
|
|
68
|
+
if (c.skipped) {
|
|
69
|
+
glyph = paint('warn', g.attention);
|
|
70
|
+
warnings++;
|
|
71
|
+
}
|
|
72
|
+
else if (c.ok) {
|
|
73
|
+
glyph = paint('ok', g.done);
|
|
74
|
+
passed++;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
glyph = paint('bad', g.failed);
|
|
78
|
+
failed++;
|
|
79
|
+
}
|
|
80
|
+
lines.push(`${glyph} ${oneLine(c.name).padEnd(NAME_WIDTH)} ${paint('dim', oneLine(c.message))}`);
|
|
81
|
+
if (c.hint && oneLine(c.hint) !== '')
|
|
82
|
+
lines.push(` ${paint('dim', 'Next:')} ${oneLine(c.hint)}`);
|
|
83
|
+
}
|
|
84
|
+
const summary = `${passed} of ${checks.length} checks passed · ${warnings} warning${warnings === 1 ? '' : 's'} · ${failed} failed`;
|
|
85
|
+
lines.push('', paint('dim', summary), '');
|
|
86
|
+
return lines;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Re-review 2 item 3 — the forge-rules probe writes to SAP (create, lock,
|
|
90
|
+
* write, snapshot list, delete); any throw there (lock refused, snapshot
|
|
91
|
+
* store unreadable, connection drop) is a failed row with a readable one-line
|
|
92
|
+
* message, never a crashed doctor run.
|
|
93
|
+
*/
|
|
94
|
+
async function safeForgeRules() {
|
|
95
|
+
try {
|
|
96
|
+
return await checkForgeRules();
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
const msg = String(err?.message ?? err).replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
100
|
+
return { name: 'forge-rules', ok: false, message: `Rule 7/10 probe failed: ${msg}` };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export async function collectChecks() {
|
|
19
104
|
const cfg = await loadConfig();
|
|
20
105
|
const checks = [];
|
|
21
106
|
checks.push(await checkProxy());
|
|
@@ -30,23 +115,19 @@ export async function runDoctor() {
|
|
|
30
115
|
checks.push(await checkCert());
|
|
31
116
|
checks.push(await checkSkill());
|
|
32
117
|
checks.push(await checkKeychainFallback());
|
|
33
|
-
checks.push(await
|
|
118
|
+
checks.push(await safeForgeRules());
|
|
34
119
|
checks.push(await checkStandards());
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
console.log(failed === 0
|
|
49
|
-
? chalk.green('\nAll checks passed.\n')
|
|
50
|
-
: chalk.yellow(`\n${failed} check(s) failed.\n`));
|
|
51
|
-
return failed === 0 ? 0 : 1;
|
|
120
|
+
return checks;
|
|
121
|
+
}
|
|
122
|
+
export async function runDoctor(deps = {}) {
|
|
123
|
+
const print = deps.print ?? ((l) => console.log(l));
|
|
124
|
+
const meta = deps.meta ?? { version: CLI_VERSION, terminal: terminalName(), colour: colourLevelName(chalk.level) };
|
|
125
|
+
// Title first: the checks take seconds (network), and the user should see
|
|
126
|
+
// that doctor is running.
|
|
127
|
+
for (const l of formatDoctorTitle(meta))
|
|
128
|
+
print(l);
|
|
129
|
+
const checks = await (deps.collect ?? collectChecks)();
|
|
130
|
+
for (const l of formatDoctorRows(checks))
|
|
131
|
+
print(l);
|
|
132
|
+
return doctorExitCode(checks);
|
|
52
133
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { main } from './cli.js';
|
|
3
|
-
|
|
3
|
+
import { gracefulExit } from './lib/graceful-exit.js';
|
|
4
|
+
// Round 7 item 5 — close fetch's sockets before exiting (Windows libuv
|
|
5
|
+
// assertion on process.exit with live undici handles, nodejs/node#56645).
|
|
6
|
+
main(process.argv).then((code) => gracefulExit(code), (err) => {
|
|
4
7
|
console.error('cspeach: unhandled error:', err);
|
|
5
|
-
|
|
8
|
+
return gracefulExit(1);
|
|
6
9
|
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round 7 item 5 — exit without tearing live network handles down under Node.
|
|
3
|
+
*
|
|
4
|
+
* On Windows, process.exit() while fetch (Node's built-in undici) still holds
|
|
5
|
+
* sockets can abort with the libuv assertion
|
|
6
|
+
* `!(handle->flags & UV_HANDLE_CLOSING), src\win\async.c line 76`
|
|
7
|
+
* (known Node bug: nodejs/node#56645, #64322). The matrix saw it once after a
|
|
8
|
+
* routing cancel + /exit. We close fetch's global dispatcher first and give the
|
|
9
|
+
* loop a short, capped chance to settle, then exit as before.
|
|
10
|
+
*
|
|
11
|
+
* Review r7 M-1 — drain with close() (in-flight requests finish) and only
|
|
12
|
+
* destroy() when that does not finish inside the cap. A fire-and-forget fetch
|
|
13
|
+
* that rejects inside the window must not surface as a crash on the way out,
|
|
14
|
+
* so a no-op unhandledRejection guard is installed for the window only.
|
|
15
|
+
*/
|
|
16
|
+
/** The global symbol Node's bundled undici stores its dispatcher under. */
|
|
17
|
+
export const UNDICI_DISPATCHER = Symbol.for('undici.globalDispatcher.1');
|
|
18
|
+
function capped(p, ms) {
|
|
19
|
+
let timer;
|
|
20
|
+
return Promise.race([
|
|
21
|
+
p.then(() => true, () => true),
|
|
22
|
+
new Promise((r) => { timer = setTimeout(() => r(false), ms); }),
|
|
23
|
+
]).finally(() => { if (timer)
|
|
24
|
+
clearTimeout(timer); });
|
|
25
|
+
}
|
|
26
|
+
function call(fn, self) {
|
|
27
|
+
try {
|
|
28
|
+
return Promise.resolve(fn?.call(self));
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
return Promise.reject(e);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function gracefulExit(code, opts = {}) {
|
|
35
|
+
const exit = opts.exit ?? ((c) => process.exit(c));
|
|
36
|
+
const graceMs = opts.graceMs ?? 250;
|
|
37
|
+
process.exitCode = code;
|
|
38
|
+
const guard = () => { };
|
|
39
|
+
process.on('unhandledRejection', guard);
|
|
40
|
+
try {
|
|
41
|
+
const dispatcher = globalThis[UNDICI_DISPATCHER];
|
|
42
|
+
if (dispatcher) {
|
|
43
|
+
let done = false;
|
|
44
|
+
if (typeof dispatcher.close === 'function')
|
|
45
|
+
done = await capped(call(dispatcher.close, dispatcher), graceMs);
|
|
46
|
+
if (!done && typeof dispatcher.destroy === 'function')
|
|
47
|
+
await capped(call(dispatcher.destroy, dispatcher), graceMs);
|
|
48
|
+
}
|
|
49
|
+
// One more loop turn so socket 'close' callbacks run before the teardown.
|
|
50
|
+
await new Promise((r) => setImmediate(r));
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
process.off('unhandledRejection', guard);
|
|
54
|
+
}
|
|
55
|
+
exit(code);
|
|
56
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// A command head: "/name" followed by a space or the end of the line —
|
|
2
|
+
// "/cost", "/abap-explain ZFOO". A path ("/sap/bc/adt/…", "/usr/sap/…") has
|
|
3
|
+
// a second "/" in its first token and is prose (final review B M-3).
|
|
4
|
+
const SCRIPT_LINE = /(^|\n)[ \t]*\/[A-Za-z][\w-]*(?=[ \t]|\r?\n|$)/;
|
|
5
|
+
export function readPipedPrompt(stdin, opts = {}) {
|
|
6
|
+
if (stdin.isTTY)
|
|
7
|
+
return Promise.resolve({ kind: 'repl' });
|
|
8
|
+
const firstMs = opts.firstMs ?? 250;
|
|
9
|
+
const endMs = opts.endMs ?? 1500;
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
const chunks = [];
|
|
12
|
+
let sawData = false;
|
|
13
|
+
let settled = false;
|
|
14
|
+
let t1;
|
|
15
|
+
let t2;
|
|
16
|
+
const text = () => Buffer.concat(chunks).toString('utf8').replace(/^/, '');
|
|
17
|
+
const done = (r) => {
|
|
18
|
+
if (settled)
|
|
19
|
+
return;
|
|
20
|
+
settled = true;
|
|
21
|
+
if (t1)
|
|
22
|
+
clearTimeout(t1);
|
|
23
|
+
if (t2)
|
|
24
|
+
clearTimeout(t2);
|
|
25
|
+
stdin.off('readable', onReadable);
|
|
26
|
+
stdin.off('end', onEnd);
|
|
27
|
+
resolve(r);
|
|
28
|
+
};
|
|
29
|
+
const giveBack = () => {
|
|
30
|
+
if (settled)
|
|
31
|
+
return;
|
|
32
|
+
if (chunks.length > 0)
|
|
33
|
+
stdin.unshift(Buffer.concat(chunks));
|
|
34
|
+
done({ kind: 'repl' });
|
|
35
|
+
};
|
|
36
|
+
const onEnd = () => {
|
|
37
|
+
const t = text().trim();
|
|
38
|
+
done(t ? { kind: 'prompt', text: t } : { kind: 'repl' });
|
|
39
|
+
};
|
|
40
|
+
const onReadable = () => {
|
|
41
|
+
// EOF with nothing read: leave it unread so the REPL sees the end.
|
|
42
|
+
if (!sawData && stdin.readableLength === 0) {
|
|
43
|
+
done({ kind: 'repl' });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let c;
|
|
47
|
+
while ((c = stdin.read()) !== null)
|
|
48
|
+
chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c));
|
|
49
|
+
if (SCRIPT_LINE.test(text())) {
|
|
50
|
+
giveBack();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!sawData) {
|
|
54
|
+
sawData = true;
|
|
55
|
+
if (t1)
|
|
56
|
+
clearTimeout(t1);
|
|
57
|
+
t2 = setTimeout(giveBack, endMs);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
stdin.on('readable', onReadable);
|
|
61
|
+
stdin.on('end', onEnd);
|
|
62
|
+
t1 = setTimeout(() => { if (!sawData)
|
|
63
|
+
done({ kind: 'repl' }); }, firstMs);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// What a saved (spilled) tool output came from, so the tool_output_read row
|
|
2
|
+
// names the object ("ZTTT_GAME") instead of the internal spill file path.
|
|
3
|
+
// In-memory for this process only: after a resume the row reads "saved output".
|
|
4
|
+
const labels = new Map();
|
|
5
|
+
/** File name only, so '/' and '\' spellings of the same path match. */
|
|
6
|
+
const keyOf = (p) => p.split(/[\\/]/).pop() ?? p;
|
|
7
|
+
export function rememberSpillLabel(filePath, label) {
|
|
8
|
+
if (label.trim().length > 0)
|
|
9
|
+
labels.set(keyOf(filePath), label.trim());
|
|
10
|
+
}
|
|
11
|
+
export function spillLabelFor(filePath) {
|
|
12
|
+
return labels.get(keyOf(filePath));
|
|
13
|
+
}
|
package/dist/lock-contention.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { select } from '@inquirer/prompts';
|
|
2
|
-
import
|
|
2
|
+
import { formatAttention } from './renderer/notice-shape.js';
|
|
3
3
|
export async function handleLockConflict(info) {
|
|
4
|
-
console.log(
|
|
4
|
+
console.log('\n' + formatAttention({ text: `${info.object} is locked`, fact: `by ${info.user} since ${info.lockTime}` }).trimEnd());
|
|
5
5
|
return (await select({
|
|
6
6
|
message: 'How do you want to proceed?',
|
|
7
7
|
choices: [
|
package/dist/models/resolve.js
CHANGED
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
//
|
|
9
9
|
// env > local config > server > built-in constant
|
|
10
10
|
//
|
|
11
|
+
// Task 12 (2026-09-27) adds the CUSTOMER layer (GET /v1/me/model-config, the
|
|
12
|
+
// admin-set per-customer override) for session_default and effort, between
|
|
13
|
+
// local and server: env > local (set) > customer > server > built-in.
|
|
14
|
+
// For managed (enforced) customers this is only a prediction — the proxy
|
|
15
|
+
// enforces and the loop adopts the served model; for BYOK it is a default the
|
|
16
|
+
// developer's local config overrides.
|
|
17
|
+
//
|
|
11
18
|
// Locked invariant: with NOTHING set (no env, default_model_set false, no server
|
|
12
19
|
// snapshot) every role resolves to today's constant — byte-identical to before
|
|
13
20
|
// this feature.
|
|
@@ -16,10 +23,11 @@
|
|
|
16
23
|
// read is getServerModelConfig(), a synchronous in-memory snapshot the caller
|
|
17
24
|
// (or the startup fetch) has already populated. `router` is proxy-side only and
|
|
18
25
|
// is deliberately NOT a CLI role.
|
|
26
|
+
import { DEFAULT_CONFIG } from '../config/loader.js';
|
|
19
27
|
import { DEFAULT_SESSION_MODEL } from '../config/model-defaults.js';
|
|
20
28
|
import { PLAN_TIER_SONNET_MODEL } from '../commands/plan-model-tier.js';
|
|
21
29
|
import { COMPACTION_MODEL } from '../commands/compact.js';
|
|
22
|
-
import { getServerModelConfig } from './server-config.js';
|
|
30
|
+
import { getCustomerModelConfig, getServerModelConfig, isEffort } from './server-config.js';
|
|
23
31
|
/** First non-empty (trimmed) candidate in precedence order, else the built-in. */
|
|
24
32
|
function firstSet(candidates, builtin) {
|
|
25
33
|
for (const c of candidates) {
|
|
@@ -31,7 +39,8 @@ function firstSet(candidates, builtin) {
|
|
|
31
39
|
}
|
|
32
40
|
/**
|
|
33
41
|
* Resolve the model for a pipeline role. Precedence: env > local config >
|
|
34
|
-
* server > built-in constant
|
|
42
|
+
* server > built-in constant; `session_default` also reads the customer layer
|
|
43
|
+
* (Task 12) between local and server.
|
|
35
44
|
*
|
|
36
45
|
* `session_default` reads the local `default_model` ONLY when `default_model_set`
|
|
37
46
|
* is true — i.e. the user's config file explicitly carried the key. The value
|
|
@@ -46,6 +55,7 @@ export function resolveModelRole(role, cfg, env = process.env) {
|
|
|
46
55
|
return firstSet([
|
|
47
56
|
env.CSPEACH_DEFAULT_MODEL,
|
|
48
57
|
cfg.default_model_set ? cfg.default_model : undefined,
|
|
58
|
+
getCustomerModelConfig()?.model ?? undefined,
|
|
49
59
|
server?.roles.session_default,
|
|
50
60
|
], DEFAULT_SESSION_MODEL);
|
|
51
61
|
case 'audit':
|
|
@@ -59,3 +69,84 @@ export function resolveModelRole(role, cfg, env = process.env) {
|
|
|
59
69
|
}
|
|
60
70
|
}
|
|
61
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Integration finding N1 (2026-09-28) — the model a call is SENT with. For a
|
|
74
|
+
* managed session-role call of a customer whose model is ENFORCED
|
|
75
|
+
* (/v1/me/model-config `enforced: true`), the proxy serves the customer's
|
|
76
|
+
* model (or, when the row names none, the global session default) whatever
|
|
77
|
+
* the body asks. The CLI sends that model itself, so everything decided from
|
|
78
|
+
* the call's model — deferred vs static tools, max_tokens, block_binding,
|
|
79
|
+
* effort acceptance — is right on call 0, not only after the served id is
|
|
80
|
+
* adopted. Mirrors the proxy's shouldEnforce: session role only (audit,
|
|
81
|
+
* compact and reader are never enforced), managed only (BYOK has no proxy to
|
|
82
|
+
* enforce). Anything else ⇒ `requested` unchanged.
|
|
83
|
+
*/
|
|
84
|
+
export function callModelFor(requested, i) {
|
|
85
|
+
if (i.providerMode !== 'managed')
|
|
86
|
+
return requested;
|
|
87
|
+
if ((i.role ?? 'session_default') !== 'session_default')
|
|
88
|
+
return requested;
|
|
89
|
+
const customer = getCustomerModelConfig();
|
|
90
|
+
if (customer?.enforced !== true)
|
|
91
|
+
return requested;
|
|
92
|
+
const enforced = customer.model?.trim() || getServerModelConfig()?.roles.session_default?.trim();
|
|
93
|
+
return enforced || requested;
|
|
94
|
+
}
|
|
95
|
+
// ── Effort (Task 4, 2026-09-27) ─────────────────────────────────────────────
|
|
96
|
+
//
|
|
97
|
+
// Ruling F2: `output_config.effort` is sent only when effort was set by an
|
|
98
|
+
// explicit act. Precedence:
|
|
99
|
+
//
|
|
100
|
+
// env CSPEACH_EFFORT > `config set effort` (effort_set) > served
|
|
101
|
+
// effort.session_default > undefined (⇒ output_config omitted)
|
|
102
|
+
//
|
|
103
|
+
// A bare stored `effort` (the pre-sparse full save wrote effort = "xhigh" into
|
|
104
|
+
// every config.toml) is ignored: loadConfig sets effort_set only when the file
|
|
105
|
+
// also carries the `effort_explicit = true` marker `config set effort` writes.
|
|
106
|
+
// Invalid values at any layer fall through. Task 12 adds the customer layer
|
|
107
|
+
// (/v1/me/model-config) between local and served.
|
|
108
|
+
export function resolveEffort(cfg, env = process.env) {
|
|
109
|
+
const fromEnv = env.CSPEACH_EFFORT?.trim();
|
|
110
|
+
if (isEffort(fromEnv))
|
|
111
|
+
return fromEnv;
|
|
112
|
+
if (cfg.effort_set === true && isEffort(cfg.effort))
|
|
113
|
+
return cfg.effort;
|
|
114
|
+
const customer = getCustomerModelConfig()?.effort;
|
|
115
|
+
if (isEffort(customer))
|
|
116
|
+
return customer;
|
|
117
|
+
const served = getServerModelConfig()?.effort?.session_default;
|
|
118
|
+
if (isEffort(served))
|
|
119
|
+
return served;
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Probe P1 (2026-09-27): claude-haiku-4-5 rejects `output_config.effort` with
|
|
124
|
+
* a 400 ("This model does not support the effort parameter."). Every
|
|
125
|
+
* claude-haiku-4-5* id (incl. dated ids such as claude-haiku-4-5-20251001) is
|
|
126
|
+
* treated as not accepting effort, so the caller drops it — this covers the
|
|
127
|
+
* compact role, which runs on Haiku by default.
|
|
128
|
+
*/
|
|
129
|
+
export function modelAcceptsEffort(model) {
|
|
130
|
+
return !model.trim().toLowerCase().startsWith('claude-haiku-4-5');
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Task 19 (model control, 2026-09-27, ruling F12) — the effective auto-compact
|
|
134
|
+
* threshold, in context tokens (the last call's input + cache read + cache
|
|
135
|
+
* write):
|
|
136
|
+
*
|
|
137
|
+
* explicit file `[compact] auto_threshold_tokens`
|
|
138
|
+
* > served `auto_compact_tokens` (admin; managed mode)
|
|
139
|
+
* > DEFAULT_CONFIG.compact.auto_threshold_tokens (150 000)
|
|
140
|
+
*
|
|
141
|
+
* "Explicit" is `cfg.compact_threshold_set` (loadConfig): a stored 1 000 000
|
|
142
|
+
* is the old full-save default and counts as unset.
|
|
143
|
+
*/
|
|
144
|
+
export function resolveAutoCompactThreshold(cfg) {
|
|
145
|
+
if (cfg.compact_threshold_set === true && Number.isFinite(cfg.compact?.auto_threshold_tokens)) {
|
|
146
|
+
return cfg.compact.auto_threshold_tokens;
|
|
147
|
+
}
|
|
148
|
+
const served = getServerModelConfig()?.auto_compact_tokens;
|
|
149
|
+
if (typeof served === 'number' && Number.isFinite(served))
|
|
150
|
+
return served;
|
|
151
|
+
return DEFAULT_CONFIG.compact.auto_threshold_tokens;
|
|
152
|
+
}
|