@pi-archimedes/session-name 2.10.1 → 2.10.3
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 -1
- package/package.json +2 -2
- package/src/index.test.ts +71 -1
- package/src/index.ts +16 -2
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,7 @@ 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. Spell it exactly: an unrecognised value isn't validated, and on some providers it means *more* thinking rather than none. |
|
|
43
45
|
|
|
44
46
|
On/off is managed by the suite: toggle via `/plugins` (`archimedes.sessionName.enabled`, default on).
|
|
45
47
|
|
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.3",
|
|
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.3"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"@earendil-works/pi-coding-agent": ">=0.86.0",
|
package/src/index.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { loadConfig } from "@pi-archimedes/core/settings-io";
|
|
1
2
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { generateTitle } from "./index.js";
|
|
3
|
+
import { generateTitle, registerSessionName } from "./index.js";
|
|
3
4
|
|
|
4
5
|
function createMockPi(sessionName: string | undefined = undefined) {
|
|
5
6
|
let currentName = sessionName;
|
|
@@ -60,6 +61,10 @@ function createMockCtx(options: {
|
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
vi.mock("@pi-archimedes/core/settings-io", () => ({
|
|
65
|
+
loadConfig: vi.fn((_namespace, defaults) => defaults),
|
|
66
|
+
}));
|
|
67
|
+
|
|
63
68
|
describe("generateTitle", () => {
|
|
64
69
|
it("generates title, trims quotes and whitespace, and calls pi.setSessionName", async () => {
|
|
65
70
|
const pi = createMockPi();
|
|
@@ -101,6 +106,16 @@ describe("generateTitle", () => {
|
|
|
101
106
|
expect(onFailure).not.toHaveBeenCalled();
|
|
102
107
|
});
|
|
103
108
|
|
|
109
|
+
it.each(["low", null])("uses configured reasoning %s", async (reasoning) => {
|
|
110
|
+
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning });
|
|
111
|
+
const ctx = createMockCtx({});
|
|
112
|
+
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
113
|
+
|
|
114
|
+
const options = (ctx.modelRegistry.streamSimple as any).mock.calls[0][2];
|
|
115
|
+
if (reasoning === null) expect(options).not.toHaveProperty("reasoning");
|
|
116
|
+
else expect(options.reasoning).toBe(reasoning);
|
|
117
|
+
});
|
|
118
|
+
|
|
104
119
|
it("triggers onFailure when streamSimple returns stopReason === 'error'", async () => {
|
|
105
120
|
const pi = createMockPi();
|
|
106
121
|
const ctx = createMockCtx({
|
|
@@ -120,6 +135,26 @@ describe("generateTitle", () => {
|
|
|
120
135
|
expect(onFailure).toHaveBeenCalledTimes(1);
|
|
121
136
|
});
|
|
122
137
|
|
|
138
|
+
it.each([
|
|
139
|
+
["This extension ctx is stale after session replacement or reload.", 0],
|
|
140
|
+
["Unexpected lookup failure", 1],
|
|
141
|
+
] as const)("handles lookup errors: %s", async (message, failures) => {
|
|
142
|
+
const pi = createMockPi();
|
|
143
|
+
pi.getSessionName.mockImplementation(() => { throw new Error(message); });
|
|
144
|
+
const onSuccess = vi.fn();
|
|
145
|
+
const onFailure = vi.fn();
|
|
146
|
+
const log = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
147
|
+
try {
|
|
148
|
+
await generateTitle(pi as any, createMockCtx({}) as any, onSuccess, onFailure);
|
|
149
|
+
expect(onFailure).toHaveBeenCalledTimes(failures);
|
|
150
|
+
expect(log).toHaveBeenCalledTimes(failures);
|
|
151
|
+
expect(onSuccess).not.toHaveBeenCalled();
|
|
152
|
+
expect(pi.setSessionName).not.toHaveBeenCalled();
|
|
153
|
+
} finally {
|
|
154
|
+
log.mockRestore();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
123
158
|
it("does not call onFailure when streamSimple returns stopReason === 'aborted'", async () => {
|
|
124
159
|
const pi = createMockPi();
|
|
125
160
|
const ctx = createMockCtx({
|
|
@@ -185,3 +220,38 @@ describe("generateTitle", () => {
|
|
|
185
220
|
expect(onFailure).not.toHaveBeenCalled();
|
|
186
221
|
});
|
|
187
222
|
});
|
|
223
|
+
|
|
224
|
+
describe("registerSessionName", () => {
|
|
225
|
+
it.each(["UI", "no UI", "named", "stale", "stale UI", "broken UI"])("reports exhaustion safely: %s", async (scenario) => {
|
|
226
|
+
const pi = createMockPi();
|
|
227
|
+
registerSessionName(pi as any);
|
|
228
|
+
const agentEnd = pi.on.mock.calls.find(([event]) => event === "agent_end")![1];
|
|
229
|
+
const error = { stopReason: "error", content: [], errorMessage: "400 boom" };
|
|
230
|
+
const notify = vi.fn();
|
|
231
|
+
const ctx = { ...createMockCtx({ streamSimpleResult: error }), hasUI: scenario !== "no UI", ui: { notify } };
|
|
232
|
+
const log = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
233
|
+
try {
|
|
234
|
+
await agentEnd({}, ctx);
|
|
235
|
+
await agentEnd({}, ctx);
|
|
236
|
+
let finish!: (response: any) => void;
|
|
237
|
+
const pending = new Promise((resolve) => { finish = resolve; });
|
|
238
|
+
ctx.modelRegistry.streamSimple.mockReturnValueOnce({ result: vi.fn(() => pending) });
|
|
239
|
+
await agentEnd({}, ctx);
|
|
240
|
+
// Change the session/UI while the third request is still pending.
|
|
241
|
+
if (scenario === "named") pi.setSessionName("Manual name");
|
|
242
|
+
if (scenario === "stale") pi.getSessionName.mockImplementation(() => { throw new Error("stale context"); });
|
|
243
|
+
if (scenario === "stale UI") Object.defineProperty(ctx, "hasUI", { get: () => { throw new Error("stale UI"); } });
|
|
244
|
+
if (scenario === "broken UI") notify.mockImplementation(() => { throw new Error("UI failed"); });
|
|
245
|
+
finish(error);
|
|
246
|
+
await pending;
|
|
247
|
+
await agentEnd({}, ctx);
|
|
248
|
+
expect(ctx.modelRegistry.streamSimple).toHaveBeenCalledTimes(3);
|
|
249
|
+
expect(log).toHaveBeenCalledTimes(3);
|
|
250
|
+
expect(log).toHaveBeenCalledWith("[archimedes] session-name failed:", "400 boom");
|
|
251
|
+
expect(notify).toHaveBeenCalledTimes(scenario === "UI" || scenario === "broken UI" ? 1 : 0);
|
|
252
|
+
if (scenario === "UI") expect(notify).toHaveBeenCalledWith(expect.stringContaining("/name"), "warning");
|
|
253
|
+
} finally {
|
|
254
|
+
log.mockRestore();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
});
|
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,10 +9,12 @@ 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";
|
|
@@ -165,7 +168,7 @@ export async function generateTitle(
|
|
|
165
168
|
],
|
|
166
169
|
},
|
|
167
170
|
{
|
|
168
|
-
reasoning:
|
|
171
|
+
...(settings.reasoning != null ? { reasoning: settings.reasoning } : {}),
|
|
169
172
|
cacheRetention: "none",
|
|
170
173
|
sessionId: crypto.randomUUID(),
|
|
171
174
|
},
|
|
@@ -177,6 +180,7 @@ export async function generateTitle(
|
|
|
177
180
|
return;
|
|
178
181
|
}
|
|
179
182
|
if (response.stopReason === "error") {
|
|
183
|
+
console.error("[archimedes] session-name failed:", response.errorMessage ?? "title request failed");
|
|
180
184
|
onFailure();
|
|
181
185
|
return;
|
|
182
186
|
}
|
|
@@ -203,6 +207,8 @@ export async function generateTitle(
|
|
|
203
207
|
pi.setSessionName(title);
|
|
204
208
|
onSuccess();
|
|
205
209
|
} catch (e) {
|
|
210
|
+
// Session replacement or print-mode teardown makes an in-flight title irrelevant.
|
|
211
|
+
if (e instanceof Error && e.message.includes("stale after session replacement")) return;
|
|
206
212
|
console.error("[archimedes] session-name failed:", e);
|
|
207
213
|
onFailure();
|
|
208
214
|
}
|
|
@@ -234,7 +240,15 @@ export function registerSessionName(pi: ExtensionAPI) {
|
|
|
234
240
|
|
|
235
241
|
// Fire-and-forget: spawn title generation in background so handler
|
|
236
242
|
// returns immediately and the UI becomes responsive.
|
|
237
|
-
void generateTitle(pi, ctx, () => { hasNamed = true; }, () => {
|
|
243
|
+
void generateTitle(pi, ctx, () => { hasNamed = true; }, () => {
|
|
244
|
+
failCount++;
|
|
245
|
+
if (failCount !== 3) return;
|
|
246
|
+
try {
|
|
247
|
+
if (ctx.hasUI && !pi.getSessionName()) {
|
|
248
|
+
ctx.ui.notify("Session naming failed 3 times. Check the model configuration or use /name.", "warning");
|
|
249
|
+
}
|
|
250
|
+
} catch { /* Reporting must not count as another naming failure. */ }
|
|
251
|
+
});
|
|
238
252
|
});
|
|
239
253
|
}
|
|
240
254
|
|