@doingdev/opencode-claude-manager-plugin 0.1.10 → 0.1.11

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.
@@ -0,0 +1,76 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ const execFileAsync = promisify(execFile);
4
+ export class GitOperations {
5
+ cwd;
6
+ constructor(cwd) {
7
+ this.cwd = cwd;
8
+ }
9
+ async diff() {
10
+ const [diffText, statOutput] = await Promise.all([
11
+ this.git(['diff', 'HEAD']),
12
+ this.git(['diff', 'HEAD', '--stat']),
13
+ ]);
14
+ const stats = parseStatLine(statOutput);
15
+ return {
16
+ hasDiff: diffText.trim().length > 0,
17
+ diffText,
18
+ stats,
19
+ };
20
+ }
21
+ async diffStat() {
22
+ return this.git(['diff', 'HEAD', '--stat']);
23
+ }
24
+ async commit(message) {
25
+ try {
26
+ await this.git(['add', '-A']);
27
+ const output = await this.git(['commit', '-m', message]);
28
+ return { success: true, output };
29
+ }
30
+ catch (error) {
31
+ return {
32
+ success: false,
33
+ output: '',
34
+ error: error instanceof Error ? error.message : String(error),
35
+ };
36
+ }
37
+ }
38
+ async resetHard() {
39
+ try {
40
+ const output = await this.git(['reset', '--hard', 'HEAD']);
41
+ // Also clean untracked files
42
+ const cleanOutput = await this.git(['clean', '-fd']);
43
+ return { success: true, output: `${output}\n${cleanOutput}`.trim() };
44
+ }
45
+ catch (error) {
46
+ return {
47
+ success: false,
48
+ output: '',
49
+ error: error instanceof Error ? error.message : String(error),
50
+ };
51
+ }
52
+ }
53
+ async currentBranch() {
54
+ const branch = await this.git(['rev-parse', '--abbrev-ref', 'HEAD']);
55
+ return branch.trim();
56
+ }
57
+ async recentCommits(count = 5) {
58
+ return this.git(['log', `--oneline`, `-${count}`]);
59
+ }
60
+ async git(args) {
61
+ const { stdout } = await execFileAsync('git', args, { cwd: this.cwd });
62
+ return stdout;
63
+ }
64
+ }
65
+ function parseStatLine(statOutput) {
66
+ // Parse the summary line like: "3 files changed, 10 insertions(+), 2 deletions(-)"
67
+ const summaryLine = statOutput.trim().split('\n').pop() ?? '';
68
+ const filesMatch = summaryLine.match(/(\d+)\s+files?\s+changed/);
69
+ const insertionsMatch = summaryLine.match(/(\d+)\s+insertions?\(\+\)/);
70
+ const deletionsMatch = summaryLine.match(/(\d+)\s+deletions?\(-\)/);
71
+ return {
72
+ filesChanged: filesMatch ? parseInt(filesMatch[1], 10) : 0,
73
+ insertions: insertionsMatch ? parseInt(insertionsMatch[1], 10) : 0,
74
+ deletions: deletionsMatch ? parseInt(deletionsMatch[1], 10) : 0,
75
+ };
76
+ }
@@ -1,19 +1,16 @@
1
1
  import type { ClaudeSessionService } from '../claude/claude-session.service.js';
2
2
  import type { FileRunStateStore } from '../state/file-run-state-store.js';
3
3
  import type { ManagerRunRecord, ManagerRunResult, ManagerTaskRequest } from '../types/contracts.js';
4
- import type { WorktreeCoordinator } from '../worktree/worktree-coordinator.js';
5
4
  import type { TaskPlanner } from './task-planner.js';
6
5
  export type ManagerRunProgressHandler = (run: ManagerRunRecord) => void | Promise<void>;
