@clinebot/core 0.0.29 → 0.0.32

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.
@@ -66,6 +66,66 @@ export interface CoreCompactionConfig {
66
66
  summarizer?: CoreCompactionSummarizerConfig;
67
67
  compact?: (context: CoreCompactionContext) => Promise<CoreCompactionResult | undefined> | CoreCompactionResult | undefined;
68
68
  }
69
+ /**
70
+ * Context passed to a custom `createCheckpoint` implementation.
71
+ */
72
+ export interface CoreCheckpointContext {
73
+ /** Absolute path to the working directory of the session. */
74
+ cwd: string;
75
+ /** The session identifier. */
76
+ sessionId: string;
77
+ /** Monotonically increasing run counter for this session (starts at 1). */
78
+ runCount: number;
79
+ }
80
+ /**
81
+ * Configuration for the built-in git-based checkpoint feature.
82
+ *
83
+ * Checkpoints capture a restorable snapshot of the workspace at the start of
84
+ * each root-agent run so that changes made during a session can be rolled back.
85
+ *
86
+ * @example Disable checkpoints entirely:
87
+ * ```ts
88
+ * checkpoint: { enabled: false }
89
+ * ```
90
+ *
91
+ * @example Bring your own checkpoint implementation:
92
+ * ```ts
93
+ * checkpoint: {
94
+ * createCheckpoint: async ({ cwd, sessionId, runCount }) => {
95
+ * const ref = await mySnapshotFn(cwd);
96
+ * return { ref, createdAt: Date.now(), runCount };
97
+ * },
98
+ * }
99
+ * ```
100
+ */
101
+ export interface CoreCheckpointConfig {
102
+ /**
103
+ * Whether to create checkpoints on each root-agent run start.
104
+ * Defaults to `false` — checkpoints are **opt-in**. Set to `true` to
105
+ * enable the built-in git stash/ref checkpoint behaviour for this session.
106
+ */
107
+ enabled?: boolean;
108
+ /**
109
+ * Replace the built-in git stash/ref checkpoint logic with a custom
110
+ * implementation. Called once at the start of each root-agent run (before
111
+ * the first agent iteration).
112
+ *
113
+ * Return an object with at least `ref`, `createdAt`, and `runCount` to have
114
+ * the entry recorded in session metadata, or return `undefined` to skip
115
+ * writing a checkpoint for that run.
116
+ */
117
+ createCheckpoint?: (context: CoreCheckpointContext) => Promise<{
118
+ ref: string;
119
+ createdAt: number;
120
+ runCount: number;
121
+ kind?: "stash" | "commit";
122
+ } | undefined> | {
123
+ ref: string;
124
+ createdAt: number;
125
+ runCount: number;
126
+ kind?: "stash" | "commit";
127
+ } | undefined;
128
+ }
69
129
  export interface CoreSessionConfig extends CoreModelConfig, CoreRuntimeFeatures, Omit<SessionWorkspaceConfig, "workspaceRoot">, Omit<SessionPromptConfig, "systemPrompt">, Omit<SessionExecutionConfig, "enableTools" | "teamName" | "missionLogIntervalSteps" | "missionLogIntervalMs" | "maxConsecutiveMistakes"> {
70
130
  sessionId?: string;
71
131
  workspaceRoot?: string;
@@ -83,6 +143,7 @@ export interface CoreSessionConfig extends CoreModelConfig, CoreRuntimeFeatures,
83
143
  extensions?: AgentConfig["extensions"];
84
144
  execution?: AgentConfig["execution"];
85
145
  compaction?: CoreCompactionConfig;
146
+ checkpoint?: CoreCheckpointConfig;
86
147
  onTeamEvent?: (event: TeamEvent) => void;
87
148
  onConsecutiveMistakeLimitReached?: (context: ConsecutiveMistakeLimitContext) => Promise<ConsecutiveMistakeLimitDecision> | ConsecutiveMistakeLimitDecision;
88
149
  toolRoutingRules?: ToolRoutingRule[];
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@clinebot/core",
3
3
  "description": "Cline Core SDK for Node Runtime",
4
- "version": "0.0.29",
4
+ "version": "0.0.32",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
7
7
  "main": "./dist/index.js",
@@ -36,8 +36,8 @@
36
36
  "test:watch": "vitest --config vitest.config.ts"
37
37
  },
