@selesai/code 0.13.25 → 0.13.26
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/CHANGELOG.md +11 -0
- package/dist/core/agent-session-auto-handoff.test.js +12 -3
- package/dist/core/agent-session.js +5 -2
- package/dist/core/handoff.d.ts +1 -0
- package/dist/core/handoff.js +1 -0
- package/dist/defaults/models.json +2 -72
- package/dist/extensions/handoff-new.test.ts +13 -13
- package/dist/extensions/handoff-new.ts +17 -6
- package/dist/modes/rpc/rpc-mode.js +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@selesai/code` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.13.26] - 2026-09-18
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Goal-preserving automatic handoff.** The automatic handoff now runs with the same goal text as an explicit `/handoff-new`, so the continuation prompt keeps the overarching objective, success criteria, key decisions, reference files, and the next concrete action. `AUTO_HANDOFF_GOAL` is exported from the core handoff module.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **Failed handoffs retry instead of silently stopping.** A failed or empty handoff generation now surfaces the underlying error (previously reported as "Cancelled") and rejects the command, and the automatic handoff retries on the next settled turn while the session stays above the threshold.
|
|
12
|
+
- **RPC handoff keeps the session name.** `handoff_new` carries the current session name into the replacement session, matching `/handoff-new`.
|
|
13
|
+
- **Default model catalog refreshed.** Added `glm-5.3` (1M context, text+image), `kimi-k3` (512K), and `gemini-3.7-flash`; `celestial-pro` now accepts images; `deepseek-v4.1-flash` uses 64K max output tokens.
|
|
14
|
+
- **Retired default models.** `deepseek-v4-flash`, `deepseek-v4-flash-vision-exp`, `kimi-k2.7-code`, and `glm-5.2` are no longer in the bundled Token-In catalog (`glm-5.3` supersedes `glm-5.2`). User-level `models.json` entries are unaffected.
|
|
15
|
+
|
|
5
16
|
## [0.13.25] - 2026-09-14
|
|
6
17
|
|
|
7
18
|
### Added
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import { AgentSession } from "./agent-session.js";
|
|
4
|
+
import { AUTO_HANDOFF_GOAL } from "./handoff.js";
|
|
4
5
|
import { SettingsManager } from "./settings-manager.js";
|
|
5
6
|
function createMockSession({ enabled, threshold = 128_000, tokens, mode = "tui", command, customTools, allowedToolNames, }) {
|
|
6
7
|
const settings = SettingsManager.inMemory({
|
|
@@ -78,6 +79,11 @@ describe("AgentSession tools", () => {
|
|
|
78
79
|
});
|
|
79
80
|
});
|
|
80
81
|
describe("AgentSession auto handoff", () => {
|
|
82
|
+
it("uses a goal that preserves the overall objective and continuation details", () => {
|
|
83
|
+
expect(AUTO_HANDOFF_GOAL).toMatch(/overall objective/i);
|
|
84
|
+
expect(AUTO_HANDOFF_GOAL).toMatch(/next concrete action/i);
|
|
85
|
+
expect(AUTO_HANDOFF_GOAL).toMatch(/reference files/i);
|
|
86
|
+
});
|
|
81
87
|
it("invokes handoff-new once when enabled and threshold reached in tui mode", async () => {
|
|
82
88
|
const command = { handler: vi.fn() };
|
|
83
89
|
const { session, getCommand, createCommandContext, emitError } = createMockSession({
|
|
@@ -90,7 +96,7 @@ describe("AgentSession auto handoff", () => {
|
|
|
90
96
|
expect(getCommand).toHaveBeenCalledWith("handoff-new");
|
|
91
97
|
expect(createCommandContext).toHaveBeenCalled();
|
|
92
98
|
expect(command.handler).toHaveBeenCalledTimes(1);
|
|
93
|
-
expect(command.handler).toHaveBeenCalledWith(
|
|
99
|
+
expect(command.handler).toHaveBeenCalledWith(AUTO_HANDOFF_GOAL, expect.anything());
|
|
94
100
|
expect(emitError).not.toHaveBeenCalled();
|
|
95
101
|
});
|
|
96
102
|
it("dispatches auto handoff via _emitAgentSettled in tui mode", async () => {
|
|
@@ -156,13 +162,16 @@ describe("AgentSession auto handoff", () => {
|
|
|
156
162
|
await session._checkAutoHandoff();
|
|
157
163
|
expect(getCommand).toHaveBeenCalledWith("handoff-new");
|
|
158
164
|
});
|
|
159
|
-
it("emits error when handoff-new
|
|
165
|
+
it("emits an error and retries on the next check when handoff-new fails", async () => {
|
|
160
166
|
const command = {
|
|
161
167
|
handler: vi.fn(() => Promise.reject(new Error("boom"))),
|
|
162
168
|
};
|
|
163
169
|
const { session, emitError } = createMockSession({ enabled: true, tokens: 200_000, command });
|
|
164
170
|
await expect(session._checkAutoHandoff()).resolves.toBeUndefined();
|
|
165
|
-
expect(
|
|
171
|
+
await expect(session._checkAutoHandoff()).resolves.toBeUndefined();
|
|
172
|
+
expect(command.handler).toHaveBeenCalledTimes(2);
|
|
173
|
+
expect(emitError).toHaveBeenCalledTimes(2);
|
|
174
|
+
expect(emitError).toHaveBeenLastCalledWith({
|
|
166
175
|
extensionPath: "command:handoff-new",
|
|
167
176
|
event: "auto-handoff",
|
|
168
177
|
error: "boom",
|
|
@@ -27,6 +27,7 @@ import { calculateContextTokens, collectEntriesForBranchSummary, compact, estima
|
|
|
27
27
|
import { DEFAULT_THINKING_LEVEL, THINKING_LEVEL_OPTIONS } from "./defaults.js";
|
|
28
28
|
import { exportSessionToHtml } from "./export-html/index.js";
|
|
29
29
|
import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
|
|
30
|
+
import { AUTO_HANDOFF_GOAL } from "./handoff.js";
|
|
30
31
|
import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
|
|
31
32
|
import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
|
32
33
|
import { ModelRegistry } from "./model-registry.js";
|
|
@@ -375,7 +376,6 @@ export class AgentSession {
|
|
|
375
376
|
}
|
|
376
377
|
if (this._autoHandoffTriggered)
|
|
377
378
|
return;
|
|
378
|
-
this._autoHandoffTriggered = true;
|
|
379
379
|
// Auto-handoff is TUI-only: it runs at agent_settled, and headless/print
|
|
380
380
|
// modes should not churn sessions on their own. The handoff-new command
|
|
381
381
|
// itself now also works in non-TUI modes (direct generation, no loader).
|
|
@@ -384,11 +384,14 @@ export class AgentSession {
|
|
|
384
384
|
const command = this._extensionRunner.getCommand("handoff-new");
|
|
385
385
|
if (!command)
|
|
386
386
|
return;
|
|
387
|
+
this._autoHandoffTriggered = true;
|
|
387
388
|
const ctx = this._extensionRunner.createCommandContext();
|
|
388
389
|
try {
|
|
389
|
-
await command.handler(
|
|
390
|
+
await command.handler(AUTO_HANDOFF_GOAL, ctx);
|
|
390
391
|
}
|
|
391
392
|
catch (err) {
|
|
393
|
+
// Retry on the next settled turn while the session remains above the threshold.
|
|
394
|
+
this._autoHandoffTriggered = false;
|
|
392
395
|
this._extensionRunner.emitError({
|
|
393
396
|
extensionPath: "command:handoff-new",
|
|
394
397
|
event: "auto-handoff",
|
package/dist/core/handoff.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { AssistantMessage, Context } from "@earendil-works/pi-ai";
|
|
|
3
3
|
import type { Model } from "@earendil-works/pi-ai";
|
|
4
4
|
import type { SessionEntry } from "./session-manager.ts";
|
|
5
5
|
export declare const DEFAULT_HANDOFF_GOAL = "Continue the previous session from this handoff.";
|
|
6
|
+
export declare const AUTO_HANDOFF_GOAL = "Continue the previous session without losing its direction. Preserve the big picture: the overall objective, why the work matters, the intended outcome and success criteria, the current approach and key decisions, relevant constraints, exact reference files or artifacts, completed and remaining work, known failures or test results, and the next concrete action. Make clear how the immediate next step contributes to the overall goal.";
|
|
6
7
|
export declare function entryToMessage(entry: SessionEntry): AgentMessage | undefined;
|
|
7
8
|
export declare function getHandoffMessages(branch: SessionEntry[]): AgentMessage[];
|
|
8
9
|
export declare function buildAiContext(conversationText: string, goal: string): Context;
|
package/dist/core/handoff.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { serializeConversation } from "./compaction/utils.js";
|
|
2
2
|
import { convertToLlm } from "./messages.js";
|
|
3
3
|
export const DEFAULT_HANDOFF_GOAL = "Continue the previous session from this handoff.";
|
|
4
|
+
export const AUTO_HANDOFF_GOAL = `Continue the previous session without losing its direction. Preserve the big picture: the overall objective, why the work matters, the intended outcome and success criteria, the current approach and key decisions, relevant constraints, exact reference files or artifacts, completed and remaining work, known failures or test results, and the next concrete action. Make clear how the immediate next step contributes to the overall goal.`;
|
|
4
5
|
const SYSTEM_PROMPT = `Write a handoff document for a fresh agent to continue the current conversation. Return only the handoff document text; do not save a file or describe saving one. No question, no fluff, just write the handoff. Do not reproduce, quote, or reformat the conversation history or its tool calls — distill it into what matters; never emit transcript-style markup or raw tool-call text.
|
|
5
6
|
|
|
6
7
|
Include a "suggested skills" section in the document, which suggests skills that the agent should invoke.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"id": "celestial-pro",
|
|
15
15
|
"name": "Celestial Pro",
|
|
16
16
|
"reasoning": true,
|
|
17
|
-
"input": ["text"],
|
|
17
|
+
"input": ["text", "image"],
|
|
18
18
|
"contextWindow": 393216,
|
|
19
19
|
"maxTokens": 64000,
|
|
20
20
|
"thinkingLevelMap": {
|
|
@@ -67,51 +67,13 @@
|
|
|
67
67
|
"requiresReasoningContentOnAssistantMessages": true
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
|
-
{
|
|
71
|
-
"id": "deepseek-v4-flash",
|
|
72
|
-
"name": "DeepSeek V4 Flash",
|
|
73
|
-
"reasoning": true,
|
|
74
|
-
"input": ["text"],
|
|
75
|
-
"contextWindow": 512000,
|
|
76
|
-
"maxTokens": 64000,
|
|
77
|
-
"thinkingLevelMap": {
|
|
78
|
-
"minimal": null,
|
|
79
|
-
"low": null,
|
|
80
|
-
"medium": null,
|
|
81
|
-
"high": "high",
|
|
82
|
-
"xhigh": null,
|
|
83
|
-
"max": "max"
|
|
84
|
-
},
|
|
85
|
-
"compat": {
|
|
86
|
-
"requiresReasoningContentOnAssistantMessages": true
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
"id": "deepseek-v4-flash-vision-exp",
|
|
91
|
-
"name": "DeepSeek V4 Flash Vision Exp",
|
|
92
|
-
"reasoning": true,
|
|
93
|
-
"input": ["text", "image"],
|
|
94
|
-
"contextWindow": 512000,
|
|
95
|
-
"maxTokens": 128000,
|
|
96
|
-
"thinkingLevelMap": {
|
|
97
|
-
"minimal": null,
|
|
98
|
-
"low": null,
|
|
99
|
-
"medium": null,
|
|
100
|
-
"high": "high",
|
|
101
|
-
"xhigh": null,
|
|
102
|
-
"max": "max"
|
|
103
|
-
},
|
|
104
|
-
"compat": {
|
|
105
|
-
"requiresReasoningContentOnAssistantMessages": true
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
70
|
{
|
|
109
71
|
"id": "deepseek-v4.1-flash",
|
|
110
72
|
"name": "DeepSeek V4.1 Flash",
|
|
111
73
|
"reasoning": true,
|
|
112
74
|
"input": ["text", "image"],
|
|
113
75
|
"contextWindow": 512000,
|
|
114
|
-
"maxTokens":
|
|
76
|
+
"maxTokens": 64000,
|
|
115
77
|
"thinkingLevelMap": {
|
|
116
78
|
"minimal": null,
|
|
117
79
|
"low": null,
|
|
@@ -207,38 +169,6 @@
|
|
|
207
169
|
"max": "max"
|
|
208
170
|
}
|
|
209
171
|
},
|
|
210
|
-
{
|
|
211
|
-
"id": "kimi-k2.7-code",
|
|
212
|
-
"name": "Kimi K2.7 Code",
|
|
213
|
-
"reasoning": true,
|
|
214
|
-
"input": ["text", "image"],
|
|
215
|
-
"contextWindow": 262128,
|
|
216
|
-
"maxTokens": 64000,
|
|
217
|
-
"thinkingLevelMap": {
|
|
218
|
-
"minimal": null,
|
|
219
|
-
"low": "low",
|
|
220
|
-
"medium": null,
|
|
221
|
-
"high": "high",
|
|
222
|
-
"xhigh": null,
|
|
223
|
-
"max": "max"
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
"id": "glm-5.2",
|
|
228
|
-
"name": "GLM-5.2",
|
|
229
|
-
"reasoning": true,
|
|
230
|
-
"input": ["text"],
|
|
231
|
-
"contextWindow": 1000000,
|
|
232
|
-
"maxTokens": 64000,
|
|
233
|
-
"thinkingLevelMap": {
|
|
234
|
-
"minimal": null,
|
|
235
|
-
"low": "low",
|
|
236
|
-
"medium": null,
|
|
237
|
-
"high": "high",
|
|
238
|
-
"xhigh": null,
|
|
239
|
-
"max": null
|
|
240
|
-
}
|
|
241
|
-
},
|
|
242
172
|
{
|
|
243
173
|
"id": "glm-5.3-flash",
|
|
244
174
|
"name": "GLM-5.3 Flash",
|
|
@@ -226,11 +226,11 @@ describe("handoff-new extension", () => {
|
|
|
226
226
|
expect(calls.newSession).not.toBeNull();
|
|
227
227
|
});
|
|
228
228
|
|
|
229
|
-
it("non-tui mode generation error notifies and opens no session", async () => {
|
|
229
|
+
it("non-tui mode generation error notifies, rejects, and opens no session", async () => {
|
|
230
230
|
(complete as any).mockRejectedValueOnce(new Error("api down"));
|
|
231
231
|
const { commands } = createPiHarness();
|
|
232
232
|
const { ctx, calls } = createCtx({ mode: "rpc" });
|
|
233
|
-
await commands.get("handoff-new")!.handler("goal", ctx);
|
|
233
|
+
await expect(commands.get("handoff-new")!.handler("goal", ctx)).rejects.toThrow("api down");
|
|
234
234
|
expect(calls.newSession).toBeNull();
|
|
235
235
|
expect(calls.notify.some((n) => /api down/.test(n.msg))).toBe(true);
|
|
236
236
|
});
|
|
@@ -259,10 +259,10 @@ describe("handoff-new extension", () => {
|
|
|
259
259
|
expect(calls.notify.some((n) => /Cancelled/.test(n.msg))).toBe(true);
|
|
260
260
|
});
|
|
261
261
|
|
|
262
|
-
it("empty generated handoff
|
|
262
|
+
it("empty generated handoff notifies, rejects, and opens no blank session", async () => {
|
|
263
263
|
const { commands } = createPiHarness();
|
|
264
264
|
const { ctx, calls } = createCtx({ customResult: " " });
|
|
265
|
-
await commands.get("handoff-new")!.handler("goal", ctx);
|
|
265
|
+
await expect(commands.get("handoff-new")!.handler("goal", ctx)).rejects.toThrow("returned no text");
|
|
266
266
|
expect(calls.newSession).toBeNull();
|
|
267
267
|
expect(calls.notify.some((n) => /returned no text/.test(n.msg))).toBe(true);
|
|
268
268
|
});
|
|
@@ -358,29 +358,29 @@ describe("handoff-new extension", () => {
|
|
|
358
358
|
expect(calls.notify.some((n) => /Cancelled/.test(n.msg))).toBe(true);
|
|
359
359
|
});
|
|
360
360
|
|
|
361
|
-
it("factory: missing API key
|
|
361
|
+
it("factory: missing API key notifies and rejects", async () => {
|
|
362
362
|
const { commands } = createPiHarness();
|
|
363
363
|
const { ctx, calls } = createFactoryCtx({ auth: { ok: true, error: "no key" } });
|
|
364
|
-
await commands.get("handoff-new")!.handler("goal", ctx);
|
|
364
|
+
await expect(commands.get("handoff-new")!.handler("goal", ctx)).rejects.toThrow("No API key");
|
|
365
365
|
expect(calls.newSession).toBeNull();
|
|
366
|
-
expect(calls.notify.some((n) => /
|
|
366
|
+
expect(calls.notify.some((n) => /No API key/.test(n.msg))).toBe(true);
|
|
367
367
|
});
|
|
368
368
|
|
|
369
|
-
it("factory: auth error
|
|
369
|
+
it("factory: auth error notifies and rejects", async () => {
|
|
370
370
|
const { commands } = createPiHarness();
|
|
371
371
|
const { ctx, calls } = createFactoryCtx({ auth: { ok: false, error: "auth failed" } });
|
|
372
|
-
await commands.get("handoff-new")!.handler("goal", ctx);
|
|
372
|
+
await expect(commands.get("handoff-new")!.handler("goal", ctx)).rejects.toThrow("auth failed");
|
|
373
373
|
expect(calls.newSession).toBeNull();
|
|
374
|
-
expect(calls.notify.some((n) => /
|
|
374
|
+
expect(calls.notify.some((n) => /auth failed/.test(n.msg))).toBe(true);
|
|
375
375
|
});
|
|
376
376
|
|
|
377
|
-
it("factory: complete rejection
|
|
377
|
+
it("factory: complete rejection notifies and rejects", async () => {
|
|
378
378
|
(complete as any).mockRejectedValueOnce(new Error("network down"));
|
|
379
379
|
const { commands } = createPiHarness();
|
|
380
380
|
const { ctx, calls } = createFactoryCtx({});
|
|
381
|
-
await commands.get("handoff-new")!.handler("goal", ctx);
|
|
381
|
+
await expect(commands.get("handoff-new")!.handler("goal", ctx)).rejects.toThrow("network down");
|
|
382
382
|
expect(calls.newSession).toBeNull();
|
|
383
|
-
expect(calls.notify.some((n) => /
|
|
383
|
+
expect(calls.notify.some((n) => /network down/.test(n.msg))).toBe(true);
|
|
384
384
|
});
|
|
385
385
|
|
|
386
386
|
it("factory: loader onAbort resolves null", async () => {
|
|
@@ -74,15 +74,20 @@ async function handoffNew(args: string, ctx: ExtensionCommandContext) {
|
|
|
74
74
|
const aiContext = buildAiContext(conversationText, goal);
|
|
75
75
|
|
|
76
76
|
let result: string | null;
|
|
77
|
+
let generationError: unknown;
|
|
77
78
|
if (ctx.mode === "tui") {
|
|
79
|
+
let cancelled = false;
|
|
78
80
|
result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
79
81
|
const loader = new BorderedLoader(tui, theme, `Generating handoff prompt...`);
|
|
80
|
-
loader.onAbort = () =>
|
|
82
|
+
loader.onAbort = () => {
|
|
83
|
+
cancelled = true;
|
|
84
|
+
done(null);
|
|
85
|
+
};
|
|
81
86
|
|
|
82
87
|
generateHandoffText(ctx, aiContext, loader.signal)
|
|
83
88
|
.then(done)
|
|
84
89
|
.catch((err) => {
|
|
85
|
-
|
|
90
|
+
if (!cancelled) generationError = err;
|
|
86
91
|
done(null);
|
|
87
92
|
});
|
|
88
93
|
|
|
@@ -94,18 +99,24 @@ async function handoffNew(args: string, ctx: ExtensionCommandContext) {
|
|
|
94
99
|
try {
|
|
95
100
|
result = await generateHandoffText(ctx, aiContext, ctx.signal);
|
|
96
101
|
} catch (err) {
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
generationError = err;
|
|
103
|
+
result = null;
|
|
99
104
|
}
|
|
100
105
|
}
|
|
101
106
|
|
|
107
|
+
if (generationError !== undefined) {
|
|
108
|
+
const error = generationError instanceof Error ? generationError : new Error(String(generationError));
|
|
109
|
+
ctx.ui.notify(error.message, "error");
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
102
112
|
if (result === null) {
|
|
103
113
|
ctx.ui.notify("Cancelled", "info");
|
|
104
114
|
return;
|
|
105
115
|
}
|
|
106
116
|
if (!result.trim()) {
|
|
107
|
-
|
|
108
|
-
|
|
117
|
+
const error = new Error("Handoff generation returned no text");
|
|
118
|
+
ctx.ui.notify(error.message, "error");
|
|
119
|
+
throw error;
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
// Read the name before newSession: the original ctx is invalidated once
|
|
@@ -466,8 +466,13 @@ export async function runRpcMode(runtimeHost) {
|
|
|
466
466
|
return error(id, "handoff_new", "No model selected");
|
|
467
467
|
try {
|
|
468
468
|
const handoff = await generateHandoff(session.model, session.modelRuntime, session.sessionManager.getBranch(), command.goal);
|
|
469
|
+
const sessionName = session.sessionManager.getSessionName();
|
|
469
470
|
const result = await runtimeHost.newSession({
|
|
470
471
|
parentSession: session.sessionManager.getSessionFile(),
|
|
472
|
+
setup: async (replacementManager) => {
|
|
473
|
+
if (sessionName)
|
|
474
|
+
replacementManager.appendSessionInfo(sessionName);
|
|
475
|
+
},
|
|
471
476
|
withSession: async (replacementSession) => replacementSession.sendUserMessage(handoff),
|
|
472
477
|
});
|
|
473
478
|
if (!result.cancelled)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selesai/code",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.26",
|
|
4
4
|
"description": "Maintained, extension-first Pi coding agent with built-in workflows, subagents, web research, questions, skills, and an enhanced terminal UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|