@pi-archimedes/session-name 2.10.4 → 2.11.0
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/package.json +5 -5
- package/src/index.test.ts +72 -80
- package/src/index.ts +9 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-archimedes/session-name",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
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.0"
|
|
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";
|
|
@@ -66,6 +67,16 @@ vi.mock("@pi-archimedes/core/settings-io", () => ({
|
|
|
66
67
|
loadConfig: vi.fn((_namespace, defaults) => defaults),
|
|
67
68
|
}));
|
|
68
69
|
|
|
70
|
+
// Diagnostics never go to the console (see packages/core/src/log.ts): a write
|
|
71
|
+
// there smears a fullscreen frame and is discarded inside a subagent child. So
|
|
72
|
+
// every "it said something / it said nothing" assertion below watches `diag`.
|
|
73
|
+
vi.mock("@pi-archimedes/core/log", async (importOriginal) => {
|
|
74
|
+
const real = await importOriginal<typeof import("@pi-archimedes/core/log")>();
|
|
75
|
+
return { ...real, diag: vi.fn() };
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const clearDiag = () => vi.mocked(diag).mockClear();
|
|
79
|
+
|
|
69
80
|
describe("generateTitle", () => {
|
|
70
81
|
it("generates title, trims quotes and whitespace, and calls pi.setSessionName", async () => {
|
|
71
82
|
const pi = createMockPi();
|
|
@@ -135,16 +146,12 @@ describe("generateTitle", () => {
|
|
|
135
146
|
it.each(["Minimal", " high ", "MAX"])("tolerates casing and padding: %s", async (raw) => {
|
|
136
147
|
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning: raw });
|
|
137
148
|
const ctx = createMockCtx({});
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
expect(log).not.toHaveBeenCalled();
|
|
145
|
-
} finally {
|
|
146
|
-
log.mockRestore();
|
|
147
|
-
}
|
|
149
|
+
clearDiag();
|
|
150
|
+
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
151
|
+
expect((ctx.modelRegistry.streamSimple as any).mock.calls[0][2].reasoning).toBe(
|
|
152
|
+
raw.trim().toLowerCase(),
|
|
153
|
+
);
|
|
154
|
+
expect(diag).not.toHaveBeenCalled();
|
|
148
155
|
});
|
|
149
156
|
|
|
150
157
|
it.each([["ultra"], ["off"], [""], [42], [true], [{}], [[]]])(
|
|
@@ -152,34 +159,28 @@ describe("generateTitle", () => {
|
|
|
152
159
|
async (raw) => {
|
|
153
160
|
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning: raw });
|
|
154
161
|
const ctx = createMockCtx({});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
log.mockRestore();
|
|
168
|
-
}
|
|
162
|
+
clearDiag();
|
|
163
|
+
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
164
|
+
|
|
165
|
+
const sent = (ctx.modelRegistry.streamSimple as any).mock.calls[0][2].reasoning;
|
|
166
|
+
// Invalid config behaves like absent config: the documented default.
|
|
167
|
+
expect(sent).toBe("minimal");
|
|
168
|
+
// The typo is invisible unless it is reported, and the report is the
|
|
169
|
+
// single log line naming the rejected value.
|
|
170
|
+
expect(diag).toHaveBeenCalledWith(
|
|
171
|
+
"session-name",
|
|
172
|
+
`[archimedes] session-name: ignoring unrecognized reasoning ${JSON.stringify(raw)}`,
|
|
173
|
+
);
|
|
169
174
|
},
|
|
170
175
|
);
|
|
171
176
|
|
|
172
177
|
it("sends no reasoning key when the value is null, and says nothing", async () => {
|
|
173
178
|
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning: null });
|
|
174
179
|
const ctx = createMockCtx({});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
expect(log).not.toHaveBeenCalled();
|
|
180
|
-
} finally {
|
|
181
|
-
log.mockRestore();
|
|
182
|
-
}
|
|
180
|
+
clearDiag();
|
|
181
|
+
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
182
|
+
expect((ctx.modelRegistry.streamSimple as any).mock.calls[0][2]).not.toHaveProperty("reasoning");
|
|
183
|
+
expect(diag).not.toHaveBeenCalled();
|
|
183
184
|
});
|
|
184
185
|
|
|
185
186
|
it("keeps the default when the setting is absent entirely, and says nothing", async () => {
|
|
@@ -187,26 +188,18 @@ describe("generateTitle", () => {
|
|
|
187
188
|
// "minimal" — the common path must stay silent. A regression that warned
|
|
188
189
|
// on every default-configured session would otherwise pass the suite.
|
|
189
190
|
const ctx = createMockCtx({});
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
expect(log).not.toHaveBeenCalled();
|
|
195
|
-
} finally {
|
|
196
|
-
log.mockRestore();
|
|
197
|
-
}
|
|
191
|
+
clearDiag();
|
|
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(diag).not.toHaveBeenCalled();
|
|
198
195
|
});
|
|
199
196
|
|
|
200
197
|
it("treats undefined like absent config, without warning", () => {
|
|
201
198
|
// Direct call: the generateTitle path can't produce this, since
|
|
202
199
|
// loadConfig fills the default in, but the helper is exported.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
expect(log).not.toHaveBeenCalled();
|
|
207
|
-
} finally {
|
|
208
|
-
log.mockRestore();
|
|
209
|
-
}
|
|
200
|
+
clearDiag();
|
|
201
|
+
expect(resolveTitleReasoning(undefined)).toBe("minimal");
|
|
202
|
+
expect(diag).not.toHaveBeenCalled();
|
|
210
203
|
});
|
|
211
204
|
|
|
212
205
|
// Pin the invariant against the arithmetic that makes this dangerous. Of
|
|
@@ -270,16 +263,16 @@ describe("generateTitle", () => {
|
|
|
270
263
|
pi.getSessionName.mockImplementation(() => { throw new Error(message); });
|
|
271
264
|
const onSuccess = vi.fn();
|
|
272
265
|
const onFailure = vi.fn();
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
expect(
|
|
280
|
-
} finally {
|
|
281
|
-
log.mockRestore();
|
|
266
|
+
clearDiag();
|
|
267
|
+
await generateTitle(pi as any, createMockCtx({}) as any, onSuccess, onFailure);
|
|
268
|
+
expect(onFailure).toHaveBeenCalledTimes(failures);
|
|
269
|
+
// A swallowed failure is only acceptable if it left a trace.
|
|
270
|
+
expect(diag).toHaveBeenCalledTimes(failures);
|
|
271
|
+
if (failures === 1) {
|
|
272
|
+
expect(diag).toHaveBeenCalledWith("session-name", expect.stringContaining(message));
|
|
282
273
|
}
|
|
274
|
+
expect(onSuccess).not.toHaveBeenCalled();
|
|
275
|
+
expect(pi.setSessionName).not.toHaveBeenCalled();
|
|
283
276
|
});
|
|
284
277
|
|
|
285
278
|
it("does not call onFailure when streamSimple returns stopReason === 'aborted'", async () => {
|
|
@@ -356,29 +349,28 @@ describe("registerSessionName", () => {
|
|
|
356
349
|
const error = { stopReason: "error", content: [], errorMessage: "400 boom" };
|
|
357
350
|
const notify = vi.fn();
|
|
358
351
|
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
|
-
}
|
|
352
|
+
clearDiag();
|
|
353
|
+
await agentEnd({}, ctx);
|
|
354
|
+
await agentEnd({}, ctx);
|
|
355
|
+
let finish!: (response: any) => void;
|
|
356
|
+
const pending = new Promise((resolve) => { finish = resolve; });
|
|
357
|
+
ctx.modelRegistry.streamSimple.mockReturnValueOnce({ result: vi.fn(() => pending) });
|
|
358
|
+
await agentEnd({}, ctx);
|
|
359
|
+
// Change the session/UI while the third request is still pending.
|
|
360
|
+
if (scenario === "named") pi.setSessionName("Manual name");
|
|
361
|
+
if (scenario === "stale") pi.getSessionName.mockImplementation(() => { throw new Error("stale context"); });
|
|
362
|
+
if (scenario === "stale UI") Object.defineProperty(ctx, "hasUI", { get: () => { throw new Error("stale UI"); } });
|
|
363
|
+
if (scenario === "broken UI") notify.mockImplementation(() => { throw new Error("UI failed"); });
|
|
364
|
+
finish(error);
|
|
365
|
+
await pending;
|
|
366
|
+
await agentEnd({}, ctx);
|
|
367
|
+
expect(ctx.modelRegistry.streamSimple).toHaveBeenCalledTimes(3);
|
|
368
|
+
// Every attempt is diagnosable…
|
|
369
|
+
expect(diag).toHaveBeenCalledTimes(3);
|
|
370
|
+
expect(diag).toHaveBeenCalledWith("session-name", "[archimedes] session-name failed: 400 boom");
|
|
371
|
+
// …but the user is told once, at the retry-budget boundary: three lines of
|
|
372
|
+
// console noise (or three notifications) is not a report, it is spam.
|
|
373
|
+
expect(notify).toHaveBeenCalledTimes(scenario === "UI" || scenario === "broken UI" ? 1 : 0);
|
|
374
|
+
if (scenario === "UI") expect(notify).toHaveBeenCalledWith(expect.stringContaining("/name"), "warning");
|
|
383
375
|
});
|
|
384
376
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { ThinkingLevel } from "@earendil-works/pi-ai";
|
|
3
3
|
import { loadConfig } from "@pi-archimedes/core/settings-io";
|
|
4
|
+
import { diag, describeError } from "@pi-archimedes/core/log";
|
|
4
5
|
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
5
6
|
|
|
6
7
|
// ── Config ──────────────────────────────────────────────────────────────────
|
|
@@ -60,10 +61,10 @@ export function resolveTitleReasoning(raw: unknown): ThinkingLevel | null {
|
|
|
60
61
|
if (match) return match;
|
|
61
62
|
}
|
|
62
63
|
if (raw !== undefined) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
);
|
|
64
|
+
// A typo in a hand-edited settings.json is invisible unless reported, but
|
|
65
|
+
// the report is for whoever debugs the config — the log file, not the
|
|
66
|
+
// console (which smears a fullscreen frame and is discarded in a child).
|
|
67
|
+
diag("session-name", `[archimedes] session-name: ignoring unrecognized reasoning ${JSON.stringify(raw)}`);
|
|
67
68
|
}
|
|
68
69
|
return DEFAULT_SESSION_NAME_CONFIG.reasoning ?? null;
|
|
69
70
|
}
|
|
@@ -231,7 +232,9 @@ export async function generateTitle(
|
|
|
231
232
|
return;
|
|
232
233
|
}
|
|
233
234
|
if (response.stopReason === "error") {
|
|
234
|
-
|
|
235
|
+
// The user's notice is the single one at the retry-budget boundary (see
|
|
236
|
+
// registerSessionName); one line per attempt goes to the log.
|
|
237
|
+
diag("session-name", `[archimedes] session-name failed: ${response.errorMessage ?? "title request failed"}`);
|
|
235
238
|
onFailure();
|
|
236
239
|
return;
|
|
237
240
|
}
|
|
@@ -260,7 +263,7 @@ export async function generateTitle(
|
|
|
260
263
|
} catch (e) {
|
|
261
264
|
// Session replacement or print-mode teardown makes an in-flight title irrelevant.
|
|
262
265
|
if (e instanceof Error && e.message.includes("stale after session replacement")) return;
|
|
263
|
-
|
|
266
|
+
diag("session-name", `[archimedes] session-name failed: ${describeError(e)}`);
|
|
264
267
|
onFailure();
|
|
265
268
|
}
|
|
266
269
|
}
|