@pi-archimedes/session-name 2.10.4 → 2.11.1
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 +1 -1
- package/package.json +5 -5
- package/src/index.test.ts +122 -89
- package/src/index.ts +36 -20
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ After installing Pi, choose one installation command above, then `cd` into your
|
|
|
41
41
|
| Setting | Type | Default | Description |
|
|
42
42
|
|---------|------|---------|-------------|
|
|
43
43
|
| `model` | string | _(current model)_ | Model used for title generation (e.g. `openai/gpt-4o-mini`). Canonical `provider/id`, bare IDs, and thinking-suffix forms are all resolved. Empty = current model. |
|
|
44
|
-
| `reasoning` | string \| null | `minimal` | Thinking level for the title request: `minimal`, `low`, `medium`, `high`, `xhigh`, `max`.
|
|
44
|
+
| `reasoning` | string \| null | `minimal` | Thinking level for the title request: `minimal`, `low`, `medium`, `high`, `xhigh`, `max`, `off`. Resolved to the nearest level the model supports (warns on change); the option is omitted only when that resolves to `"off"`. `off`/`null` therefore request no thinking but can still send a level on models that exclude it — use `"minimal"` for bounded-small everywhere. Case and stray spaces are tolerated (`" Minimal "` works). Anything unrecognised is ignored in favour of the default and logged. Only supported levels reach the provider: the setting resolves through pi's clamp to the nearest level the model supports. |
|
|
45
45
|
|
|
46
46
|
`model` is also editable in the `/archimedes` panel (**Model for naming** — Enter to type, blank to go back to the current model); `reasoning` is JSON-only.
|
|
47
47
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-archimedes/session-name",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/danielcherubini/pi-archimedes.git"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
".": "./src/index.ts"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@pi-archimedes/core": "2.
|
|
21
|
+
"@pi-archimedes/core": "2.11.1"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"@earendil-works/pi-coding-agent": ">=0.86.0",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"@earendil-works/pi-tui": ">=0.86.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@earendil-works/pi-ai": "
|
|
30
|
-
"@earendil-works/pi-coding-agent": "
|
|
31
|
-
"@earendil-works/pi-tui": "
|
|
29
|
+
"@earendil-works/pi-ai": "1.2.0",
|
|
30
|
+
"@earendil-works/pi-coding-agent": "1.2.0",
|
|
31
|
+
"@earendil-works/pi-tui": "1.2.0",
|
|
32
32
|
"typescript": "^6.0.0"
|
|
33
33
|
},
|
|
34
34
|
"pi": {
|
package/src/index.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadConfig } from "@pi-archimedes/core/settings-io";
|
|
2
|
+
import { diag } from "@pi-archimedes/core/log";
|
|
2
3
|
import { describe, expect, it, vi } from "vitest";
|
|
3
4
|
import { thinkingBudgetForLevel } from "@earendil-works/pi-ai/api/simple-options";
|
|
4
5
|
import { generateTitle, registerSessionName, resolveTitleReasoning } from "./index.js";
|
|
@@ -39,7 +40,9 @@ function createMockCtx(options: {
|
|
|
39
40
|
},
|
|
40
41
|
];
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
// Reasoning model without a level map: every named level passes clamp
|
|
44
|
+
// through, so existing assertions on forwarded levels keep holding.
|
|
45
|
+
const defaultModel = { provider: "anthropic", id: "claude-3-5-sonnet", reasoning: true };
|
|
43
46
|
|
|
44
47
|
return {
|
|
45
48
|
sessionManager: {
|
|
@@ -66,6 +69,16 @@ vi.mock("@pi-archimedes/core/settings-io", () => ({
|
|
|
66
69
|
loadConfig: vi.fn((_namespace, defaults) => defaults),
|
|
67
70
|
}));
|
|
68
71
|
|
|
72
|
+
// Diagnostics never go to the console (see packages/core/src/log.ts): a write
|
|
73
|
+
// there smears a fullscreen frame and is discarded inside a subagent child. So
|
|
74
|
+
// every "it said something / it said nothing" assertion below watches `diag`.
|
|
75
|
+
vi.mock("@pi-archimedes/core/log", async (importOriginal) => {
|
|
76
|
+
const real = await importOriginal<typeof import("@pi-archimedes/core/log")>();
|
|
77
|
+
return { ...real, diag: vi.fn() };
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const clearDiag = () => vi.mocked(diag).mockClear();
|
|
81
|
+
|
|
69
82
|
describe("generateTitle", () => {
|
|
70
83
|
it("generates title, trims quotes and whitespace, and calls pi.setSessionName", async () => {
|
|
71
84
|
const pi = createMockPi();
|
|
@@ -121,8 +134,10 @@ describe("generateTitle", () => {
|
|
|
121
134
|
// anything. A non-level must never reach the provider — see the pin at the
|
|
122
135
|
// end of this block for what that costs.
|
|
123
136
|
describe("reasoning validation", () => {
|
|
124
|
-
it("passes every
|
|
125
|
-
|
|
137
|
+
it("passes every supported thinking level through untouched", async () => {
|
|
138
|
+
// The mock model has no level map, like the real title model — so only
|
|
139
|
+
// minimal..high pass through; xhigh/max step down (covered below).
|
|
140
|
+
for (const reasoning of ["minimal", "low", "medium", "high"]) {
|
|
126
141
|
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning });
|
|
127
142
|
const ctx = createMockCtx({});
|
|
128
143
|
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
@@ -132,54 +147,71 @@ describe("generateTitle", () => {
|
|
|
132
147
|
}
|
|
133
148
|
});
|
|
134
149
|
|
|
135
|
-
it.each(["
|
|
150
|
+
it.each([["xhigh", "high"], ["MAX", "high"]])("clamps %s to nearest supported %s, warning", async (raw, clamped) => {
|
|
136
151
|
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning: raw });
|
|
137
152
|
const ctx = createMockCtx({});
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
} finally {
|
|
146
|
-
log.mockRestore();
|
|
147
|
-
}
|
|
153
|
+
clearDiag();
|
|
154
|
+
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
155
|
+
expect((ctx.modelRegistry.streamSimple as any).mock.calls[0][2].reasoning).toBe(clamped);
|
|
156
|
+
expect(diag).toHaveBeenCalledWith(
|
|
157
|
+
"session-name",
|
|
158
|
+
expect.stringContaining(`using "${clamped}"`),
|
|
159
|
+
);
|
|
148
160
|
});
|
|
149
161
|
|
|
150
|
-
it.each([
|
|
162
|
+
it.each(["Minimal", " high "])("tolerates casing and padding: %s", async (raw) => {
|
|
163
|
+
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning: raw });
|
|
164
|
+
const ctx = createMockCtx({});
|
|
165
|
+
clearDiag();
|
|
166
|
+
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
167
|
+
expect((ctx.modelRegistry.streamSimple as any).mock.calls[0][2].reasoning).toBe(
|
|
168
|
+
raw.trim().toLowerCase(),
|
|
169
|
+
);
|
|
170
|
+
expect(diag).not.toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it.each([["ultra"], [""], [42], [true], [{}], [[]]])(
|
|
151
174
|
"never forwards a non-level: %s",
|
|
152
175
|
async (raw) => {
|
|
153
176
|
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning: raw });
|
|
154
177
|
const ctx = createMockCtx({});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
log.mockRestore();
|
|
168
|
-
}
|
|
178
|
+
clearDiag();
|
|
179
|
+
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
180
|
+
|
|
181
|
+
const sent = (ctx.modelRegistry.streamSimple as any).mock.calls[0][2].reasoning;
|
|
182
|
+
// Invalid config behaves like absent config: the documented default.
|
|
183
|
+
expect(sent).toBe("minimal");
|
|
184
|
+
// The typo is invisible unless it is reported, and the report is the
|
|
185
|
+
// single log line naming the rejected value.
|
|
186
|
+
expect(diag).toHaveBeenCalledWith(
|
|
187
|
+
"session-name",
|
|
188
|
+
`[archimedes] session-name: ignoring unrecognized reasoning ${JSON.stringify(raw)}`,
|
|
189
|
+
);
|
|
169
190
|
},
|
|
170
191
|
);
|
|
171
192
|
|
|
172
|
-
it("
|
|
173
|
-
|
|
193
|
+
it.each([["off"], [null]])("omits the key for %s on models that take it", async (reasoning) => {
|
|
194
|
+
// Default mock has no level map, so "off" is supported and omitted silently.
|
|
195
|
+
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning });
|
|
174
196
|
const ctx = createMockCtx({});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
197
|
+
clearDiag();
|
|
198
|
+
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
199
|
+
expect((ctx.modelRegistry.streamSimple as any).mock.calls[0][2]).not.toHaveProperty("reasoning");
|
|
200
|
+
expect(diag).not.toHaveBeenCalled();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("clamps off to minimal with a warning where the model excludes it", async () => {
|
|
204
|
+
// Muse-spark shape: thinking model whose map excludes "off" (null).
|
|
205
|
+
const model = { provider: "op", id: "spark", reasoning: true, thinkingLevelMap: { off: null } };
|
|
206
|
+
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning: "off" });
|
|
207
|
+
const ctx = createMockCtx({ model });
|
|
208
|
+
clearDiag();
|
|
209
|
+
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
210
|
+
expect((ctx.modelRegistry.streamSimple as any).mock.calls[0][2].reasoning).toBe("minimal");
|
|
211
|
+
expect(diag).toHaveBeenCalledWith(
|
|
212
|
+
"session-name",
|
|
213
|
+
expect.stringContaining('using "minimal"'),
|
|
214
|
+
);
|
|
183
215
|
});
|
|
184
216
|
|
|
185
217
|
it("keeps the default when the setting is absent entirely, and says nothing", async () => {
|
|
@@ -187,26 +219,25 @@ describe("generateTitle", () => {
|
|
|
187
219
|
// "minimal" — the common path must stay silent. A regression that warned
|
|
188
220
|
// on every default-configured session would otherwise pass the suite.
|
|
189
221
|
const ctx = createMockCtx({});
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
expect(log).not.toHaveBeenCalled();
|
|
195
|
-
} finally {
|
|
196
|
-
log.mockRestore();
|
|
197
|
-
}
|
|
222
|
+
clearDiag();
|
|
223
|
+
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
224
|
+
expect((ctx.modelRegistry.streamSimple as any).mock.calls[0][2].reasoning).toBe("minimal");
|
|
225
|
+
expect(diag).not.toHaveBeenCalled();
|
|
198
226
|
});
|
|
199
227
|
|
|
200
228
|
it("treats undefined like absent config, without warning", () => {
|
|
201
229
|
// Direct call: the generateTitle path can't produce this, since
|
|
202
230
|
// loadConfig fills the default in, but the helper is exported.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
231
|
+
clearDiag();
|
|
232
|
+
expect(resolveTitleReasoning(undefined)).toBe("minimal");
|
|
233
|
+
expect(diag).not.toHaveBeenCalled();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("accepts off for the call site to clamp per model", () => {
|
|
237
|
+
clearDiag();
|
|
238
|
+
expect(resolveTitleReasoning("off")).toBe("off");
|
|
239
|
+
expect(resolveTitleReasoning(" Off ")).toBe("off");
|
|
240
|
+
expect(diag).not.toHaveBeenCalled();
|
|
210
241
|
});
|
|
211
242
|
|
|
212
243
|
// Pin the invariant against the arithmetic that makes this dangerous. Of
|
|
@@ -228,15 +259,18 @@ describe("generateTitle", () => {
|
|
|
228
259
|
for (const level of levels) {
|
|
229
260
|
expect(thinkingBudgetForLevel(level)).toBeTypeOf("number");
|
|
230
261
|
}
|
|
231
|
-
const junk: unknown[] = ["ultra", "MINIMAL", "
|
|
262
|
+
const junk: unknown[] = ["ultra", "MINIMAL", "", "Max ", 42, true, {}, []];
|
|
232
263
|
for (const value of junk) {
|
|
233
264
|
expect(thinkingBudgetForLevel(value as never)).toBeUndefined();
|
|
234
265
|
}
|
|
235
266
|
|
|
236
|
-
// Whatever the setting holds, the value
|
|
267
|
+
// Whatever the setting holds, the value reaching clamp is a real level
|
|
268
|
+
// or "off" — never raw input. "off" is resolved per model at the call
|
|
269
|
+
// site (nearest supported, warn on change), never forwarded verbatim.
|
|
237
270
|
for (const raw of [...levels, ...junk, null, undefined]) {
|
|
238
271
|
const resolved = resolveTitleReasoning(raw);
|
|
239
272
|
if (resolved === null) continue; // omitted → no level requested
|
|
273
|
+
if (resolved === "off") continue; // clamped per model at the call site
|
|
240
274
|
expect(levels).toContain(resolved);
|
|
241
275
|
expect(thinkingBudgetForLevel(resolved)).toBeTypeOf("number");
|
|
242
276
|
}
|
|
@@ -270,16 +304,16 @@ describe("generateTitle", () => {
|
|
|
270
304
|
pi.getSessionName.mockImplementation(() => { throw new Error(message); });
|
|
271
305
|
const onSuccess = vi.fn();
|
|
272
306
|
const onFailure = vi.fn();
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
expect(
|
|
280
|
-
} finally {
|
|
281
|
-
log.mockRestore();
|
|
307
|
+
clearDiag();
|
|
308
|
+
await generateTitle(pi as any, createMockCtx({}) as any, onSuccess, onFailure);
|
|
309
|
+
expect(onFailure).toHaveBeenCalledTimes(failures);
|
|
310
|
+
// A swallowed failure is only acceptable if it left a trace.
|
|
311
|
+
expect(diag).toHaveBeenCalledTimes(failures);
|
|
312
|
+
if (failures === 1) {
|
|
313
|
+
expect(diag).toHaveBeenCalledWith("session-name", expect.stringContaining(message));
|
|
282
314
|
}
|
|
315
|
+
expect(onSuccess).not.toHaveBeenCalled();
|
|
316
|
+
expect(pi.setSessionName).not.toHaveBeenCalled();
|
|
283
317
|
});
|
|
284
318
|
|
|
285
319
|
it("does not call onFailure when streamSimple returns stopReason === 'aborted'", async () => {
|
|
@@ -356,29 +390,28 @@ describe("registerSessionName", () => {
|
|
|
356
390
|
const error = { stopReason: "error", content: [], errorMessage: "400 boom" };
|
|
357
391
|
const notify = vi.fn();
|
|
358
392
|
const ctx = { ...createMockCtx({ streamSimpleResult: error }), hasUI: scenario !== "no UI", ui: { notify } };
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
393
|
+
clearDiag();
|
|
394
|
+
await agentEnd({}, ctx);
|
|
395
|
+
await agentEnd({}, ctx);
|
|
396
|
+
let finish!: (response: any) => void;
|
|
397
|
+
const pending = new Promise((resolve) => { finish = resolve; });
|
|
398
|
+
ctx.modelRegistry.streamSimple.mockReturnValueOnce({ result: vi.fn(() => pending) });
|
|
399
|
+
await agentEnd({}, ctx);
|
|
400
|
+
// Change the session/UI while the third request is still pending.
|
|
401
|
+
if (scenario === "named") pi.setSessionName("Manual name");
|
|
402
|
+
if (scenario === "stale") pi.getSessionName.mockImplementation(() => { throw new Error("stale context"); });
|
|
403
|
+
if (scenario === "stale UI") Object.defineProperty(ctx, "hasUI", { get: () => { throw new Error("stale UI"); } });
|
|
404
|
+
if (scenario === "broken UI") notify.mockImplementation(() => { throw new Error("UI failed"); });
|
|
405
|
+
finish(error);
|
|
406
|
+
await pending;
|
|
407
|
+
await agentEnd({}, ctx);
|
|
408
|
+
expect(ctx.modelRegistry.streamSimple).toHaveBeenCalledTimes(3);
|
|
409
|
+
// Every attempt is diagnosable…
|
|
410
|
+
expect(diag).toHaveBeenCalledTimes(3);
|
|
411
|
+
expect(diag).toHaveBeenCalledWith("session-name", "[archimedes] session-name failed: 400 boom");
|
|
412
|
+
// …but the user is told once, at the retry-budget boundary: three lines of
|
|
413
|
+
// console noise (or three notifications) is not a report, it is spam.
|
|
414
|
+
expect(notify).toHaveBeenCalledTimes(scenario === "UI" || scenario === "broken UI" ? 1 : 0);
|
|
415
|
+
if (scenario === "UI") expect(notify).toHaveBeenCalledWith(expect.stringContaining("/name"), "warning");
|
|
383
416
|
});
|
|
384
417
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import
|
|
2
|
+
import { clampThinkingLevel } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { ModelThinkingLevel, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
3
4
|
import { loadConfig } from "@pi-archimedes/core/settings-io";
|
|
5
|
+
import { diag, describeError } from "@pi-archimedes/core/log";
|
|
4
6
|
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
5
7
|
|
|
6
8
|
// ── Config ──────────────────────────────────────────────────────────────────
|
|
@@ -9,7 +11,7 @@ export interface SessionNameSettings {
|
|
|
9
11
|
// suite-managed by meta's plugin gate (archimedes.sessionName.enabled — see ADR 0012); session-name never reads this
|
|
10
12
|
enabled?: boolean | undefined;
|
|
11
13
|
model?: string | undefined;
|
|
12
|
-
reasoning?: ThinkingLevel | null | undefined;
|
|
14
|
+
reasoning?: ThinkingLevel | "off" | null | undefined;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
const DEFAULT_SESSION_NAME_CONFIG: SessionNameSettings = {
|
|
@@ -19,14 +21,16 @@ const DEFAULT_SESSION_NAME_CONFIG: SessionNameSettings = {
|
|
|
19
21
|
|
|
20
22
|
const NAMESPACE = "archimedes.sessionName";
|
|
21
23
|
|
|
22
|
-
/** The thinking levels we accept — pi-ai's set,
|
|
23
|
-
|
|
24
|
+
/** The thinking levels we accept — pi-ai's set, plus `off` (resolved per
|
|
25
|
+
* model at the call site: omitting is the only portable spelling of none). */
|
|
26
|
+
const REASONING_LEVELS: readonly (ThinkingLevel | "off")[] = [
|
|
24
27
|
"minimal",
|
|
25
28
|
"low",
|
|
26
29
|
"medium",
|
|
27
30
|
"high",
|
|
28
31
|
"xhigh",
|
|
29
32
|
"max",
|
|
33
|
+
"off",
|
|
30
34
|
];
|
|
31
35
|
|
|
32
36
|
/**
|
|
@@ -45,14 +49,15 @@ const REASONING_LEVELS: readonly ThinkingLevel[] = [
|
|
|
45
49
|
* budget-based Claude it instead misses the four-key budget table, and the
|
|
46
50
|
* resulting `undefined` reaches the provider as an invalid `max_tokens`.
|
|
47
51
|
*
|
|
48
|
-
* Outcomes: a real level (casing/padding tolerated) is
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* the typo. `"off"` is
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* Outcomes: a real level (casing/padding tolerated) is returned for the call
|
|
53
|
+
* site to resolve per model; `null` means omit the option entirely; anything
|
|
54
|
+
* else — including absent — is treated like an absent setting and gets the
|
|
55
|
+
* default, with a log line so the user can find the typo. `"off"` is
|
|
56
|
+
* accepted but never forwarded raw: it resolves through pi's
|
|
57
|
+
* `clampThinkingLevel` like every other level (nearest supported, warn on
|
|
58
|
+
* change), because unclamped providers map unknown levels to maximum effort.
|
|
54
59
|
*/
|
|
55
|
-
export function resolveTitleReasoning(raw: unknown): ThinkingLevel | null {
|
|
60
|
+
export function resolveTitleReasoning(raw: unknown): ThinkingLevel | "off" | null {
|
|
56
61
|
if (raw === null) return null;
|
|
57
62
|
if (typeof raw === "string") {
|
|
58
63
|
const value = raw.trim().toLowerCase();
|
|
@@ -60,10 +65,10 @@ export function resolveTitleReasoning(raw: unknown): ThinkingLevel | null {
|
|
|
60
65
|
if (match) return match;
|
|
61
66
|
}
|
|
62
67
|
if (raw !== undefined) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
);
|
|
68
|
+
// A typo in a hand-edited settings.json is invisible unless reported, but
|
|
69
|
+
// the report is for whoever debugs the config — the log file, not the
|
|
70
|
+
// console (which smears a fullscreen frame and is discarded in a child).
|
|
71
|
+
diag("session-name", `[archimedes] session-name: ignoring unrecognized reasoning ${JSON.stringify(raw)}`);
|
|
67
72
|
}
|
|
68
73
|
return DEFAULT_SESSION_NAME_CONFIG.reasoning ?? null;
|
|
69
74
|
}
|
|
@@ -205,8 +210,17 @@ export async function generateTitle(
|
|
|
205
210
|
if (!ctx.modelRegistry.hasConfiguredAuth(model)) return;
|
|
206
211
|
|
|
207
212
|
// 5. Stream simple with built-in auth resolution.
|
|
208
|
-
// Reasoning is resolved, not forwarded raw: see
|
|
209
|
-
|
|
213
|
+
// Reasoning is resolved per model, not forwarded raw: see
|
|
214
|
+
// resolveTitleReasoning(). `clampThinkingLevel` maps to the nearest level
|
|
215
|
+
// the model supports (warn on change); only an `off` result omits the
|
|
216
|
+
// option, which is the single portable spelling of none.
|
|
217
|
+
const resolved = resolveTitleReasoning(settings.reasoning);
|
|
218
|
+
const requested: ModelThinkingLevel = resolved === null ? "off" : (resolved ?? "minimal");
|
|
219
|
+
const clamped = clampThinkingLevel(model, requested);
|
|
220
|
+
if (clamped !== requested) {
|
|
221
|
+
diag("session-name", `[archimedes] session-name: reasoning "${requested}" is not supported — using "${clamped}"`);
|
|
222
|
+
}
|
|
223
|
+
const reasoning = clamped === "off" ? undefined : clamped;
|
|
210
224
|
const stream = ctx.modelRegistry.streamSimple(
|
|
211
225
|
model,
|
|
212
226
|
{
|
|
@@ -219,7 +233,7 @@ export async function generateTitle(
|
|
|
219
233
|
],
|
|
220
234
|
},
|
|
221
235
|
{
|
|
222
|
-
...(reasoning !==
|
|
236
|
+
...(reasoning !== undefined ? { reasoning } : {}),
|
|
223
237
|
cacheRetention: "none",
|
|
224
238
|
sessionId: crypto.randomUUID(),
|
|
225
239
|
},
|
|
@@ -231,7 +245,9 @@ export async function generateTitle(
|
|
|
231
245
|
return;
|
|
232
246
|
}
|
|
233
247
|
if (response.stopReason === "error") {
|
|
234
|
-
|
|
248
|
+
// The user's notice is the single one at the retry-budget boundary (see
|
|
249
|
+
// registerSessionName); one line per attempt goes to the log.
|
|
250
|
+
diag("session-name", `[archimedes] session-name failed: ${response.errorMessage ?? "title request failed"}`);
|
|
235
251
|
onFailure();
|
|
236
252
|
return;
|
|
237
253
|
}
|
|
@@ -260,7 +276,7 @@ export async function generateTitle(
|
|
|
260
276
|
} catch (e) {
|
|
261
277
|
// Session replacement or print-mode teardown makes an in-flight title irrelevant.
|
|
262
278
|
if (e instanceof Error && e.message.includes("stale after session replacement")) return;
|
|
263
|
-
|
|
279
|
+
diag("session-name", `[archimedes] session-name failed: ${describeError(e)}`);
|
|
264
280
|
onFailure();
|
|
265
281
|
}
|
|
266
282
|
}
|