@cjhyy/code-shell-core 0.8.13 → 0.9.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/dist/automation/desktop-authority-client.d.ts +9 -0
- package/dist/automation/desktop-authority-client.js +47 -0
- package/dist/automation/scheduler.d.ts +8 -0
- package/dist/automation/scheduler.js +37 -1
- package/dist/automation/store.js +14 -1
- package/dist/capabilities/index.d.ts +1 -0
- package/dist/cli/agent-server-stdio.js +8 -4
- package/dist/engine/engine-workspace-authority.d.ts +35 -0
- package/dist/engine/engine-workspace-authority.js +136 -0
- package/dist/engine/engine.d.ts +5 -10
- package/dist/engine/engine.js +108 -112
- package/dist/engine/input-attachments.d.ts +1 -0
- package/dist/engine/input-attachments.js +6 -2
- package/dist/engine/run-environment.d.ts +8 -3
- package/dist/engine/run-environment.js +33 -8
- package/dist/engine/run-image-input.d.ts +1 -0
- package/dist/engine/run-image-input.js +1 -0
- package/dist/engine/run-session-open.d.ts +2 -1
- package/dist/engine/run-session-open.js +7 -1
- package/dist/engine/run-setup.d.ts +1 -0
- package/dist/engine/run-setup.js +2 -1
- package/dist/engine/run-tooling.d.ts +2 -0
- package/dist/engine/run-tooling.js +3 -0
- package/dist/engine/run-types.d.ts +4 -0
- package/dist/engine/run-workspace.d.ts +6 -1
- package/dist/engine/run-workspace.js +47 -0
- package/dist/engine/subagent-spawner.d.ts +1 -0
- package/dist/engine/subagent-spawner.js +1 -0
- package/dist/engine/turn-loop.js +17 -0
- package/dist/engine/types.d.ts +2 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.extension.d.ts +1 -1
- package/dist/index.internal.d.ts +3 -2
- package/dist/index.internal.js +2 -0
- package/dist/index.js +2 -2
- package/dist/llm/client-base.d.ts +2 -1
- package/dist/llm/client-base.js +3 -1
- package/dist/llm/providers/anthropic.js +24 -25
- package/dist/llm/providers/openai.d.ts +4 -1
- package/dist/llm/providers/openai.js +76 -13
- package/dist/panel-apps/index.d.ts +1 -1
- package/dist/panel-apps/index.js +1 -1
- package/dist/panel-apps/installer.d.ts +29 -0
- package/dist/panel-apps/installer.js +124 -15
- package/dist/plugins/pluginAutomationTemplates.d.ts +2 -0
- package/dist/plugins/pluginAutomationTemplates.js +4 -0
- package/dist/plugins/pluginCatalog.d.ts +6 -0
- package/dist/plugins/pluginCatalog.js +7 -2
- package/dist/plugins/pluginContent.d.ts +1 -1
- package/dist/plugins/pluginContent.js +2 -10
- package/dist/prompt/composer.d.ts +4 -1
- package/dist/prompt/composer.js +11 -3
- package/dist/protocol/chat-session-manager.d.ts +42 -1
- package/dist/protocol/chat-session-manager.js +167 -4
- package/dist/protocol/chat-session.d.ts +11 -1
- package/dist/protocol/chat-session.js +20 -2
- package/dist/protocol/mobile-remote-types.d.ts +15 -0
- package/dist/protocol/server.d.ts +1 -2
- package/dist/protocol/server.js +28 -51
- package/dist/protocol/session-workspace-rpc.d.ts +23 -0
- package/dist/protocol/session-workspace-rpc.js +171 -0
- package/dist/protocol/types.d.ts +47 -1
- package/dist/protocol/types.js +4 -0
- package/dist/session/session-manager.d.ts +25 -0
- package/dist/session/session-manager.js +106 -6
- package/dist/session/transcript.d.ts +9 -0
- package/dist/session/transcript.js +81 -0
- package/dist/settings/manager.d.ts +12 -0
- package/dist/settings/manager.js +31 -0
- package/dist/tool-system/builtin/configure-model-connection.d.ts +36 -0
- package/dist/tool-system/builtin/configure-model-connection.js +396 -0
- package/dist/tool-system/builtin/cron.d.ts +11 -0
- package/dist/tool-system/builtin/cron.js +27 -2
- package/dist/tool-system/builtin/edit-model-catalog.d.ts +4 -6
- package/dist/tool-system/builtin/edit-model-catalog.js +5 -8
- package/dist/tool-system/builtin/edit.js +5 -3
- package/dist/tool-system/builtin/index.js +14 -0
- package/dist/tool-system/builtin/install-capability.js +22 -8
- package/dist/tool-system/builtin/settings-changed.d.ts +9 -0
- package/dist/tool-system/builtin/settings-changed.js +18 -0
- package/dist/tool-system/builtin/write.js +5 -3
- package/dist/tool-system/context.d.ts +12 -3
- package/dist/tool-system/executor.js +1 -1
- package/dist/tool-system/path-policy.d.ts +11 -3
- package/dist/tool-system/path-policy.js +56 -36
- package/dist/types.d.ts +11 -0
- package/dist/workspace/canonical-key.d.ts +7 -0
- package/dist/workspace/canonical-key.js +39 -0
- package/dist/workspace/workspace-context.d.ts +26 -0
- package/dist/workspace/workspace-context.js +112 -0
- package/package.json +1 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { canonicalKey } from "../workspace/canonical-key.js";
|
|
2
|
+
import { validateWorkspaceContext, workspacePrimaryRoot, } from "../workspace/workspace-context.js";
|
|
3
|
+
import { createErrorResponse, createResponse, ErrorCodes, } from "./types.js";
|
|
4
|
+
/** Protocol boundary for Session workspace ownership and root migration. */
|
|
5
|
+
export class SessionWorkspaceRpcHandlers {
|
|
6
|
+
deps;
|
|
7
|
+
constructor(deps) {
|
|
8
|
+
this.deps = deps;
|
|
9
|
+
}
|
|
10
|
+
release(req) {
|
|
11
|
+
const params = (req.params ?? {});
|
|
12
|
+
if (typeof params.sessionId !== "string" || params.sessionId.length === 0) {
|
|
13
|
+
this.deps.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const manager = this.deps.getChatManager();
|
|
17
|
+
if (manager) {
|
|
18
|
+
const session = manager.get(params.sessionId);
|
|
19
|
+
if (!session) {
|
|
20
|
+
this.deps.transport.send(createResponse(req.id, { ok: true, workspace: null }));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const workspace = session.engine.releaseSessionWorkspace?.(params.sessionId) ?? null;
|
|
24
|
+
this.deps.transport.send(createResponse(req.id, { ok: true, workspace }));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const workspace = this.deps.getLegacyEngine()?.releaseSessionWorkspace?.(params.sessionId) ?? null;
|
|
28
|
+
this.deps.transport.send(createResponse(req.id, { ok: true, workspace }));
|
|
29
|
+
}
|
|
30
|
+
set(req) {
|
|
31
|
+
const params = (req.params ?? {});
|
|
32
|
+
if (typeof params.sessionId !== "string" || params.sessionId.length === 0) {
|
|
33
|
+
this.deps.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!params.workspace ||
|
|
37
|
+
typeof params.workspace !== "object" ||
|
|
38
|
+
typeof params.workspace.root !== "string" ||
|
|
39
|
+
params.workspace.root.length === 0 ||
|
|
40
|
+
(params.workspace.kind !== "main" && params.workspace.kind !== "worktree")) {
|
|
41
|
+
this.deps.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "valid workspace is required"));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const manager = this.deps.getChatManager();
|
|
45
|
+
const engine = manager ? manager.get(params.sessionId)?.engine : this.deps.getLegacyEngine();
|
|
46
|
+
if (!engine) {
|
|
47
|
+
this.deps.transport.send(createResponse(req.id, { ok: true, workspace: null }));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const workspace = engine.setSessionWorkspace?.(params.sessionId, params.workspace);
|
|
51
|
+
this.deps.transport.send(createResponse(req.id, {
|
|
52
|
+
ok: workspace !== undefined && workspace !== null,
|
|
53
|
+
workspace: workspace ?? null,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
async migrateMainRoot(req) {
|
|
57
|
+
const params = (req.params ?? {});
|
|
58
|
+
if (!validMigrationParams(params)) {
|
|
59
|
+
this.deps.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "valid Session root migration is required"));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let workspaceContext;
|
|
63
|
+
try {
|
|
64
|
+
workspaceContext = validateWorkspaceContext(params.workspaceContext);
|
|
65
|
+
const primary = workspacePrimaryRoot(workspaceContext);
|
|
66
|
+
if (workspaceContext.projectId !== params.project.projectId ||
|
|
67
|
+
workspaceContext.sessionMainRootId !== params.project.mainRootId ||
|
|
68
|
+
canonicalKey(primary.path) !== canonicalKey(params.mainRoot)) {
|
|
69
|
+
throw new Error("migration target authority does not match the requested root");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
this.deps.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, error instanceof Error ? error.message : "valid target authority is required"));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const manager = this.deps.getChatManager();
|
|
77
|
+
let engine;
|
|
78
|
+
let residentSession;
|
|
79
|
+
if (manager) {
|
|
80
|
+
const ownership = manager.beginSessionMigration(params.sessionId, params.ownershipToken);
|
|
81
|
+
if (ownership.status === "not-resident") {
|
|
82
|
+
this.deps.transport.send(createResponse(req.id, {
|
|
83
|
+
status: "not-resident",
|
|
84
|
+
ownershipToken: ownership.ownershipToken,
|
|
85
|
+
}));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (ownership.status === "failed") {
|
|
89
|
+
this.deps.transport.send(createResponse(req.id, {
|
|
90
|
+
status: "failed",
|
|
91
|
+
error: ownership.error,
|
|
92
|
+
}));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
residentSession = ownership.session;
|
|
96
|
+
engine = residentSession.engine;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
engine = this.deps.getLegacyEngine();
|
|
100
|
+
}
|
|
101
|
+
if (!engine) {
|
|
102
|
+
this.deps.transport.send(createResponse(req.id, {
|
|
103
|
+
status: "failed",
|
|
104
|
+
error: "Session migration owner is unavailable",
|
|
105
|
+
}));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const workspace = residentSession
|
|
110
|
+
? await manager.migrateResidentSessionMainRoot(params.sessionId, {
|
|
111
|
+
project: params.project,
|
|
112
|
+
mainRoot: params.mainRoot,
|
|
113
|
+
workspaceContext,
|
|
114
|
+
projectTrusted: params.projectTrusted,
|
|
115
|
+
})
|
|
116
|
+
: engine.migrateSessionMainRoot?.(params.sessionId, params.project, params.mainRoot);
|
|
117
|
+
if (!workspace)
|
|
118
|
+
throw new Error(`Session ${params.sessionId} migration was not committed`);
|
|
119
|
+
if (residentSession) {
|
|
120
|
+
this.deps.rememberSessionSlice(params.sessionId, {
|
|
121
|
+
...(this.deps.getLastSlice(params.sessionId) ?? {}),
|
|
122
|
+
cwd: params.mainRoot,
|
|
123
|
+
workspaceContext,
|
|
124
|
+
projectTrusted: params.projectTrusted,
|
|
125
|
+
});
|
|
126
|
+
this.deps.wireInteractiveSession(residentSession, params.sessionId);
|
|
127
|
+
}
|
|
128
|
+
this.deps.transport.send(createResponse(req.id, {
|
|
129
|
+
status: "migrated",
|
|
130
|
+
workspace,
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
this.deps.transport.send(createResponse(req.id, {
|
|
135
|
+
status: "failed",
|
|
136
|
+
error: error instanceof Error ? error.message : String(error),
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
completeMainRootMigration(req) {
|
|
141
|
+
const params = (req.params ?? {});
|
|
142
|
+
if (typeof params.sessionId !== "string" ||
|
|
143
|
+
params.sessionId.length === 0 ||
|
|
144
|
+
typeof params.ownershipToken !== "string" ||
|
|
145
|
+
params.ownershipToken.length === 0 ||
|
|
146
|
+
params.ownershipToken.length > 128) {
|
|
147
|
+
this.deps.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "valid migration claim is required"));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (!this.deps.getChatManager()?.completeSessionMigration(params.sessionId, params.ownershipToken)) {
|
|
151
|
+
this.deps.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "migration claim is not active"));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.deps.transport.send(createResponse(req.id, { released: true }));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function validMigrationParams(params) {
|
|
158
|
+
return (typeof params.sessionId === "string" &&
|
|
159
|
+
params.sessionId.length > 0 &&
|
|
160
|
+
Boolean(params.project) &&
|
|
161
|
+
typeof params.project?.projectId === "string" &&
|
|
162
|
+
params.project.projectId.length > 0 &&
|
|
163
|
+
typeof params.project.mainRootId === "string" &&
|
|
164
|
+
params.project.mainRootId.length > 0 &&
|
|
165
|
+
typeof params.mainRoot === "string" &&
|
|
166
|
+
params.mainRoot.length > 0 &&
|
|
167
|
+
typeof params.projectTrusted === "boolean" &&
|
|
168
|
+
typeof params.ownershipToken === "string" &&
|
|
169
|
+
params.ownershipToken.length > 0 &&
|
|
170
|
+
params.ownershipToken.length <= 128);
|
|
171
|
+
}
|
package/dist/protocol/types.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Server → Client: notifications (stream events, approval requests)
|
|
8
8
|
* Server → Client: responses (results of requests)
|
|
9
9
|
*/
|
|
10
|
-
import type { InputAttachmentMeta, LegacyPetWorkspaceOption, LegacyPetWorkDelegation, StreamEvent, TokenUsage, TerminalReason, ApprovalRequest, ApprovalResult, PermissionMode, SessionForkLineage, SessionKind, SessionWorkspace } from "../types.js";
|
|
10
|
+
import type { InputAttachmentMeta, LegacyPetWorkspaceOption, LegacyPetWorkDelegation, StreamEvent, TokenUsage, TerminalReason, ApprovalRequest, ApprovalResult, PermissionMode, SessionForkLineage, SessionKind, SessionProjectBinding, SessionWorkspace } from "../types.js";
|
|
11
11
|
import type { RunBehaviorMode } from "../engine/run-types.js";
|
|
12
12
|
import type { SessionMessageTarget } from "../session/session-message.js";
|
|
13
13
|
export type PendingApprovalKind = "tool_approval" | "ask_user" | "internal";
|
|
@@ -69,6 +69,12 @@ export type { InputAttachmentKind, InputAttachmentOrigin, InputAttachmentMeta }
|
|
|
69
69
|
export interface RunParams {
|
|
70
70
|
sessionId: string;
|
|
71
71
|
task: string;
|
|
72
|
+
/** Renderer/mobile project hint. Desktop Main resolves it to authoritative roots. */
|
|
73
|
+
projectId?: string;
|
|
74
|
+
/** Renderer/mobile root hint. Desktop Main resolves it within projectId. */
|
|
75
|
+
rootId?: string;
|
|
76
|
+
/** Trusted host-injected roots. Desktop Main strips any renderer/mobile supplied value. */
|
|
77
|
+
workspaceContext?: import("../workspace/workspace-context.js").WorkspaceContext;
|
|
72
78
|
/**
|
|
73
79
|
* Optional user-facing representation of `task`. The model still receives
|
|
74
80
|
* the full task, while session feeds and transcript replay show this text.
|
|
@@ -87,6 +93,8 @@ export interface RunParams {
|
|
|
87
93
|
archiveBeforeCurrentTurn?: {
|
|
88
94
|
fromClientMessageId?: string;
|
|
89
95
|
segmentId?: string;
|
|
96
|
+
/** Host-authored replacement for the archived span; skips the summarizer. */
|
|
97
|
+
summary?: string;
|
|
90
98
|
};
|
|
91
99
|
/** Desktop host ownership generation for process-local Quick Chat runs. */
|
|
92
100
|
quickChatClaimId?: string;
|
|
@@ -271,6 +279,40 @@ export interface SetWorkspaceParams {
|
|
|
271
279
|
sessionId: string;
|
|
272
280
|
workspace: SessionWorkspace;
|
|
273
281
|
}
|
|
282
|
+
/** Atomically rebind every main-root field through the live Session owner. */
|
|
283
|
+
export interface MigrateSessionMainRootParams {
|
|
284
|
+
sessionId: string;
|
|
285
|
+
project: SessionProjectBinding;
|
|
286
|
+
mainRoot: string;
|
|
287
|
+
/** Main-resolved target authority used to construct the replacement Engine. */
|
|
288
|
+
workspaceContext: import("../workspace/workspace-context.js").WorkspaceContext;
|
|
289
|
+
/** Trust decision for the target main root, resolved by the host trust store. */
|
|
290
|
+
projectTrusted: boolean;
|
|
291
|
+
/** Main-minted nonce used to fence and later release a non-resident handoff. */
|
|
292
|
+
ownershipToken: string;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Proven ownership result for a Session main-root migration.
|
|
296
|
+
*
|
|
297
|
+
* `not-resident` is the only result that authorizes a durable host fallback.
|
|
298
|
+
* The ownership token fences a concurrent getOrCreate until the host reports
|
|
299
|
+
* that its atomic disk commit (or failed attempt) has finished.
|
|
300
|
+
*/
|
|
301
|
+
export type MigrateSessionMainRootResult = {
|
|
302
|
+
status: "migrated";
|
|
303
|
+
workspace: SessionWorkspace;
|
|
304
|
+
} | {
|
|
305
|
+
status: "not-resident";
|
|
306
|
+
ownershipToken: string;
|
|
307
|
+
} | {
|
|
308
|
+
status: "failed";
|
|
309
|
+
error: string;
|
|
310
|
+
};
|
|
311
|
+
/** Release the non-resident ownership fence after Main's durable attempt. */
|
|
312
|
+
export interface CompleteSessionMainRootMigrationParams {
|
|
313
|
+
sessionId: string;
|
|
314
|
+
ownershipToken: string;
|
|
315
|
+
}
|
|
274
316
|
/** Inject context into a session transcript. */
|
|
275
317
|
export interface InjectParams {
|
|
276
318
|
sessionId: string;
|
|
@@ -498,6 +540,10 @@ export declare const Methods: {
|
|
|
498
540
|
readonly ReleaseWorkspace: "agent/releaseWorkspace";
|
|
499
541
|
/** Persist a workspace pointer through the live session owner. */
|
|
500
542
|
readonly SetWorkspace: "agent/setWorkspace";
|
|
543
|
+
/** Atomically migrate a Session's main-root authority through its live owner. */
|
|
544
|
+
readonly MigrateSessionMainRoot: "agent/migrateSessionMainRoot";
|
|
545
|
+
/** Release a non-resident migration fence after Main's durable attempt. */
|
|
546
|
+
readonly CompleteSessionMainRootMigration: "agent/completeSessionMainRootMigration";
|
|
501
547
|
/** Extend a running goal's turn/budget ceilings mid-run (TODO 3.1). */
|
|
502
548
|
readonly GoalExtend: "agent/goalExtend";
|
|
503
549
|
/** Edit or pause/resume a session's persisted goal. */
|
package/dist/protocol/types.js
CHANGED
|
@@ -46,6 +46,10 @@ export const Methods = {
|
|
|
46
46
|
ReleaseWorkspace: "agent/releaseWorkspace",
|
|
47
47
|
/** Persist a workspace pointer through the live session owner. */
|
|
48
48
|
SetWorkspace: "agent/setWorkspace",
|
|
49
|
+
/** Atomically migrate a Session's main-root authority through its live owner. */
|
|
50
|
+
MigrateSessionMainRoot: "agent/migrateSessionMainRoot",
|
|
51
|
+
/** Release a non-resident migration fence after Main's durable attempt. */
|
|
52
|
+
CompleteSessionMainRootMigration: "agent/completeSessionMainRootMigration",
|
|
49
53
|
/** Extend a running goal's turn/budget ceilings mid-run (TODO 3.1). */
|
|
50
54
|
GoalExtend: "agent/goalExtend",
|
|
51
55
|
/** Edit or pause/resume a session's persisted goal. */
|
|
@@ -118,6 +118,8 @@ export declare class SessionManager {
|
|
|
118
118
|
* fallback when a worktree is unavailable or is being released.
|
|
119
119
|
*/
|
|
120
120
|
readSessionMainRoot(sessionId: string): string | undefined;
|
|
121
|
+
/** Cheap durable project-binding read; absent for legacy/no-repo Sessions. */
|
|
122
|
+
readSessionProjectBinding(sessionId: string): import("../types.js").SessionProjectBinding | undefined;
|
|
121
123
|
/** Cheap durable classification read. Legacy sessions are ordinary work sessions. */
|
|
122
124
|
readSessionKind(sessionId: string): SessionKind | undefined;
|
|
123
125
|
/** Cheap durable digital-human binding read. */
|
|
@@ -154,6 +156,22 @@ export declare class SessionManager {
|
|
|
154
156
|
* is a safety pointer and resume breadcrumb.
|
|
155
157
|
*/
|
|
156
158
|
setSessionWorkspace(sessionId: string, workspace: SessionWorkspace): number;
|
|
159
|
+
/**
|
|
160
|
+
* Commit a Session main-root migration as one state.json replacement.
|
|
161
|
+
*
|
|
162
|
+
* `cwd`, the durable project binding, and the execution workspace are one
|
|
163
|
+
* consistency unit: exposing any subset would let a resumed run combine the
|
|
164
|
+
* old authority with a new path. Derived availability such as dir_missing is
|
|
165
|
+
* deliberately absent from this patch and is recomputed by the host.
|
|
166
|
+
*/
|
|
167
|
+
migrateSessionMainRoot(sessionId: string, project: import("../types.js").SessionProjectBinding, mainRoot: string): number;
|
|
168
|
+
/**
|
|
169
|
+
* Commit a host-owned offline migration only if the durable snapshot Main
|
|
170
|
+
* just revalidated is still current. Unlike the ordinary field-level writer,
|
|
171
|
+
* this must not retry onto a newer revision: a new writer means ownership is
|
|
172
|
+
* no longer provably unchanged, so the migration fails closed.
|
|
173
|
+
*/
|
|
174
|
+
migrateSessionMainRootIfRevision(sessionId: string, project: import("../types.js").SessionProjectBinding, mainRoot: string, expectedStateRevision: number | undefined): number;
|
|
157
175
|
/** Read the durable archival timestamp; undefined = not archived / unprovable. */
|
|
158
176
|
readSessionArchivedAt(sessionId: string): number | undefined;
|
|
159
177
|
/** Set (number) or clear (undefined) the durable archival marker. */
|
|
@@ -221,6 +239,13 @@ export declare class SessionManager {
|
|
|
221
239
|
state: SessionState;
|
|
222
240
|
} | undefined;
|
|
223
241
|
resume(sessionId: string): SessionBundle;
|
|
242
|
+
/**
|
|
243
|
+
* Resume a model run with a bounded active replay for large Pet transcripts
|
|
244
|
+
* that already contain a full-history archive boundary. Audit/detail callers
|
|
245
|
+
* continue to use resume() and receive every persisted event.
|
|
246
|
+
*/
|
|
247
|
+
resumeForRun(sessionId: string): SessionBundle;
|
|
248
|
+
private resumeWithTranscriptMode;
|
|
224
249
|
/**
|
|
225
250
|
* Return the exact persistence directory owned by this manager.
|
|
226
251
|
*
|
|
@@ -505,6 +505,29 @@ export class SessionManager {
|
|
|
505
505
|
return undefined;
|
|
506
506
|
}
|
|
507
507
|
}
|
|
508
|
+
/** Cheap durable project-binding read; absent for legacy/no-repo Sessions. */
|
|
509
|
+
readSessionProjectBinding(sessionId) {
|
|
510
|
+
try {
|
|
511
|
+
assertSafeSessionId(sessionId);
|
|
512
|
+
const processLocal = this.processLocalBundle(sessionId);
|
|
513
|
+
const state = processLocal
|
|
514
|
+
? processLocal.state
|
|
515
|
+
: sessionId.startsWith("qchat-")
|
|
516
|
+
? undefined
|
|
517
|
+
: JSON.parse(readFileSync(join(this.sessionsDir, sessionId, "state.json"), "utf-8"));
|
|
518
|
+
const project = state?.project;
|
|
519
|
+
return project &&
|
|
520
|
+
typeof project.projectId === "string" &&
|
|
521
|
+
project.projectId.length > 0 &&
|
|
522
|
+
typeof project.mainRootId === "string" &&
|
|
523
|
+
project.mainRootId.length > 0
|
|
524
|
+
? { projectId: project.projectId, mainRootId: project.mainRootId }
|
|
525
|
+
: undefined;
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
return undefined;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
508
531
|
/** Cheap durable classification read. Legacy sessions are ordinary work sessions. */
|
|
509
532
|
readSessionKind(sessionId) {
|
|
510
533
|
try {
|
|
@@ -686,6 +709,70 @@ export class SessionManager {
|
|
|
686
709
|
}
|
|
687
710
|
return this.updateSessionState(sessionId, { workspace });
|
|
688
711
|
}
|
|
712
|
+
/**
|
|
713
|
+
* Commit a Session main-root migration as one state.json replacement.
|
|
714
|
+
*
|
|
715
|
+
* `cwd`, the durable project binding, and the execution workspace are one
|
|
716
|
+
* consistency unit: exposing any subset would let a resumed run combine the
|
|
717
|
+
* old authority with a new path. Derived availability such as dir_missing is
|
|
718
|
+
* deliberately absent from this patch and is recomputed by the host.
|
|
719
|
+
*/
|
|
720
|
+
migrateSessionMainRoot(sessionId, project, mainRoot) {
|
|
721
|
+
if (!project ||
|
|
722
|
+
typeof project.projectId !== "string" ||
|
|
723
|
+
project.projectId.length === 0 ||
|
|
724
|
+
typeof project.mainRootId !== "string" ||
|
|
725
|
+
project.mainRootId.length === 0 ||
|
|
726
|
+
typeof mainRoot !== "string" ||
|
|
727
|
+
mainRoot.length === 0) {
|
|
728
|
+
throw new SessionError(`invalid main-root migration for ${sessionId}`);
|
|
729
|
+
}
|
|
730
|
+
return this.updateSessionState(sessionId, {
|
|
731
|
+
project: { ...project },
|
|
732
|
+
cwd: mainRoot,
|
|
733
|
+
workspace: { root: mainRoot, kind: "main" },
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Commit a host-owned offline migration only if the durable snapshot Main
|
|
738
|
+
* just revalidated is still current. Unlike the ordinary field-level writer,
|
|
739
|
+
* this must not retry onto a newer revision: a new writer means ownership is
|
|
740
|
+
* no longer provably unchanged, so the migration fails closed.
|
|
741
|
+
*/
|
|
742
|
+
migrateSessionMainRootIfRevision(sessionId, project, mainRoot, expectedStateRevision) {
|
|
743
|
+
assertSafeSessionId(sessionId);
|
|
744
|
+
if (!project ||
|
|
745
|
+
typeof project.projectId !== "string" ||
|
|
746
|
+
project.projectId.length === 0 ||
|
|
747
|
+
typeof project.mainRootId !== "string" ||
|
|
748
|
+
project.mainRootId.length === 0 ||
|
|
749
|
+
typeof mainRoot !== "string" ||
|
|
750
|
+
mainRoot.length === 0) {
|
|
751
|
+
throw new SessionError(`invalid main-root migration for ${sessionId}`);
|
|
752
|
+
}
|
|
753
|
+
const state = this.readPersistedState(sessionId);
|
|
754
|
+
if (state.stateRevision !== expectedStateRevision) {
|
|
755
|
+
throw new SessionError(`Session state revision conflict for ${sessionId}`);
|
|
756
|
+
}
|
|
757
|
+
Object.assign(state, {
|
|
758
|
+
project: { ...project },
|
|
759
|
+
cwd: mainRoot,
|
|
760
|
+
workspace: { root: mainRoot, kind: "main" },
|
|
761
|
+
});
|
|
762
|
+
const result = this.saveStateAttempt(state);
|
|
763
|
+
if (result.ok)
|
|
764
|
+
return state.stateRevision;
|
|
765
|
+
if (result.reason === "generation_conflict") {
|
|
766
|
+
throw new SessionError(`Session generation conflict for ${sessionId}`);
|
|
767
|
+
}
|
|
768
|
+
if (result.reason === "lock_conflict") {
|
|
769
|
+
throw new SessionError(`Session state lock contention for ${sessionId}`);
|
|
770
|
+
}
|
|
771
|
+
if (result.reason === "kind_conflict") {
|
|
772
|
+
throw new SessionError(`Session kind is immutable for ${sessionId}`);
|
|
773
|
+
}
|
|
774
|
+
throw new SessionError(`Session state revision conflict for ${sessionId}`);
|
|
775
|
+
}
|
|
689
776
|
/** Read the durable archival timestamp; undefined = not archived / unprovable. */
|
|
690
777
|
readSessionArchivedAt(sessionId) {
|
|
691
778
|
try {
|
|
@@ -1001,6 +1088,17 @@ export class SessionManager {
|
|
|
1001
1088
|
return undefined;
|
|
1002
1089
|
}
|
|
1003
1090
|
resume(sessionId) {
|
|
1091
|
+
return this.resumeWithTranscriptMode(sessionId, false);
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Resume a model run with a bounded active replay for large Pet transcripts
|
|
1095
|
+
* that already contain a full-history archive boundary. Audit/detail callers
|
|
1096
|
+
* continue to use resume() and receive every persisted event.
|
|
1097
|
+
*/
|
|
1098
|
+
resumeForRun(sessionId) {
|
|
1099
|
+
return this.resumeWithTranscriptMode(sessionId, true);
|
|
1100
|
+
}
|
|
1101
|
+
resumeWithTranscriptMode(sessionId, contextOnly) {
|
|
1004
1102
|
assertSafeSessionId(sessionId);
|
|
1005
1103
|
const processLocal = this.processLocalBundle(sessionId);
|
|
1006
1104
|
if (processLocal) {
|
|
@@ -1037,8 +1135,10 @@ export class SessionManager {
|
|
|
1037
1135
|
chmodSync(sessionDir, 0o700);
|
|
1038
1136
|
chmodSync(stateFile, 0o600);
|
|
1039
1137
|
}
|
|
1040
|
-
const transcript = Transcript.loadFromFile(transcriptFile);
|
|
1041
1138
|
state.kind = normalizedSessionKind(state.kind);
|
|
1139
|
+
const transcript = contextOnly && state.kind === "pet"
|
|
1140
|
+
? Transcript.loadContextFromFile(transcriptFile)
|
|
1141
|
+
: Transcript.loadFromFile(transcriptFile);
|
|
1042
1142
|
state.status = "active";
|
|
1043
1143
|
delete state.lastCompletionKind;
|
|
1044
1144
|
Object.assign(state, normalizeCumulativeUsageCounters(state, state.tokenUsage));
|
|
@@ -1697,11 +1797,11 @@ export class SessionManager {
|
|
|
1697
1797
|
list(limit = 20, opts) {
|
|
1698
1798
|
if (!existsSync(this.sessionsDir))
|
|
1699
1799
|
return [];
|
|
1700
|
-
//
|
|
1701
|
-
//
|
|
1702
|
-
//
|
|
1703
|
-
//
|
|
1704
|
-
const excludeKinds = opts?.excludeKinds ?? [
|
|
1800
|
+
// Core is domain-agnostic: only the composition root knows which
|
|
1801
|
+
// extension-owned kinds belong outside its generic list. AgentServer passes
|
|
1802
|
+
// the resolved hidden-kind union explicitly; direct SDK callers see every
|
|
1803
|
+
// kind unless they choose exclusions themselves.
|
|
1804
|
+
const excludeKinds = opts?.excludeKinds ?? [];
|
|
1705
1805
|
const dirs = readdirSync(this.sessionsDir, { withFileTypes: true })
|
|
1706
1806
|
.filter((d) => d.isDirectory() && !d.name.startsWith(".pending-") && !d.name.startsWith("qchat-"))
|
|
1707
1807
|
.map((d) => d.name);
|
|
@@ -184,6 +184,15 @@ export declare class Transcript {
|
|
|
184
184
|
*/
|
|
185
185
|
static selectContextRange(events: readonly TranscriptEvent[], range: ContextEventRange): SelectedContextRange;
|
|
186
186
|
static loadFromFile(filePath: string): Transcript;
|
|
187
|
+
/**
|
|
188
|
+
* Load the active replay for a model run while leaving the append-only audit
|
|
189
|
+
* transcript untouched on disk. A host-authored from-less range archive
|
|
190
|
+
* replaces everything before its `to` anchor, so a large Mimi transcript can
|
|
191
|
+
* retain only that boundary and the live tail in memory. If the boundary is
|
|
192
|
+
* absent, too old, malformed, or not self-contained in the bounded tail, we
|
|
193
|
+
* fail open to the full loader.
|
|
194
|
+
*/
|
|
195
|
+
static loadContextFromFile(filePath: string, maxTailBytes?: number): Transcript;
|
|
187
196
|
private loadEvents;
|
|
188
197
|
}
|
|
189
198
|
export {};
|
|
@@ -6,6 +6,7 @@ import { appendFileSync, chmodSync, closeSync, existsSync, fchmodSync, fstatSync
|
|
|
6
6
|
import { dirname } from "node:path";
|
|
7
7
|
import { nanoid } from "nanoid";
|
|
8
8
|
import { logger } from "../logging/logger.js";
|
|
9
|
+
const DEFAULT_CONTEXT_TAIL_SCAN_BYTES = 32 * 1024 * 1024;
|
|
9
10
|
function appendTranscriptLine(filePath, data) {
|
|
10
11
|
// Use one append-mode descriptor so concurrent OS writers cannot overwrite
|
|
11
12
|
// one another. Also repair the record boundary after a crash-torn final line;
|
|
@@ -678,6 +679,86 @@ export class Transcript {
|
|
|
678
679
|
transcript.repairToolResultPairs();
|
|
679
680
|
return transcript;
|
|
680
681
|
}
|
|
682
|
+
/**
|
|
683
|
+
* Load the active replay for a model run while leaving the append-only audit
|
|
684
|
+
* transcript untouched on disk. A host-authored from-less range archive
|
|
685
|
+
* replaces everything before its `to` anchor, so a large Mimi transcript can
|
|
686
|
+
* retain only that boundary and the live tail in memory. If the boundary is
|
|
687
|
+
* absent, too old, malformed, or not self-contained in the bounded tail, we
|
|
688
|
+
* fail open to the full loader.
|
|
689
|
+
*/
|
|
690
|
+
static loadContextFromFile(filePath, maxTailBytes = DEFAULT_CONTEXT_TAIL_SCAN_BYTES) {
|
|
691
|
+
if (!existsSync(filePath))
|
|
692
|
+
return new Transcript(filePath);
|
|
693
|
+
let tail;
|
|
694
|
+
try {
|
|
695
|
+
const fd = openSync(filePath, "r");
|
|
696
|
+
try {
|
|
697
|
+
const fileSize = fstatSync(fd).size;
|
|
698
|
+
if (fileSize <= maxTailBytes)
|
|
699
|
+
return Transcript.loadFromFile(filePath);
|
|
700
|
+
const length = Math.min(fileSize, Math.max(1, maxTailBytes));
|
|
701
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
702
|
+
const bytesRead = readSync(fd, buffer, 0, length, fileSize - length);
|
|
703
|
+
let window = buffer.subarray(0, bytesRead);
|
|
704
|
+
const newline = window.indexOf(0x0a);
|
|
705
|
+
if (newline < 0)
|
|
706
|
+
return Transcript.loadFromFile(filePath);
|
|
707
|
+
window = window.subarray(newline + 1);
|
|
708
|
+
tail = window.toString("utf8");
|
|
709
|
+
}
|
|
710
|
+
finally {
|
|
711
|
+
closeSync(fd);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
return Transcript.loadFromFile(filePath);
|
|
716
|
+
}
|
|
717
|
+
const events = [];
|
|
718
|
+
for (const line of tail.split("\n")) {
|
|
719
|
+
if (!line.trim())
|
|
720
|
+
continue;
|
|
721
|
+
try {
|
|
722
|
+
events.push(JSON.parse(line));
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
// A malformed tail must not turn a partial window into authoritative
|
|
726
|
+
// context. The normal loader keeps its established skip behavior.
|
|
727
|
+
return Transcript.loadFromFile(filePath);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
let markerIndex = -1;
|
|
731
|
+
let toClientMessageId;
|
|
732
|
+
for (const [index, event] of events.entries()) {
|
|
733
|
+
if (event.type !== "range_archive")
|
|
734
|
+
continue;
|
|
735
|
+
const data = event.data;
|
|
736
|
+
if (data.fromClientMessageId === undefined &&
|
|
737
|
+
typeof data.summary === "string" &&
|
|
738
|
+
typeof data.toClientMessageId === "string") {
|
|
739
|
+
markerIndex = index;
|
|
740
|
+
toClientMessageId = data.toClientMessageId;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (markerIndex < 0 || !toClientMessageId)
|
|
744
|
+
return Transcript.loadFromFile(filePath);
|
|
745
|
+
const anchorIndex = events.findIndex((event, index) => index < markerIndex &&
|
|
746
|
+
event.type === "message" &&
|
|
747
|
+
event.data.clientMessageId === toClientMessageId);
|
|
748
|
+
if (anchorIndex < 0)
|
|
749
|
+
return Transcript.loadFromFile(filePath);
|
|
750
|
+
let startIndex = anchorIndex;
|
|
751
|
+
for (let index = anchorIndex - 1; index >= 0; index -= 1) {
|
|
752
|
+
if (events[index]?.type !== "turn_boundary")
|
|
753
|
+
continue;
|
|
754
|
+
startIndex = index;
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
const transcript = new Transcript(filePath);
|
|
758
|
+
transcript.loadEvents(events.slice(startIndex));
|
|
759
|
+
transcript.repairToolResultPairs();
|
|
760
|
+
return transcript;
|
|
761
|
+
}
|
|
681
762
|
loadEvents(events) {
|
|
682
763
|
this.events = structuredClone([...events]);
|
|
683
764
|
this.currentTurn = 0;
|
|
@@ -153,6 +153,18 @@ export declare class SettingsManager {
|
|
|
153
153
|
* be shared with collaborators.
|
|
154
154
|
*/
|
|
155
155
|
saveLocalSetting(key: string, value: unknown, cwd: string): void;
|
|
156
|
+
/**
|
|
157
|
+
* Mutate one writable settings layer under the same cross-process lock used
|
|
158
|
+
* by the desktop settings service. Domain tools use this when two related
|
|
159
|
+
* fields must change atomically (for example modelConnections + defaults):
|
|
160
|
+
* composing multiple save*Setting calls would expose an intermediate state
|
|
161
|
+
* and could interleave with another process between writes.
|
|
162
|
+
*
|
|
163
|
+
* The callback receives only the selected layer's raw object, not the merged
|
|
164
|
+
* settings view. The result is schema-validated before it replaces the file.
|
|
165
|
+
* Returning false makes the operation a no-op.
|
|
166
|
+
*/
|
|
167
|
+
mutateSettingsForScope(scope: "user" | "project", cwd: string, mutate: (current: Record<string, unknown>) => boolean | void): void;
|
|
156
168
|
/**
|
|
157
169
|
* Delete a single dotted key from the PROJECT-level config file. Used to
|
|
158
170
|
* express "inherit" — we don't persist the literal "inherit"; we remove the
|
package/dist/settings/manager.js
CHANGED
|
@@ -417,6 +417,37 @@ export class SettingsManager {
|
|
|
417
417
|
});
|
|
418
418
|
this.invalidate();
|
|
419
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* Mutate one writable settings layer under the same cross-process lock used
|
|
422
|
+
* by the desktop settings service. Domain tools use this when two related
|
|
423
|
+
* fields must change atomically (for example modelConnections + defaults):
|
|
424
|
+
* composing multiple save*Setting calls would expose an intermediate state
|
|
425
|
+
* and could interleave with another process between writes.
|
|
426
|
+
*
|
|
427
|
+
* The callback receives only the selected layer's raw object, not the merged
|
|
428
|
+
* settings view. The result is schema-validated before it replaces the file.
|
|
429
|
+
* Returning false makes the operation a no-op.
|
|
430
|
+
*/
|
|
431
|
+
mutateSettingsForScope(scope, cwd, mutate) {
|
|
432
|
+
const path = scope === "user"
|
|
433
|
+
? join(this.userConfigDir(), "settings.json")
|
|
434
|
+
: this.projectSettingsPath(cwd);
|
|
435
|
+
if (scope === "project") {
|
|
436
|
+
this.validateProjectCwd(cwd, "project");
|
|
437
|
+
if (!existsSync(cwd))
|
|
438
|
+
throw new Error(`project directory does not exist: ${cwd}`);
|
|
439
|
+
}
|
|
440
|
+
this.mutateSettingsFile(path, (current) => {
|
|
441
|
+
if (mutate(current) === false)
|
|
442
|
+
return false;
|
|
443
|
+
// Validate the complete resulting layer before persistence. We keep the
|
|
444
|
+
// original object for serialization so forward-compatible unknown keys
|
|
445
|
+
// are preserved instead of being stripped by Zod's parsed result.
|
|
446
|
+
validateSettings(current);
|
|
447
|
+
return true;
|
|
448
|
+
});
|
|
449
|
+
this.invalidate();
|
|
450
|
+
}
|
|
420
451
|
/**
|
|
421
452
|
* Delete a single dotted key from the PROJECT-level config file. Used to
|
|
422
453
|
* express "inherit" — we don't persist the literal "inherit"; we remove the
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfigureModelConnection — safely materialize one catalog model into the
|
|
3
|
+
* unified settings.modelConnections store without exposing or copying API
|
|
4
|
+
* keys. The write is schema-validated, lock-protected, atomic, and updates the
|
|
5
|
+
* selected tag default in the same transaction when requested.
|
|
6
|
+
*/
|
|
7
|
+
import type { ToolDefinition } from "../../types.js";
|
|
8
|
+
import type { ToolContext } from "../context.js";
|
|
9
|
+
import { SettingsManager } from "../../settings/manager.js";
|
|
10
|
+
import { type CatalogEntry } from "../../model-catalog/index.js";
|
|
11
|
+
import { createLLMClient } from "../../llm/client-factory.js";
|
|
12
|
+
import { type Credential, type ModelInstance } from "../../model-catalog/resolve.js";
|
|
13
|
+
interface ConfigureModelConnectionDeps {
|
|
14
|
+
makeSettingsManager(cwd: string, scope: "full" | "project"): SettingsManager;
|
|
15
|
+
getCatalog(): CatalogEntry[];
|
|
16
|
+
notifySettingsChanged(): void;
|
|
17
|
+
testTextConnection(connection: ModelInstance, credentials: Credential[], catalog: CatalogEntry[]): Promise<ConnectionTestResult>;
|
|
18
|
+
}
|
|
19
|
+
interface ConnectionTestResult {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
response?: string;
|
|
22
|
+
stopReason?: string;
|
|
23
|
+
usage?: {
|
|
24
|
+
promptTokens: number;
|
|
25
|
+
completionTokens: number;
|
|
26
|
+
totalTokens: number;
|
|
27
|
+
};
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function probeTextModelConnection(connection: ModelInstance, credentials: Credential[], catalog: CatalogEntry[], options?: {
|
|
31
|
+
fetch?: typeof globalThis.fetch;
|
|
32
|
+
createClient?: typeof createLLMClient;
|
|
33
|
+
}): Promise<ConnectionTestResult>;
|
|
34
|
+
export declare const configureModelConnectionToolDef: ToolDefinition;
|
|
35
|
+
export declare function configureModelConnectionTool(args: Record<string, unknown>, ctx?: ToolContext, deps?: ConfigureModelConnectionDeps): Promise<string>;
|
|
36
|
+
export {};
|