@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.
Files changed (65) hide show
  1. package/dist/automation/desktop-authority-client.d.ts +9 -0
  2. package/dist/automation/desktop-authority-client.js +47 -0
  3. package/dist/automation/scheduler.d.ts +8 -0
  4. package/dist/automation/scheduler.js +37 -1
  5. package/dist/automation/store.js +14 -1
  6. package/dist/capabilities/index.d.ts +1 -0
  7. package/dist/cli/agent-server-stdio.js +4 -1
  8. package/dist/engine/engine-workspace-authority.d.ts +35 -0
  9. package/dist/engine/engine-workspace-authority.js +136 -0
  10. package/dist/engine/engine.d.ts +5 -10
  11. package/dist/engine/engine.js +84 -101
  12. package/dist/engine/input-attachments.d.ts +1 -0
  13. package/dist/engine/input-attachments.js +6 -2
  14. package/dist/engine/run-environment.d.ts +8 -3
  15. package/dist/engine/run-environment.js +33 -8
  16. package/dist/engine/run-image-input.d.ts +1 -0
  17. package/dist/engine/run-image-input.js +1 -0
  18. package/dist/engine/run-session-open.d.ts +2 -1
  19. package/dist/engine/run-session-open.js +6 -0
  20. package/dist/engine/run-setup.d.ts +1 -0
  21. package/dist/engine/run-setup.js +2 -1
  22. package/dist/engine/run-tooling.d.ts +2 -0
  23. package/dist/engine/run-tooling.js +3 -0
  24. package/dist/engine/run-types.d.ts +2 -0
  25. package/dist/engine/run-workspace.d.ts +6 -1
  26. package/dist/engine/run-workspace.js +47 -0
  27. package/dist/engine/subagent-spawner.d.ts +1 -0
  28. package/dist/engine/subagent-spawner.js +1 -0
  29. package/dist/engine/types.d.ts +2 -0
  30. package/dist/index.d.ts +3 -3
  31. package/dist/index.extension.d.ts +1 -1
  32. package/dist/index.internal.d.ts +2 -2
  33. package/dist/index.internal.js +1 -0
  34. package/dist/index.js +1 -1
  35. package/dist/plugins/pluginAutomationTemplates.d.ts +2 -0
  36. package/dist/plugins/pluginAutomationTemplates.js +4 -0
  37. package/dist/prompt/composer.d.ts +4 -1
  38. package/dist/prompt/composer.js +11 -3
  39. package/dist/protocol/chat-session-manager.d.ts +42 -1
  40. package/dist/protocol/chat-session-manager.js +167 -4
  41. package/dist/protocol/chat-session.d.ts +9 -1
  42. package/dist/protocol/chat-session.js +20 -2
  43. package/dist/protocol/mobile-remote-types.d.ts +15 -0
  44. package/dist/protocol/server.d.ts +1 -2
  45. package/dist/protocol/server.js +21 -51
  46. package/dist/protocol/session-workspace-rpc.d.ts +23 -0
  47. package/dist/protocol/session-workspace-rpc.js +171 -0
  48. package/dist/protocol/types.d.ts +45 -1
  49. package/dist/protocol/types.js +4 -0
  50. package/dist/session/session-manager.d.ts +18 -0
  51. package/dist/session/session-manager.js +87 -0
  52. package/dist/tool-system/builtin/cron.d.ts +11 -0
  53. package/dist/tool-system/builtin/cron.js +27 -2
  54. package/dist/tool-system/builtin/edit.js +5 -3
  55. package/dist/tool-system/builtin/write.js +5 -3
  56. package/dist/tool-system/context.d.ts +4 -1
  57. package/dist/tool-system/executor.js +1 -1
  58. package/dist/tool-system/path-policy.d.ts +11 -3
  59. package/dist/tool-system/path-policy.js +56 -36
  60. package/dist/types.d.ts +6 -0
  61. package/dist/workspace/canonical-key.d.ts +7 -0
  62. package/dist/workspace/canonical-key.js +39 -0
  63. package/dist/workspace/workspace-context.d.ts +26 -0
  64. package/dist/workspace/workspace-context.js +112 -0
  65. package/package.json +1 -1
@@ -66,10 +66,18 @@ export interface MobilePermissionModeSnapshotEntry {
66
66
  mode: PermissionMode;
67
67
  }