38
38
  "dependencies": {
39
- "@clinebot/agents": "0.0.29",
40
- "@clinebot/llms": "0.0.29",
39
+ "@clinebot/agents": "0.0.32",
40
+ "@clinebot/llms": "0.0.32",
41
41
  "@opentelemetry/api": "^1.9.0",
42
42
  "@opentelemetry/api-logs": "^0.214.0",
43
43
  "@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
@@ -58,7 +58,7 @@
58
58
  },
59
59
  "devDependencies": {
60
60
  "@clinebot/rpc": "0.0.28",
61
- "@clinebot/shared": "0.0.29"
61
+ "@clinebot/shared": "0.0.32"
62
62
  },
63
63
  "engines": {
64
64
  "node": ">=22"
package/src/index.ts CHANGED
@@ -535,6 +535,8 @@ export type { SessionStatus } from "./types/common";
535
535
  export { SESSION_STATUSES, SessionSource } from "./types/common";
536
536
  export type {
537
537
  CoreAgentMode,
538
+ CoreCheckpointConfig,
539
+ CoreCheckpointContext,
538
540
  CoreCompactionConfig,
539
541
  CoreCompactionContext,
540
542
  CoreCompactionResult,
@@ -30,7 +30,8 @@ async function createGitRepo(): Promise<string> {
30
30
  return cwd;
31
31
  }
32
32
 
33
- describe("createCheckpointHooks", () => {
33
+ // Currently disabled for process?.env?.CLINE_CHECKPOINT
34
+ describe.skip("createCheckpointHooks", () => {
34
35
  it("creates one checkpoint at the start of each root run and appends metadata", async () => {
35
36
  const cwd = await createGitRepo();
36
37
  let metadata: Record<string, unknown> | undefined;
@@ -24,6 +24,16 @@ type CreateCheckpointHooksOptions = {
24
24
  writeSessionMetadata: (
25
25
  metadata: Record<string, unknown>,
26
26
  ) => Promise<void> | void;
27
+ /**
28
+ * Optional custom checkpoint implementation. When provided, the built-in
29
+ * git stash/ref logic is skipped entirely and this function is called
30
+ * instead. Return `undefined` to skip writing a checkpoint for that run.
31
+ */
32
+ createCheckpoint?: (context: {
33
+ cwd: string;
34
+ sessionId: string;
35
+ runCount: number;
36
+ }) => Promise<CheckpointEntry | undefined> | CheckpointEntry | undefined;
27
37
  };
28
38
 
29
39
  function warn(logger: BasicLogger | undefined, message: string): void {
@@ -99,6 +109,14 @@ export function createCheckpointHooks(
99
109
  };
100
110
 
101
111
  const createCheckpoint = async (): Promise<CheckpointEntry | undefined> => {
112
+ if (options.createCheckpoint) {
113
+ return await options.createCheckpoint({
114
+ cwd: options.cwd,
115
+ sessionId: options.sessionId,
116
+ runCount,
117
+ });
118
+ }
119
+
102
120
  if (!(await ensureGitRepository())) {
103
121
  return undefined;
104
122
  }
@@ -137,8 +155,15 @@ export function createCheckpointHooks(
137
155
  };
138
156
  }
139
157
 
158
+ // Store the stash commit under a private ref namespace so it is
159
+ // invisible to the user's normal `git stash list` workflow.
160
+ // `refs/stash` is what populates that list; writing to any other
161
+ // ref path keeps the object reachable (GC-safe) without surfacing
162
+ // it to the user. The raw SHA already works with `git stash apply`
163
+ // on the restore path, so no restore-side changes are needed.
164
+ const privateRef = `refs/cline/checkpoints/${options.sessionId}/${runCount}`;
140
165
  try {
141
- await runGit(options.cwd, ["stash", "store", "-m", message, ref]);
166
+ await runGit(options.cwd, ["update-ref", privateRef, ref]);
142
167
  } catch (error) {
143
168
  warn(
144
169
  options.logger,
@@ -150,7 +150,6 @@ describe("DefaultSessionManager", () => {
150
150
  const envSnapshot = {
151
151
  HOME: process.env.HOME,
152
152
  CLINE_DIR: process.env.CLINE_DIR,
153
- CLINE_CHECKPOINT: process.env.CLINE_CHECKPOINT,
154
153
  };
155
154
  let isolatedHomeDir = "";
156
155
 
@@ -158,7 +157,6 @@ describe("DefaultSessionManager", () => {
158
157
  isolatedHomeDir = mkdtempSync(join(tmpdir(), "core-session-home-"));
159
158
  process.env.HOME = isolatedHomeDir;
160
159
  process.env.CLINE_DIR = join(isolatedHomeDir, ".cline");
161
- delete process.env.CLINE_CHECKPOINT;
162
160
  setHomeDir(isolatedHomeDir);
163
161
  setClineDir(process.env.CLINE_DIR);
164
162
  });
@@ -166,7 +164,6 @@ describe("DefaultSessionManager", () => {
166
164
  afterEach(() => {
167
165
  process.env.HOME = envSnapshot.HOME;
168
166
  process.env.CLINE_DIR = envSnapshot.CLINE_DIR;
169
- process.env.CLINE_CHECKPOINT = envSnapshot.CLINE_CHECKPOINT;
170
167
  setHomeDir(envSnapshot.HOME ?? "~");
171
168
  setClineDir(envSnapshot.CLINE_DIR ?? join("~", ".cline"));
172
169
  rmSync(isolatedHomeDir, { recursive: true, force: true });
@@ -652,7 +649,7 @@ describe("DefaultSessionManager", () => {
652
649
  );
653
650
  });
654
651
 
655
- it("does not install checkpoint hooks unless CLINE_CHECKPOINT=true", async () => {
652
+ it("does not install checkpoint hooks when checkpoint.enabled is not set in config", async () => {
656
653
  const sessionId = "sess-checkpoint-default-off";
657
654
  const manifest = createManifest(sessionId);
658
655
  const updateSession = vi.fn().mockResolvedValue({ updated: true });
@@ -737,10 +734,8 @@ describe("DefaultSessionManager", () => {
737
734
  });
738
735
  });
739
736
 
740
- it("installs checkpoint hooks when CLINE_CHECKPOINT=true", async () => {
741
- process.env.CLINE_CHECKPOINT = "true";
742
-
743
- const sessionId = "sess-checkpoint-env-on";
737
+ it("installs checkpoint hooks when checkpoint.enabled=true in config", async () => {
738
+ const sessionId = "sess-checkpoint-config-on";
744
739
  const repoCwd = mkdtempSync(join(isolatedHomeDir, "checkpoint-repo-"));
745
740
  createGitRepo(repoCwd);
746
741
  const manifest = createManifest(sessionId);
@@ -813,7 +808,10 @@ describe("DefaultSessionManager", () => {
813
808
  });
814
809
 
815
810
  await manager.start({
816
- config: createConfig({ sessionId, cwd: repoCwd }),
811
+ config: {
812
+ ...createConfig({ sessionId, cwd: repoCwd }),
813
+ checkpoint: { enabled: true },
814
+ },
817
815
  prompt: "hello",
818
816
  interactive: false,
819
817
  });
@@ -115,11 +115,6 @@ type SessionBackend =
115
115
  const MAX_SCAN_LIMIT = 5000;
116
116
  const MAX_USER_FILE_BYTES = 20 * 1_000 * 1_024;
117
117
 
118
- // NOTE: Temporarily disabled until checkpoint is ready for production.
119
- function isCheckpointFeatureEnabled(): boolean {
120
- return process?.env?.CLINE_CHECKPOINT === "true";
121
- }
122
-
123
118
  async function loadUserFileContent(path: string): Promise<string> {
124
119
  const fileStat = await stat(path);
125
120
  if (!fileStat.isFile()) {
@@ -277,11 +272,12 @@ export class DefaultSessionManager implements SessionManager {
277
272
  };
278
273
  configWithProvider.hooks = mergeAgentHooks([
279
274
  effectiveConfig.hooks,
280
- isCheckpointFeatureEnabled()
275
+ effectiveConfig.checkpoint?.enabled === true
281
276
  ? createCheckpointHooks({
282
277
  cwd: configWithProvider.cwd,
283
278
  sessionId,
284
279
  logger: configWithProvider.logger,
280
+ createCheckpoint: effectiveConfig.checkpoint?.createCheckpoint,
285
281
  readSessionMetadata: async () =>
286
282
  (await this.get(sessionId))?.metadata as
287
283
  | Record<string, unknown>
@@ -344,6 +340,7 @@ export class DefaultSessionManager implements SessionManager {
344
340
  onConsecutiveMistakeLimitReached:
345
341
  configWithProvider.onConsecutiveMistakeLimitReached,
346
342
  completionGuard: runtime.completionGuard,
343
+ consumePendingUserMessage: () => this.consumeSteerMessage(sessionId),
347
344
  logger: runtime.logger ?? configWithProvider.logger,
348
345
  extensionContext: configWithProvider.extensionContext,
349
346
  onEvent: (event: AgentEvent) =>
@@ -964,6 +961,27 @@ export class DefaultSessionManager implements SessionManager {
964
961
  });
965
962
  }
966
963
 
964
+ /**
965
+ * Consume the first steer-delivery pending prompt for injection into the
966
+ * running agent loop. Called synchronously by the agent between iterations.
967
+ */
968
+ private consumeSteerMessage(sessionId: string): string | undefined {
969
+ const session = this.sessions.get(sessionId);
970
+ if (!session) {
971
+ return undefined;
972
+ }
973
+ const steerIndex = session.pendingPrompts.findIndex(
974
+ (entry) => entry.delivery === "steer",
975
+ );
976
+ if (steerIndex < 0) {
977
+ return undefined;
978
+ }
979
+ const [steer] = session.pendingPrompts.splice(steerIndex, 1);
980
+ this.emitPendingPrompts(session);
981
+ this.emitPendingPromptSubmitted(session, steer);
982
+ return steer.prompt;
983
+ }
984
+
967
985
  private enqueuePendingPrompt(
968
986
  sessionId: string,
969
987
  entry: {
@@ -160,13 +160,11 @@ describe("UnifiedSessionPersistenceService", () => {
160
160
  row.sessionId.includes("__teamtask__java-haiku-agent__"),
161
161
  )?.sessionId;
162
162
  expect(teammateSessionId).toBeTruthy();
163
- const path = join(
164
- sessionsDir,
165
- rootSessionId,
166
- "java-haiku-agent__" +
167
- teammateSessionId?.slice(teammateSessionId?.lastIndexOf("__") + 2) +
168
- ".messages.json",
163
+ const row = childSessions.find(
164
+ (item) => item.sessionId === teammateSessionId,
169
165
  );
166
+ expect(row?.messagesPath).toBeTruthy();
167
+ const path = row?.messagesPath as string;
170
168
  const payload = JSON.parse(readFileSync(path, "utf8")) as {
171
169
  agent?: string;
172
170
  sessionId?: string;
@@ -195,18 +193,11 @@ describe("UnifiedSessionPersistenceService", () => {
195
193
  cacheWriteTokens: 0,
196
194
  cost: 0.123,
197
195
  });
198
- const row = childSessions.find(
199
- (item) => item.sessionId === teammateSessionId,
200
- );
201
196
  expect(row?.messagesPath).toBe(path);
202
- expect(row?.transcriptPath).toBe(
203
- join(
204
- sessionsDir,
205
- rootSessionId,
206
- "java-haiku-agent__" +
207
- teammateSessionId?.slice(teammateSessionId?.lastIndexOf("__") + 2) +
208
- ".log",
209
- ),
197
+ expect(row?.transcriptPath).toBeTruthy();
198
+ expect(row?.transcriptPath).toContain(
199
+ join(sessionsDir, rootSessionId, "java-haiku-agent__"),
210
200
  );
201
+ expect(row?.transcriptPath).toMatch(/\.log$/);
211
202
  });
212
203
  });
@@ -229,8 +229,8 @@ export class OpenTelemetryAdapter implements ITelemetryAdapter {
229
229
  ): TelemetryProperties {
230
230
  return {
231
231
  ...this.commonProperties,
232
- ...properties,
233
232
  ...this.metadata,
233
+ ...properties,
234
234
  ...(this.distinctId ? { distinct_id: this.distinctId } : {}),
235
235
  ...(required ? { _required: true } : {}),
236
236
  };
@@ -98,6 +98,197 @@ describe("createOpenTelemetryTelemetryService", () => {
98
98
  expect(telemetry).toBeInstanceOf(TelemetryService);
99
99
  });
100
100
 
101
+ it("preserves metadata when disabled", () => {
102
+ const metadata = {
103
+ extension_version: "1.0.0",
104
+ cline_type: "kanban",
105
+ platform: "kanban",
106
+ platform_version: "v22.0.0",
107
+ os_type: "darwin",
108
+ os_version: "15.0",
109
+ };
110
+ const { telemetry } = createConfiguredTelemetryService({
111
+ metadata,
112
+ enabled: false,
113
+ });
114
+ const spy = vi.fn();
115
+ (telemetry as any).adapters.push({
116
+ name: "test",
117
+ emit: spy,
118
+ emitRequired: spy,
119
+ isEnabled: () => true,
120
+ recordCounter: vi.fn(),
121
+ recordHistogram: vi.fn(),
122
+ recordGauge: vi.fn(),
123
+ flush: async () => {},
124
+ dispose: async () => {},
125
+ });
126
+ telemetry.captureRequired("test.event", {});
127
+ expect(spy).toHaveBeenCalledWith(
128
+ "test.event",
129
+ expect.objectContaining({
130
+ cline_type: "kanban",
131
+ platform: "kanban",
132
+ }),
133
+ );
134
+ });
135
+
136
+ it("preserves metadata in the enabled (OTEL) path", async () => {
137
+ const metadata = {
138
+ extension_version: "1.0.0",
139
+ cline_type: "kanban",
140
+ platform: "kanban",
141
+ platform_version: "v22.0.0",
142
+ os_type: "darwin",
143
+ os_version: "15.0",
144
+ };
145
+ const { telemetry, provider } = createOpenTelemetryTelemetryService({
146
+ metadata,
147
+ enabled: true,
148
+ logsExporter: "console",
149
+ });
150
+ const spy = vi.fn();
151
+ (telemetry as any).adapters.push({
152
+ name: "test",
153
+ emit: spy,
154
+ emitRequired: spy,
155
+ isEnabled: () => true,
156
+ recordCounter: vi.fn(),
157
+ recordHistogram: vi.fn(),
158
+ recordGauge: vi.fn(),
159
+ flush: async () => {},
160
+ dispose: async () => {},
161
+ });
162
+ telemetry.captureRequired("test.event", {});
163
+ expect(spy).toHaveBeenCalledWith(
164
+ "test.event",
165
+ expect.objectContaining({
166
+ cline_type: "kanban",
167
+ platform: "kanban",
168
+ }),
169
+ );
170
+ await provider.dispose();
171
+ });
172
+
173
+ it("delivers metadata to the OTEL logger without duplication", async () => {
174
+ const otelEmit = vi.fn();
175
+ const provider = new OpenTelemetryProvider({
176
+ enabled: true,
177
+ });
178
+ // Replace the loggerProvider with a mock so we can inspect emit calls
179
+ (provider as any).loggerProvider = {
180
+ getLogger: () => ({ emit: otelEmit }),
181
+ forceFlush: async () => {},
182
+ shutdown: async () => {},
183
+ };
184
+
185
+ const metadata = {
186
+ extension_version: "1.0.0",
187
+ cline_type: "kanban",
188
+ platform: "kanban",
189
+ platform_version: "v22.0.0",
190
+ os_type: "darwin",
191
+ os_version: "15.0",
192
+ };
193
+
194
+ const telemetry = provider.createTelemetryService({ metadata });
195
+
196
+ telemetry.captureRequired("test.otel_event", { custom_prop: "value" });
197
+
198
+ expect(otelEmit).toHaveBeenCalledTimes(1);
199
+ const emittedAttributes = otelEmit.mock.calls[0][0].attributes;
200
+
201
+ // Metadata fields must be present
202
+ expect(emittedAttributes).toMatchObject({
203
+ cline_type: "kanban",
204
+ platform: "kanban",
205
+ extension_version: "1.0.0",
206
+ custom_prop: "value",
207
+ });
208
+
209
+ // Verify no key appears more than once (flattened object can't have
210
+ // duplicate keys, but this guards against nested duplication patterns
211
+ // like metadata appearing under a sub-prefix)
212
+ const keys = Object.keys(emittedAttributes);
213
+ const metadataKeys = Object.keys(metadata);
214
+ for (const mk of metadataKeys) {
215
+ const occurrences = keys.filter((k) => k === mk || k.endsWith(`.${mk}`));
216
+ expect(
217
+ occurrences,
218
+ `metadata key "${mk}" should appear exactly once, found: ${occurrences.join(", ")}`,
219
+ ).toHaveLength(1);
220
+ }
221
+
222
+ await provider.dispose();
223
+ });
224
+
225
+ it("propagates updateMetadata to OTEL logger output", async () => {
226
+ const otelEmit = vi.fn();
227
+ const provider = new OpenTelemetryProvider({
228
+ enabled: true,
229
+ });
230
+ (provider as any).loggerProvider = {
231
+ getLogger: () => ({ emit: otelEmit }),
232
+ forceFlush: async () => {},
233
+ shutdown: async () => {},
234
+ };
235
+
236
+ const metadata = {
237
+ extension_version: "1.0.0",
238
+ cline_type: "kanban",
239
+ platform: "kanban",
240
+ platform_version: "v22.0.0",
241
+ os_type: "darwin",
242
+ os_version: "15.0",
243
+ };
244
+
245
+ const telemetry = provider.createTelemetryService({ metadata });
246
+
247
+ // Update metadata after construction
248
+ telemetry.updateMetadata({ cline_type: "kanban-updated" });
249
+
250
+ telemetry.captureRequired("test.updated_event", {});
251
+
252
+ // The OTEL logger should see the updated value
253
+ const emittedAttributes =
254
+ otelEmit.mock.calls[otelEmit.mock.calls.length - 1][0].attributes;
255
+ expect(emittedAttributes.cline_type).toBe("kanban-updated");
256
+
257
+ await provider.dispose();
258
+ });
259
+
260
+ it("preserves logger when disabled", () => {
261
+ const logger: BasicLogger = {
262
+ debug: vi.fn(),
263
+ log: vi.fn(),
264
+ error: vi.fn(),
265
+ };
266
+ const { telemetry } = createConfiguredTelemetryService({
267
+ metadata: {
268
+ extension_version: "1.0.0",
269
+ cline_type: "kanban",
270
+ platform: "kanban",
271
+ platform_version: "v22.0.0",
272
+ os_type: "darwin",
273
+ os_version: "15.0",
274
+ },
275
+ enabled: false,
276
+ logger,
277
+ });
278
+
279
+ telemetry.capture({
280
+ event: "session.started",
281
+ properties: { sessionId: "session-1" },
282
+ });
283
+
284
+ expect(logger.log).toHaveBeenCalledWith(
285
+ "telemetry.event",
286
+ expect.objectContaining({
287
+ event: "session.started",
288
+ }),
289
+ );
290
+ });
291
+
101
292
  it("attaches the logger adapter when creating configured telemetry", () => {
102
293
  const logger: BasicLogger = {
103
294
  debug: vi.fn(),
@@ -133,10 +133,9 @@ export class OpenTelemetryProvider {
133
133
  metadata: options.metadata,
134
134
  });
135
135
  return new TelemetryService({
136
+ ...options,
136
137
  adapters: [adapter],
137
138
  distinctId: resolveCoreDistinctId(options.distinctId),
138
- commonProperties: options.commonProperties,
139
- logger: options.logger,
140
139
  });
141
140
  }
142
141
 
@@ -299,6 +298,7 @@ export function createConfiguredTelemetryService(
299
298
  if (options.enabled !== true) {
300
299
  return {
301
300
  telemetry: new TelemetryService({
301
+ ...options,
302
302
  distinctId: resolveCoreDistinctId(options.distinctId),
303
303
  }),
304
304
  };
@@ -95,6 +95,74 @@ export interface CoreCompactionConfig {
95
95
  | undefined;
96
96
  }
97
97
 
98
+ /**
99
+ * Context passed to a custom `createCheckpoint` implementation.
100
+ */
101
+ export interface CoreCheckpointContext {
102
+ /** Absolute path to the working directory of the session. */
103
+ cwd: string;
104
+ /** The session identifier. */
105
+ sessionId: string;
106
+ /** Monotonically increasing run counter for this session (starts at 1). */
107
+ runCount: number;
108
+ }
109
+
110
+ /**
111
+ * Configuration for the built-in git-based checkpoint feature.
112
+ *
113
+ * Checkpoints capture a restorable snapshot of the workspace at the start of
114
+ * each root-agent run so that changes made during a session can be rolled back.
115
+ *
116
+ * @example Disable checkpoints entirely:
117
+ * ```ts
118
+ * checkpoint: { enabled: false }
119
+ * ```
120
+ *
121
+ * @example Bring your own checkpoint implementation:
122
+ * ```ts
123
+ * checkpoint: {
124
+ * createCheckpoint: async ({ cwd, sessionId, runCount }) => {
125
+ * const ref = await mySnapshotFn(cwd);
126
+ * return { ref, createdAt: Date.now(), runCount };
127
+ * },
128
+ * }
129
+ * ```
130
+ */
131
+ export interface CoreCheckpointConfig {
132
+ /**
133
+ * Whether to create checkpoints on each root-agent run start.
134
+ * Defaults to `false` — checkpoints are **opt-in**. Set to `true` to
135
+ * enable the built-in git stash/ref checkpoint behaviour for this session.
136
+ */
137
+ enabled?: boolean;
138
+ /**
139
+ * Replace the built-in git stash/ref checkpoint logic with a custom
140
+ * implementation. Called once at the start of each root-agent run (before
141
+ * the first agent iteration).
142
+ *
143
+ * Return an object with at least `ref`, `createdAt`, and `runCount` to have
144
+ * the entry recorded in session metadata, or return `undefined` to skip
145
+ * writing a checkpoint for that run.
146
+ */
147
+ createCheckpoint?: (context: CoreCheckpointContext) =>
148
+ | Promise<
149
+ | {
150
+ ref: string;
151
+ createdAt: number;
152
+ runCount: number;
153
+ kind?: "stash" | "commit";
154
+ }
155
+ | undefined
156
+ >
157
+ | {
158
+ ref: string;
159
+ createdAt: number;
160
+ runCount: number;
161
+ kind?: "stash" | "commit";
162
+ }
163
+ | undefined;
164
+ }
165
+
98
166
  export interface CoreSessionConfig
99
167
  extends CoreModelConfig,
100
168
  CoreRuntimeFeatures,
@@ -124,6 +192,7 @@ export interface CoreSessionConfig
124
192
  extensions?: AgentConfig["extensions"];
125
193
  execution?: AgentConfig["execution"];
126
194
  compaction?: CoreCompactionConfig;
195
+ checkpoint?: CoreCheckpointConfig;
127
196
  onTeamEvent?: (event: TeamEvent) => void;
128
197
  onConsecutiveMistakeLimitReached?: (
129
198
  context: ConsecutiveMistakeLimitContext,