@selesai/code 0.13.24 → 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 +19 -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/extensions/pi-graft/commands.test.ts +1 -1
- package/dist/extensions/pi-graft/commands.ts +1 -1
- package/dist/extensions/pi-graft/index.ts +135 -36
- package/dist/extensions/pi-graft/lifecycle.test.ts +78 -5
- package/dist/extensions/pi-graft/state.test.ts +6 -0
- package/dist/extensions/pi-graft/state.ts +6 -0
- package/dist/modes/rpc/rpc-mode.js +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
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
|
+
|
|
16
|
+
## [0.13.25] - 2026-09-14
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **Graft deep-build model override.** Set `graft.deepModel` to pin a model on the active provider for Graft deep builds.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- **Graft deep-build fallback.** When the active model returns no usable Graft summaries, the deep build retries up to two other models scoped to that provider before reporting the deep-build failure.
|
|
23
|
+
|
|
5
24
|
## [0.13.24] - 2026-09-13
|
|
6
25
|
|
|
7
26
|
### 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
|
|
@@ -387,7 +387,7 @@ describe("mutation-capable commands", () => {
|
|
|
387
387
|
const [title, message] = ctx.ui.confirm.mock.calls[0] as [string, string];
|
|
388
388
|
expect(title).toContain("provider-backed");
|
|
389
389
|
expect(message).toContain("leaves this machine");
|
|
390
|
-
expect(message).toContain("
|
|
390
|
+
expect(message).toContain("Credentials are passed only to this Graft child process");
|
|
391
391
|
expect(harness.runBuild).toHaveBeenCalledWith(true, ctx);
|
|
392
392
|
});
|
|
393
393
|
|
|
@@ -330,7 +330,7 @@ export function registerGraftCommands(pi: ExtensionAPI, runtime: GraftCommandRun
|
|
|
330
330
|
? "Set up Graft for this repository?"
|
|
331
331
|
: "Build the Graft graph?",
|
|
332
332
|
deep
|
|
333
|
-
? `This runs \`graft build --deep\` in ${repo}.\n\nThe structural graph stays local and deterministic. The deep pass summarizes each changed file and extracts per-symbol cruxes using Selesai's active compatible model,
|
|
333
|
+
? `This runs \`graft build --deep\` in ${repo}.\n\nThe structural graph stays local and deterministic. The deep pass summarizes each changed file and extracts per-symbol cruxes using Selesai's active compatible model, falling back to another model you scoped on the same provider when that model cannot produce Graft summaries. Source-derived content leaves this machine either way. Credentials are passed only to this Graft child process.\n\nFiles it can write:\n${buildEffects(true)}`
|
|
334
334
|
: `This runs \`graft build\` in ${repo} and can write:\n\n${buildEffects(false)}`,
|
|
335
335
|
manualCommand,
|
|
336
336
|
);
|
|
@@ -183,32 +183,94 @@ const GRAFT_PROVIDER_FOR_SELESAI_PROVIDER: Record<string, "openai" | "anthropic"
|
|
|
183
183
|
tokenin: "litellm",
|
|
184
184
|
};
|
|
185
185
|
|
|
186
|
-
/**
|
|
187
|
-
|
|
186
|
+
/**
|
|
187
|
+
* The deep tier either has a plan, or a human-readable reason it does not.
|
|
188
|
+
* Automatic builds fall back to the structural tier on `ok: false`; the
|
|
189
|
+
* explicit `/graft deep` command reports the reason instead.
|
|
190
|
+
*/
|
|
191
|
+
type DeepBuildPlan =
|
|
192
|
+
| { ok: true; env: NodeJS.ProcessEnv | undefined; models: readonly string[] }
|
|
193
|
+
| { ok: false; reason: string };
|
|
194
|
+
|
|
195
|
+
/** Graft's per-symbol summary pass is model-sensitive; this is its failure signature. */
|
|
196
|
+
const UNUSABLE_SUMMARIES = /no usable symbol summaries|empty-parsed/i;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Deep-build candidates in order: an explicit `graft.deepModel` pin, the active
|
|
200
|
+
* model, then the models this session scoped on the same provider. No new
|
|
201
|
+
* setting is needed — models the user already chose are tried before giving up
|
|
202
|
+
* on the deep tier.
|
|
203
|
+
*/
|
|
204
|
+
function deepBuildModels(ctx: ExtensionContext, settings: { deepModel?: string }): readonly string[] {
|
|
205
|
+
const active = ctx.model;
|
|
206
|
+
const scoped = (ctx.scopedModels ?? [])
|
|
207
|
+
.map((entry) => entry.model)
|
|
208
|
+
.filter((candidate) => candidate.provider === active?.provider)
|
|
209
|
+
.map((candidate) => candidate.id);
|
|
210
|
+
const ordered = [settings.deepModel, active?.id, ...scoped].filter((id): id is string => Boolean(id));
|
|
211
|
+
return [...new Set(ordered)].slice(0, 3);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Resolve the active Selesai model into the isolated environment Graft needs.
|
|
216
|
+
*
|
|
217
|
+
* Credentials are read from Selesai's own auth storage — TokenIn keys live in
|
|
218
|
+
* `~/.selesai/agent`, not in the shell environment — and passed only to the
|
|
219
|
+
* Graft child process.
|
|
220
|
+
*/
|
|
221
|
+
async function resolveDeepBuildPlan(
|
|
222
|
+
ctx: ExtensionContext,
|
|
223
|
+
settings: { deepModel?: string } = {},
|
|
224
|
+
): Promise<DeepBuildPlan> {
|
|
225
|
+
const models = deepBuildModels(ctx, settings);
|
|
226
|
+
// A user who already configured Graft's own provider environment outranks us.
|
|
227
|
+
if (process.env.GRAFT_PROVIDER && process.env.GRAFT_API_KEY) return { ok: true, env: undefined, models };
|
|
228
|
+
|
|
188
229
|
const model = ctx.model;
|
|
189
|
-
|
|
190
|
-
|
|
230
|
+
if (!model) return { ok: false, reason: "no active model is selected" };
|
|
231
|
+
const provider = GRAFT_PROVIDER_FOR_SELESAI_PROVIDER[model.provider];
|
|
232
|
+
if (!provider) {
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
reason: `provider "${model.provider}" has no Graft wire format (use TokenIn, LiteLLM, OpenAI, Anthropic, or OrcaRouter)`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
191
238
|
|
|
192
|
-
let
|
|
239
|
+
let apiKey: string | undefined;
|
|
240
|
+
let authBaseUrl: string | undefined;
|
|
193
241
|
try {
|
|
194
|
-
auth = await ctx.modelRegistry.getProviderAuth(model.provider);
|
|
242
|
+
const auth = await ctx.modelRegistry.getProviderAuth(model.provider);
|
|
243
|
+
apiKey = auth?.auth.apiKey;
|
|
244
|
+
authBaseUrl = auth?.auth.baseUrl;
|
|
195
245
|
} catch {
|
|
196
|
-
|
|
246
|
+
// Fall through to the missing-credential reason below.
|
|
247
|
+
}
|
|
248
|
+
if (!apiKey) return { ok: false, reason: `no credential is stored for "${model.provider}"` };
|
|
249
|
+
|
|
250
|
+
// The composed model carries the endpoint; auth only sometimes does.
|
|
251
|
+
const baseUrl = authBaseUrl || model.baseUrl || ctx.modelRegistry.getProvider(model.provider)?.baseUrl;
|
|
252
|
+
if (provider === "litellm" && !baseUrl) {
|
|
253
|
+
return { ok: false, reason: `"${model.provider}" has no base URL to send Graft to` };
|
|
197
254
|
}
|
|
198
|
-
const apiKey = auth?.auth.apiKey;
|
|
199
|
-
const baseUrl = auth?.auth.baseUrl;
|
|
200
|
-
if (!apiKey || (provider === "litellm" && !baseUrl)) return undefined;
|
|
201
255
|
|
|
202
|
-
const { GRAFT_PROVIDER:
|
|
256
|
+
const { GRAFT_PROVIDER: _p, GRAFT_MODEL: _m, GRAFT_API_KEY: _k, GRAFT_BASE_URL: _u, ...env } = process.env;
|
|
203
257
|
return {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
258
|
+
ok: true,
|
|
259
|
+
models,
|
|
260
|
+
env: {
|
|
261
|
+
...env,
|
|
262
|
+
GRAFT_PROVIDER: provider,
|
|
263
|
+
GRAFT_API_KEY: apiKey,
|
|
264
|
+
...(baseUrl ? { GRAFT_BASE_URL: baseUrl } : {}),
|
|
265
|
+
},
|
|
209
266
|
};
|
|
210
267
|
}
|
|
211
268
|
|
|
269
|
+
/** Graft's environment for one candidate model; undefined keeps the user's own GRAFT_* setup. */
|
|
270
|
+
function deepEnvFor(plan: Extract<DeepBuildPlan, { ok: true }>, modelId: string): NodeJS.ProcessEnv | undefined {
|
|
271
|
+
return plan.env ? { ...plan.env, GRAFT_MODEL: modelId } : undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
212
274
|
export default function graftExtension(pi: ExtensionAPI): void {
|
|
213
275
|
registerBuiltinAgentAugmentation(pi);
|
|
214
276
|
const exec: ExecLike = (command, args, options) => pi.exec(command, args, options);
|
|
@@ -277,6 +339,48 @@ export default function graftExtension(pi: ExtensionAPI): void {
|
|
|
277
339
|
}
|
|
278
340
|
};
|
|
279
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Build the graph: deep with a compatible active model, structural otherwise.
|
|
344
|
+
*
|
|
345
|
+
* A model that cannot produce Graft-parsable summaries is not fatal — the
|
|
346
|
+
* next candidate the session already offers is tried before downgrading.
|
|
347
|
+
*/
|
|
348
|
+
const buildGraph = async (
|
|
349
|
+
ctx: ExtensionContext,
|
|
350
|
+
activeSession: GraftSession,
|
|
351
|
+
phase: "build" | "refresh" = "build",
|
|
352
|
+
): Promise<GraftRun> => {
|
|
353
|
+
const repoRoot = activeSession.repoRoot!;
|
|
354
|
+
// A refresh has already announced its own "syncing" phase at the caller.
|
|
355
|
+
const started = (deep: boolean): void => {
|
|
356
|
+
if (phase === "refresh") return;
|
|
357
|
+
if (session === activeSession && !activeSession.disposed) applyEvent(ctx, { type: "build-started", deep });
|
|
358
|
+
};
|
|
359
|
+
const structural = async (): Promise<GraftRun> => {
|
|
360
|
+
started(false);
|
|
361
|
+
return runGraft(exec, repoRoot, { kind: "build", deep: false });
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const plan = await resolveDeepBuildPlan(ctx, activeSession.settings);
|
|
365
|
+
if (!plan.ok) return structural();
|
|
366
|
+
|
|
367
|
+
// The user's own GRAFT_* environment names the model; never second-guess it.
|
|
368
|
+
const candidates = plan.env ? plan.models : plan.models.slice(0, 1);
|
|
369
|
+
let failed: GraftRun | undefined;
|
|
370
|
+
for (const [index, model] of candidates.entries()) {
|
|
371
|
+
started(true);
|
|
372
|
+
const run = await runGraft(exec, repoRoot, { kind: "build", deep: true }, { env: deepEnvFor(plan, model) });
|
|
373
|
+
if (run.code === 0 || !UNUSABLE_SUMMARIES.test(`${run.stderr}\n${run.stdout}`)) return run;
|
|
374
|
+
failed = run;
|
|
375
|
+
if (index === 0 && candidates.length > 1 && ctx.hasUI) {
|
|
376
|
+
ctx.ui.notify(`Graft cannot summarize with ${model}; retrying the deep pass with another scoped model.`, "info");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Nothing left to try: report the last run rather than pretending the
|
|
380
|
+
// structural tier explained the failure.
|
|
381
|
+
return failed ?? (await structural());
|
|
382
|
+
};
|
|
383
|
+
|
|
280
384
|
/**
|
|
281
385
|
* Start the graph once. Compatible active Selesai models add the deep tier;
|
|
282
386
|
* otherwise the free local structural tier remains usable.
|
|
@@ -290,11 +394,7 @@ export default function graftExtension(pi: ExtensionAPI): void {
|
|
|
290
394
|
activeSession.state.name === "failed"
|
|
291
395
|
) return undefined;
|
|
292
396
|
|
|
293
|
-
const pending =
|
|
294
|
-
const deep = env !== undefined;
|
|
295
|
-
if (session === activeSession && !activeSession.disposed) applyEvent(ctx, { type: "build-started", deep });
|
|
296
|
-
return runGraft(exec, activeSession.repoRoot!, { kind: "build", deep }, { env });
|
|
297
|
-
});
|
|
397
|
+
const pending = buildGraph(ctx, activeSession);
|
|
298
398
|
structuralBuildPromise = pending;
|
|
299
399
|
structuralBuildSession = activeSession;
|
|
300
400
|
void pending.then(
|
|
@@ -544,11 +644,9 @@ export default function graftExtension(pi: ExtensionAPI): void {
|
|
|
544
644
|
|
|
545
645
|
void (async () => {
|
|
546
646
|
try {
|
|
547
|
-
const
|
|
548
|
-
const deep = env !== undefined;
|
|
549
|
-
const run = await runGraft(exec, session.repoRoot!, { kind: "build", deep }, { env });
|
|
647
|
+
const run = await buildGraph(ctx, session, "refresh");
|
|
550
648
|
if (session.disposed) return;
|
|
551
|
-
recordBuild(ctx, run, deep);
|
|
649
|
+
recordBuild(ctx, run, run.op.kind === "build" && run.op.deep);
|
|
552
650
|
} catch (error) {
|
|
553
651
|
if (session.disposed) return;
|
|
554
652
|
applyEvent(ctx, {
|
|
@@ -584,24 +682,27 @@ export default function graftExtension(pi: ExtensionAPI): void {
|
|
|
584
682
|
if (!deep) {
|
|
585
683
|
const pending = autoBuild(ctx, session);
|
|
586
684
|
if (pending) return pending;
|
|
685
|
+
applyEvent(ctx, { type: "build-started", deep: false });
|
|
686
|
+
const structural = await runGraft(exec, session.repoRoot!, { kind: "build", deep: false });
|
|
687
|
+
recordBuild(ctx, structural, false);
|
|
688
|
+
return structural;
|
|
587
689
|
}
|
|
588
|
-
const
|
|
589
|
-
if (
|
|
690
|
+
const plan = await resolveDeepBuildPlan(ctx, session.settings);
|
|
691
|
+
if (!plan.ok) {
|
|
590
692
|
return {
|
|
591
|
-
op: { kind: "build", deep },
|
|
693
|
+
op: { kind: "build", deep: true },
|
|
592
694
|
argv: ["graft", "build", "--deep"],
|
|
593
695
|
cwd: session.repoRoot!,
|
|
594
696
|
code: 1,
|
|
595
697
|
stdout: "",
|
|
596
|
-
stderr:
|
|
698
|
+
stderr: `No deep build is possible for the active model: ${plan.reason}.`,
|
|
597
699
|
killed: false,
|
|
598
700
|
cancelled: false,
|
|
599
701
|
timedOut: false,
|
|
600
702
|
};
|
|
601
703
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
recordBuild(ctx, run, deep);
|
|
704
|
+
const run = await buildGraph(ctx, session);
|
|
705
|
+
recordBuild(ctx, run, run.op.kind === "build" && run.op.deep);
|
|
605
706
|
return run;
|
|
606
707
|
},
|
|
607
708
|
runRefresh: async (ctx) => {
|
|
@@ -609,10 +710,8 @@ export default function graftExtension(pi: ExtensionAPI): void {
|
|
|
609
710
|
await ensureReady(ctx, { kind: "build", deep: false });
|
|
610
711
|
session.lastRefreshAt = Date.now();
|
|
611
712
|
applyEvent(ctx, { type: "refresh-started" });
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
const run = await runGraft(exec, session.repoRoot!, { kind: "build", deep }, { env });
|
|
615
|
-
recordBuild(ctx, run, deep);
|
|
713
|
+
const run = await buildGraph(ctx, session, "refresh");
|
|
714
|
+
recordBuild(ctx, run, run.op.kind === "build" && run.op.deep);
|
|
616
715
|
return run;
|
|
617
716
|
},
|
|
618
717
|
providerEnvNames: () => PROVIDER_ENV_NAMES.filter((name) => Boolean(process.env[name])),
|
|
@@ -304,11 +304,11 @@ describe("proactive refresh", () => {
|
|
|
304
304
|
});
|
|
305
305
|
graftExtension(harness.pi as never);
|
|
306
306
|
const ctx = makeCtx({ cwd: bare });
|
|
307
|
+
// Mirrors the real TokenIn shape: the key lives in ~/.selesai auth storage and
|
|
308
|
+
// the endpoint comes from the composed model, not from the auth resolution.
|
|
307
309
|
Object.assign(ctx, {
|
|
308
|
-
model: { provider: "tokenin", id: "deepseek-v4-flash" },
|
|
309
|
-
modelRegistry: {
|
|
310
|
-
getProviderAuth: vi.fn(async () => ({ auth: { apiKey: "test-token", baseUrl: "https://lite.andlet.me/v1" } })),
|
|
311
|
-
},
|
|
310
|
+
model: { provider: "tokenin", id: "deepseek-v4.1-flash", baseUrl: "https://lite.andlet.me/v1" },
|
|
311
|
+
modelRegistry: { getProviderAuth: vi.fn(async () => ({ auth: { apiKey: "test-token" } })) },
|
|
312
312
|
});
|
|
313
313
|
await (handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>)(
|
|
314
314
|
{ type: "session_start", reason: "startup" },
|
|
@@ -317,7 +317,80 @@ describe("proactive refresh", () => {
|
|
|
317
317
|
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● deep v0.18.0"));
|
|
318
318
|
const build = harness.exec.mock.calls.find((call) => (call[1] as string[]).includes("build"))!;
|
|
319
319
|
expect(build[1]).toEqual(["build", "--deep"]);
|
|
320
|
-
expect(build[2]).toMatchObject({
|
|
320
|
+
expect(build[2]).toMatchObject({
|
|
321
|
+
env: {
|
|
322
|
+
GRAFT_PROVIDER: "litellm",
|
|
323
|
+
GRAFT_MODEL: "deepseek-v4.1-flash",
|
|
324
|
+
GRAFT_API_KEY: "test-token",
|
|
325
|
+
GRAFT_BASE_URL: "https://lite.andlet.me/v1",
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("retries the deep pass with another scoped model when the active one cannot summarize", async () => {
|
|
331
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
332
|
+
const bare = join(root, "retry-repo");
|
|
333
|
+
mkdirSync(join(bare, ".selesai"), { recursive: true });
|
|
334
|
+
writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
|
|
335
|
+
const models: string[] = [];
|
|
336
|
+
const harness = makePi(async (command: string, args: string[], options?: unknown) => {
|
|
337
|
+
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
338
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
|
|
339
|
+
if (args.includes("build")) {
|
|
340
|
+
const model = (options as { env?: Record<string, string> }).env?.GRAFT_MODEL ?? "";
|
|
341
|
+
models.push(model);
|
|
342
|
+
if (model === "deepseek-v4.1-flash") {
|
|
343
|
+
return { ...OK, code: 1, stderr: "calc.ts: model returned no usable symbol summaries [empty-parsed]" };
|
|
344
|
+
}
|
|
345
|
+
mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
|
|
346
|
+
writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
|
|
347
|
+
writeFileSync(join(bare, "graft", "concept.md"), "# Concept", "utf-8");
|
|
348
|
+
}
|
|
349
|
+
return { ...OK, stdout: ASK_JSON };
|
|
350
|
+
});
|
|
351
|
+
graftExtension(harness.pi as never);
|
|
352
|
+
const ctx = makeCtx({ cwd: bare });
|
|
353
|
+
Object.assign(ctx, {
|
|
354
|
+
model: { provider: "tokenin", id: "deepseek-v4.1-flash", baseUrl: "https://lite.andlet.me/v1" },
|
|
355
|
+
modelRegistry: { getProviderAuth: vi.fn(async () => ({ auth: { apiKey: "test-token" } })) },
|
|
356
|
+
scopedModels: [{ model: { provider: "tokenin", id: "glm-5.3-flash" } }],
|
|
357
|
+
});
|
|
358
|
+
await (handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>)(
|
|
359
|
+
{ type: "session_start", reason: "startup" },
|
|
360
|
+
ctx,
|
|
361
|
+
);
|
|
362
|
+
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● deep v0.18.0"));
|
|
363
|
+
expect(models).toEqual(["deepseek-v4.1-flash", "glm-5.3-flash"]);
|
|
364
|
+
expect(notified(ctx)).toContain("retrying the deep pass");
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("falls back to the structural build when the active model cannot go deep", async () => {
|
|
368
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
369
|
+
const bare = join(root, "fallback-repo");
|
|
370
|
+
mkdirSync(join(bare, ".selesai"), { recursive: true });
|
|
371
|
+
writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
|
|
372
|
+
const harness = makePi(async (command: string, args: string[]) => {
|
|
373
|
+
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
374
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
|
|
375
|
+
if (args.includes("build")) {
|
|
376
|
+
mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
|
|
377
|
+
writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
|
|
378
|
+
}
|
|
379
|
+
return { ...OK, stdout: ASK_JSON };
|
|
380
|
+
});
|
|
381
|
+
graftExtension(harness.pi as never);
|
|
382
|
+
const ctx = makeCtx({ cwd: bare });
|
|
383
|
+
Object.assign(ctx, {
|
|
384
|
+
model: { provider: "google", id: "gemini-3-pro" },
|
|
385
|
+
modelRegistry: { getProviderAuth: vi.fn(async () => ({ auth: { apiKey: "test-key" } })) },
|
|
386
|
+
});
|
|
387
|
+
await (handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>)(
|
|
388
|
+
{ type: "session_start", reason: "startup" },
|
|
389
|
+
ctx,
|
|
390
|
+
);
|
|
391
|
+
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.18.0"));
|
|
392
|
+
const build = harness.exec.mock.calls.find((call) => (call[1] as string[]).includes("build"))!;
|
|
393
|
+
expect(build[1]).toEqual(["build"]);
|
|
321
394
|
});
|
|
322
395
|
|
|
323
396
|
it("re-indexes after edits, shows the sync phase, and returns to fresh", async () => {
|
|
@@ -240,12 +240,18 @@ describe("readGraftSettings", () => {
|
|
|
240
240
|
maxInjectionBytes: -1,
|
|
241
241
|
maxResultBytes: 0,
|
|
242
242
|
refreshDebounceSeconds: "60",
|
|
243
|
+
deepModel: " ",
|
|
243
244
|
in: " ",
|
|
244
245
|
},
|
|
245
246
|
});
|
|
246
247
|
expect(read(false)).toEqual({});
|
|
247
248
|
});
|
|
248
249
|
|
|
250
|
+
it("keeps a deep-build model override for the active provider", () => {
|
|
251
|
+
write(join(agentDir, "settings.json"), { graft: { deepModel: " glm-5.3-flash " } });
|
|
252
|
+
expect(read(false)).toEqual({ deepModel: "glm-5.3-flash" });
|
|
253
|
+
});
|
|
254
|
+
|
|
249
255
|
it("keeps a valid enabled flag and telemetry preference", () => {
|
|
250
256
|
write(join(agentDir, "settings.json"), { graft: { enabled: false, telemetry: "inherit" } });
|
|
251
257
|
expect(read(false)).toEqual({ enabled: false, telemetry: "inherit" });
|
|
@@ -57,6 +57,11 @@ export interface GraftSettings {
|
|
|
57
57
|
in?: string;
|
|
58
58
|
/** Seconds a mutation stays "changed" before a proactive refresh is attempted. */
|
|
59
59
|
refreshDebounceSeconds?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Model id for deep builds, on the active provider. Defaults to the active
|
|
62
|
+
* model; set it when that model cannot produce Graft-parsable summaries.
|
|
63
|
+
*/
|
|
64
|
+
deepModel?: string;
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
export const SETTINGS_KEY = "graft";
|
|
@@ -97,6 +102,7 @@ function normalizeSettings(raw: unknown): GraftSettings {
|
|
|
97
102
|
if (typeof candidate === "number" && Number.isFinite(candidate) && candidate > 0) settings[key] = candidate;
|
|
98
103
|
}
|
|
99
104
|
if (typeof value.in === "string" && value.in.trim()) settings.in = value.in.trim();
|
|
105
|
+
if (typeof value.deepModel === "string" && value.deepModel.trim()) settings.deepModel = value.deepModel.trim();
|
|
100
106
|
return settings;
|
|
101
107
|
}
|
|
102
108
|
|
|
@@ -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": {
|