68
68
  export interface MobileProjectMeta {
69
+ id?: string;
69
70
  path: string;
70
71
  name: string;
71
72
  addedAt?: number;
72
73
  pinned?: boolean;
74
+ roots?: Array<{
75
+ id: string;
76
+ path: string;
77
+ name: string;
78
+ role: "primary" | "secondary";
79
+ }>;
80
+ primaryRootId?: string;
73
81
  }
74
82
  export type MobileImageMime = "image/png" | "image/jpeg" | "image/webp" | "image/gif";
75
83
  export interface MobileImageBase {
@@ -112,6 +120,11 @@ export type MobileClientEvent = {
112
120
  sessionId: string;
113
121
  } | {
114
122
  type: "session.create";
123
+ /** Stable V2 project identity. null explicitly selects the no-repo workspace. */
124
+ projectId?: string | null;
125
+ /** Stable V2 root identity. Omit to use the project's current primary root. */
126
+ rootId?: string;
127
+ /** Legacy client compatibility only; Main validates it against mounted roots. */
115
128
  cwd?: string | null;
116
129
  name?: string;
117
130
  } | {
@@ -230,6 +243,8 @@ export type MobileServerEvent = {
230
243
  type: "chat.accepted";
231
244
  sessionId?: string;
232
245
  cwd?: string | null;
246
+ projectId?: string | null;
247
+ rootId?: string;
233
248
  clientMessageId?: string;
234
249
  attachments?: MobileAttachmentSummary[];
235
250
  } | {
@@ -198,6 +198,7 @@ export declare class AgentServer {
198
198
  */
199
199
  private bgAgentBusUnsubscribe;
200
200
  private readonly wakeupsInFlight;
201
+ private readonly sessionWorkspaceRpc;
201
202
  /**
202
203
  * Effective session manager for the connection this server serves. Without
203
204
  * a resolveIdentity hook this is exactly the host-supplied manager —
@@ -310,8 +311,6 @@ export declare class AgentServer {
310
311
  */
311
312
  private handleBackgroundWork;
312
313
  private handleCloseSession;
313
- private handleReleaseWorkspace;
314
- private handleSetWorkspace;
315
314
  private handleConfigure;
316
315
  /**
317
316
  * Resolve the Engine that owns a session-scoped query (compact, archive_range,
@@ -30,6 +30,7 @@ import { redactSecrets } from "../logging/sanitize-messages.js";
30
30
  import { compileComposition } from "../composition/compiler.js";
31
31
  import { attachProtocolContributions } from "../composition/protocol-attach.js";
32
32
  import { computeEffectiveDisabledLists } from "../capability-control/disabled-lists.js";
33
+ import { SessionWorkspaceRpcHandlers } from "./session-workspace-rpc.js";
33
34
  import { describePluginCommands, expandPluginCommandBody, scanPluginCommands, MAX_PLUGIN_COMMAND_ARGUMENT_CHARS, } from "../plugins/pluginCommandsLoader.js";
34
35
  function isValidRunAttachment(value) {
35
36
  if (!value || typeof value !== "object")
@@ -380,6 +381,7 @@ export class AgentServer {
380
381
  */
381
382
  bgAgentBusUnsubscribe = null;
382
383
  wakeupsInFlight = new Set();
384
+ sessionWorkspaceRpc;
383
385
  /**
384
386
  * Effective session manager for the connection this server serves. Without
385
387
  * a resolveIdentity hook this is exactly the host-supplied manager —
@@ -425,6 +427,14 @@ export class AgentServer {
425
427
  throw new Error("AgentServer: either chatManager or engine must be supplied");
426
428
  }
427
429
  this.transport = options.transport;
430
+ this.sessionWorkspaceRpc = new SessionWorkspaceRpcHandlers({
431
+ transport: this.transport,
432
+ getChatManager: () => this.chatManager,
433
+ getLegacyEngine: () => this.legacyEngine,
434
+ getLastSlice: (sessionId) => this.lastSliceBySession.get(sessionId),
435
+ rememberSessionSlice: (sessionId, slice) => this.rememberSessionSlice(sessionId, slice),
436
+ wireInteractiveSession: (session, sessionId) => this.wireInteractiveSession(session, sessionId),
437
+ });
428
438
  // Protocol surface from the compiled composition. Each contributing
429
439
  // module may attach one observer; the server calls them at every
430
440
  // lifecycle hook point and isolates per-observer failures.
@@ -787,10 +797,16 @@ export class AgentServer {
787
797
  await this.handleCloseSession(req);
788
798
  break;
789
799
  case Methods.ReleaseWorkspace:
790
- this.handleReleaseWorkspace(req);
800
+ this.sessionWorkspaceRpc.release(req);
791
801
  break;
792
802
  case Methods.SetWorkspace:
793
- this.handleSetWorkspace(req);
803
+ this.sessionWorkspaceRpc.set(req);
804
+ break;
805
+ case Methods.MigrateSessionMainRoot:
806
+ await this.sessionWorkspaceRpc.migrateMainRoot(req);
807
+ break;
808
+ case Methods.CompleteSessionMainRootMigration:
809
+ this.sessionWorkspaceRpc.completeMainRootMigration(req);
794
810
  break;
795
811
  case Methods.GoalExtend:
796
812
  this.handleGoalExtend(req);
@@ -1153,6 +1169,7 @@ export class AgentServer {
1153
1169
  }
1154
1170
  const sessionConfig = {
1155
1171
  cwd: params.cwd,
1172
+ workspaceContext: params.workspaceContext,
1156
1173
  projectTrusted: params.projectTrusted,
1157
1174
  preset: params.preset ??
1158
1175
  (params.behaviorMode === ISOLATED_TASK_BEHAVIOR_MODE ? "general" : undefined),
@@ -1226,6 +1243,7 @@ export class AgentServer {
1226
1243
  }
1227
1244
  const run = session.enqueueTurn(params.task, {
1228
1245
  cwd: params.cwd,
1246
+ workspaceContext: params.workspaceContext,
1229
1247
  displayText: displayText || undefined,
1230
1248
  injected: params.injected === true,
1231
1249
  attachments: Array.isArray(params.attachments) ? params.attachments : undefined,
@@ -1358,6 +1376,7 @@ export class AgentServer {
1358
1376
  });
1359
1377
  const result = await this.legacyEngine.run(params.task, {
1360
1378
  cwd: params.cwd,
1379
+ workspaceContext: params.workspaceContext,
1361
1380
  sessionId: params.sessionId,
1362
1381
  displayText: displayText || undefined,
1363
1382
  injected: params.injected === true,
@@ -1944,55 +1963,6 @@ export class AgentServer {
1944
1963
  backgroundJobRegistry.dropForSession(params.sessionId);
1945
1964
  this.transport.send(createResponse(req.id, { ok: true }));
1946
1965
  }
1947
- // ─── ReleaseWorkspace ──────────────────────────────────────────
1948
- handleReleaseWorkspace(req) {
1949
- const params = (req.params ?? {});
1950
- if (typeof params.sessionId !== "string" || params.sessionId.length === 0) {
1951
- this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
1952
- return;
1953
- }
1954
- if (this.chatManager) {
1955
- const session = this.chatManager.get(params.sessionId);
1956
- if (!session) {
1957
- this.transport.send(createResponse(req.id, { ok: true, workspace: null }));
1958
- return;
1959
- }
1960
- const engine = session.engine;
1961
- const workspace = engine.releaseSessionWorkspace?.(params.sessionId) ?? null;
1962
- this.transport.send(createResponse(req.id, { ok: true, workspace }));
1963
- return;
1964
- }
1965
- const engine = this.legacyEngine;
1966
- const workspace = engine?.releaseSessionWorkspace?.(params.sessionId) ?? null;
1967
- this.transport.send(createResponse(req.id, { ok: true, workspace }));
1968
- }
1969
- handleSetWorkspace(req) {
1970
- const params = (req.params ?? {});
1971
- if (typeof params.sessionId !== "string" || params.sessionId.length === 0) {
1972
- this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
1973
- return;
1974
- }
1975
- if (!params.workspace ||
1976
- typeof params.workspace !== "object" ||
1977
- typeof params.workspace.root !== "string" ||
1978
- params.workspace.root.length === 0 ||
1979
- (params.workspace.kind !== "main" && params.workspace.kind !== "worktree")) {
1980
- this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "valid workspace is required"));
1981
- return;
1982
- }
1983
- const engine = this.chatManager
1984
- ? this.chatManager.get(params.sessionId)?.engine
1985
- : this.legacyEngine;
1986
- if (!engine) {
1987
- this.transport.send(createResponse(req.id, { ok: true, workspace: null }));
1988
- return;
1989
- }
1990
- const workspace = engine.setSessionWorkspace?.(params.sessionId, params.workspace);
1991
- this.transport.send(createResponse(req.id, {
1992
- ok: workspace !== undefined && workspace !== null,
1993
- workspace: workspace ?? null,
1994
- }));
1995
- }
1996
1966
  // ─── Configure ──────────────────────────────────────────────────
1997
1967
  handleConfigure(req) {
1998
1968
  const params = (req.params ?? {});
@@ -0,0 +1,23 @@
1
+ import type { Engine } from "../engine/engine.js";
2
+ import type { ChatSession } from "./chat-session.js";
3
+ import type { ChatSessionManager, EngineConfigSlice } from "./chat-session-manager.js";
4
+ import type { Transport } from "./transport.js";
5
+ import { type RpcRequest } from "./types.js";
6
+ interface SessionWorkspaceRpcDependencies {
7
+ transport: Transport;
8
+ getChatManager(): ChatSessionManager | null;
9
+ getLegacyEngine(): Engine | null;
10
+ getLastSlice(sessionId: string): EngineConfigSlice | undefined;
11
+ rememberSessionSlice(sessionId: string, slice: EngineConfigSlice): void;
12
+ wireInteractiveSession(session: ChatSession, sessionId: string): void;
13
+ }
14
+ /** Protocol boundary for Session workspace ownership and root migration. */
15
+ export declare class SessionWorkspaceRpcHandlers {
16
+ private readonly deps;
17
+ constructor(deps: SessionWorkspaceRpcDependencies);
18
+ release(req: RpcRequest): void;
19
+ set(req: RpcRequest): void;
20
+ migrateMainRoot(req: RpcRequest): Promise<void>;
21
+ completeMainRootMigration(req: RpcRequest): void;
22
+ }
23
+ export {};
@@ -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
+ }
@@ -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.
@@ -273,6 +279,40 @@ export interface SetWorkspaceParams {
273
279
  sessionId: string;
274
280
  workspace: SessionWorkspace;
275
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
+ }
276
316
  /** Inject context into a session transcript. */
277
317
  export interface InjectParams {
278
318
  sessionId: string;
@@ -500,6 +540,10 @@ export declare const Methods: {
500
540
  readonly ReleaseWorkspace: "agent/releaseWorkspace";
501
541
  /** Persist a workspace pointer through the live session owner. */
502
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";
503
547
  /** Extend a running goal's turn/budget ceilings mid-run (TODO 3.1). */
504
548
  readonly GoalExtend: "agent/goalExtend";
505
549
  /** Edit or pause/resume a session's persisted goal. */
@@ -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. */
@@ -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 {
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import type { ToolDefinition } from "../../types.js";
5
5
  import type { ToolContext } from "../context.js";
6
+ import type { CronJob, CreateJobOptions } from "../../automation/scheduler.js";
6
7
  export { cronListToolDef } from "./cron-list.definition.js";
7
8
  /** Sink notified after a cron job is created/deleted, so the host (Electron
8
9
  * main) can reload+arm the scheduler that actually executes jobs. The worker
@@ -10,6 +11,16 @@ export { cronListToolDef } from "./cron-list.definition.js";
10
11
  * host never learns about an AI-created job until the user opens the UI. */
11
12
  type CronChangedSink = () => void;
12
13
  export declare function setCronChangedSink(sink: CronChangedSink | null): void;
14
+ export interface CronCreateAuthorityInput extends CreateJobOptions {
15
+ name: string;
16
+ schedule: string;
17
+ prompt: string;
18
+ /** Current Engine Session, used by Desktop Main as authority even for standalone jobs. */
19
+ authoritySessionId?: string;
20
+ }
21
+ export type CronCreateAuthority = (input: CronCreateAuthorityInput) => Promise<CronJob>;
22
+ /** Desktop installs a Main-process delegate; other hosts keep the local scheduler path. */
23
+ export declare function setCronCreateAuthority(authority: CronCreateAuthority | null): void;
13
24
  export declare const cronCreateToolDef: ToolDefinition;
14
25
  export declare function cronCreateTool(args: Record<string, unknown>, context?: ToolContext): Promise<string>;
15
26
  export declare const cronDeleteToolDef: ToolDefinition;