@cjhyy/code-shell-core 0.8.20 → 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 +4 -1
- 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 +84 -101
- 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 +6 -0
- 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 +2 -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/types.d.ts +2 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.extension.d.ts +1 -1
- package/dist/index.internal.d.ts +2 -2
- package/dist/index.internal.js +1 -0
- package/dist/index.js +1 -1
- package/dist/plugins/pluginAutomationTemplates.d.ts +2 -0
- package/dist/plugins/pluginAutomationTemplates.js +4 -0
- 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 +9 -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 +21 -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 +45 -1
- package/dist/protocol/types.js +4 -0
- package/dist/session/session-manager.d.ts +18 -0
- package/dist/session/session-manager.js +87 -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.js +5 -3
- package/dist/tool-system/builtin/write.js +5 -3
- package/dist/tool-system/context.d.ts +4 -1
- 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 +6 -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
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { resolveRunProfileState } from "./run-setup.js";
|
|
2
|
+
import { canonicalKey } from "../workspace/canonical-key.js";
|
|
3
|
+
import { legacySingleRootWorkspace, validateWorkspaceContext, workspacePrimaryRoot, } from "../workspace/workspace-context.js";
|
|
2
4
|
/**
|
|
3
5
|
* Resolve the working directory for a run. Precedence for legacy sessions:
|
|
4
6
|
* options.cwd > resumed session's state.cwd > config.cwd > process.cwd()
|
|
@@ -93,6 +95,37 @@ export async function resolveRunWorkspace(args) {
|
|
|
93
95
|
configCwd: args.configCwd,
|
|
94
96
|
processCwd: args.processCwd,
|
|
95
97
|
});
|
|
98
|
+
const rawWorkspaceContext = options?.workspaceContext ?? args.configWorkspaceContext;
|
|
99
|
+
let workspaceContext;
|
|
100
|
+
try {
|
|
101
|
+
workspaceContext = rawWorkspaceContext
|
|
102
|
+
? validateWorkspaceContext(rawWorkspaceContext)
|
|
103
|
+
: legacySingleRootWorkspace(cwd);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
return workspaceError(options?.sessionId, error instanceof Error ? error.message : String(error));
|
|
107
|
+
}
|
|
108
|
+
const authoritativeWorkspaceContext = rawWorkspaceContext !== undefined;
|
|
109
|
+
const primary = workspacePrimaryRoot(workspaceContext);
|
|
110
|
+
if (canonicalKey(primary.path) !== canonicalKey(cwd)) {
|
|
111
|
+
return workspaceError(options?.sessionId, `WorkspaceContext primary does not match effective cwd: ${primary.path} != ${cwd}`);
|
|
112
|
+
}
|
|
113
|
+
if (workspaceResume?.ok &&
|
|
114
|
+
workspaceResume.reason !== "legacy" &&
|
|
115
|
+
canonicalKey(workspaceResume.workspace.root) !== canonicalKey(primary.path)) {
|
|
116
|
+
return workspaceError(options?.sessionId, "WorkspaceContext primary does not match SessionWorkspace");
|
|
117
|
+
}
|
|
118
|
+
const binding = options?.sessionId
|
|
119
|
+
? args.sessionManager.readSessionProjectBinding(options.sessionId)
|
|
120
|
+
: undefined;
|
|
121
|
+
if (binding && !authoritativeWorkspaceContext) {
|
|
122
|
+
return workspaceError(options?.sessionId, "bound Session requires an authoritative WorkspaceContext");
|
|
123
|
+
}
|
|
124
|
+
if (binding &&
|
|
125
|
+
(binding.projectId !== workspaceContext.projectId ||
|
|
126
|
+
binding.mainRootId !== workspaceContext.sessionMainRootId)) {
|
|
127
|
+
return workspaceError(options?.sessionId, "WorkspaceContext does not match persisted project binding");
|
|
128
|
+
}
|
|
96
129
|
const profileState = profile?.disableWorkspaceProfile
|
|
97
130
|
? {
|
|
98
131
|
workspaceProfile: undefined,
|
|
@@ -114,7 +147,21 @@ export async function resolveRunWorkspace(args) {
|
|
|
114
147
|
runPermissionMode,
|
|
115
148
|
runPlanMode,
|
|
116
149
|
cwd,
|
|
150
|
+
workspaceContext,
|
|
151
|
+
authoritativeWorkspaceContext,
|
|
117
152
|
profileState,
|
|
118
153
|
},
|
|
119
154
|
};
|
|
120
155
|
}
|
|
156
|
+
function workspaceError(sessionId, message) {
|
|
157
|
+
return {
|
|
158
|
+
ok: false,
|
|
159
|
+
result: {
|
|
160
|
+
text: `ERROR: ${message}`,
|
|
161
|
+
reason: "completed",
|
|
162
|
+
sessionId: sessionId ?? "workspace-invalid",
|
|
163
|
+
turnCount: 0,
|
|
164
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -21,6 +21,7 @@ export interface CreateSubAgentSpawnerDeps {
|
|
|
21
21
|
parentSandbox: SandboxConfig;
|
|
22
22
|
presetName: AgentPresetName;
|
|
23
23
|
cwd: string;
|
|
24
|
+
workspaceContext?: import("../workspace/workspace-context.js").WorkspaceContext;
|
|
24
25
|
permissionMode: NonNullable<EngineConfig["permissionMode"]>;
|
|
25
26
|
modelPool?: ModelPool;
|
|
26
27
|
appendParentSubagent: (agentId: string, description: string) => void;
|
|
@@ -233,6 +233,7 @@ export function createSubAgentSpawner(deps) {
|
|
|
233
233
|
retryMaxAttempts: 2,
|
|
234
234
|
},
|
|
235
235
|
cwd: deps.cwd,
|
|
236
|
+
workspaceContext: deps.workspaceContext ?? deps.parentConfig.workspaceContext,
|
|
236
237
|
permissionMode: deps.permissionMode,
|
|
237
238
|
preset: deps.presetName,
|
|
238
239
|
enabledBuiltinTools: scope.enabled,
|
package/dist/engine/types.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface EngineConfig {
|
|
|
33
33
|
*/
|
|
34
34
|
clientDefaults?: ClientDefaults;
|
|
35
35
|
cwd?: string;
|
|
36
|
+
/** Trusted host-provided root authorization for runs created by this Engine. */
|
|
37
|
+
workspaceContext?: import("../workspace/workspace-context.js").WorkspaceContext;
|
|
36
38
|
maxTurns?: number;
|
|
37
39
|
/**
|
|
38
40
|
* Override the goal-mode consecutive-stop-block cap (TODO 3.1). Falls back to
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Public API exports.
|
|
5
5
|
*/
|
|
6
|
-
export declare const VERSION = "0.
|
|
7
|
-
export type { Message, ContentBlock, ToolDefinition, ToolCall, ToolResult, RegisteredTool, TranscriptEvent, TranscriptEventType, SessionState, SessionKind, SessionWorkspace, SessionForkLineage, ContextUsageAnchor, SessionStatus, TokenUsage, CompiledInput, PermissionDecision, PermissionMode, PermissionRule, TurnPhase, TurnResult, TerminalReason, TurnCompletionKind, StreamEvent, StreamCallback, LLMConfig, ClientDefaults, LLMResponse, Settings, MCPServerConfig, } from "./types.js";
|
|
6
|
+
export declare const VERSION = "0.9.0";
|
|
7
|
+
export type { Message, ContentBlock, ToolDefinition, ToolCall, ToolResult, RegisteredTool, TranscriptEvent, TranscriptEventType, SessionState, SessionKind, SessionWorkspace, SessionProjectBinding, SessionForkLineage, ContextUsageAnchor, SessionStatus, TokenUsage, CompiledInput, PermissionDecision, PermissionMode, PermissionRule, TurnPhase, TurnResult, TerminalReason, TurnCompletionKind, StreamEvent, StreamCallback, LLMConfig, ClientDefaults, LLMResponse, Settings, MCPServerConfig, } from "./types.js";
|
|
8
8
|
export type { GoalConfig, GoalLifecycleConfig, GoalLifecyclePhase, GoalLifecycleTerminalReason, GoalLifecycleV1, } from "./goal/lifecycle.js";
|
|
9
9
|
export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, CompositionError, SandboxUnavailableError, } from "./exceptions.js";
|
|
10
10
|
export { compileComposition, toCompositionSnapshot, computeCompositionDigest, compositionToolCatalog, resolvePresetFromComposition, } from "./composition/index.js";
|
|
@@ -62,7 +62,7 @@ export { AgentClient, type BackgroundAgentCompletedHandler } from "./protocol/cl
|
|
|
62
62
|
export { createInProcessTransport, StdioTransport, type Transport } from "./protocol/transport.js";
|
|
63
63
|
export { SocketTransport, listenTcp, type TcpListenResult } from "./protocol/tcp-transport.js";
|
|
64
64
|
export { createServer, createClient, type CreateServerOptions, type CreateClientOptions, type ServerHandle, } from "./protocol/factories.js";
|
|
65
|
-
export { Methods, ErrorCodes, type RpcMessage, type RunResult, type ForkSessionParams, type ForkSessionResult as ProtocolForkSessionResult, } from "./protocol/types.js";
|
|
65
|
+
export { Methods, ErrorCodes, type RpcMessage, type RunResult, type ForkSessionParams, type ForkSessionResult as ProtocolForkSessionResult, type MigrateSessionMainRootResult, } from "./protocol/types.js";
|
|
66
66
|
export type * from "./protocol/mobile-remote-types.js";
|
|
67
67
|
export { Transcript } from "./session/transcript.js";
|
|
68
68
|
export { SessionManager, sessionMainRoot, codeShellHome, sessionsRoot, buildForkState, buildForkTranscript, type ForkSessionOptions, type ForkSessionResult, } from "./session/session-manager.js";
|
|
@@ -26,7 +26,7 @@ export type { ExposureRationale } from "./tool-system/external-tool-exposure.js"
|
|
|
26
26
|
export { webSearchTool } from "./tool-system/builtin/web-search.js";
|
|
27
27
|
export { webFetchTool } from "./tool-system/builtin/web-fetch.js";
|
|
28
28
|
export { extractJSON, extractJSONArray } from "./utils/json.js";
|
|
29
|
-
export type { ExtensionQueryHandler, ExtensionTool
|
|
29
|
+
export type { ExtensionQueryHandler, ExtensionTool } from "./tool-system/capability-module.js";
|
|
30
30
|
export type { SessionWorkspace } from "./types.js";
|
|
31
31
|
export type { CapabilityArtifactDetector, CapabilityDynamicContextProvider, CapabilityToolServiceHost, } from "./capabilities/index.js";
|
|
32
32
|
export type { AgentModule, AgentEngineContributions, AgentProtocolContributions, AgentModuleToolContribution, ResolvedComposition, } from "./composition/types.js";
|
package/dist/index.internal.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export { getGraphemeSegmenter, firstGrapheme, lastGrapheme, getWordSegmenter, getRelativeTimeFormat, getTimeZone, getSystemLocaleLanguage, } from "./utils/intl.js";
|
|
9
9
|
export { env } from "./utils/env.js";
|
|
10
|
+
export { canonicalKey, canonicalPath, computeWorkspaceRootsDigest, createWorkspaceContext, legacySingleRootWorkspace, validateWorkspaceContext, workspacePrimaryRoot, type ProjectId, type ProjectRootContext, type ProjectRootId, type WorkspaceContext, type WorkspaceContextInput, } from "./workspace/workspace-context.js";
|
|
10
11
|
export { default as sliceAnsi } from "./utils/sliceAnsi.js";
|
|
11
12
|
export { execFileNoThrow } from "./utils/execFileNoThrow.js";
|
|
12
13
|
export { findExecutable, resolveExecutable, setGitPathOverride, resolveGit, isGitAvailable, resolveGitPath, } from "./utils/exec.js";
|
|
@@ -27,7 +28,6 @@ export { getInteractiveApprovalBackend } from "./tool-system/permission.js";
|
|
|
27
28
|
export { defaultSandboxConfig, type SandboxConfig } from "./tool-system/sandbox/index.js";
|
|
28
29
|
export { buildNotificationMessage, buildNotificationSummary, notificationQueue, agentNotificationBus, notificationItemToStreamEvent, type NotificationItem, } from "./tool-system/builtin/agent-notifications.js";
|
|
29
30
|
export { backgroundJobRegistry } from "./tool-system/builtin/background-jobs.js";
|
|
30
|
-
export type { BackgroundAgentCompletedEvent } from "./types.js";
|
|
31
31
|
export { startAutomation, type StartAutomationDeps, type AutomationHandle, CronScheduler, cronScheduler, type CronExecutionOutcome, type CronJob, type CronJobLifecycleEvent, type CronPermissionLevel, type CronTemplateSource, type CreateJobOptions, type UpdateJobPatch, CronStore, defaultCronStorePath, bindCronToEngine, bindCronToRunManager, type CronRunner, type CronRunRequest, type CronRunResult, type RunSubmitter, isCronExpression, parseCronExpression, nextCronTime, validateSchedule, type ParsedCron, resolveWritePolicy, wrapUntrustedInput, type WritePolicy, runWriteJobInWorktree, type WriteJobGitOps, type RunWriteJobInput, type RunWriteJobResult, } from "./automation/index.js";
|
|
32
32
|
export { asyncAgentRegistry, type AsyncAgentEntry } from "./tool-system/builtin/agent-registry.js";
|
|
33
33
|
export { backgroundShellManager, BackgroundShellManager, type BgShell, type BgShellStatus, } from "./runtime/background-shell.js";
|
|
@@ -46,7 +46,7 @@ export { capabilitiesFor, type Capability } from "./llm/capabilities/index.js";
|
|
|
46
46
|
export { reasoningControlFor, type ReasoningControl, } from "./llm/capabilities/reasoning-control.js";
|
|
47
47
|
export { REASONING_EFFORTS, type ReasoningSetting } from "./llm/reasoning-setting.js";
|
|
48
48
|
export { type ProviderConfig } from "./llm/provider-catalog.js";
|
|
49
|
-
export type { ApprovalRequest, ApprovalResult, ApprovalScope, TaskInfo } from "./types.js";
|
|
49
|
+
export type { ApprovalRequest, ApprovalResult, ApprovalScope, BackgroundAgentCompletedEvent, TaskInfo, } from "./types.js";
|
|
50
50
|
export { fileCache } from "./tool-system/builtin/file-cache.js";
|
|
51
51
|
export { validateToolArgs } from "./tool-system/validation.js";
|
|
52
52
|
export { createOffBackend } from "./tool-system/sandbox/off.js";
|
package/dist/index.internal.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// ─── Utils (shared primitives used by TUI) ───────────────────────
|
|
9
9
|
export { getGraphemeSegmenter, firstGrapheme, lastGrapheme, getWordSegmenter, getRelativeTimeFormat, getTimeZone, getSystemLocaleLanguage, } from "./utils/intl.js";
|
|
10
10
|
export { env } from "./utils/env.js";
|
|
11
|
+
export { canonicalKey, canonicalPath, computeWorkspaceRootsDigest, createWorkspaceContext, legacySingleRootWorkspace, validateWorkspaceContext, workspacePrimaryRoot, } from "./workspace/workspace-context.js";
|
|
11
12
|
export { default as sliceAnsi } from "./utils/sliceAnsi.js";
|
|
12
13
|
export { execFileNoThrow } from "./utils/execFileNoThrow.js";
|
|
13
14
|
export { findExecutable, resolveExecutable, setGitPathOverride, resolveGit, isGitAvailable, resolveGitPath, } from "./utils/exec.js";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Public API exports.
|
|
5
5
|
*/
|
|
6
|
-
export const VERSION = "0.
|
|
6
|
+
export const VERSION = "0.9.0";
|
|
7
7
|
// ─── Exceptions ──────────────────────────────────────────────────
|
|
8
8
|
export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, CompositionError, SandboxUnavailableError, } from "./exceptions.js";
|
|
9
9
|
// ─── Composition (AgentModule / ResolvedComposition) ─────────────
|
|
@@ -6,6 +6,8 @@ export interface InstantiatePluginAutomationTemplateOptions {
|
|
|
6
6
|
templateId: string;
|
|
7
7
|
expectedRevision: string;
|
|
8
8
|
cwd?: string;
|
|
9
|
+
projectId?: string;
|
|
10
|
+
rootId?: string;
|
|
9
11
|
/** Effective project/global disable list computed by the host for this cwd. */
|
|
10
12
|
disabledPluginNames: ReadonlySet<string>;
|
|
11
13
|
maxJobs?: number;
|
|
@@ -38,6 +38,10 @@ export function instantiatePluginAutomationTemplate(options) {
|
|
|
38
38
|
const template = contribution.template;
|
|
39
39
|
return options.scheduler.create(template.title.default, template.schedule, template.prompt, {
|
|
40
40
|
...(template.workspace === "current" && options.cwd ? { cwd: options.cwd } : {}),
|
|
41
|
+
...(template.workspace === "current" && options.projectId
|
|
42
|
+
? { projectId: options.projectId }
|
|
43
|
+
: {}),
|
|
44
|
+
...(template.workspace === "current" && options.rootId ? { rootId: options.rootId } : {}),
|
|
41
45
|
...(template.timezone ? { timezone: template.timezone } : {}),
|
|
42
46
|
permissionLevel: template.permissionLevel,
|
|
43
47
|
templateSource: {
|
|
@@ -10,8 +10,11 @@ import { type ScanOptions } from "./instruction-scanner.js";
|
|
|
10
10
|
import { type AgentPreset } from "../preset/index.js";
|
|
11
11
|
import type { BuiltinTool } from "../tool-system/builtin/index.js";
|
|
12
12
|
import type { CapabilityDynamicContextProvider } from "../capabilities/index.js";
|
|
13
|
+
import { type WorkspaceContext } from "../workspace/workspace-context.js";
|
|
13
14
|
export interface ComposerOptions {
|
|
14
15
|
cwd: string;
|
|
16
|
+
/** Run workspace; omitted by legacy callers that only provide cwd. */
|
|
17
|
+
workspace?: WorkspaceContext;
|
|
15
18
|
model: string;
|
|
16
19
|
instructionOptions?: ScanOptions;
|
|
17
20
|
/** Resolved preset — used to load section-based prompt. */
|
|
@@ -106,9 +109,9 @@ export interface ComposerOptions {
|
|
|
106
109
|
disableSourcesContext?: boolean;
|
|
107
110
|
}
|
|
108
111
|
export declare class PromptComposer {
|
|
109
|
-
private readonly options;
|
|
110
112
|
private sectionCache;
|
|
111
113
|
private cachedInstructions;
|
|
114
|
+
private readonly options;
|
|
112
115
|
constructor(options: ComposerOptions);
|
|
113
116
|
/**
|
|
114
117
|
* Build the system prompt from sections.
|
package/dist/prompt/composer.js
CHANGED
|
@@ -11,12 +11,18 @@ import { MemoryManager } from "../session/memory.js";
|
|
|
11
11
|
import { scanSkills } from "../skills/index.js";
|
|
12
12
|
import { buildSkillListing } from "../tool-system/builtin/skill-prompt.js";
|
|
13
13
|
import { resolveAgentPreset, buildPresetSystemPrompt } from "../preset/index.js";
|
|
14
|
+
import { legacySingleRootWorkspace, validateWorkspaceContext, } from "../workspace/workspace-context.js";
|
|
14
15
|
export class PromptComposer {
|
|
15
|
-
options;
|
|
16
16
|
sectionCache = new SectionCache();
|
|
17
17
|
cachedInstructions = null;
|
|
18
|
+
options;
|
|
18
19
|
constructor(options) {
|
|
19
|
-
this.options =
|
|
20
|
+
this.options = {
|
|
21
|
+
...options,
|
|
22
|
+
workspace: options.workspace === undefined
|
|
23
|
+
? legacySingleRootWorkspace(options.cwd)
|
|
24
|
+
: validateWorkspaceContext(options.workspace),
|
|
25
|
+
};
|
|
20
26
|
}
|
|
21
27
|
/**
|
|
22
28
|
* Build the system prompt from sections.
|
|
@@ -58,7 +64,7 @@ export class PromptComposer {
|
|
|
58
64
|
const preset = this.options.preset ?? resolveAgentPreset();
|
|
59
65
|
const parts = await Promise.all((this.options.dynamicContextProviders ?? []).map(async (provider) => {
|
|
60
66
|
try {
|
|
61
|
-
return await provider({ cwd: this.options.cwd, preset });
|
|
67
|
+
return await provider({ cwd: this.options.cwd, workspace: this.options.workspace, preset });
|
|
62
68
|
}
|
|
63
69
|
catch {
|
|
64
70
|
// A capability's optional context must not make a turn fail.
|
|
@@ -164,6 +170,8 @@ export class PromptComposer {
|
|
|
164
170
|
const lines = [
|
|
165
171
|
`You are an AI agent powered by ${this.options.model}.`,
|
|
166
172
|
`Working directory: ${this.options.cwd}`,
|
|
173
|
+
`Workspace roots (${this.options.workspace.roots.length}):`,
|
|
174
|
+
...this.options.workspace.roots.map((root) => `- [${root.role}] ${root.path} (rootId: ${root.id})`),
|
|
167
175
|
`Platform: ${process.platform}`,
|
|
168
176
|
`Shell: ${process.env.SHELL ?? "unknown"}`,
|
|
169
177
|
];
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ChatSession } from "./chat-session.js";
|
|
2
2
|
import type { SessionKind } from "../types.js";
|
|
3
|
+
import type { SessionProjectBinding, SessionWorkspace } from "../types.js";
|
|
3
4
|
import type { Engine } from "../engine/engine.js";
|
|
4
5
|
import type { EngineRuntime } from "../engine/runtime.js";
|
|
5
6
|
import type { EngineConfig } from "../engine/types.js";
|
|
6
|
-
export type EngineConfigSlice = Pick<EngineConfig, "permissionMode" | "preset" | "customSystemPrompt" | "appendSystemPrompt" | "goal" | "maxTurns" | "maxContextTokens" | "cwd" | "projectTrusted" | "sessionStorageDir">;
|
|
7
|
+
export type EngineConfigSlice = Pick<EngineConfig, "permissionMode" | "preset" | "customSystemPrompt" | "appendSystemPrompt" | "goal" | "maxTurns" | "maxContextTokens" | "cwd" | "workspaceContext" | "projectTrusted" | "sessionStorageDir">;
|
|
7
8
|
/** Identity scope every ChatSessionManager belongs to when none is injected. */
|
|
8
9
|
export declare const LOCAL_CHAT_IDENTITY = "local";
|
|
9
10
|
export interface ChatSessionManagerOptions {
|
|
@@ -48,11 +49,30 @@ export interface LiveChatSessionSnapshot {
|
|
|
48
49
|
}>;
|
|
49
50
|
}
|
|
50
51
|
export declare const CLOSED_CHAT_SESSION_TOMBSTONE_LIMIT = 4096;
|
|
52
|
+
export interface ResidentSessionMainRootMigration {
|
|
53
|
+
project: SessionProjectBinding;
|
|
54
|
+
mainRoot: string;
|
|
55
|
+
workspaceContext: import("../workspace/workspace-context.js").WorkspaceContext;
|
|
56
|
+
projectTrusted: boolean;
|
|
57
|
+
}
|
|
58
|
+
export type SessionMigrationOwnership = {
|
|
59
|
+
status: "resident";
|
|
60
|
+
session: ChatSession;
|
|
61
|
+
} | {
|
|
62
|
+
status: "not-resident";
|
|
63
|
+
ownershipToken: string;
|
|
64
|
+
} | {
|
|
65
|
+
status: "failed";
|
|
66
|
+
error: string;
|
|
67
|
+
};
|
|
51
68
|
export declare class ChatSessionManager {
|
|
52
69
|
private readonly sessions;
|
|
53
70
|
private readonly closingSessions;
|
|
54
71
|
private readonly closedSessions;
|
|
55
72
|
private readonly sessionGeneration;
|
|
73
|
+
private readonly sessionSlices;
|
|
74
|
+
private readonly migrationClaims;
|
|
75
|
+
private readonly residentMigrations;
|
|
56
76
|
readonly runtime: EngineRuntime;
|
|
57
77
|
/** Identity scope this manager serves ("local" unless injected). */
|
|
58
78
|
readonly identity: string;
|
|
@@ -76,6 +96,26 @@ export declare class ChatSessionManager {
|
|
|
76
96
|
*/
|
|
77
97
|
forIdentity(identity: string): ChatSessionManager;
|
|
78
98
|
getOrCreate(sessionId: string, slice: EngineConfigSlice): Promise<ChatSession>;
|
|
99
|
+
/**
|
|
100
|
+
* Prove whether this worker currently owns a resident Engine, or fence the
|
|
101
|
+
* Session so Main can perform one durable migration without a re-resume race.
|
|
102
|
+
*/
|
|
103
|
+
beginSessionMigration(sessionId: string, ownershipToken: string): SessionMigrationOwnership;
|
|
104
|
+
/** Release only the exact claim minted for this Session. */
|
|
105
|
+
completeSessionMigration(sessionId: string, ownershipToken: string): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Rebuild an idle resident Engine against a new authoritative main root.
|
|
108
|
+
*
|
|
109
|
+
* Transaction order is intentional:
|
|
110
|
+
* 1. construct and restore the candidate while durable state still points at
|
|
111
|
+
* the old owner (factory/restore failure is therefore a no-op),
|
|
112
|
+
* 2. atomically commit durable state through the old owning Engine,
|
|
113
|
+
* 3. synchronously swap the Engine inside the existing ChatSession,
|
|
114
|
+
* 4. dispose the unreachable old Engine before reporting success.
|
|
115
|
+
*
|
|
116
|
+
* No await occurs between the idle check, durable commit and owner swap.
|
|
117
|
+
*/
|
|
118
|
+
migrateResidentSessionMainRoot(sessionId: string, target: ResidentSessionMainRootMigration): Promise<SessionWorkspace>;
|
|
79
119
|
/**
|
|
80
120
|
* Cold-resume a persisted session using its own cwd. The first detached
|
|
81
121
|
* Engine is only a storage probe; all actual work runs on the second Engine
|
|
@@ -121,6 +161,7 @@ export declare class ChatSessionManager {
|
|
|
121
161
|
startIdleSweeper(intervalMs?: number): void;
|
|
122
162
|
stopIdleSweeper(): void;
|
|
123
163
|
private unregisterMcpOwner;
|
|
164
|
+
private disposeEngine;
|
|
124
165
|
private engineSessionManager;
|
|
125
166
|
}
|
|
126
167
|
/**
|
|
@@ -34,6 +34,9 @@ export class ChatSessionManager {
|
|
|
34
34
|
closingSessions = new Map();
|
|
35
35
|
closedSessions = new Set();
|
|
36
36
|
sessionGeneration = new Map();
|
|
37
|
+
sessionSlices = new Map();
|
|
38
|
+
migrationClaims = new Map();
|
|
39
|
+
residentMigrations = new Map();
|
|
37
40
|
runtime;
|
|
38
41
|
/** Identity scope this manager serves ("local" unless injected). */
|
|
39
42
|
identity;
|
|
@@ -78,11 +81,133 @@ export class ChatSessionManager {
|
|
|
78
81
|
});
|
|
79
82
|
}
|
|
80
83
|
async getOrCreate(sessionId, slice) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
// A non-resident migration claim is a short ownership handoff to Main.
|
|
85
|
+
// Wait rather than fail the user's run: once Main atomically commits (or
|
|
86
|
+
// aborts) and releases the token, the Engine is created from current disk.
|
|
87
|
+
for (;;) {
|
|
88
|
+
const residentMigration = this.residentMigrations.get(sessionId);
|
|
89
|
+
if (residentMigration) {
|
|
90
|
+
await residentMigration.released;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const claim = this.migrationClaims.get(sessionId);
|
|
94
|
+
if (claim) {
|
|
95
|
+
await claim.released;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const closing = this.closingSessions.get(sessionId);
|
|
99
|
+
if (closing) {
|
|
100
|
+
await closing;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// No await separates the final claim check from getOrCreateNow. On this
|
|
104
|
+
// process's event loop, either this creates the resident owner first or
|
|
105
|
+
// beginSessionMigration installs the fence first; both cannot win.
|
|
106
|
+
return this.getOrCreateNow(sessionId, slice);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Prove whether this worker currently owns a resident Engine, or fence the
|
|
111
|
+
* Session so Main can perform one durable migration without a re-resume race.
|
|
112
|
+
*/
|
|
113
|
+
beginSessionMigration(sessionId, ownershipToken) {
|
|
114
|
+
if (this.residentMigrations.has(sessionId)) {
|
|
115
|
+
return { status: "failed", error: `Session ${sessionId} migration is already in progress` };
|
|
116
|
+
}
|
|
117
|
+
const resident = this.sessions.get(sessionId);
|
|
118
|
+
if (resident)
|
|
119
|
+
return { status: "resident", session: resident };
|
|
120
|
+
if (this.closingSessions.has(sessionId)) {
|
|
121
|
+
return { status: "failed", error: `Session ${sessionId} is closing` };
|
|
122
|
+
}
|
|
123
|
+
if (this.migrationClaims.has(sessionId)) {
|
|
124
|
+
return { status: "failed", error: `Session ${sessionId} migration is already in progress` };
|
|
125
|
+
}
|
|
126
|
+
let release;
|
|
127
|
+
const released = new Promise((resolve) => {
|
|
128
|
+
release = resolve;
|
|
129
|
+
});
|
|
130
|
+
this.migrationClaims.set(sessionId, { ownershipToken, released, release });
|
|
131
|
+
return { status: "not-resident", ownershipToken };
|
|
132
|
+
}
|
|
133
|
+
/** Release only the exact claim minted for this Session. */
|
|
134
|
+
completeSessionMigration(sessionId, ownershipToken) {
|
|
135
|
+
const claim = this.migrationClaims.get(sessionId);
|
|
136
|
+
if (!claim || claim.ownershipToken !== ownershipToken)
|
|
137
|
+
return false;
|
|
138
|
+
this.migrationClaims.delete(sessionId);
|
|
139
|
+
claim.release();
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Rebuild an idle resident Engine against a new authoritative main root.
|
|
144
|
+
*
|
|
145
|
+
* Transaction order is intentional:
|
|
146
|
+
* 1. construct and restore the candidate while durable state still points at
|
|
147
|
+
* the old owner (factory/restore failure is therefore a no-op),
|
|
148
|
+
* 2. atomically commit durable state through the old owning Engine,
|
|
149
|
+
* 3. synchronously swap the Engine inside the existing ChatSession,
|
|
150
|
+
* 4. dispose the unreachable old Engine before reporting success.
|
|
151
|
+
*
|
|
152
|
+
* No await occurs between the idle check, durable commit and owner swap.
|
|
153
|
+
*/
|
|
154
|
+
async migrateResidentSessionMainRoot(sessionId, target) {
|
|
155
|
+
const session = this.sessions.get(sessionId);
|
|
156
|
+
if (!session)
|
|
157
|
+
throw new Error(`Session ${sessionId} is not resident`);
|
|
158
|
+
if (session.isBusy() || session.queueDepth() > 0) {
|
|
159
|
+
throw new Error(`Session ${sessionId} is running or has queued turns`);
|
|
160
|
+
}
|
|
161
|
+
if (this.residentMigrations.has(sessionId) || this.migrationClaims.has(sessionId)) {
|
|
162
|
+
throw new Error(`Session ${sessionId} migration is already in progress`);
|
|
163
|
+
}
|
|
164
|
+
let release;
|
|
165
|
+
const released = new Promise((resolve) => {
|
|
166
|
+
release = resolve;
|
|
167
|
+
});
|
|
168
|
+
this.residentMigrations.set(sessionId, { released, release });
|
|
169
|
+
const previous = session.engine;
|
|
170
|
+
let candidate;
|
|
171
|
+
let committed = false;
|
|
172
|
+
try {
|
|
173
|
+
const previousSlice = this.sessionSlices.get(sessionId) ?? {};
|
|
174
|
+
const permissionMode = previous.getPermissionMode?.() ?? previousSlice.permissionMode;
|
|
175
|
+
const nextSlice = {
|
|
176
|
+
...previousSlice,
|
|
177
|
+
cwd: target.mainRoot,
|
|
178
|
+
workspaceContext: target.workspaceContext,
|
|
179
|
+
projectTrusted: target.projectTrusted,
|
|
180
|
+
...(permissionMode ? { permissionMode } : {}),
|
|
181
|
+
};
|
|
182
|
+
const built = this.factory(nextSlice);
|
|
183
|
+
if (built === previous) {
|
|
184
|
+
throw new Error(`Session ${sessionId} Engine factory reused the resident owner`);
|
|
185
|
+
}
|
|
186
|
+
candidate = built;
|
|
187
|
+
if (permissionMode && candidate.getPermissionMode?.() !== permissionMode) {
|
|
188
|
+
candidate.setPermissionMode?.(permissionMode);
|
|
189
|
+
}
|
|
190
|
+
const candidateGeneration = this.engineSessionManager(candidate)?.registerSessionGeneration(sessionId) ??
|
|
191
|
+
this.sessionGeneration.get(sessionId) ??
|
|
192
|
+
1;
|
|
193
|
+
candidate.restoreSessionModel?.(sessionId);
|
|
194
|
+
const workspace = previous.migrateSessionMainRoot(sessionId, target.project, target.mainRoot);
|
|
195
|
+
session.replaceEngine(previous, candidate);
|
|
196
|
+
this.sessionGeneration.set(sessionId, candidateGeneration);
|
|
197
|
+
this.sessionSlices.set(sessionId, nextSlice);
|
|
198
|
+
committed = true;
|
|
199
|
+
await this.disposeEngine(previous, sessionId);
|
|
200
|
+
return workspace;
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (!committed && candidate)
|
|
204
|
+
await this.disposeEngine(candidate, sessionId);
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
this.residentMigrations.delete(sessionId);
|
|
209
|
+
release();
|
|
84
210
|
}
|
|
85
|
-
return this.getOrCreateNow(sessionId, slice);
|
|
86
211
|
}
|
|
87
212
|
/**
|
|
88
213
|
* Cold-resume a persisted session using its own cwd. The first detached
|
|
@@ -129,6 +254,7 @@ export class ChatSessionManager {
|
|
|
129
254
|
const sessionManager = this.engineSessionManager(engine);
|
|
130
255
|
const generation = sessionManager?.registerSessionGeneration(sessionId) ?? 1;
|
|
131
256
|
this.sessionGeneration.set(sessionId, generation);
|
|
257
|
+
this.sessionSlices.set(sessionId, { ...slice });
|
|
132
258
|
this.sessions.set(sessionId, session);
|
|
133
259
|
// A direct user run is an explicit resume/open and may clear the tombstone.
|
|
134
260
|
// Background wakeups must check isUnavailable() before reaching this path.
|
|
@@ -201,6 +327,10 @@ export class ChatSessionManager {
|
|
|
201
327
|
return this.closeSession(sessionId, true);
|
|
202
328
|
}
|
|
203
329
|
closeSession(sessionId, markClosed) {
|
|
330
|
+
const migration = this.residentMigrations.get(sessionId);
|
|
331
|
+
if (migration) {
|
|
332
|
+
return migration.released.then(() => this.closeSession(sessionId, markClosed));
|
|
333
|
+
}
|
|
204
334
|
const alreadyClosing = this.closingSessions.get(sessionId);
|
|
205
335
|
if (alreadyClosing)
|
|
206
336
|
return alreadyClosing;
|
|
@@ -237,6 +367,7 @@ export class ChatSessionManager {
|
|
|
237
367
|
else
|
|
238
368
|
this.closedSessions.delete(sessionId);
|
|
239
369
|
this.sessionGeneration.delete(sessionId);
|
|
370
|
+
this.sessionSlices.delete(sessionId);
|
|
240
371
|
};
|
|
241
372
|
if (!s.isBusy()) {
|
|
242
373
|
finishClose();
|
|
@@ -278,6 +409,9 @@ export class ChatSessionManager {
|
|
|
278
409
|
* immediately afterward (the TUI REPL) doesn't orphan detached dev servers.
|
|
279
410
|
*/
|
|
280
411
|
async closeAllAsync() {
|
|
412
|
+
for (const [sessionId, claim] of [...this.migrationClaims]) {
|
|
413
|
+
this.completeSessionMigration(sessionId, claim.ownershipToken);
|
|
414
|
+
}
|
|
281
415
|
await Promise.all([...this.sessions.keys()].map((id) => this.close(id)));
|
|
282
416
|
// App/worker shutdown — reap every background shell so a detached
|
|
283
417
|
// `npm run dev` doesn't outlive the process as an orphan holding a port
|
|
@@ -335,6 +469,35 @@ export class ChatSessionManager {
|
|
|
335
469
|
});
|
|
336
470
|
});
|
|
337
471
|
}
|
|
472
|
+
async disposeEngine(engine, sessionId) {
|
|
473
|
+
const dispose = engine.dispose;
|
|
474
|
+
if (typeof dispose === "function") {
|
|
475
|
+
try {
|
|
476
|
+
await dispose.call(engine);
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
logger.warn("chat_session.engine_dispose_failed", {
|
|
480
|
+
sessionId,
|
|
481
|
+
identity: this.identity,
|
|
482
|
+
error: error instanceof Error ? error.message : String(error),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const mcpPool = this.runtime.mcpPool;
|
|
488
|
+
if (typeof mcpPool?.unregisterOwner !== "function")
|
|
489
|
+
return;
|
|
490
|
+
try {
|
|
491
|
+
await mcpPool.unregisterOwner(engine);
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
logger.warn("chat_session.mcp_owner_unregister_failed", {
|
|
495
|
+
sessionId,
|
|
496
|
+
identity: this.identity,
|
|
497
|
+
error: error instanceof Error ? error.message : String(error),
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
338
501
|
engineSessionManager(engine) {
|
|
339
502
|
const candidate = engine;
|
|
340
503
|
const manager = candidate.getSessionManager?.();
|
|
@@ -21,6 +21,7 @@ export interface TurnOpts {
|
|
|
21
21
|
/** Working directory override for this turn. If omitted, Engine uses its
|
|
22
22
|
* configured cwd. */
|
|
23
23
|
cwd?: string;
|
|
24
|
+
workspaceContext?: import("../workspace/workspace-context.js").WorkspaceContext;
|
|
24
25
|
onStream?: (event: StreamEvent) => void;
|
|
25
26
|
/** User-facing text persisted beside the full model-facing task. */
|
|
26
27
|
displayText?: string;
|
|
@@ -70,7 +71,7 @@ export interface TurnOpts {
|
|
|
70
71
|
*/
|
|
71
72
|
export declare class ChatSession {
|
|
72
73
|
readonly id: string;
|
|
73
|
-
|
|
74
|
+
private currentEngine;
|
|
74
75
|
/**
|
|
75
76
|
* Per-session approval callbacks and resolver-free metadata indexed by requestId.
|
|
76
77
|
* `readonly` guards the Map reference (preventing reassignment); the
|
|
@@ -111,6 +112,13 @@ export declare class ChatSession {
|
|
|
111
112
|
private settlePromise;
|
|
112
113
|
private resolveSettled;
|
|
113
114
|
constructor(opts: ChatSessionOptions);
|
|
115
|
+
get engine(): Engine;
|
|
116
|
+
/**
|
|
117
|
+
* Swap the Engine at an idle run boundary without replacing the ChatSession
|
|
118
|
+
* that owns the queue, stream callback, approvals, idle timestamp and id.
|
|
119
|
+
* The expected-owner check makes a stale migration fail closed.
|
|
120
|
+
*/
|
|
121
|
+
replaceEngine(expected: Engine, replacement: Engine): void;
|
|
114
122
|
enqueueTurn(task: string, opts: TurnOpts): Promise<EngineResult>;
|
|
115
123
|
/**
|
|
116
124
|
* Run session maintenance (for example context-package summarization) under
|
|
@@ -7,7 +7,7 @@ import { isSameGoalInstance } from "../goal/lifecycle.js";
|
|
|
7
7
|
*/
|
|
8
8
|
export class ChatSession {
|
|
9
9
|
id;
|
|
10
|
-
|
|
10
|
+
currentEngine;
|
|
11
11
|
/**
|
|
12
12
|
* Per-session approval callbacks and resolver-free metadata indexed by requestId.
|
|
13
13
|
* `readonly` guards the Map reference (preventing reassignment); the
|
|
@@ -49,9 +49,26 @@ export class ChatSession {
|
|
|
49
49
|
resolveSettled = null;
|
|
50
50
|
constructor(opts) {
|
|
51
51
|
this.id = opts.id;
|
|
52
|
-
this.
|
|
52
|
+
this.currentEngine = opts.engine;
|
|
53
53
|
this.defaultOnStream = opts.onStream;
|
|
54
54
|
}
|
|
55
|
+
get engine() {
|
|
56
|
+
return this.currentEngine;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Swap the Engine at an idle run boundary without replacing the ChatSession
|
|
60
|
+
* that owns the queue, stream callback, approvals, idle timestamp and id.
|
|
61
|
+
* The expected-owner check makes a stale migration fail closed.
|
|
62
|
+
*/
|
|
63
|
+
replaceEngine(expected, replacement) {
|
|
64
|
+
if (this.active || this.exclusiveOperation || this.queue.length > 0) {
|
|
65
|
+
throw new Error(`Session ${this.id} is running or has queued turns`);
|
|
66
|
+
}
|
|
67
|
+
if (this.currentEngine !== expected) {
|
|
68
|
+
throw new Error(`Session ${this.id} Engine ownership changed during migration`);
|
|
69
|
+
}
|
|
70
|
+
this.currentEngine = replacement;
|
|
71
|
+
}
|
|
55
72
|
enqueueTurn(task, opts) {
|
|
56
73
|
this.lastActivityAt = Date.now();
|
|
57
74
|
// The user (or a wakeup the guard already let through) is starting a turn —
|
|
@@ -283,6 +300,7 @@ export class ChatSession {
|
|
|
283
300
|
const onStream = next.opts.onStream ?? this.defaultOnStream;
|
|
284
301
|
const result = await this.engine.run(next.task, {
|
|
285
302
|
cwd: next.opts.cwd,
|
|
303
|
+
workspaceContext: next.opts.workspaceContext,
|
|
286
304
|
sessionId: this.id,
|
|
287
305
|
displayText: next.opts.displayText,
|
|
288
306
|
signal: this.controller.signal,
|