@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.
Files changed (91) 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 +8 -4
  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 +108 -112
  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 +7 -1
  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 +4 -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/turn-loop.js +17 -0
  30. package/dist/engine/types.d.ts +2 -0
  31. package/dist/index.d.ts +4 -4
  32. package/dist/index.extension.d.ts +1 -1
  33. package/dist/index.internal.d.ts +3 -2
  34. package/dist/index.internal.js +2 -0
  35. package/dist/index.js +2 -2
  36. package/dist/llm/client-base.d.ts +2 -1
  37. package/dist/llm/client-base.js +3 -1
  38. package/dist/llm/providers/anthropic.js +24 -25
  39. package/dist/llm/providers/openai.d.ts +4 -1
  40. package/dist/llm/providers/openai.js +76 -13
  41. package/dist/panel-apps/index.d.ts +1 -1
  42. package/dist/panel-apps/index.js +1 -1
  43. package/dist/panel-apps/installer.d.ts +29 -0
  44. package/dist/panel-apps/installer.js +124 -15
  45. package/dist/plugins/pluginAutomationTemplates.d.ts +2 -0
  46. package/dist/plugins/pluginAutomationTemplates.js +4 -0
  47. package/dist/plugins/pluginCatalog.d.ts +6 -0
  48. package/dist/plugins/pluginCatalog.js +7 -2
  49. package/dist/plugins/pluginContent.d.ts +1 -1
  50. package/dist/plugins/pluginContent.js +2 -10
  51. package/dist/prompt/composer.d.ts +4 -1
  52. package/dist/prompt/composer.js +11 -3
  53. package/dist/protocol/chat-session-manager.d.ts +42 -1
  54. package/dist/protocol/chat-session-manager.js +167 -4
  55. package/dist/protocol/chat-session.d.ts +11 -1
  56. package/dist/protocol/chat-session.js +20 -2
  57. package/dist/protocol/mobile-remote-types.d.ts +15 -0
  58. package/dist/protocol/server.d.ts +1 -2
  59. package/dist/protocol/server.js +28 -51
  60. package/dist/protocol/session-workspace-rpc.d.ts +23 -0
  61. package/dist/protocol/session-workspace-rpc.js +171 -0
  62. package/dist/protocol/types.d.ts +47 -1
  63. package/dist/protocol/types.js +4 -0
  64. package/dist/session/session-manager.d.ts +25 -0
  65. package/dist/session/session-manager.js +106 -6
  66. package/dist/session/transcript.d.ts +9 -0
  67. package/dist/session/transcript.js +81 -0
  68. package/dist/settings/manager.d.ts +12 -0
  69. package/dist/settings/manager.js +31 -0
  70. package/dist/tool-system/builtin/configure-model-connection.d.ts +36 -0
  71. package/dist/tool-system/builtin/configure-model-connection.js +396 -0
  72. package/dist/tool-system/builtin/cron.d.ts +11 -0
  73. package/dist/tool-system/builtin/cron.js +27 -2
  74. package/dist/tool-system/builtin/edit-model-catalog.d.ts +4 -6
  75. package/dist/tool-system/builtin/edit-model-catalog.js +5 -8
  76. package/dist/tool-system/builtin/edit.js +5 -3
  77. package/dist/tool-system/builtin/index.js +14 -0
  78. package/dist/tool-system/builtin/install-capability.js +22 -8
  79. package/dist/tool-system/builtin/settings-changed.d.ts +9 -0
  80. package/dist/tool-system/builtin/settings-changed.js +18 -0
  81. package/dist/tool-system/builtin/write.js +5 -3
  82. package/dist/tool-system/context.d.ts +12 -3
  83. package/dist/tool-system/executor.js +1 -1
  84. package/dist/tool-system/path-policy.d.ts +11 -3
  85. package/dist/tool-system/path-policy.js +56 -36
  86. package/dist/types.d.ts +11 -0
  87. package/dist/workspace/canonical-key.d.ts +7 -0
  88. package/dist/workspace/canonical-key.js +39 -0
  89. package/dist/workspace/workspace-context.d.ts +26 -0
  90. package/dist/workspace/workspace-context.js +112 -0
  91. 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
+ }
@@ -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. */
@@ -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
- // Extension-owned session kinds stay out of generic lists. Defaults to
1701
- // hiding "pet" so default-arg callers keep today's behavior.
1702
- // TODO(pet-out-of-core): drop the default once every caller passes the
1703
- // host's hidden-kind union explicitly.
1704
- const excludeKinds = opts?.excludeKinds ?? ["pet"];
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
@@ -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 {};