@pi-archimedes/session-name 2.10.2 → 2.10.4
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 +5 -1
- package/package.json +2 -2
- package/src/index.test.ts +198 -1
- package/src/index.ts +68 -3
package/README.md
CHANGED
|
@@ -31,7 +31,8 @@ After installing Pi, choose one installation command above, then `cd` into your
|
|
|
31
31
|
- After the first user + assistant exchange settles, it takes the first exchange (500 characters per side), asks a model to write a 3–8-word title, caps it at 80 characters, and sets the session name — with a final re-check so a name you set yourself can never be overwritten.
|
|
32
32
|
- **Manual names win.** If you named the session (`--name` or `/name`), naming is skipped, and it re-checks before writing.
|
|
33
33
|
- **It uses your current model unless you configure another.** The title is a separate model call outside the main run — with its own cost, **not reflected in the footer's totals**. A `model` setting (e.g. a cheap model) avoids spending your main model on titles.
|
|
34
|
-
- **
|
|
34
|
+
- **Failures tell you.** A failed title request is retried on a later turn, and the provider's error is logged as `[archimedes] session-name failed: <provider error>`. After three failures in one session it stops trying and shows a single warning: *Session naming failed 3 times. Check the model configuration or use /name.* The warning is skipped when there's no UI to show it in (`pi -p`) or if you named the session in the meantime. Cancelling a request doesn't count as a failure, and neither does a session that's replaced or torn down while a title is in flight.
|
|
35
|
+
- **Skips** — Ephemeral sessions (no session file) are skipped, and so is a model with no configured auth. Neither counts against the three attempts.
|
|
35
36
|
|
|
36
37
|
## Settings
|
|
37
38
|
|
|
@@ -40,6 +41,9 @@ After installing Pi, choose one installation command above, then `cd` into your
|
|
|
40
41
|
| Setting | Type | Default | Description |
|
|
41
42
|
|---------|------|---------|-------------|
|
|
42
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`. Set `null` to omit the option entirely — that's the fix for a provider that rejects thinking levels outright, which shows up as the naming warning plus a `400 Invalid request parameters` line in the log. Case and stray spaces are tolerated (`" Minimal "` works). Anything unrecognised is ignored in favour of the default and logged as `[archimedes] session-name: ignoring unrecognized reasoning <value>` — it is never sent to the provider, because what an unknown level *means* is provider-dependent: some read it as more thinking, some reject the request, and we'd rather not depend on which. |
|
|
45
|
+
|
|
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.
|
|
43
47
|
|
|
44
48
|
On/off is managed by the suite: toggle via `/plugins` (`archimedes.sessionName.enabled`, default on).
|
|
45
49
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-archimedes/session-name",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.4",
|
|
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.10.
|
|
21
|
+
"@pi-archimedes/core": "2.10.4"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"@earendil-works/pi-coding-agent": ">=0.86.0",
|
package/src/index.test.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { loadConfig } from "@pi-archimedes/core/settings-io";
|
|
1
2
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import {
|
|
3
|
+
import { thinkingBudgetForLevel } from "@earendil-works/pi-ai/api/simple-options";
|
|
4
|
+
import { generateTitle, registerSessionName, resolveTitleReasoning } from "./index.js";
|
|
3
5
|
|
|
4
6
|
function createMockPi(sessionName: string | undefined = undefined) {
|
|
5
7
|
let currentName = sessionName;
|
|
@@ -60,6 +62,10 @@ function createMockCtx(options: {
|
|
|
60
62
|
};
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
vi.mock("@pi-archimedes/core/settings-io", () => ({
|
|
66
|
+
loadConfig: vi.fn((_namespace, defaults) => defaults),
|
|
67
|
+
}));
|
|
68
|
+
|
|
63
69
|
describe("generateTitle", () => {
|
|
64
70
|
it("generates title, trims quotes and whitespace, and calls pi.setSessionName", async () => {
|
|
65
71
|
const pi = createMockPi();
|
|
@@ -101,6 +107,142 @@ describe("generateTitle", () => {
|
|
|
101
107
|
expect(onFailure).not.toHaveBeenCalled();
|
|
102
108
|
});
|
|
103
109
|
|
|
110
|
+
it.each(["low", null])("uses configured reasoning %s", async (reasoning) => {
|
|
111
|
+
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning });
|
|
112
|
+
const ctx = createMockCtx({});
|
|
113
|
+
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
114
|
+
|
|
115
|
+
const options = (ctx.modelRegistry.streamSimple as any).mock.calls[0][2];
|
|
116
|
+
if (reasoning === null) expect(options).not.toHaveProperty("reasoning");
|
|
117
|
+
else expect(options.reasoning).toBe(reasoning);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Issue #86: settings.json is hand-edited strict JSON, so the value can be
|
|
121
|
+
// anything. A non-level must never reach the provider — see the pin at the
|
|
122
|
+
// end of this block for what that costs.
|
|
123
|
+
describe("reasoning validation", () => {
|
|
124
|
+
it("passes every real thinking level through untouched", async () => {
|
|
125
|
+
for (const reasoning of ["minimal", "low", "medium", "high", "xhigh", "max"]) {
|
|
126
|
+
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning });
|
|
127
|
+
const ctx = createMockCtx({});
|
|
128
|
+
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
129
|
+
|
|
130
|
+
const options = (ctx.modelRegistry.streamSimple as any).mock.calls[0][2];
|
|
131
|
+
expect(options.reasoning).toBe(reasoning);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it.each(["Minimal", " high ", "MAX"])("tolerates casing and padding: %s", async (raw) => {
|
|
136
|
+
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning: raw });
|
|
137
|
+
const ctx = createMockCtx({});
|
|
138
|
+
const log = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
139
|
+
try {
|
|
140
|
+
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
141
|
+
expect((ctx.modelRegistry.streamSimple as any).mock.calls[0][2].reasoning).toBe(
|
|
142
|
+
raw.trim().toLowerCase(),
|
|
143
|
+
);
|
|
144
|
+
expect(log).not.toHaveBeenCalled();
|
|
145
|
+
} finally {
|
|
146
|
+
log.mockRestore();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it.each([["ultra"], ["off"], [""], [42], [true], [{}], [[]]])(
|
|
151
|
+
"never forwards a non-level: %s",
|
|
152
|
+
async (raw) => {
|
|
153
|
+
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning: raw });
|
|
154
|
+
const ctx = createMockCtx({});
|
|
155
|
+
const log = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
156
|
+
try {
|
|
157
|
+
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
158
|
+
|
|
159
|
+
const sent = (ctx.modelRegistry.streamSimple as any).mock.calls[0][2].reasoning;
|
|
160
|
+
// Invalid config behaves like absent config: the documented default.
|
|
161
|
+
expect(sent).toBe("minimal");
|
|
162
|
+
expect(log).toHaveBeenCalledWith(
|
|
163
|
+
"[archimedes] session-name: ignoring unrecognized reasoning",
|
|
164
|
+
JSON.stringify(raw),
|
|
165
|
+
);
|
|
166
|
+
} finally {
|
|
167
|
+
log.mockRestore();
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
it("sends no reasoning key when the value is null, and says nothing", async () => {
|
|
173
|
+
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning: null });
|
|
174
|
+
const ctx = createMockCtx({});
|
|
175
|
+
const log = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
176
|
+
try {
|
|
177
|
+
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
178
|
+
expect((ctx.modelRegistry.streamSimple as any).mock.calls[0][2]).not.toHaveProperty("reasoning");
|
|
179
|
+
expect(log).not.toHaveBeenCalled();
|
|
180
|
+
} finally {
|
|
181
|
+
log.mockRestore();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("keeps the default when the setting is absent entirely, and says nothing", async () => {
|
|
186
|
+
// loadConfig merges the package default, so an absent key arrives as
|
|
187
|
+
// "minimal" — the common path must stay silent. A regression that warned
|
|
188
|
+
// on every default-configured session would otherwise pass the suite.
|
|
189
|
+
const ctx = createMockCtx({});
|
|
190
|
+
const log = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
191
|
+
try {
|
|
192
|
+
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
193
|
+
expect((ctx.modelRegistry.streamSimple as any).mock.calls[0][2].reasoning).toBe("minimal");
|
|
194
|
+
expect(log).not.toHaveBeenCalled();
|
|
195
|
+
} finally {
|
|
196
|
+
log.mockRestore();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("treats undefined like absent config, without warning", () => {
|
|
201
|
+
// Direct call: the generateTitle path can't produce this, since
|
|
202
|
+
// loadConfig fills the default in, but the helper is exported.
|
|
203
|
+
const log = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
204
|
+
try {
|
|
205
|
+
expect(resolveTitleReasoning(undefined)).toBe("minimal");
|
|
206
|
+
expect(log).not.toHaveBeenCalled();
|
|
207
|
+
} finally {
|
|
208
|
+
log.mockRestore();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Pin the invariant against the arithmetic that makes this dangerous. Of
|
|
213
|
+
// pi-ai's adapters that export a streamSimple, three never call
|
|
214
|
+
// `clampThinkingLevel` — anthropic-messages, bedrock-converse-stream, and
|
|
215
|
+
// pi-messages (Radius) — so a forwarded non-level reaches their level
|
|
216
|
+
// tables verbatim:
|
|
217
|
+
// - adaptive-thinking models: mapThinkingLevelToEffort has no `off` case
|
|
218
|
+
// and its `default:` returns "high" — a typo silently buys maximum
|
|
219
|
+
// effort on a one-line title. This is the real damage: nothing fails.
|
|
220
|
+
// - budget-based models: thinkingBudgetForLevel's table has four keys, so
|
|
221
|
+
// a non-level yields undefined, which propagates into max_tokens as NaN
|
|
222
|
+
// (serialized `null`) and the provider rejects it. Silly, but loud.
|
|
223
|
+
// We can't fix pi's tables, so guarantee we never hand it a non-level:
|
|
224
|
+
// whatever we send must be a level the budget table recognises.
|
|
225
|
+
it("resolved reasoning is always a level the Anthropic budget table knows", () => {
|
|
226
|
+
// The hazard, read from pi-ai itself: these levels have a budget, junk doesn't.
|
|
227
|
+
const levels = ["minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
228
|
+
for (const level of levels) {
|
|
229
|
+
expect(thinkingBudgetForLevel(level)).toBeTypeOf("number");
|
|
230
|
+
}
|
|
231
|
+
const junk: unknown[] = ["ultra", "MINIMAL", "off", "", "Max ", 42, true, {}, []];
|
|
232
|
+
for (const value of junk) {
|
|
233
|
+
expect(thinkingBudgetForLevel(value as never)).toBeUndefined();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Whatever the setting holds, the value we forward is always safe to send.
|
|
237
|
+
for (const raw of [...levels, ...junk, null, undefined]) {
|
|
238
|
+
const resolved = resolveTitleReasoning(raw);
|
|
239
|
+
if (resolved === null) continue; // omitted → no level requested
|
|
240
|
+
expect(levels).toContain(resolved);
|
|
241
|
+
expect(thinkingBudgetForLevel(resolved)).toBeTypeOf("number");
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
104
246
|
it("triggers onFailure when streamSimple returns stopReason === 'error'", async () => {
|
|
105
247
|
const pi = createMockPi();
|
|
106
248
|
const ctx = createMockCtx({
|
|
@@ -120,6 +262,26 @@ describe("generateTitle", () => {
|
|
|
120
262
|
expect(onFailure).toHaveBeenCalledTimes(1);
|
|
121
263
|
});
|
|
122
264
|
|
|
265
|
+
it.each([
|
|
266
|
+
["This extension ctx is stale after session replacement or reload.", 0],
|
|
267
|
+
["Unexpected lookup failure", 1],
|
|
268
|
+
] as const)("handles lookup errors: %s", async (message, failures) => {
|
|
269
|
+
const pi = createMockPi();
|
|
270
|
+
pi.getSessionName.mockImplementation(() => { throw new Error(message); });
|
|
271
|
+
const onSuccess = vi.fn();
|
|
272
|
+
const onFailure = vi.fn();
|
|
273
|
+
const log = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
274
|
+
try {
|
|
275
|
+
await generateTitle(pi as any, createMockCtx({}) as any, onSuccess, onFailure);
|
|
276
|
+
expect(onFailure).toHaveBeenCalledTimes(failures);
|
|
277
|
+
expect(log).toHaveBeenCalledTimes(failures);
|
|
278
|
+
expect(onSuccess).not.toHaveBeenCalled();
|
|
279
|
+
expect(pi.setSessionName).not.toHaveBeenCalled();
|
|
280
|
+
} finally {
|
|
281
|
+
log.mockRestore();
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
123
285
|
it("does not call onFailure when streamSimple returns stopReason === 'aborted'", async () => {
|
|
124
286
|
const pi = createMockPi();
|
|
125
287
|
const ctx = createMockCtx({
|
|
@@ -185,3 +347,38 @@ describe("generateTitle", () => {
|
|
|
185
347
|
expect(onFailure).not.toHaveBeenCalled();
|
|
186
348
|
});
|
|
187
349
|
});
|
|
350
|
+
|
|
351
|
+
describe("registerSessionName", () => {
|
|
352
|
+
it.each(["UI", "no UI", "named", "stale", "stale UI", "broken UI"])("reports exhaustion safely: %s", async (scenario) => {
|
|
353
|
+
const pi = createMockPi();
|
|
354
|
+
registerSessionName(pi as any);
|
|
355
|
+
const agentEnd = pi.on.mock.calls.find(([event]) => event === "agent_end")![1];
|
|
356
|
+
const error = { stopReason: "error", content: [], errorMessage: "400 boom" };
|
|
357
|
+
const notify = vi.fn();
|
|
358
|
+
const ctx = { ...createMockCtx({ streamSimpleResult: error }), hasUI: scenario !== "no UI", ui: { notify } };
|
|
359
|
+
const log = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
360
|
+
try {
|
|
361
|
+
await agentEnd({}, ctx);
|
|
362
|
+
await agentEnd({}, ctx);
|
|
363
|
+
let finish!: (response: any) => void;
|
|
364
|
+
const pending = new Promise((resolve) => { finish = resolve; });
|
|
365
|
+
ctx.modelRegistry.streamSimple.mockReturnValueOnce({ result: vi.fn(() => pending) });
|
|
366
|
+
await agentEnd({}, ctx);
|
|
367
|
+
// Change the session/UI while the third request is still pending.
|
|
368
|
+
if (scenario === "named") pi.setSessionName("Manual name");
|
|
369
|
+
if (scenario === "stale") pi.getSessionName.mockImplementation(() => { throw new Error("stale context"); });
|
|
370
|
+
if (scenario === "stale UI") Object.defineProperty(ctx, "hasUI", { get: () => { throw new Error("stale UI"); } });
|
|
371
|
+
if (scenario === "broken UI") notify.mockImplementation(() => { throw new Error("UI failed"); });
|
|
372
|
+
finish(error);
|
|
373
|
+
await pending;
|
|
374
|
+
await agentEnd({}, ctx);
|
|
375
|
+
expect(ctx.modelRegistry.streamSimple).toHaveBeenCalledTimes(3);
|
|
376
|
+
expect(log).toHaveBeenCalledTimes(3);
|
|
377
|
+
expect(log).toHaveBeenCalledWith("[archimedes] session-name failed:", "400 boom");
|
|
378
|
+
expect(notify).toHaveBeenCalledTimes(scenario === "UI" || scenario === "broken UI" ? 1 : 0);
|
|
379
|
+
if (scenario === "UI") expect(notify).toHaveBeenCalledWith(expect.stringContaining("/name"), "warning");
|
|
380
|
+
} finally {
|
|
381
|
+
log.mockRestore();
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ThinkingLevel } from "@earendil-works/pi-ai";
|
|
2
3
|
import { loadConfig } from "@pi-archimedes/core/settings-io";
|
|
3
4
|
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
4
5
|
|
|
@@ -8,14 +9,65 @@ export interface SessionNameSettings {
|
|
|
8
9
|
// suite-managed by meta's plugin gate (archimedes.sessionName.enabled — see ADR 0012); session-name never reads this
|
|
9
10
|
enabled?: boolean | undefined;
|
|
10
11
|
model?: string | undefined;
|
|
12
|
+
reasoning?: ThinkingLevel | null | undefined;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
const DEFAULT_SESSION_NAME_CONFIG: SessionNameSettings = {
|
|
14
16
|
model: undefined,
|
|
17
|
+
reasoning: "minimal",
|
|
15
18
|
};
|
|
16
19
|
|
|
17
20
|
const NAMESPACE = "archimedes.sessionName";
|
|
18
21
|
|
|
22
|
+
/** The thinking levels we accept — pi-ai's set, minus `off` (see below). */
|
|
23
|
+
const REASONING_LEVELS: readonly ThinkingLevel[] = [
|
|
24
|
+
"minimal",
|
|
25
|
+
"low",
|
|
26
|
+
"medium",
|
|
27
|
+
"high",
|
|
28
|
+
"xhigh",
|
|
29
|
+
"max",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Decide what reasoning level the title request asks for, given whatever was
|
|
34
|
+
* found in settings.json.
|
|
35
|
+
*
|
|
36
|
+
* `settings.json` is hand-edited strict JSON and `loadConfig` returns untyped
|
|
37
|
+
* JSON, so the declared `ThinkingLevel` type narrows nothing at runtime: a
|
|
38
|
+
* wrong case, a stray space, or a typo all get through. Junk must not be
|
|
39
|
+
* forwarded, because pi cannot rescue it everywhere — of the ten adapters that
|
|
40
|
+
* export a `streamSimple`, three never call `clampThinkingLevel`:
|
|
41
|
+
* `anthropic-messages`, `bedrock-converse-stream`, and `pi-messages` (Radius,
|
|
42
|
+
* which forwards the level verbatim to its backend). On the Anthropic pair an
|
|
43
|
+
* unknown value hits `mapThinkingLevelToEffort`, whose `default:` returns
|
|
44
|
+
* `"high"`: a typo silently buys maximum effort on a one-line title. On
|
|
45
|
+
* budget-based Claude it instead misses the four-key budget table, and the
|
|
46
|
+
* resulting `undefined` reaches the provider as an invalid `max_tokens`.
|
|
47
|
+
*
|
|
48
|
+
* Outcomes: a real level (casing/padding tolerated) is forwarded; `null` means
|
|
49
|
+
* omit the option entirely; anything else — including absent — is treated like
|
|
50
|
+
* an absent setting and gets the default, with a log line so the user can find
|
|
51
|
+
* the typo. `"off"` is deliberately not accepted: it would be a fourth
|
|
52
|
+
* spelling of "unset" next to `null` and an absent key, and pi accepts `off` as
|
|
53
|
+
* no-effort anyway wherever it maps one.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveTitleReasoning(raw: unknown): ThinkingLevel | null {
|
|
56
|
+
if (raw === null) return null;
|
|
57
|
+
if (typeof raw === "string") {
|
|
58
|
+
const value = raw.trim().toLowerCase();
|
|
59
|
+
const match = REASONING_LEVELS.find((level) => level === value);
|
|
60
|
+
if (match) return match;
|
|
61
|
+
}
|
|
62
|
+
if (raw !== undefined) {
|
|
63
|
+
console.error(
|
|
64
|
+
"[archimedes] session-name: ignoring unrecognized reasoning",
|
|
65
|
+
JSON.stringify(raw),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return DEFAULT_SESSION_NAME_CONFIG.reasoning ?? null;
|
|
69
|
+
}
|
|
70
|
+
|
|
19
71
|
export function loadSessionNameConfig(): SessionNameSettings {
|
|
20
72
|
return loadConfig(NAMESPACE, DEFAULT_SESSION_NAME_CONFIG);
|
|
21
73
|
}
|
|
@@ -152,7 +204,9 @@ export async function generateTitle(
|
|
|
152
204
|
// 4. Check auth — skip without incrementing failCount if not configured
|
|
153
205
|
if (!ctx.modelRegistry.hasConfiguredAuth(model)) return;
|
|
154
206
|
|
|
155
|
-
// 5. Stream simple with built-in auth resolution
|
|
207
|
+
// 5. Stream simple with built-in auth resolution.
|
|
208
|
+
// Reasoning is resolved, not forwarded raw: see resolveTitleReasoning().
|
|
209
|
+
const reasoning = resolveTitleReasoning(settings.reasoning);
|
|
156
210
|
const stream = ctx.modelRegistry.streamSimple(
|
|
157
211
|
model,
|
|
158
212
|
{
|
|
@@ -165,7 +219,7 @@ export async function generateTitle(
|
|
|
165
219
|
],
|
|
166
220
|
},
|
|
167
221
|
{
|
|
168
|
-
reasoning:
|
|
222
|
+
...(reasoning !== null ? { reasoning } : {}),
|
|
169
223
|
cacheRetention: "none",
|
|
170
224
|
sessionId: crypto.randomUUID(),
|
|
171
225
|
},
|
|
@@ -177,6 +231,7 @@ export async function generateTitle(
|
|
|
177
231
|
return;
|
|
178
232
|
}
|
|
179
233
|
if (response.stopReason === "error") {
|
|
234
|
+
console.error("[archimedes] session-name failed:", response.errorMessage ?? "title request failed");
|
|
180
235
|
onFailure();
|
|
181
236
|
return;
|
|
182
237
|
}
|
|
@@ -203,6 +258,8 @@ export async function generateTitle(
|
|
|
203
258
|
pi.setSessionName(title);
|
|
204
259
|
onSuccess();
|
|
205
260
|
} catch (e) {
|
|
261
|
+
// Session replacement or print-mode teardown makes an in-flight title irrelevant.
|
|
262
|
+
if (e instanceof Error && e.message.includes("stale after session replacement")) return;
|
|
206
263
|
console.error("[archimedes] session-name failed:", e);
|
|
207
264
|
onFailure();
|
|
208
265
|
}
|
|
@@ -234,7 +291,15 @@ export function registerSessionName(pi: ExtensionAPI) {
|
|
|
234
291
|
|
|
235
292
|
// Fire-and-forget: spawn title generation in background so handler
|
|
236
293
|
// returns immediately and the UI becomes responsive.
|
|
237
|
-
void generateTitle(pi, ctx, () => { hasNamed = true; }, () => {
|
|
294
|
+
void generateTitle(pi, ctx, () => { hasNamed = true; }, () => {
|
|
295
|
+
failCount++;
|
|
296
|
+
if (failCount !== 3) return;
|
|
297
|
+
try {
|
|
298
|
+
if (ctx.hasUI && !pi.getSessionName()) {
|
|
299
|
+
ctx.ui.notify("Session naming failed 3 times. Check the model configuration or use /name.", "warning");
|
|
300
|
+
}
|
|
301
|
+
} catch { /* Reporting must not count as another naming failure. */ }
|
|
302
|
+
});
|
|
238
303
|
});
|
|
239
304
|
}
|
|
240
305
|
|