@cspeach/cli 1.1.33 → 1.1.34
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 +2 -0
- package/dist/agent/azure-profile.js +7 -0
- package/dist/agent/connection-profile.js +49 -0
- package/dist/agent/loop.js +61 -8
- package/dist/agent/provider-shape.js +36 -0
- package/dist/agent/providers/ai-hub-provider.js +6 -5
- package/dist/agent/providers/azure-provider.js +125 -0
- package/dist/agent/providers/byok-provider.js +8 -7
- package/dist/agent/providers/factory.js +8 -3
- package/dist/agent/providers/local-provider.js +18 -22
- package/dist/agent/risk-gate.js +202 -0
- package/dist/agent/summarise-via-provider.js +55 -8
- package/dist/agent/tool-dispatch.js +19 -0
- package/dist/agent/turn-error-ux.js +22 -0
- package/dist/auth/api-key.js +10 -5
- package/dist/auth/auth-file.js +35 -6
- package/dist/auth/azure/azure-key.js +21 -0
- package/dist/auth/azure/azure-log.js +45 -0
- package/dist/auth/azure/cache-store.js +171 -0
- package/dist/auth/azure/clear.js +28 -0
- package/dist/auth/azure/loopback.js +192 -0
- package/dist/auth/azure/msal-client.js +41 -0
- package/dist/auth/azure/sign-in.js +219 -0
- package/dist/auth/azure/startup-sign-in.js +64 -0
- package/dist/auth/azure/token-provider.js +116 -0
- package/dist/auth/key-resolver.js +258 -0
- package/dist/auth/me.js +11 -5
- package/dist/cli.js +21 -2
- package/dist/commands/compact.js +12 -1
- package/dist/commands/config-set.js +74 -3
- package/dist/commands/config-show.js +25 -0
- package/dist/commands/cost.js +19 -6
- package/dist/commands/help.js +2 -2
- package/dist/commands/login-azure.js +33 -0
- package/dist/commands/login.js +56 -8
- package/dist/commands/logout.js +34 -10
- package/dist/commands/plan-boundary.js +4 -4
- package/dist/commands/plan-model-tier.js +8 -3
- package/dist/commands/usage.js +6 -0
- package/dist/commands/whoami.js +113 -9
- package/dist/config/loader.js +5 -0
- package/dist/connection/azure-auth.js +135 -0
- package/dist/connection/azure-errors.js +103 -0
- package/dist/connection/azure-facts.js +21 -0
- package/dist/connection/azure-gate.js +40 -0
- package/dist/connection/azure-last-call.js +5 -0
- package/dist/connection/azure-turn-error.js +25 -0
- package/dist/connection/cache.js +70 -0
- package/dist/connection/fetch.js +33 -0
- package/dist/connection/guard.js +94 -0
- package/dist/connection/limit-gate.js +26 -0
- package/dist/connection/local-usage.js +71 -0
- package/dist/connection/login-step.js +62 -0
- package/dist/connection/startup.js +67 -0
- package/dist/connection/state.js +68 -0
- package/dist/connection/types.js +1 -0
- package/dist/cost/cost-log.js +3 -1
- package/dist/cost/credits-wire.js +1 -1
- package/dist/cost/credits.js +4 -0
- package/dist/cost/display-mode.js +7 -0
- package/dist/cost/router-spend.js +3 -0
- package/dist/cost/session-cost.js +4 -0
- package/dist/doctor/checks/azure.js +100 -0
- package/dist/doctor/checks/credits.js +4 -3
- package/dist/doctor/checks/usage-reporting.js +22 -0
- package/dist/doctor/run.js +7 -0
- package/dist/licence/licence-check.js +205 -0
- package/dist/licence/paths.js +6 -0
- package/dist/licence/turn-gate.js +66 -0
- package/dist/models/model-identity.js +65 -0
- package/dist/models/resolve.js +8 -0
- package/dist/models/server-config.js +3 -2
- package/dist/one-shot.js +86 -2
- package/dist/renderer/footer-line.js +7 -1
- package/dist/renderer/status-footer.js +13 -6
- package/dist/repl/ask-or-no.js +18 -0
- package/dist/repl/builtin-commands.js +10 -2
- package/dist/repl/post-turn-status.js +3 -2
- package/dist/repl/safety-confirm.js +47 -2
- package/dist/repl/session-spend-line.js +4 -1
- package/dist/repl/startup-account.js +51 -0
- package/dist/repl.js +211 -48
- package/dist/router/byok-classifier.js +41 -14
- package/dist/router/classifier.js +5 -16
- package/dist/router/router-target.js +41 -0
- package/dist/sap/onboarding.js +70 -64
- package/dist/session/audit-export.js +11 -4
- package/dist/skills/manifest-client.js +37 -4
- package/dist/skills/skills-cache.js +115 -0
- package/dist/skills/source-authed.js +276 -0
- package/dist/tools/filesystem/glob.js +12 -3
- package/dist/tools/filesystem/grep.js +14 -3
- package/dist/tools/tool-loading.js +9 -10
- package/dist/usage/usage-queue.js +323 -0
- package/dist/usage/usage-reporter.js +270 -0
- package/package.json +3 -2
- package/dist/skills/bundled-skills.js +0 -286
- package/dist/skills/source-bundled.js +0 -20
- package/dist/skills/source-manifest.js +0 -36
|
@@ -8,22 +8,30 @@
|
|
|
8
8
|
* read of the process-wide cost display mode (cost/credits.ts); no
|
|
9
9
|
* inquirer, so the router can import it.
|
|
10
10
|
*/
|
|
11
|
-
import { creditsActive } from '../cost/credits.js';
|
|
11
|
+
import { creditsActive, tokensOnlyActive } from '../cost/credits.js';
|
|
12
12
|
// Pure generated data (type-only import inside), so this file stays light.
|
|
13
13
|
import { BUNDLED_RUNS } from '../runs/bundled-runs.js';
|
|
14
14
|
export const COST_DESCRIPTION_USD = 'Show this session\'s spend so far ($ and tokens)';
|
|
15
15
|
export const COST_DESCRIPTION_CREDITS = 'Show the credits this session has used so far';
|
|
16
|
+
/** Azure tenant mode (Task 7, ruling 5): tokens only — never "$" or credits. */
|
|
17
|
+
export const COST_DESCRIPTION_TOKENS = "Show this session's tokens so far";
|
|
16
18
|
/** Piece 2 §10: managed customers see credits, never "$" (cost/display-mode.ts). */
|
|
17
19
|
/** /usage (2026-10-09): this month in credits — see commands/usage.ts. */
|
|
18
20
|
export const USAGE_DESCRIPTION = 'Show this month’s credits, your balance and the top skills';
|
|
21
|
+
export const USAGE_DESCRIPTION_TOKENS = "Show where your usage is: your company's Azure invoice";
|
|
19
22
|
export function costCommandDescription() {
|
|
23
|
+
if (tokensOnlyActive())
|
|
24
|
+
return COST_DESCRIPTION_TOKENS;
|
|
20
25
|
return creditsActive() ? COST_DESCRIPTION_CREDITS : COST_DESCRIPTION_USD;
|
|
21
26
|
}
|
|
27
|
+
export function usageCommandDescription() {
|
|
28
|
+
return tokensOnlyActive() ? USAGE_DESCRIPTION_TOKENS : USAGE_DESCRIPTION;
|
|
29
|
+
}
|
|
22
30
|
export const BUILTIN_ENTRIES = [
|
|
23
31
|
// Piece 2: /cancel is not listed — it exists only in the classic loop
|
|
24
32
|
// (repl.tsx), and Esc / Ctrl+C already cancel under Ink.
|
|
25
33
|
{ name: '/cost', get description() { return costCommandDescription(); } },
|
|
26
|
-
{ name: '/usage', description
|
|
34
|
+
{ name: '/usage', get description() { return usageCommandDescription(); } },
|
|
27
35
|
{ name: '/compact', description: 'Shrink a long session and keep the facts (later turns cost far less)' },
|
|
28
36
|
{ name: '/export', description: 'Export the signable session audit record — /export audit [filename]' },
|
|
29
37
|
{ name: '/context', description: 'Show what fills the model context window — a composition grid of bars' },
|
|
@@ -21,7 +21,7 @@ import { computeCost } from '../cost/pricing.js';
|
|
|
21
21
|
import { computeSessionCost } from '../cost/session-cost.js';
|
|
22
22
|
import { globalStore } from '../ui/sap-state-store.js';
|
|
23
23
|
import { turnStatusEmitter } from '../ui/turn-status-emitter.js';
|
|
24
|
-
import { globalCredits } from '../cost/credits.js';
|
|
24
|
+
import { globalCredits, tokensOnlyActive } from '../cost/credits.js';
|
|
25
25
|
import { printStatusFooter, formatStatusFooter } from '../renderer/status-footer.js';
|
|
26
26
|
import { shouldUseInk } from '../renderer/tty.js';
|
|
27
27
|
import { isUserPrompt } from '../session/user-prompt.js';
|
|
@@ -49,7 +49,8 @@ export function computeTurnFacts(session, snapshot, turnModel) {
|
|
|
49
49
|
const userTurns = session.messages.filter(isUserPrompt).length;
|
|
50
50
|
const lifetimeAfter = session.usage.input_tokens + session.usage.output_tokens;
|
|
51
51
|
const turnTokens = Math.max(0, lifetimeAfter - (snapshot.input + snapshot.output));
|
|
52
|
-
|
|
52
|
+
// Azure: no price for the customer's deployment — never consult one.
|
|
53
|
+
const turnCost = tokensOnlyActive() ? 0 : computeCost(turnModel ?? session.model, {
|
|
53
54
|
input: session.usage.input_tokens - snapshot.input,
|
|
54
55
|
output: session.usage.output_tokens - snapshot.output,
|
|
55
56
|
cacheRead: session.usage.cache_read_input_tokens - snapshot.cacheRead,
|
|
@@ -29,6 +29,8 @@ const RULE_LABEL = {
|
|
|
29
29
|
RULE_9_TRANSPORT_ISOLATION: 'RULE 9 — transport isolation',
|
|
30
30
|
RULE_10_VERIFY_AFTER_WRITE: 'RULE 10 — verify after write',
|
|
31
31
|
CREDENTIAL_HANDOVER: 'SAP login handed to a local process',
|
|
32
|
+
RISKY_COMMAND: 'risky command (known pattern)',
|
|
33
|
+
CREDENTIAL_FILE_READ: 'credential file read',
|
|
32
34
|
};
|
|
33
35
|
/** Piece 2 §5 — plain-words heading: what could go wrong. No rule codes, no tool names. */
|
|
34
36
|
export const SAFETY_HEADING = {
|
|
@@ -39,6 +41,8 @@ export const SAFETY_HEADING = {
|
|
|
39
41
|
RULE_9_TRANSPORT_ISOLATION: 'This change may land in a transport shared with other work',
|
|
40
42
|
RULE_10_VERIFY_AFTER_WRITE: 'This change is not proven until it is checked',
|
|
41
43
|
CREDENTIAL_HANDOVER: 'Your SAP login will be handed to a local program',
|
|
44
|
+
RISKY_COMMAND: 'This command matches a known risky pattern',
|
|
45
|
+
CREDENTIAL_FILE_READ: 'This reads a file that may hold passwords or keys',
|
|
42
46
|
};
|
|
43
47
|
/** Bottom-edge note: which rule is asking. */
|
|
44
48
|
export const SAFETY_FOOTNOTE = {
|
|
@@ -49,6 +53,8 @@ export const SAFETY_FOOTNOTE = {
|
|
|
49
53
|
RULE_9_TRANSPORT_ISOLATION: 'Forge Rule 9',
|
|
50
54
|
RULE_10_VERIFY_AFTER_WRITE: 'Forge Rule 10',
|
|
51
55
|
CREDENTIAL_HANDOVER: 'Credential handover',
|
|
56
|
+
RISKY_COMMAND: 'Risky command check',
|
|
57
|
+
CREDENTIAL_FILE_READ: 'Credential file check',
|
|
52
58
|
};
|
|
53
59
|
/** Rule 7 covers both overwrite and delete; the delete says so. */
|
|
54
60
|
export function safetyHeading(a) {
|
|
@@ -103,8 +109,40 @@ function whatLabel(a) {
|
|
|
103
109
|
return fallback;
|
|
104
110
|
return toolActivityLabel(tool, args);
|
|
105
111
|
}
|
|
112
|
+
/** The risk gate's two rules — their subject is a model-written command or path. */
|
|
113
|
+
function isRiskRule(a) {
|
|
114
|
+
return a.rule === 'RISKY_COMMAND' || a.rule === 'CREDENTIAL_FILE_READ';
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Fix round 1 (F9) — the most of a risky command or path the card shows. A
|
|
118
|
+
* longer one shows its head and its tail with the hidden length between them
|
|
119
|
+
* (fix round 2, L2: a command padded past the cap cannot hide its target on
|
|
120
|
+
* the classic card, which has no scrollback copy). Ink also writes the whole
|
|
121
|
+
* text to scrollback first, like the Rule 8 summary. Never redacted.
|
|
122
|
+
*/
|
|
123
|
+
export const RISK_SUBJECT_MAX = 300;
|
|
124
|
+
const RISK_SUBJECT_HEAD = 200;
|
|
125
|
+
const RISK_SUBJECT_TAIL = 80;
|
|
126
|
+
/** The subject as shown on a card: display-cleaned (F8), cut to head + tail past RISK_SUBJECT_MAX (F9, L2). */
|
|
127
|
+
function riskSubject(name) {
|
|
128
|
+
const c = clean(name);
|
|
129
|
+
if (c.length <= RISK_SUBJECT_MAX)
|
|
130
|
+
return c;
|
|
131
|
+
const hidden = c.length - RISK_SUBJECT_HEAD - RISK_SUBJECT_TAIL;
|
|
132
|
+
return `${c.slice(0, RISK_SUBJECT_HEAD)} ${getGlyphs().more} (+${hidden} chars) ${c.slice(-RISK_SUBJECT_TAIL)}`;
|
|
133
|
+
}
|
|
106
134
|
/** What / Where / Undo rows (plain text; the card paints them dim). */
|
|
107
135
|
export function safetyFacts(a) {
|
|
136
|
+
// Security batch item 4 — the risk gate's cards: the exact command or path,
|
|
137
|
+
// why it was flagged, and that it runs on this computer.
|
|
138
|
+
if (isRiskRule(a)) {
|
|
139
|
+
return [
|
|
140
|
+
`What ${a.rule === 'RISKY_COMMAND' ? 'Run' : 'Read'} ${riskSubject(a.object.name)}`,
|
|
141
|
+
...boundedWhatLines(whatLines(a)).map((t) => ` ${t}`),
|
|
142
|
+
'Where this computer',
|
|
143
|
+
`Undo ${a.rule === 'RISKY_COMMAND' ? 'Cannot be undone' : 'Once read, its text goes to the model'}`,
|
|
144
|
+
];
|
|
145
|
+
}
|
|
108
146
|
const what = a.rule === 'CREDENTIAL_HANDOVER'
|
|
109
147
|
? `Hand the SAP login for ${clean(a.object.name)} to a local program`
|
|
110
148
|
: whatLabel(a);
|
|
@@ -162,6 +200,10 @@ async function confirmOnCards(action) {
|
|
|
162
200
|
// The card keeps the header and the last lines; the whole text lands above it.
|
|
163
201
|
emitTrace(what.join('\n'));
|
|
164
202
|
}
|
|
203
|
+
if (isRiskRule(action) && clean(action.object.name).length > RISK_SUBJECT_MAX) {
|
|
204
|
+
// F9 — the card shows the cut; the whole (cleaned) command lands above it.
|
|
205
|
+
emitTrace(clean(action.object.name));
|
|
206
|
+
}
|
|
165
207
|
const first = await askConfirm({
|
|
166
208
|
id: 'safety-confirm',
|
|
167
209
|
tag: release ? 'CANNOT BE UNDONE' : 'SAFETY CHECK',
|
|
@@ -260,14 +302,17 @@ function renderCard(action) {
|
|
|
260
302
|
lines.push('');
|
|
261
303
|
lines.push(`${chalk.yellow(getGlyphs().attention)} ${chalk.yellow.bold(RULE_LABEL[action.rule])}`);
|
|
262
304
|
lines.push(` ${chalk.dim('op: ')} ${action.op}`);
|
|
263
|
-
|
|
305
|
+
// F8 — a risk card's subject is model-written: cleaned (no terminal escapes)
|
|
306
|
+
// and cut (F9). Other rules print exactly as before.
|
|
307
|
+
const risk = isRiskRule(action);
|
|
308
|
+
lines.push(` ${chalk.dim('object: ')} ${action.object.type} ${risk ? riskSubject(action.object.name) : action.object.name}`);
|
|
264
309
|
lines.push(` ${chalk.dim('transport:')} ${transport}`);
|
|
265
310
|
if (action.rule === 'RULE_8_BATCH_PLAN' && typeof action.batch_count === 'number') {
|
|
266
311
|
lines.push(` ${chalk.dim('batch: ')} ${action.batch_count} objects this turn`);
|
|
267
312
|
}
|
|
268
313
|
// what_will_happen may include embedded newlines (e.g. Rule 8 plan summary).
|
|
269
314
|
// Indent the continuation lines so they align under the header field.
|
|
270
|
-
const whatLines = action.what_will_happen.split('\n');
|
|
315
|
+
const whatLines = risk ? action.what_will_happen.split('\n').map(clean) : action.what_will_happen.split('\n');
|
|
271
316
|
if (whatLines.length === 1) {
|
|
272
317
|
lines.push(` ${chalk.dim('what: ')} ${whatLines[0]}`);
|
|
273
318
|
}
|
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
* credits existed: ` spend: $1.37`.
|
|
7
7
|
*/
|
|
8
8
|
import { formatCost } from '../cost/pricing.js';
|
|
9
|
-
import { creditsActive, formatCredits, CREDITS_PENDING } from '../cost/credits.js';
|
|
9
|
+
import { creditsActive, tokensOnlyActive, formatCredits, CREDITS_PENDING } from '../cost/credits.js';
|
|
10
|
+
import { AZURE_INVOICE_LINE } from '../cost/display-mode.js';
|
|
10
11
|
/** The spend VALUE in the current display mode (piece 2: goodbye block). */
|
|
11
12
|
export function formatSessionSpendValue(sessionCostUsd, credits) {
|
|
13
|
+
if (tokensOnlyActive())
|
|
14
|
+
return AZURE_INVOICE_LINE;
|
|
12
15
|
if (!creditsActive())
|
|
13
16
|
return formatCost(sessionCostUsd);
|
|
14
17
|
if (!credits)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { formatFailure, formatInfo } from '../renderer/notice-shape.js';
|
|
2
|
+
export async function runStartupAccount(deps, io) {
|
|
3
|
+
let licenceCheckedAtMs;
|
|
4
|
+
try {
|
|
5
|
+
const cfgEarly = await deps.loadConfig();
|
|
6
|
+
if (cfgEarly.llm.mode === 'managed') {
|
|
7
|
+
const authFile = await deps.readAuthFile();
|
|
8
|
+
const startupKey = authFile ? (await deps.resolveApiKey()).key : null;
|
|
9
|
+
if (authFile && startupKey) {
|
|
10
|
+
const meResult = await deps.fetchMe({ proxyUrl: cfgEarly.proxy_url, apiKey: startupKey });
|
|
11
|
+
if (!meResult.ok) {
|
|
12
|
+
if (meResult.status === 401) {
|
|
13
|
+
io.closeBrand();
|
|
14
|
+
io.print(formatFailure({ what: 'Your CSPeach sign-in is no longer valid', next: 'cspeach login' }));
|
|
15
|
+
return { exit: 1 };
|
|
16
|
+
}
|
|
17
|
+
io.notice(formatInfo(`Could not refresh your account (${meResult.error})`));
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
io.showProfile(meResult.profile);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const { key: startupKey } = await deps.resolveApiKey();
|
|
26
|
+
if (startupKey) {
|
|
27
|
+
const licence = await deps.runLicenceCheck({ proxyUrl: cfgEarly.proxy_url, apiKey: startupKey }, { headers: await deps.licenceCheckHeaders(cfgEarly.llm.mode).catch(() => ({})) });
|
|
28
|
+
licenceCheckedAtMs = (deps.now ?? Date.now)();
|
|
29
|
+
if (!licence.allow) {
|
|
30
|
+
io.closeBrand();
|
|
31
|
+
io.print(licence.message);
|
|
32
|
+
return { exit: 1 };
|
|
33
|
+
}
|
|
34
|
+
if (licence.verdict === 'unknown') {
|
|
35
|
+
io.notice(formatInfo(licence.line));
|
|
36
|
+
}
|
|
37
|
+
else if (licence.profile) {
|
|
38
|
+
io.showProfile(licence.profile);
|
|
39
|
+
}
|
|
40
|
+
if (licence.verdict === 'ok') {
|
|
41
|
+
try {
|
|
42
|
+
deps.startSkillsWarmup?.(cfgEarly, licence.verdict);
|
|
43
|
+
}
|
|
44
|
+
catch { /* never blocks start-up */ }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch { /* banner is best-effort — never block REPL startup */ }
|
|
50
|
+
return licenceCheckedAtMs === undefined ? { exit: null } : { exit: null, licenceCheckedAtMs };
|
|
51
|
+
}
|
package/dist/repl.js
CHANGED
|
@@ -27,7 +27,12 @@ import { applyResumedSkill, isShortFollowUp } from './router/resume-routing.js';
|
|
|
27
27
|
import { readableInterruptReason, isUserStopReason } from './session/interrupt-reason.js';
|
|
28
28
|
import { getStore } from './auth/api-key.js';
|
|
29
29
|
import { readAuthFile } from './auth/auth-file.js';
|
|
30
|
+
import { resolveApiKey } from './auth/key-resolver.js';
|
|
30
31
|
import { fetchMe } from './auth/me.js';
|
|
32
|
+
import { runLicenceCheck, licenceCheckHeaders } from './licence/licence-check.js';
|
|
33
|
+
import { LicenceTurnGate } from './licence/turn-gate.js';
|
|
34
|
+
import { runStartupAccount } from './repl/startup-account.js';
|
|
35
|
+
import { startSkillsCacheWarmup } from './skills/source-authed.js';
|
|
31
36
|
import { formatLoginBanner, signInFragment } from './ui/login-banner.js';
|
|
32
37
|
import { printStartupBanner, printKeytarFallbackBanner, keychainFallbackNotice } from './renderer/banners.js';
|
|
33
38
|
import { paint } from './renderer/theme.js';
|
|
@@ -39,15 +44,18 @@ import { AdtClient, snapshots } from '@cspeach/sap-client';
|
|
|
39
44
|
import { makeSessionId, saveSession, sessionSaved } from './session/store.js';
|
|
40
45
|
import { newSession } from './session/schema.js';
|
|
41
46
|
import { resolveModelRole } from './models/resolve.js';
|
|
47
|
+
import { familyOf, planTierModelFor, getActiveDeployments } from './models/model-identity.js';
|
|
42
48
|
import { loadServerModelConfig, loadCustomerModelConfig, fetchToolLoadingMode } from './models/server-config.js';
|
|
43
49
|
import { prefetchRouterPrompt } from './router/router-prompt.js';
|
|
50
|
+
import { profileForMode } from './agent/connection-profile.js';
|
|
44
51
|
import { repinResumedSessionModel } from './session/repin-model.js';
|
|
45
52
|
import { computeCost } from './cost/pricing.js';
|
|
46
53
|
import { formatSessionSpendValue } from './repl/session-spend-line.js';
|
|
47
54
|
import { formatGoodbye } from './renderer/goodbye.js';
|
|
48
|
-
import { globalCredits } from './cost/credits.js';
|
|
55
|
+
import { globalCredits, tokensOnlyActive } from './cost/credits.js';
|
|
49
56
|
import { initCredits } from './cost/credits-wire.js';
|
|
50
57
|
import { createProviderForMode } from './agent/providers/factory.js';
|
|
58
|
+
import { startUsageFlushAtStartup } from './usage/usage-reporter.js';
|
|
51
59
|
import { assertModeLicensed, CspeachLicenseError } from './agent/providers/license-gate.js';
|
|
52
60
|
import { runTurn } from './agent/loop.js';
|
|
53
61
|
import { IdleKeepalive } from './agent/cache-keepalive.js';
|
|
@@ -294,7 +302,8 @@ function existingStreamDir(sessionId) {
|
|
|
294
302
|
}
|
|
295
303
|
function printGoodbye(session) {
|
|
296
304
|
const userTurns = session.messages.filter((m) => m.role === 'user' && typeof m.content === 'string').length;
|
|
297
|
-
|
|
305
|
+
// Azure: no price for the customer's deployment — never consult one.
|
|
306
|
+
const sessionCost = tokensOnlyActive() ? 0 : computeCost(session.model, {
|
|
298
307
|
input: session.usage.input_tokens ?? 0,
|
|
299
308
|
output: session.usage.output_tokens ?? 0,
|
|
300
309
|
cacheRead: session.usage.cache_read_input_tokens ?? 0,
|
|
@@ -457,46 +466,51 @@ export async function runRepl(opts) {
|
|
|
457
466
|
}
|
|
458
467
|
}
|
|
459
468
|
catch { /* startup banner is best-effort */ }
|
|
460
|
-
// Customer-onboarding (Task 9)
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
469
|
+
// Customer-onboarding (Task 9) + security batch item 1.
|
|
470
|
+
// Managed: today's call exactly — only when auth.json exists, the key is
|
|
471
|
+
// resolved only then, the same request, and a 401 exits. Falls through to the
|
|
472
|
+
// legacy keytar flow below when the file is absent (back-compat for
|
|
473
|
+
// pre-`cspeach login` users).
|
|
474
|
+
// byok / ai-hub / local: the shared licence check — server verdict, 72-hour
|
|
475
|
+
// offline grace, licence.json — whether or not auth.json exists. It IS this
|
|
476
|
+
// start-up /v1/me call (one request, the same 5 s budget), never a second one.
|
|
477
|
+
// The block lives in repl/startup-account.ts (behaviour-identical lift, driven
|
|
478
|
+
// by repl/__tests__/startup-account.test.ts).
|
|
464
479
|
let signIn;
|
|
465
480
|
let signedInAs;
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if (
|
|
472
|
-
|
|
473
|
-
earlyBrand?.close();
|
|
474
|
-
console.log(formatFailure({ what: 'Your CSPeach sign-in is no longer valid', next: 'cspeach login' }));
|
|
475
|
-
return 1;
|
|
476
|
-
}
|
|
477
|
-
const note = formatInfo(`Could not refresh your account (${meResult.error})`);
|
|
478
|
-
if (getLook() === 'panels')
|
|
479
|
-
startupNotices.push(note);
|
|
480
|
-
else
|
|
481
|
-
console.log(note.trimEnd());
|
|
482
|
-
}
|
|
483
|
-
else {
|
|
484
|
-
signedInAs = meResult.profile.email;
|
|
485
|
-
const banner = formatLoginBanner(meResult.profile);
|
|
486
|
-
if (getLook() === 'panels') {
|
|
487
|
-
signIn = signInFragment(meResult.profile);
|
|
488
|
-
if (banner.warningLine)
|
|
489
|
-
startupNotices.push(formatAttention({ text: banner.warningLine }));
|
|
490
|
-
}
|
|
491
|
-
else {
|
|
492
|
-
console.log(banner.line);
|
|
493
|
-
if (banner.warningLine)
|
|
494
|
-
console.log(formatAttention({ text: banner.warningLine }).trimEnd());
|
|
495
|
-
}
|
|
496
|
-
}
|
|
481
|
+
const showProfile = (profile) => {
|
|
482
|
+
signedInAs = profile.email;
|
|
483
|
+
const banner = formatLoginBanner(profile);
|
|
484
|
+
if (getLook() === 'panels') {
|
|
485
|
+
signIn = signInFragment(profile);
|
|
486
|
+
if (banner.warningLine)
|
|
487
|
+
startupNotices.push(formatAttention({ text: banner.warningLine }));
|
|
497
488
|
}
|
|
498
|
-
|
|
499
|
-
|
|
489
|
+
else {
|
|
490
|
+
console.log(banner.line);
|
|
491
|
+
if (banner.warningLine)
|
|
492
|
+
console.log(formatAttention({ text: banner.warningLine }).trimEnd());
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
const startupAccount = await runStartupAccount({
|
|
496
|
+
loadConfig, readAuthFile, resolveApiKey: () => resolveApiKey(), fetchMe, runLicenceCheck, licenceCheckHeaders,
|
|
497
|
+
// Final review finding 1 — fill the skills cache for offline use. Fire-and-forget.
|
|
498
|
+
startSkillsWarmup: (c, verdict) => { void startSkillsCacheWarmup(c, verdict); },
|
|
499
|
+
}, {
|
|
500
|
+
print: (line) => console.log(line),
|
|
501
|
+
notice: (note) => {
|
|
502
|
+
if (getLook() === 'panels')
|
|
503
|
+
startupNotices.push(note);
|
|
504
|
+
else
|
|
505
|
+
console.log(note.trimEnd());
|
|
506
|
+
},
|
|
507
|
+
showProfile,
|
|
508
|
+
closeBrand: () => earlyBrand?.close(),
|
|
509
|
+
});
|
|
510
|
+
if (startupAccount.exit === 1)
|
|
511
|
+
return 1;
|
|
512
|
+
// Read by the between-turns recheck (Task 6): the 4-hour clock starts here.
|
|
513
|
+
const licenceCheckedAtMs = startupAccount.licenceCheckedAtMs;
|
|
500
514
|
earlyBrand?.finish(signIn);
|
|
501
515
|
const store = await getStore();
|
|
502
516
|
const keychainMissing = !(await store.isKeychainAvailable());
|
|
@@ -601,8 +615,55 @@ export async function runRepl(opts) {
|
|
|
601
615
|
fetchToolLoadingMode(cfg, async () => apiKey ?? '', { timeoutMs: 1500 }),
|
|
602
616
|
// 2026-10-02 — BYOK / ai-hub route on the laptop: prefetch the signed
|
|
603
617
|
// router prompt (public manifest, no prompt data) under the same cap.
|
|
604
|
-
cfg.llm?.mode
|
|
618
|
+
profileForMode(cfg.llm?.mode).router === 'direct' ? prefetchRouterPrompt({ timeoutMs: 1500 }) : Promise.resolve(),
|
|
605
619
|
]);
|
|
620
|
+
// Azure tenant mode (Task 4) — the company's connection settings, before
|
|
621
|
+
// the session model is pinned (resolveModelRole reads the deployments).
|
|
622
|
+
// Cached connection.json, refreshed under the same 1.5 s cap; a changed
|
|
623
|
+
// endpoint is shown and asked about. A refusal is shown now and the gate
|
|
624
|
+
// refuses turns — never a fall-back to managed.
|
|
625
|
+
if (cfg.llm.mode === 'azure') {
|
|
626
|
+
const { refreshAzureConnection } = await import('./connection/startup.js');
|
|
627
|
+
const { confirm } = await import('./repl/themed-prompts.js');
|
|
628
|
+
const { askOrNo } = await import('./repl/ask-or-no.js');
|
|
629
|
+
const note = (line) => {
|
|
630
|
+
if (getLook() === 'panels')
|
|
631
|
+
startupNotices.push(line.endsWith('\n') ? line : `${line}\n`);
|
|
632
|
+
else
|
|
633
|
+
console.log(line.trimEnd());
|
|
634
|
+
};
|
|
635
|
+
// The "endpoint changed" rows must be on screen BEFORE the question, so
|
|
636
|
+
// they are held here and printed straight away when a question opens;
|
|
637
|
+
// anything left over joins the start-up notices.
|
|
638
|
+
const held = [];
|
|
639
|
+
const az = await refreshAzureConnection({
|
|
640
|
+
proxyUrl: cfg.proxy_url, apiKey: apiKey ?? null, interactive: true,
|
|
641
|
+
print: (line) => { held.push(line); },
|
|
642
|
+
confirm: async (message) => {
|
|
643
|
+
for (const line of held.splice(0))
|
|
644
|
+
console.log(line.trimEnd());
|
|
645
|
+
// Ctrl+C here is a no (pending kept), never an unhandled ExitPromptError.
|
|
646
|
+
return askOrNo((m) => confirm({ message: m, default: false }))(message);
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
for (const line of held)
|
|
650
|
+
note(line);
|
|
651
|
+
if (!az.verdict.allow)
|
|
652
|
+
note(az.verdict.message);
|
|
653
|
+
else if (az.cached) {
|
|
654
|
+
// Task 6b — the Microsoft sign-in, before the first turn: asks first,
|
|
655
|
+
// then the browser. Esc / Ctrl+C during it stops it at once (the
|
|
656
|
+
// listener closes). Its own lines (the address to copy when no browser
|
|
657
|
+
// opens) print straight away, never held for after the sign-in.
|
|
658
|
+
const { azureSignInAtStart } = await import('./auth/azure/startup-sign-in.js');
|
|
659
|
+
const { loginOpener } = await import('./util/open-in-browser.js');
|
|
660
|
+
const s = await azureSignInAtStart(az.cached.connection, {
|
|
661
|
+
ask: askOrNo((message) => confirm({ message, default: true })), print: (line) => console.log(line.trimEnd()), open: loginOpener(),
|
|
662
|
+
});
|
|
663
|
+
if (!s.ok)
|
|
664
|
+
note(s.message);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
606
667
|
// Credits (2026-09-16) — resolve ONCE, before any cost figure can render.
|
|
607
668
|
// A managed customer is billed in credits at a multiplier we do not
|
|
608
669
|
// disclose, so the dollar figures (our Anthropic COGS) must never reach
|
|
@@ -815,6 +876,41 @@ export async function runRepl(opts) {
|
|
|
815
876
|
throw new Error('No API key configured');
|
|
816
877
|
return key;
|
|
817
878
|
});
|
|
879
|
+
// Security batch item 1 — licence re-check between turns (byok / ai-hub /
|
|
880
|
+
// local, every 4 hours from the start-up check). Managed: never calls.
|
|
881
|
+
const licenceGate = new LicenceTurnGate({
|
|
882
|
+
mode: cfg.llm.mode,
|
|
883
|
+
proxyUrl: cfg.proxy_url,
|
|
884
|
+
getKey: async () => (await store.get()) ?? null,
|
|
885
|
+
// No start-up check ran (no key yet, e.g. pasted at the prompt below):
|
|
886
|
+
// non-managed checks on the very first turn. Managed never reads it.
|
|
887
|
+
lastCheckAtMs: licenceCheckedAtMs ?? (cfg.llm.mode === 'managed' ? Date.now() : -Infinity),
|
|
888
|
+
...(cfg.llm.mode === 'azure' ? {
|
|
889
|
+
extra: async () => (await import('./connection/azure-gate.js')).azureGateCheck(),
|
|
890
|
+
// Plan D13 — never a prompt between turns (no inquirer inside the Ink
|
|
891
|
+
// UI): a changed endpoint or tenant is held as pending and the `extra`
|
|
892
|
+
// gate refuses with "…waiting for you to confirm it · start cspeach to
|
|
893
|
+
// review it". Nothing is printed here; the gate's refusal is the notice.
|
|
894
|
+
// So on this 4-hourly path the cache-write-failure line and the ignored
|
|
895
|
+
// env-override lines are dropped (they show again at the next start).
|
|
896
|
+
onRecheck: async () => {
|
|
897
|
+
const { refreshAzureConnection } = await import('./connection/startup.js');
|
|
898
|
+
await refreshAzureConnection({
|
|
899
|
+
proxyUrl: cfg.proxy_url, apiKey: (await store.get()) ?? null, interactive: false,
|
|
900
|
+
print: () => { }, confirm: async () => false,
|
|
901
|
+
});
|
|
902
|
+
},
|
|
903
|
+
} : {}),
|
|
904
|
+
});
|
|
905
|
+
// Security batch item 3 — send the usage counts still waiting from earlier
|
|
906
|
+
// runs. Non-managed only (a managed seat makes no call); fire-and-forget:
|
|
907
|
+
// never awaited, never throws into start-up.
|
|
908
|
+
if (cfg.llm.mode !== 'managed') {
|
|
909
|
+
try {
|
|
910
|
+
startUsageFlushAtStartup(cfg);
|
|
911
|
+
}
|
|
912
|
+
catch { /* best effort */ }
|
|
913
|
+
}
|
|
818
914
|
// Task 15 — keep the prompt cache warm while the user is idle between
|
|
819
915
|
// turns: armed after a turn, disarmed on the next submit and on exit;
|
|
820
916
|
// `[keepalive] idle_pings` per idle period (managed / BYOK only).
|
|
@@ -1148,6 +1244,16 @@ export async function runRepl(opts) {
|
|
|
1148
1244
|
// older turns via Haiku so subsequent turns ingest ~5k tokens of
|
|
1149
1245
|
// context instead of 500k+. Saves \$3-8 per follow-up turn.
|
|
1150
1246
|
if (trimmed === '/compact') {
|
|
1247
|
+
// Security batch item 1 — /compact makes a model call: gated like a turn.
|
|
1248
|
+
{
|
|
1249
|
+
const g = await licenceGate.beforeTurn();
|
|
1250
|
+
if (!g.allow) {
|
|
1251
|
+
chunkEmitter.emit('chunk', '\n' + g.message + '\n');
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
if (g.line)
|
|
1255
|
+
chunkEmitter.emit('chunk', formatInfo(g.line));
|
|
1256
|
+
}
|
|
1151
1257
|
const { runCompact, formatCompactResult, buildSummarisationPrompt, serialiseForSummariser } = await import('./commands/compact.js');
|
|
1152
1258
|
const { summariseViaProvider } = await import('./agent/summarise-via-provider.js');
|
|
1153
1259
|
const { loadConfig } = await import('./config/loader.js');
|
|
@@ -1156,7 +1262,7 @@ export async function runRepl(opts) {
|
|
|
1156
1262
|
return await summariseViaProvider(provider, buildSummarisationPrompt(), serialiseForSummariser(toSummarise),
|
|
1157
1263
|
// model-governance step 2d — compact model resolves env > local >
|
|
1158
1264
|
// server > built-in (== COMPACTION_MODEL when nothing set).
|
|
1159
|
-
{ model: resolveModelRole('compact', cfg), maxTokens: 4000, headers: { 'X-CSForge-Skill': 'compact', 'X-CSPeach-Model-Role': 'compact' } });
|
|
1265
|
+
{ model: resolveModelRole('compact', cfg), maxTokens: 4000, headers: { 'X-CSForge-Skill': 'compact', 'X-CSPeach-Model-Role': 'compact' }, sessionId: session.id });
|
|
1160
1266
|
};
|
|
1161
1267
|
try {
|
|
1162
1268
|
chunkEmitter.emit('chunk', '\n' + formatInfo('Compacting older turns into a summary…'));
|
|
@@ -1219,6 +1325,15 @@ export async function runRepl(opts) {
|
|
|
1219
1325
|
chunkEmitter.emit('chunk', '\n' + formatInfo(`Rerouting "${preview}" to /${rr.skill}`));
|
|
1220
1326
|
if (rr.warning)
|
|
1221
1327
|
chunkEmitter.emit('chunk', chalk.dim(` (${rr.warning})\n`));
|
|
1328
|
+
{
|
|
1329
|
+
const g = await licenceGate.beforeTurn();
|
|
1330
|
+
if (!g.allow) {
|
|
1331
|
+
chunkEmitter.emit('chunk', '\n' + g.message + '\n');
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
if (g.line)
|
|
1335
|
+
chunkEmitter.emit('chunk', formatInfo(g.line));
|
|
1336
|
+
}
|
|
1222
1337
|
// Re-review 1 — its own AbortController, wired like the main path, so
|
|
1223
1338
|
// Ctrl+C and `/exit` stop a rerouted turn the same way.
|
|
1224
1339
|
const rerouteAbort = new AbortController();
|
|
@@ -1576,6 +1691,20 @@ export async function runRepl(opts) {
|
|
|
1576
1691
|
session.last_object = extractedObject ?? previousObject ?? null;
|
|
1577
1692
|
return;
|
|
1578
1693
|
}
|
|
1694
|
+
// Security batch item 1 — never mid-turn: checked here, before the turn
|
|
1695
|
+
// and before any plan-resume work (run lease, object-list grant,
|
|
1696
|
+
// re-audit, phase context, plan_in_flight). A plan step refused here
|
|
1697
|
+
// never starts; the plan file keeps its state, so `/abap-plan --resume`
|
|
1698
|
+
// continues later.
|
|
1699
|
+
{
|
|
1700
|
+
const g = await licenceGate.beforeTurn();
|
|
1701
|
+
if (!g.allow) {
|
|
1702
|
+
chunkEmitter.emit('chunk', '\n' + g.message + '\n');
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
if (g.line)
|
|
1706
|
+
chunkEmitter.emit('chunk', formatInfo(g.line));
|
|
1707
|
+
}
|
|
1579
1708
|
// B3 (2026-06-06) — `/abap-plan --resume @plan-file`. Token-free
|
|
1580
1709
|
// pre-work (load envelope, pick next eligible phase, print the
|
|
1581
1710
|
// tracker board, inline the phase's rule files) happens in
|
|
@@ -1724,7 +1853,7 @@ export async function runRepl(opts) {
|
|
|
1724
1853
|
const { beginPlanPhaseContext, makePhaseBoundarySummariser } = await import('./commands/plan-boundary.js');
|
|
1725
1854
|
await beginPlanPhaseContext({
|
|
1726
1855
|
session,
|
|
1727
|
-
summarise: makePhaseBoundarySummariser(provider, resolveModelRole('compact', cfg)),
|
|
1856
|
+
summarise: makePhaseBoundarySummariser(provider, resolveModelRole('compact', cfg), session.id),
|
|
1728
1857
|
log: (line) => chunkEmitter.emit('chunk', line + '\n'),
|
|
1729
1858
|
});
|
|
1730
1859
|
}
|
|
@@ -1752,7 +1881,9 @@ export async function runRepl(opts) {
|
|
|
1752
1881
|
delegateTo: planResume.nextDelegateTo,
|
|
1753
1882
|
// model-governance step 2d — the session default now resolves
|
|
1754
1883
|
// env > local > server > built-in (== cfg.default_model when nothing set).
|
|
1755
|
-
defaultModel: resolveModelRole('session_default', cfg),
|
|
1884
|
+
defaultModel: familyOf(resolveModelRole('session_default', cfg), provider),
|
|
1885
|
+
// Azure tenant mode (Task 2) — undefined (today's Sonnet) with no connection table.
|
|
1886
|
+
tierModel: planTierModelFor(getActiveDeployments()),
|
|
1756
1887
|
});
|
|
1757
1888
|
if (choice.model) {
|
|
1758
1889
|
planModelOverride = choice.model;
|
|
@@ -2178,7 +2309,7 @@ export async function runRepl(opts) {
|
|
|
2178
2309
|
session,
|
|
2179
2310
|
config: { ...cfg.compact, auto_threshold_tokens: resolveAutoCompactThreshold(cfg) },
|
|
2180
2311
|
queued: pendingDispatchAccessor.get() !== null,
|
|
2181
|
-
summarise: makeAutoCompactSummariser(provider, resolveModelRole('compact', cfg)),
|
|
2312
|
+
summarise: makeAutoCompactSummariser(provider, resolveModelRole('compact', cfg), session.id),
|
|
2182
2313
|
emit: (line) => chunkEmitter.emit('chunk', line + '\n'),
|
|
2183
2314
|
});
|
|
2184
2315
|
}
|
|
@@ -3074,6 +3205,16 @@ export async function runRepl(opts) {
|
|
|
3074
3205
|
// Summarises older turns via Haiku 4.5 so subsequent turns ingest ~5k
|
|
3075
3206
|
// tokens of context instead of 500k+. Backs up session pre-mutation.
|
|
3076
3207
|
if (trimmed === '/compact') {
|
|
3208
|
+
// Security batch item 1 — /compact makes a model call: gated like a turn.
|
|
3209
|
+
{
|
|
3210
|
+
const g = await licenceGate.beforeTurn();
|
|
3211
|
+
if (!g.allow) {
|
|
3212
|
+
console.log(g.message);
|
|
3213
|
+
continue;
|
|
3214
|
+
}
|
|
3215
|
+
if (g.line)
|
|
3216
|
+
console.log(formatInfo(g.line).trimEnd());
|
|
3217
|
+
}
|
|
3077
3218
|
const { runCompact, formatCompactResult, buildSummarisationPrompt, serialiseForSummariser } = await import('./commands/compact.js');
|
|
3078
3219
|
const { summariseViaProvider } = await import('./agent/summarise-via-provider.js');
|
|
3079
3220
|
const { loadConfig } = await import('./config/loader.js');
|
|
@@ -3082,7 +3223,7 @@ export async function runRepl(opts) {
|
|
|
3082
3223
|
return await summariseViaProvider(provider, buildSummarisationPrompt(), serialiseForSummariser(toSummarise),
|
|
3083
3224
|
// model-governance step 2d — compact model resolves env > local >
|
|
3084
3225
|
// server > built-in (== COMPACTION_MODEL when nothing set).
|
|
3085
|
-
{ model: resolveModelRole('compact', cfg), maxTokens: 4000, headers: { 'X-CSForge-Skill': 'compact', 'X-CSPeach-Model-Role': 'compact' } });
|
|
3226
|
+
{ model: resolveModelRole('compact', cfg), maxTokens: 4000, headers: { 'X-CSForge-Skill': 'compact', 'X-CSPeach-Model-Role': 'compact' }, sessionId: session.id });
|
|
3086
3227
|
};
|
|
3087
3228
|
try {
|
|
3088
3229
|
console.log('\n' + formatInfo('Compacting older turns into a summary…').trimEnd());
|
|
@@ -3154,6 +3295,15 @@ export async function runRepl(opts) {
|
|
|
3154
3295
|
console.log(formatInfo(`Rerouting "${preview}" to /${rr.skill}`).trimEnd());
|
|
3155
3296
|
if (rr.warning)
|
|
3156
3297
|
console.log(chalk.dim(` (${rr.warning})`));
|
|
3298
|
+
{
|
|
3299
|
+
const g = await licenceGate.beforeTurn();
|
|
3300
|
+
if (!g.allow) {
|
|
3301
|
+
console.log(g.message);
|
|
3302
|
+
continue;
|
|
3303
|
+
}
|
|
3304
|
+
if (g.line)
|
|
3305
|
+
console.log(formatInfo(g.line).trimEnd());
|
|
3306
|
+
}
|
|
3157
3307
|
// A turn error prints the same stderr block as the normal turn path.
|
|
3158
3308
|
await runRerouteTurn({
|
|
3159
3309
|
run: () => runTurn({
|
|
@@ -3574,6 +3724,17 @@ export async function runRepl(opts) {
|
|
|
3574
3724
|
session.last_object = extractedObject ?? previousObject ?? null;
|
|
3575
3725
|
continue;
|
|
3576
3726
|
}
|
|
3727
|
+
// Security batch item 1 — checked before the turn and before any
|
|
3728
|
+
// plan-resume work (classic mirror of the Ink path).
|
|
3729
|
+
{
|
|
3730
|
+
const g = await licenceGate.beforeTurn();
|
|
3731
|
+
if (!g.allow) {
|
|
3732
|
+
console.log(g.message);
|
|
3733
|
+
continue;
|
|
3734
|
+
}
|
|
3735
|
+
if (g.line)
|
|
3736
|
+
console.log(formatInfo(g.line).trimEnd());
|
|
3737
|
+
}
|
|
3577
3738
|
// B3 (2026-06-06) — `/abap-plan --resume @plan-file` (classic-mode
|
|
3578
3739
|
// mirror of the Ink path above; see that block for the full design
|
|
3579
3740
|
// note). Token-free pre-work; terminal states end the turn with no
|
|
@@ -3700,7 +3861,7 @@ export async function runRepl(opts) {
|
|
|
3700
3861
|
const { beginPlanPhaseContext, makePhaseBoundarySummariser } = await import('./commands/plan-boundary.js');
|
|
3701
3862
|
await beginPlanPhaseContext({
|
|
3702
3863
|
session,
|
|
3703
|
-
summarise: makePhaseBoundarySummariser(provider, resolveModelRole('compact', cfg)),
|
|
3864
|
+
summarise: makePhaseBoundarySummariser(provider, resolveModelRole('compact', cfg), session.id),
|
|
3704
3865
|
log: (line) => console.log(line),
|
|
3705
3866
|
});
|
|
3706
3867
|
}
|
|
@@ -3723,7 +3884,9 @@ export async function runRepl(opts) {
|
|
|
3723
3884
|
delegateTo: planResume.nextDelegateTo,
|
|
3724
3885
|
// model-governance step 2d — the session default now resolves
|
|
3725
3886
|
// env > local > server > built-in (== cfg.default_model when nothing set).
|
|
3726
|
-
defaultModel: resolveModelRole('session_default', cfg),
|
|
3887
|
+
defaultModel: familyOf(resolveModelRole('session_default', cfg), provider),
|
|
3888
|
+
// Azure tenant mode (Task 2) — undefined (today's Sonnet) with no connection table.
|
|
3889
|
+
tierModel: planTierModelFor(getActiveDeployments()),
|
|
3727
3890
|
});
|
|
3728
3891
|
if (choice.model) {
|
|
3729
3892
|
planModelOverride = choice.model;
|
|
@@ -4127,7 +4290,7 @@ export async function runRepl(opts) {
|
|
|
4127
4290
|
session,
|
|
4128
4291
|
config: { ...cfg.compact, auto_threshold_tokens: resolveAutoCompactThreshold(cfg) },
|
|
4129
4292
|
queued: pendingDispatchAccessor.get() !== null,
|
|
4130
|
-
summarise: makeAutoCompactSummariser(provider, resolveModelRole('compact', cfg)),
|
|
4293
|
+
summarise: makeAutoCompactSummariser(provider, resolveModelRole('compact', cfg), session.id),
|
|
4131
4294
|
emit: (line) => console.log(line),
|
|
4132
4295
|
});
|
|
4133
4296
|
}
|