7
6
  export declare class ManagerOrchestrator {
8
7
  private readonly sessionService;
9
8
  private readonly stateStore;
10
- private readonly worktreeCoordinator;
11
9
  private readonly taskPlanner;
12
- constructor(sessionService: ClaudeSessionService, stateStore: FileRunStateStore, worktreeCoordinator: WorktreeCoordinator, taskPlanner: TaskPlanner);
10
+ constructor(sessionService: ClaudeSessionService, stateStore: FileRunStateStore, taskPlanner: TaskPlanner);
13
11
  run(request: ManagerTaskRequest, onProgress?: ManagerRunProgressHandler): Promise<ManagerRunResult>;
14
12
  listRuns(cwd: string): Promise<ManagerRunRecord[]>;
15
13
  getRun(cwd: string, runId: string): Promise<ManagerRunRecord | null>;
16
- cleanupRunWorktrees(cwd: string, runId: string): Promise<ManagerRunRecord | null>;
17
14
  private executePlan;
18
15
  private patchSession;
19
16
  private updateRunAndNotify;
@@ -1,65 +1,53 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { managerPromptRegistry } from '../prompts/registry.js';
3
+ import { appendTranscriptEvents, stripTrailingPartials, } from '../util/transcript-append.js';
3
4
  export class ManagerOrchestrator {
4
5
  sessionService;
5
6
  stateStore;
6
- worktreeCoordinator;
7
7
  taskPlanner;
8
- constructor(sessionService, stateStore, worktreeCoordinator, taskPlanner) {
8
+ constructor(sessionService, stateStore, taskPlanner) {
9
9
  this.sessionService = sessionService;
10
10
  this.stateStore = stateStore;
11
- this.worktreeCoordinator = worktreeCoordinator;
12
11
  this.taskPlanner = taskPlanner;
13
12
  }
14
13
  async run(request, onProgress) {
15
- const mode = request.mode ?? 'auto';
16
14
  const maxSubagents = Math.max(1, request.maxSubagents ?? 3);
17
- const useWorktrees = request.useWorktrees ?? maxSubagents > 1;
18
15
  const includeProjectSettings = request.includeProjectSettings ?? true;
19
16
  const metadata = await this.sessionService.inspectRepository(request.cwd);
20
- const plans = this.taskPlanner.plan(request.task, mode, maxSubagents);
17
+ const plans = this.taskPlanner.plan(request.task, maxSubagents);
21
18
  const runId = randomUUID();
22
19
  const createdAt = new Date().toISOString();
23
- const assignments = await Promise.all(plans.map((plan) => this.worktreeCoordinator.prepareAssignment({
24
- cwd: request.cwd,
25
- runId,
26
- title: plan.title,
27
- useWorktree: useWorktrees && plans.length > 1,
28
- })));
29
- const plannedAssignments = plans.map((plan, index) => ({
20
+ const plannedAssignments = plans.map((plan) => ({
30
21
  plan,
31
- assignment: assignments[index],
22
+ cwd: request.cwd,
32
23
  }));
33
24
  const runRecord = {
34
25
  id: runId,
35
26
  cwd: request.cwd,
36
27
  task: request.task,
37
- mode,
38
- useWorktrees,
39
28
  includeProjectSettings,
40
29
  status: 'running',
41
30
  createdAt,
42
31
  updatedAt: createdAt,
43
32
  metadata,
44
- sessions: plannedAssignments.map(({ plan, assignment }) => createManagedSessionRecord(plan, assignment.cwd, assignment)),
33
+ sessions: plannedAssignments.map(({ plan, cwd }) => createManagedSessionRecord(plan, cwd)),
45
34
  };
46
35
  await this.stateStore.saveRun(runRecord);
47
36
  await onProgress?.(runRecord);
48
- const canRunInParallel = plannedAssignments.every(({ assignment }) => assignment.mode === 'git-worktree');
49
- const settledResults = canRunInParallel
50
- ? await Promise.allSettled(plannedAssignments.map(({ plan, assignment }) => this.executePlan({
37
+ const settledResults = plannedAssignments.length <= 1
38
+ ? await Promise.allSettled(plannedAssignments.map(({ plan, cwd }) => this.executePlan({
51
39
  request,
52
40
  runId,
53
41
  plan,
54
- assignment,
42
+ cwd,
55
43
  includeProjectSettings,
56
44
  onProgress,
57
45
  })))
58
- : await runSequentially(plannedAssignments.map(({ plan, assignment }) => () => this.executePlan({
46
+ : await runSequentially(plannedAssignments.map(({ plan, cwd }) => () => this.executePlan({
59
47
  request,
60
48
  runId,
61
49
  plan,
62
- assignment,
50
+ cwd,
63
51
  includeProjectSettings,
64
52
  onProgress,
65
53
  })));
@@ -78,43 +66,28 @@ export class ManagerOrchestrator {
78
66
  getRun(cwd, runId) {
79
67
  return this.stateStore.getRun(cwd, runId);
80
68
  }
81
- async cleanupRunWorktrees(cwd, runId) {
82
- const run = await this.stateStore.getRun(cwd, runId);
83
- if (!run) {
84
- return null;
85
- }
86
- for (const session of run.sessions) {
87
- if (session.worktreeMode !== 'git-worktree') {
88
- continue;
89
- }
90
- await this.worktreeCoordinator.cleanupAssignment({
91
- mode: 'git-worktree',
92
- cwd: session.cwd,
93
- rootCwd: run.cwd,
94
- branchName: session.branchName,
95
- });
96
- }
97
- return run;
98
- }
99
69
  async executePlan(input) {
100
- const { request, runId, plan, assignment, includeProjectSettings, onProgress, } = input;
70
+ const { request, runId, plan, cwd, includeProjectSettings, onProgress } = input;
101
71
  await this.patchSession(request.cwd, runId, plan.id, (session) => ({
102
72
  ...session,
103
73
  status: 'running',
104
74
  }), onProgress);
105
75
  try {
106
- const sessionResult = await this.sessionService.runTask({
107
- cwd: assignment.cwd,
76
+ const sessionInput = {
77
+ cwd,
108
78
  prompt: buildWorkerPrompt(plan.prompt, request.task),
109
79
  systemPrompt: managerPromptRegistry.subagentSystemPrompt,
110
80
  model: request.model,
111
81
  includePartialMessages: true,
112
82
  settingSources: includeProjectSettings ? ['project', 'local'] : [],
113
- }, async (event) => {
83
+ };
84
+ const sessionResult = await this.sessionService.runTask(sessionInput, async (event) => {
114
85
  await this.patchSession(request.cwd, runId, plan.id, (session) => ({
115
86
  ...session,
116
87
  claudeSessionId: event.sessionId ?? session.claudeSessionId,
117
- events: [...session.events, compactEvent(event)],
88
+ events: appendTranscriptEvents(session.events, [
89
+ compactEvent(event),
90
+ ]),
118
91
  }), onProgress);
119
92
  });
120
93
  await this.patchSession(request.cwd, runId, plan.id, (session) => ({
@@ -124,6 +97,7 @@ export class ManagerOrchestrator {
124
97
  finalText: sessionResult.finalText,
125
98
  turns: sessionResult.turns,
126
99
  totalCostUsd: sessionResult.totalCostUsd,
100
+ events: stripTrailingPartials(session.events),
127
101
  }), onProgress);
128
102
  }
129
103
  catch (error) {
@@ -131,6 +105,7 @@ export class ManagerOrchestrator {
131
105
  ...session,
132
106
  status: 'failed',
133
107
  error: error instanceof Error ? error.message : String(error),
108
+ events: stripTrailingPartials(session.events),
134
109
  }), onProgress);
135
110
  throw error;
136
111
  }
@@ -148,22 +123,23 @@ export class ManagerOrchestrator {
148
123
  return updatedRun;
149
124
  }
150
125
  }
151
- function createManagedSessionRecord(plan, cwd, assignment) {
126
+ function createManagedSessionRecord(plan, cwd) {
152
127
  return {
153
128
  id: plan.id,
154
129
  title: plan.title,
155
130
  prompt: plan.prompt,
156
131
  status: 'pending',
157
132
  cwd,
158
- worktreeMode: assignment.mode,
159
- branchName: assignment.branchName,
160
133
  events: [],
161
134
  };
162
135
  }
163
- function buildWorkerPrompt(subtaskPrompt, parentTask) {
136
+ function buildWorkerPrompt(subtaskPrompt, parentTasks) {
137
+ const parentContext = parentTasks.length === 1
138
+ ? `Parent task: ${parentTasks[0]}`
139
+ : `Parent tasks:\n${parentTasks.map((t, i) => `${i + 1}. ${t}`).join('\n')}`;
164
140
  return [
165
141
  'You are executing a delegated Claude Code subtask.',
166
- `Parent task: ${parentTask}`,
142
+ parentContext,
167
143
  `Assigned subtask: ${subtaskPrompt}`,
168
144
  'Stay within scope, finish the requested work, and end with a concise verification summary.',
169
145
  ].join('\n\n');
@@ -174,10 +150,18 @@ function summarizeRun(sessions) {
174
150
  .join('\n');
175
151
  }
176
152
  function compactEvent(event) {
177
- return {
178
- ...event,
153
+ const compact = {
154
+ type: event.type,
155
+ sessionId: event.sessionId,
179
156
  text: event.text.slice(0, 4000),
180
157
  };
158
+ if (event.turns !== undefined) {
159
+ compact.turns = event.turns;
160
+ }
161
+ if (event.totalCostUsd !== undefined) {
162
+ compact.totalCostUsd = event.totalCostUsd;
163
+ }
164
+ return compact;
181
165
  }
182
166
  async function runSequentially(tasks) {
183
167
  const results = [];
@@ -0,0 +1,64 @@
1
+ import type { ClaudeSessionRunResult, PersistentRunRecord, PersistentRunResult, SessionContextSnapshot, GitDiffResult, GitOperationResult } from '../types/contracts.js';
2
+ import type { ClaudeSessionEventHandler } from '../claude/claude-agent-sdk-adapter.js';
3
+ import type { FileRunStateStore } from '../state/file-run-state-store.js';
4
+ import type { SessionController } from './session-controller.js';
5
+ import type { GitOperations } from './git-operations.js';
6
+ import type { ContextTracker } from './context-tracker.js';
7
+ export type PersistentManagerProgressHandler = (run: PersistentRunRecord) => void | Promise<void>;
8
+ export declare class PersistentManager {
9
+ private readonly sessionController;
10
+ private readonly gitOps;
11
+ private readonly stateStore;
12
+ private readonly contextTracker;
13
+ constructor(sessionController: SessionController, gitOps: GitOperations, stateStore: FileRunStateStore, contextTracker: ContextTracker);
14
+ /**
15
+ * Send a message to the persistent Claude Code session.
16
+ * Creates a new session if none exists.
17
+ */
18
+ sendMessage(cwd: string, message: string, options?: {
19
+ model?: string;
20
+ }, onEvent?: ClaudeSessionEventHandler): Promise<{
21
+ sessionId: string | undefined;
22
+ finalText: string;
23
+ turns?: number;
24
+ totalCostUsd?: number;
25
+ context: SessionContextSnapshot;
26
+ }>;
27
+ /**
28
+ * Get the current git diff.
29
+ */
30
+ gitDiff(): Promise<GitDiffResult>;
31
+ /**
32
+ * Commit all current changes.
33
+ */
34
+ gitCommit(message: string): Promise<GitOperationResult>;
35
+ /**
36
+ * Hard reset to discard all uncommitted changes.
37
+ */
38
+ gitReset(): Promise<GitOperationResult>;
39
+ /**
40
+ * Get current session status and context health.
41
+ */
42
+ getStatus(): SessionContextSnapshot;
43
+ /**
44
+ * Clear the active session. Next send creates a fresh one.
45
+ */
46
+ clearSession(cwd: string): Promise<string | null>;
47
+ /**
48
+ * Compact the current session to free context.
49
+ */
50
+ compactSession(cwd: string, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
51
+ /**
52
+ * Execute a full task with run tracking.
53
+ * Creates a run record, sends the message, and persists the result.
54
+ */
55
+ executeTask(cwd: string, task: string, options?: {
56
+ model?: string;
57
+ }, onProgress?: PersistentManagerProgressHandler): Promise<PersistentRunResult>;
58
+ /**
59
+ * Try to restore session state from disk on startup.
60
+ */
61
+ tryRestore(cwd: string): Promise<boolean>;
62
+ listRuns(cwd: string): Promise<PersistentRunRecord[]>;
63
+ getRun(cwd: string, runId: string): Promise<PersistentRunRecord | null>;
64
+ }
@@ -0,0 +1,152 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ export class PersistentManager {
3
+ sessionController;
4
+ gitOps;
5
+ stateStore;
6
+ contextTracker;
7
+ constructor(sessionController, gitOps, stateStore, contextTracker) {
8
+ this.sessionController = sessionController;
9
+ this.gitOps = gitOps;
10
+ this.stateStore = stateStore;
11
+ this.contextTracker = contextTracker;
12
+ }
13
+ /**
14
+ * Send a message to the persistent Claude Code session.
15
+ * Creates a new session if none exists.
16
+ */
17
+ async sendMessage(cwd, message, options, onEvent) {
18
+ const result = await this.sessionController.sendMessage(cwd, message, options, onEvent);
19
+ return {
20
+ sessionId: result.sessionId,
21
+ finalText: result.finalText,
22
+ turns: result.turns,
23
+ totalCostUsd: result.totalCostUsd,
24
+ context: this.sessionController.getContextSnapshot(),
25
+ };
26
+ }
27
+ /**
28
+ * Get the current git diff.
29
+ */
30
+ async gitDiff() {
31
+ return this.gitOps.diff();
32
+ }
33
+ /**
34
+ * Commit all current changes.
35
+ */
36
+ async gitCommit(message) {
37
+ return this.gitOps.commit(message);
38
+ }
39
+ /**
40
+ * Hard reset to discard all uncommitted changes.
41
+ */
42
+ async gitReset() {
43
+ return this.gitOps.resetHard();
44
+ }
45
+ /**
46
+ * Get current session status and context health.
47
+ */
48
+ getStatus() {
49
+ return this.sessionController.getContextSnapshot();
50
+ }
51
+ /**
52
+ * Clear the active session. Next send creates a fresh one.
53
+ */
54
+ async clearSession(cwd) {
55
+ return this.sessionController.clearSession(cwd);
56
+ }
57
+ /**
58
+ * Compact the current session to free context.
59
+ */
60
+ async compactSession(cwd, onEvent) {
61
+ return this.sessionController.compactSession(cwd, onEvent);
62
+ }
63
+ /**
64
+ * Execute a full task with run tracking.
65
+ * Creates a run record, sends the message, and persists the result.
66
+ */
67
+ async executeTask(cwd, task, options, onProgress) {
68
+ const runId = randomUUID();
69
+ const createdAt = new Date().toISOString();
70
+ const runRecord = {
71
+ id: runId,
72
+ cwd,
73
+ task,
74
+ status: 'running',
75
+ createdAt,
76
+ updatedAt: createdAt,
77
+ sessionId: this.sessionController.sessionId,
78
+ sessionHistory: [],
79
+ messages: [
80
+ {
81
+ timestamp: createdAt,
82
+ direction: 'sent',
83
+ text: task,
84
+ },
85
+ ],
86
+ actions: [],
87
+ commits: [],
88
+ context: this.sessionController.getContextSnapshot(),
89
+ };
90
+ await this.stateStore.saveRun(runRecord);
91
+ await onProgress?.(runRecord);
92
+ try {
93
+ const result = await this.sessionController.sendMessage(cwd, task, options, async (event) => {
94
+ // Update run record with progress events
95
+ const currentRun = await this.stateStore.getRun(cwd, runId);
96
+ if (currentRun) {
97
+ const updated = {
98
+ ...currentRun,
99
+ updatedAt: new Date().toISOString(),
100
+ sessionId: event.sessionId ?? currentRun.sessionId,
101
+ context: this.sessionController.getContextSnapshot(),
102
+ };
103
+ await this.stateStore.saveRun(updated);
104
+ await onProgress?.(updated);
105
+ }
106
+ });
107
+ const completedRun = await this.stateStore.updateRun(cwd, runId, (run) => ({
108
+ ...run,
109
+ status: 'completed',
110
+ updatedAt: new Date().toISOString(),
111
+ sessionId: result.sessionId ?? run.sessionId,
112
+ messages: [
113
+ ...run.messages,
114
+ {
115
+ timestamp: new Date().toISOString(),
116
+ direction: 'received',
117
+ text: result.finalText,
118
+ turns: result.turns,
119
+ totalCostUsd: result.totalCostUsd,
120
+ inputTokens: result.inputTokens,
121
+ outputTokens: result.outputTokens,
122
+ },
123
+ ],
124
+ context: this.sessionController.getContextSnapshot(),
125
+ finalSummary: result.finalText,
126
+ }));
127
+ return { run: completedRun };
128
+ }
129
+ catch (error) {
130
+ const failedRun = await this.stateStore.updateRun(cwd, runId, (run) => ({
131
+ ...run,
132
+ status: 'failed',
133
+ updatedAt: new Date().toISOString(),
134
+ context: this.sessionController.getContextSnapshot(),
135
+ finalSummary: error instanceof Error ? error.message : String(error),
136
+ }));
137
+ return { run: failedRun };
138
+ }
139
+ }
140
+ /**
141
+ * Try to restore session state from disk on startup.
142
+ */
143
+ async tryRestore(cwd) {
144
+ return this.sessionController.tryRestore(cwd);
145
+ }
146
+ listRuns(cwd) {
147
+ return this.stateStore.listRuns(cwd);
148
+ }
149
+ getRun(cwd, runId) {
150
+ return this.stateStore.getRun(cwd, runId);
151
+ }
152
+ }
@@ -0,0 +1,38 @@
1
+ import type { ClaudeSessionRunResult, SessionContextSnapshot } from '../types/contracts.js';
2
+ import type { ClaudeAgentSdkAdapter, ClaudeSessionEventHandler } from '../claude/claude-agent-sdk-adapter.js';
3
+ import type { ContextTracker } from './context-tracker.js';
4
+ export declare class SessionController {
5
+ private readonly sdkAdapter;
6
+ private readonly contextTracker;
7
+ private readonly sessionPrompt;
8
+ private activeSessionId;
9
+ constructor(sdkAdapter: ClaudeAgentSdkAdapter, contextTracker: ContextTracker, sessionPrompt: string);
10
+ get isActive(): boolean;
11
+ get sessionId(): string | null;
12
+ /**
13
+ * Send a message to the persistent session. Creates one if none exists.
14
+ * Returns the session result including usage data.
15
+ */
16
+ sendMessage(cwd: string, message: string, options?: {
17
+ model?: string;
18
+ settingSources?: Array<'user' | 'project' | 'local'>;
19
+ }, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
20
+ /**
21
+ * Send /compact to the current session to compress context.
22
+ */
23
+ compactSession(cwd: string, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
24
+ /**
25
+ * Clear the current session. The next sendMessage will create a fresh one.
26
+ */
27
+ clearSession(cwd: string): Promise<string | null>;
28
+ /**
29
+ * Get current context tracking snapshot.
30
+ */
31
+ getContextSnapshot(): SessionContextSnapshot;
32
+ /**
33
+ * Try to restore active session from persisted state on startup.
34
+ */
35
+ tryRestore(cwd: string): Promise<boolean>;
36
+ private persistActiveSession;
37
+ private removeActiveSession;
38
+ }
@@ -0,0 +1,135 @@
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { join, dirname } from 'node:path';
3
+ const ACTIVE_SESSION_FILE = '.claude-manager/active-session.json';
4
+ export class SessionController {
5
+ sdkAdapter;
6
+ contextTracker;
7
+ sessionPrompt;
8
+ activeSessionId = null;
9
+ constructor(sdkAdapter, contextTracker, sessionPrompt) {
10
+ this.sdkAdapter = sdkAdapter;
11
+ this.contextTracker = contextTracker;
12
+ this.sessionPrompt = sessionPrompt;
13
+ }
14
+ get isActive() {
15
+ return this.activeSessionId !== null;
16
+ }
17
+ get sessionId() {
18
+ return this.activeSessionId;
19
+ }
20
+ /**
21
+ * Send a message to the persistent session. Creates one if none exists.
22
+ * Returns the session result including usage data.
23
+ */
24
+ async sendMessage(cwd, message, options, onEvent) {
25
+ const input = {
26
+ cwd,
27
+ prompt: message,
28
+ persistSession: true,
29
+ permissionMode: 'acceptEdits',
30
+ includePartialMessages: true,
31
+ model: options?.model,
32
+ settingSources: options?.settingSources ?? ['project', 'local'],
33
+ };
34
+ if (this.activeSessionId) {
35
+ // Resume existing session
36
+ input.resumeSessionId = this.activeSessionId;
37
+ }
38
+ else {
39
+ // New session — apply the expert operator system prompt
40
+ input.systemPrompt = this.sessionPrompt;
41
+ }
42
+ const result = await this.sdkAdapter.runSession(input, onEvent);
43
+ // Track the session ID
44
+ if (result.sessionId) {
45
+ this.activeSessionId = result.sessionId;
46
+ }
47
+ // Update context tracking
48
+ this.contextTracker.recordResult({
49
+ sessionId: result.sessionId,
50
+ turns: result.turns,
51
+ totalCostUsd: result.totalCostUsd,
52
+ inputTokens: result.inputTokens,
53
+ outputTokens: result.outputTokens,
54
+ contextWindowSize: result.contextWindowSize,
55
+ });
56
+ // Persist active session state
57
+ await this.persistActiveSession(cwd);
58
+ return result;
59
+ }
60
+ /**
61
+ * Send /compact to the current session to compress context.
62
+ */
63
+ async compactSession(cwd, onEvent) {
64
+ if (!this.activeSessionId) {
65
+ throw new Error('No active session to compact');
66
+ }
67
+ const result = await this.sendMessage(cwd, '/compact', undefined, onEvent);
68
+ this.contextTracker.recordCompaction();
69
+ return result;
70
+ }
71
+ /**
72
+ * Clear the current session. The next sendMessage will create a fresh one.
73
+ */
74
+ async clearSession(cwd) {
75
+ const clearedId = this.activeSessionId;
76
+ this.activeSessionId = null;
77
+ this.contextTracker.reset();
78
+ await this.removeActiveSession(cwd);
79
+ return clearedId;
80
+ }
81
+ /**
82
+ * Get current context tracking snapshot.
83
+ */
84
+ getContextSnapshot() {
85
+ return this.contextTracker.snapshot();
86
+ }
87
+ /**
88
+ * Try to restore active session from persisted state on startup.
89
+ */
90
+ async tryRestore(cwd) {
91
+ const filePath = join(cwd, ACTIVE_SESSION_FILE);
92
+ try {
93
+ const raw = await readFile(filePath, 'utf-8');
94
+ const state = JSON.parse(raw);
95
+ if (state.sessionId && state.cwd === cwd) {
96
+ this.activeSessionId = state.sessionId;
97
+ this.contextTracker.restore(state);
98
+ return true;
99
+ }
100
+ }
101
+ catch {
102
+ // File doesn't exist or is corrupt — start fresh
103
+ }
104
+ return false;
105
+ }
106
+ async persistActiveSession(cwd) {
107
+ if (!this.activeSessionId) {
108
+ return;
109
+ }
110
+ const snap = this.contextTracker.snapshot();
111
+ const state = {
112
+ sessionId: this.activeSessionId,
113
+ cwd,
114
+ startedAt: new Date().toISOString(),
115
+ totalTurns: snap.totalTurns,
116
+ totalCostUsd: snap.totalCostUsd,
117
+ estimatedContextPercent: snap.estimatedContextPercent,
118
+ contextWindowSize: snap.contextWindowSize,
119
+ latestInputTokens: snap.latestInputTokens,
120
+ };
121
+ const filePath = join(cwd, ACTIVE_SESSION_FILE);
122
+ await mkdir(dirname(filePath), { recursive: true });
123
+ await writeFile(filePath, JSON.stringify(state, null, 2));
124
+ }
125
+ async removeActiveSession(cwd) {
126
+ const filePath = join(cwd, ACTIVE_SESSION_FILE);
127
+ try {
128
+ const { unlink } = await import('node:fs/promises');
129
+ await unlink(filePath);
130
+ }
131
+ catch {
132
+ // File doesn't exist — that's fine
133
+ }
134
+ }
135
+ }