@borgee/agents-host 0.2.44 → 0.2.62

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/README.md +24 -27
  2. package/dist/agents-host.d.ts +27 -5
  3. package/dist/agents-host.js +266 -201
  4. package/dist/chat/chat-control-plane.d.ts +3 -0
  5. package/dist/chat/sdk-chat-control-plane.d.ts +4 -3
  6. package/dist/chat/sdk-chat-control-plane.js +3 -0
  7. package/dist/cli.js +1 -5
  8. package/dist/compatibility-gates.d.ts +4 -0
  9. package/dist/compatibility-gates.js +20 -1
  10. package/dist/config.d.ts +2 -0
  11. package/dist/config.js +21 -0
  12. package/dist/context/claude-file-brief.d.ts +2 -0
  13. package/dist/context/claude-file-brief.js +83 -0
  14. package/dist/context/compaction.d.ts +20 -0
  15. package/dist/context/compaction.js +59 -0
  16. package/dist/context/injection.d.ts +58 -7
  17. package/dist/context/injection.js +370 -36
  18. package/dist/context/main-session-delegation.d.ts +1 -1
  19. package/dist/context/projection-strategy.d.ts +24 -0
  20. package/dist/context/projection-strategy.js +90 -0
  21. package/dist/context/prompt.d.ts +16 -1
  22. package/dist/context/prompt.js +464 -26
  23. package/dist/context/resolved-workspace.d.ts +3 -0
  24. package/dist/context/resolved-workspace.js +106 -0
  25. package/dist/context/skill-manual.d.ts +1 -0
  26. package/dist/context/skill-manual.js +4 -1
  27. package/dist/context/turn-preparation.d.ts +8 -2
  28. package/dist/context/turn-preparation.js +64 -14
  29. package/dist/gateway/localhost-gateway.js +4 -5
  30. package/dist/local-config.js +11 -1
  31. package/dist/managed-daemon.js +127 -9
  32. package/dist/plugin-sdk.js +264 -364
  33. package/dist/plugin-sdk.js.map +4 -4
  34. package/dist/policy/copilot-permission.d.ts +1 -0
  35. package/dist/policy/copilot-permission.js +18 -0
  36. package/dist/policy/gateway-authorization.js +2 -2
  37. package/dist/progress-to-activity.d.ts +16 -0
  38. package/dist/progress-to-activity.js +24 -0
  39. package/dist/projection-strategy-values.d.ts +4 -0
  40. package/dist/projection-strategy-values.js +28 -0
  41. package/dist/providers/acp-progress-collector.d.ts +44 -0
  42. package/dist/providers/acp-progress-collector.js +130 -0
  43. package/dist/providers/awaiting-user.d.ts +2 -3
  44. package/dist/providers/awaiting-user.js +5 -7
  45. package/dist/providers/claude/activity-metadata.d.ts +14 -0
  46. package/dist/providers/claude/activity-metadata.js +81 -0
  47. package/dist/providers/claude/cli-client.d.ts +2 -3
  48. package/dist/providers/claude/cli-client.js +220 -120
  49. package/dist/providers/codex/cli-client.d.ts +2 -0
  50. package/dist/providers/codex/cli-client.js +28 -104
  51. package/dist/providers/codex/project-doc.js +12 -11
  52. package/dist/providers/copilot/cli-client.d.ts +1 -1
  53. package/dist/providers/copilot/cli-client.js +12 -86
  54. package/dist/providers/create-provider.d.ts +2 -0
  55. package/dist/providers/create-provider.js +22 -4
  56. package/dist/state-paths.d.ts +1 -1
  57. package/dist/state-paths.js +3 -3
  58. package/dist/task-thread-resolution.d.ts +3 -2
  59. package/dist/types.d.ts +143 -6
  60. package/package.json +2 -2
  61. package/skills/borgee-agent/SKILL.md +12 -4
  62. package/skills/borgee-agent/references/errors.md +2 -2
  63. package/skills/borgee-agent/references/task-properties.md +6 -2
  64. package/dist/durable-cursor-store.d.ts +0 -5
  65. package/dist/durable-cursor-store.js +0 -7
@@ -1,12 +1,18 @@
1
1
  import { Readable, Writable } from 'node:stream';
2
+ import { createHash } from 'node:crypto';
3
+ import { readFileSync } from 'node:fs';
2
4
  import { createRequire } from 'node:module';
3
5
  import { dirname, resolve } from 'node:path';
4
6
  import spawn from 'cross-spawn';
7
+ import { DEFAULT_PROJECTION_STRATEGY, normalizeProjectionStrategy, } from '../../projection-strategy-values.js';
5
8
  import { assertClaudeCommandCompatibility, DEFAULT_CLAUDE_COMMAND, isLegacyClaudeCompatibilityAlias, } from '../../config.js';
6
9
  import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from '@agentclientprotocol/sdk';
7
10
  import { HostLogger, summarizeChildStderr, summarizeError } from '../../debug.js';
11
+ import { AcpProgressCollector } from '../acp-progress-collector.js';
12
+ import { readClaudeActivityMetadata } from './activity-metadata.js';
8
13
  import { resolveCopilotPermissionResponse } from '../../policy/copilot-permission.js';
9
- import { buildSkillManualLines } from '../../context/skill-manual.js';
14
+ import { buildClaudeSessionPromptAppend } from '../../context/prompt.js';
15
+ import { isDiscussionOnlyResolvedWorkspace } from '../../context/resolved-workspace.js';
10
16
  import { IDLE_BACKEND_SHUTDOWN_DISABLED_MS, IdleBackendShutdownScheduler, } from '../idle-backend-shutdown.js';
11
17
  const SESSION_TAINTED_ERRORS = new WeakSet();
12
18
  const IDLE_BACKEND_STOPPED_MESSAGE = 'Claude ACP backend stopped after idle timeout';
@@ -24,6 +30,7 @@ const DEFAULT_SESSION_CAPABILITIES = {
24
30
  loadSession: false,
25
31
  resumeSession: false,
26
32
  };
33
+ const CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD_ENV = 'CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD';
27
34
  const require = createRequire(import.meta.url);
28
35
  const DEFAULT_RUNTIME = {
29
36
  spawn,
@@ -111,82 +118,95 @@ class ClaudeCliProcessError extends Error {
111
118
  this.stderrLineCount = stderrLineCount;
112
119
  }
113
120
  }
114
- function formatToolProgress(title, status) {
115
- const normalizedTitle = hasVisibleText(title) ? title.trim() : null;
116
- switch (status) {
117
- case 'completed':
118
- return normalizedTitle ? `Completed ${normalizedTitle}` : 'Completed tool call';
119
- case 'failed':
120
- return normalizedTitle ? `Failed ${normalizedTitle}` : 'Tool call failed';
121
- case 'pending':
122
- case 'in_progress':
123
- case undefined:
124
- case null:
125
- return normalizedTitle ? `Running ${normalizedTitle}…` : 'Running tool…';
126
- default:
127
- return normalizedTitle ? `${status} ${normalizedTitle}` : status;
121
+ function parseCompactionBanner(text) {
122
+ if (/(?:^|\n\n)Compacting\.\.\.\s*$/u.test(text)) {
123
+ return { stage: 'started' };
128
124
  }
125
+ if (/(?:^|\n\n)Compacting completed\.\s*$/u.test(text)) {
126
+ return { stage: 'completed' };
127
+ }
128
+ const match = /(?:^|\n\n)Compacting failed(?::\s*(.+?)|\.)\s*$/u.exec(text);
129
+ if (!match) {
130
+ return null;
131
+ }
132
+ return {
133
+ stage: 'failed',
134
+ ...(match?.[1] ? { failureReason: match[1].trim() } : {}),
135
+ };
129
136
  }
130
- function formatPlanProgress(entries) {
131
- const current = entries.find((entry) => entry.status === 'in_progress') ??
132
- entries.find((entry) => entry.status === 'pending') ??
133
- entries[0];
134
- return hasVisibleText(current?.content) ? `Plan: ${current.content.trim()}` : null;
135
- }
136
- class ClaudeProgressCollector {
137
+ class ClaudeCompactionObserver {
137
138
  onProgress;
138
- publicText = '';
139
- lastPublished = null;
140
- toolTitles = new Map();
139
+ answerText = '';
140
+ compactionState = 'idle';
141
+ lastCompactionBannerKey = null;
141
142
  constructor(onProgress) {
142
143
  this.onProgress = onProgress;
143
144
  }
144
- consume(update) {
145
- switch (update.update.sessionUpdate) {
146
- case 'agent_message_chunk':
147
- if (update.update.content.type !== 'text') {
148
- return;
149
- }
150
- this.publicText += update.update.content.text;
151
- this.publish(this.publicText);
152
- return;
153
- case 'tool_call':
154
- this.toolTitles.set(update.update.toolCallId, update.update.title);
155
- this.publishFallback(formatToolProgress(update.update.title, update.update.status));
156
- return;
157
- case 'tool_call_update': {
158
- const nextTitle = update.update.title ?? this.toolTitles.get(update.update.toolCallId);
159
- if (hasVisibleText(nextTitle)) {
160
- this.toolTitles.set(update.update.toolCallId, nextTitle);
161
- }
162
- this.publishFallback(formatToolProgress(nextTitle, update.update.status));
163
- return;
164
- }
165
- case 'plan':
166
- this.publishFallback(formatPlanProgress(update.update.entries.map((entry) => ({
167
- content: entry.content,
168
- status: entry.status,
169
- }))));
170
- return;
171
- default:
172
- return;
173
- }
174
- }
175
- getFinalText() {
176
- return this.publicText.trim();
145
+ consumeAnswerChunk(chunk) {
146
+ this.answerText += chunk;
147
+ this.publishCompactionFromText(this.answerText);
177
148
  }
178
- publishFallback(text) {
179
- if (hasVisibleText(this.publicText)) {
149
+ consumeUsageUpdate(usedTokens, contextWindowTokens) {
150
+ if (!this.onProgress || this.compactionState !== 'completed-awaiting-usage') {
180
151
  return;
181
152
  }
182
- this.publish(text);
153
+ this.compactionState = 'idle';
154
+ this.onProgress({
155
+ type: 'compaction',
156
+ compaction: {
157
+ provider: 'claude',
158
+ stage: 'completed',
159
+ usedTokens,
160
+ contextWindowTokens,
161
+ },
162
+ });
183
163
  }
184
- publish(text) {
185
- if (!this.onProgress || !hasVisibleText(text) || text === this.lastPublished) {
164
+ publishCompactionFromText(text) {
165
+ if (!this.onProgress) {
186
166
  return;
187
167
  }
188
- this.lastPublished = text;
189
- this.onProgress({ text });
168
+ const banner = parseCompactionBanner(text);
169
+ if (!banner) {
170
+ return;
171
+ }
172
+ const bannerKey = `${banner.stage}:${banner.stage === 'failed' ? banner.failureReason ?? '' : ''}:${text.trimEnd().length}`;
173
+ if (bannerKey === this.lastCompactionBannerKey) {
174
+ return;
175
+ }
176
+ this.lastCompactionBannerKey = bannerKey;
177
+ switch (banner.stage) {
178
+ case 'started':
179
+ this.compactionState = 'active';
180
+ this.onProgress({
181
+ type: 'compaction',
182
+ compaction: {
183
+ provider: 'claude',
184
+ stage: 'started',
185
+ },
186
+ });
187
+ return;
188
+ case 'completed':
189
+ this.compactionState = 'completed-awaiting-usage';
190
+ this.onProgress({
191
+ type: 'compaction',
192
+ compaction: {
193
+ provider: 'claude',
194
+ stage: 'completed',
195
+ },
196
+ });
197
+ return;
198
+ case 'failed':
199
+ this.compactionState = 'idle';
200
+ this.onProgress({
201
+ type: 'compaction',
202
+ compaction: {
203
+ provider: 'claude',
204
+ stage: 'failed',
205
+ ...(banner.failureReason ? { failureReason: banner.failureReason } : {}),
206
+ },
207
+ });
208
+ return;
209
+ }
190
210
  }
191
211
  }
192
212
  function createDeferredTurn(channelId, preparedTurn, sessionPersistence, options) {
@@ -290,39 +310,65 @@ function resolveClaudeLaunch(command, args, baseCwd) {
290
310
  args: args.map((arg) => (isRelativePathLike(arg) ? resolve(baseCwd, arg) : arg)),
291
311
  };
292
312
  }
293
- export function buildClaudeSessionSystemPrompt(promptContext) {
294
- if (!promptContext) {
313
+ function resolveClaudeSdkCompactionControl() {
314
+ const rawThreshold = process.env[CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD_ENV];
315
+ if (!hasVisibleText(rawThreshold)) {
295
316
  return undefined;
296
317
  }
297
- const lines = [];
298
- // Every command the CLI carries needs the gateway credential file, so the session names it only
299
- // when one exists; otherwise the model is handed an invocation template it cannot fill.
300
- if (promptContext.skillRuntime && promptContext.gatewayCredentialPath) {
301
- lines.push(...buildSkillManualLines(promptContext.skillRuntime));
302
- }
303
- if (promptContext.localhostGateway && promptContext.gatewayCredentialPath) {
304
- lines.push(`Gateway credential file for this session: ${promptContext.gatewayCredentialPath}`);
305
- }
306
- if (promptContext.taskWorkspace) {
307
- lines.push(`Task-scoped writable workspace root: ${promptContext.taskWorkspace.rootPath}.`);
308
- lines.push('This writable workspace is local to agents-host for the current task thread and does not imply that the target repository is already checked out there.');
318
+ const parsedThreshold = Number(rawThreshold.trim());
319
+ if (!Number.isSafeInteger(parsedThreshold) || parsedThreshold <= 0) {
320
+ throw new Error(`Invalid ${CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD_ENV}: expected a positive integer context token threshold for the experiment-scoped Claude SDK compaction opt-in`);
309
321
  }
310
- if (promptContext.taskAssignmentContext?.currentTaskId) {
311
- lines.push(`Current assigned task id: ${promptContext.taskAssignmentContext.currentTaskId}.`);
312
- }
313
- return lines.length > 0 ? lines.join('\n') : undefined;
322
+ return {
323
+ enabled: true,
324
+ context_token_threshold: parsedThreshold,
325
+ };
314
326
  }
315
- function buildClaudeSessionMeta(promptContext) {
316
- const systemPromptAppend = buildClaudeSessionSystemPrompt(promptContext);
317
- if (!systemPromptAppend) {
327
+ function buildClaudeSessionMeta(systemPromptAppend) {
328
+ const sdkCompactionControl = resolveClaudeSdkCompactionControl();
329
+ if (!systemPromptAppend && !sdkCompactionControl) {
318
330
  return undefined;
319
331
  }
320
332
  return {
321
- systemPrompt: {
322
- append: systemPromptAppend,
323
- },
333
+ ...(systemPromptAppend
334
+ ? {
335
+ systemPrompt: {
336
+ append: systemPromptAppend,
337
+ },
338
+ }
339
+ : {}),
340
+ ...(sdkCompactionControl
341
+ ? {
342
+ claudeCode: {
343
+ options: {
344
+ compaction_control: sdkCompactionControl,
345
+ },
346
+ },
347
+ }
348
+ : {}),
324
349
  };
325
350
  }
351
+ function buildFallbackClaudeSessionPrompt(agentName, promptContext, promptStrategy) {
352
+ if (!promptContext) {
353
+ return undefined;
354
+ }
355
+ return buildClaudeSessionPromptAppend({
356
+ agentName: agentName ?? 'Borgee agent',
357
+ promptContext,
358
+ promptStrategy,
359
+ });
360
+ }
361
+ function resolveProjectionStrategy(promptStrategy) {
362
+ return normalizeProjectionStrategy(promptStrategy) ?? DEFAULT_PROJECTION_STRATEGY;
363
+ }
364
+ function resolveTurnSessionPromptAppend(turn) {
365
+ const promptStrategy = resolveProjectionStrategy(turn.projectionStrategy);
366
+ if (promptStrategy === 'turn-full') {
367
+ return undefined;
368
+ }
369
+ return turn.claudeSessionPromptAppend
370
+ ?? buildFallbackClaudeSessionPrompt(turn.agentName, turn.promptContext, promptStrategy);
371
+ }
326
372
  function resolveSessionAdditionalDirectories(promptContext) {
327
373
  const directories = new Set();
328
374
  if (promptContext?.channelContextPayloadPath) {
@@ -336,27 +382,68 @@ function resolveSessionAdditionalDirectories(promptContext) {
336
382
  }
337
383
  return directories.size > 0 ? [...directories].sort((left, right) => left.localeCompare(right)) : undefined;
338
384
  }
339
- /**
340
- * A session carries its scope from `session/new`: the directories it may read, plus the append that
341
- * names the CLI manual and this channel's gateway credential file. The credential file sits in the
342
- * channel context directory the payload already contributes, so its absence is invisible in the
343
- * directory set alone — without the path itself in the key, a session opened while the credential
344
- * write failed keeps an append that never names the CLI for as long as the session lives.
345
- */
346
- function resolveSessionVisibilityKey(promptContext) {
385
+ function hashSessionAppend(systemPromptAppend) {
386
+ if (!systemPromptAppend) {
387
+ return undefined;
388
+ }
389
+ return createHash('sha256').update(systemPromptAppend).digest('hex');
390
+ }
391
+ function resolveSkillManualContentHash(promptContext) {
392
+ const skillMarkdownPath = promptContext?.skillRuntime?.skillMarkdownPath;
393
+ if (!skillMarkdownPath) {
394
+ return undefined;
395
+ }
396
+ try {
397
+ return createHash('sha256')
398
+ .update(skillMarkdownPath)
399
+ .update('\0')
400
+ .update(readFileSync(skillMarkdownPath))
401
+ .digest('hex');
402
+ }
403
+ catch {
404
+ return `unreadable:${skillMarkdownPath}`;
405
+ }
406
+ }
407
+ function resolveSessionVisibilityKey(promptContext, systemPromptAppend, promptStrategy) {
408
+ const resolvedPromptStrategy = resolveProjectionStrategy(promptStrategy);
409
+ const explicitPromptStrategy = resolvedPromptStrategy === DEFAULT_PROJECTION_STRATEGY && promptStrategy == null
410
+ ? undefined
411
+ : resolvedPromptStrategy === DEFAULT_PROJECTION_STRATEGY
412
+ ? undefined
413
+ : resolvedPromptStrategy;
347
414
  const additionalDirectories = resolveSessionAdditionalDirectories(promptContext);
348
- if (!additionalDirectories) {
415
+ const sessionAppendHash = hashSessionAppend(systemPromptAppend);
416
+ const skillManualContentHash = resolveSkillManualContentHash(promptContext);
417
+ const projectedBriefHash = promptContext?.claudeProjectedBriefHash;
418
+ if (!additionalDirectories
419
+ && !sessionAppendHash
420
+ && !skillManualContentHash
421
+ && !projectedBriefHash
422
+ && !promptContext?.gatewayCredentialPath
423
+ && !explicitPromptStrategy) {
349
424
  return undefined;
350
425
  }
426
+ if (sessionAppendHash) {
427
+ return JSON.stringify({
428
+ ...(additionalDirectories ? { additionalDirectories } : {}),
429
+ sessionAppendHash,
430
+ ...(skillManualContentHash ? { skillManualContentHash } : {}),
431
+ ...(projectedBriefHash ? { projectedBriefHash } : {}),
432
+ ...(explicitPromptStrategy ? { promptStrategy: explicitPromptStrategy } : {}),
433
+ });
434
+ }
351
435
  return JSON.stringify({
352
- additionalDirectories,
436
+ ...(additionalDirectories ? { additionalDirectories } : {}),
437
+ ...(skillManualContentHash ? { skillManualContentHash } : {}),
353
438
  ...(promptContext?.gatewayCredentialPath
354
439
  ? { gatewayCredentialPath: promptContext.gatewayCredentialPath }
355
440
  : {}),
441
+ ...(projectedBriefHash ? { projectedBriefHash } : {}),
442
+ ...(explicitPromptStrategy ? { promptStrategy: explicitPromptStrategy } : {}),
356
443
  });
357
444
  }
358
- function buildSessionRequest(cwd, promptContext, additionalDirectories) {
359
- const meta = buildClaudeSessionMeta(promptContext);
445
+ function buildSessionRequest(cwd, systemPromptAppend, additionalDirectories) {
446
+ const meta = buildClaudeSessionMeta(systemPromptAppend);
360
447
  return {
361
448
  cwd,
362
449
  mcpServers: [],
@@ -656,10 +743,14 @@ export class ClaudeCliClient {
656
743
  state.activeTurn = turn;
657
744
  try {
658
745
  state.cwd = this.resolveSessionCwd(turn.preparedTurn.promptContext);
659
- state.visibilityKey = resolveSessionVisibilityKey(turn.preparedTurn.promptContext);
746
+ state.toolPermissionMode = isDiscussionOnlyResolvedWorkspace(turn.preparedTurn.promptContext)
747
+ ? 'deny-all'
748
+ : 'default';
749
+ const sessionPromptAppend = resolveTurnSessionPromptAppend(turn.preparedTurn);
750
+ state.visibilityKey = resolveSessionVisibilityKey(turn.preparedTurn.promptContext, sessionPromptAppend, turn.preparedTurn.projectionStrategy);
660
751
  await this.ensureStarted();
661
752
  await this.recycleSessionIfScopeChanged(channelId, state);
662
- const session = await this.getOrCreateSession(channelId, state, turn.preparedTurn.promptContext);
753
+ const session = await this.getOrCreateSession(channelId, state, turn.preparedTurn, sessionPromptAppend);
663
754
  let reply;
664
755
  try {
665
756
  const promptInput = await this.buildPromptInput(turn.preparedTurn);
@@ -703,7 +794,7 @@ export class ClaudeCliClient {
703
794
  }
704
795
  })();
705
796
  }
706
- async getOrCreateSession(channelId, state, promptContext) {
797
+ async getOrCreateSession(channelId, state, turn, systemPromptAppend) {
707
798
  if (state.session) {
708
799
  return state.session;
709
800
  }
@@ -717,10 +808,10 @@ export class ClaudeCliClient {
717
808
  const persistedSessionId = state.sessionPersistence === 'persistent'
718
809
  ? await this.readPersistedSessionId(channelId, cwd, state.visibilityKey)
719
810
  : undefined;
720
- const additionalDirectories = resolveSessionAdditionalDirectories(promptContext);
811
+ const additionalDirectories = resolveSessionAdditionalDirectories(turn.promptContext);
721
812
  const sessionPromise = persistedSessionId
722
- ? this.restoreOrCreateSession(channelId, persistedSessionId, cwd, promptContext, additionalDirectories)
723
- : this.startFreshSession(channelId, cwd, promptContext, additionalDirectories);
813
+ ? this.restoreOrCreateSession(channelId, persistedSessionId, cwd, systemPromptAppend, additionalDirectories)
814
+ : this.startFreshSession(channelId, cwd, systemPromptAppend, additionalDirectories);
724
815
  this.pendingSessionStarts.add(sessionPromise);
725
816
  state.sessionPromise = sessionPromise;
726
817
  let sessionAdopted = false;
@@ -759,11 +850,11 @@ export class ClaudeCliClient {
759
850
  }
760
851
  }
761
852
  }
762
- async startFreshSession(channelId, cwd, promptContext, additionalDirectories) {
853
+ async startFreshSession(channelId, cwd, systemPromptAppend, additionalDirectories) {
763
854
  if (!this.connection) {
764
855
  throw new Error('Claude ACP connection is not available');
765
856
  }
766
- const session = await this.connection.agent.buildSession(buildSessionRequest(cwd, promptContext, additionalDirectories)).start();
857
+ const session = await this.connection.agent.buildSession(buildSessionRequest(cwd, systemPromptAppend, additionalDirectories)).start();
767
858
  this.logger?.debug('started fresh Claude ACP session', { channelId, cwd });
768
859
  const state = this.channels.get(channelId);
769
860
  if (state?.sessionPersistence === 'persistent') {
@@ -771,13 +862,13 @@ export class ClaudeCliClient {
771
862
  }
772
863
  return session;
773
864
  }
774
- async restoreOrCreateSession(channelId, sessionId, cwd, promptContext, additionalDirectories) {
865
+ async restoreOrCreateSession(channelId, sessionId, cwd, systemPromptAppend, additionalDirectories) {
775
866
  if (!this.sessionCapabilities.resumeSession) {
776
867
  this.logger?.debug('Claude ACP restore unsupported; starting fresh session', { channelId });
777
- return this.startFreshSession(channelId, cwd, promptContext, additionalDirectories);
868
+ return this.startFreshSession(channelId, cwd, systemPromptAppend, additionalDirectories);
778
869
  }
779
870
  try {
780
- const session = await this.restoreSession(sessionId, cwd, promptContext, additionalDirectories);
871
+ const session = await this.restoreSession(sessionId, cwd, systemPromptAppend, additionalDirectories);
781
872
  this.logger?.debug('restored Claude ACP session', {
782
873
  channelId,
783
874
  restoreMethod: this.sessionCapabilities.resumeSession ? 'session/resume' : 'session/load',
@@ -790,10 +881,10 @@ export class ClaudeCliClient {
790
881
  }
791
882
  this.logger?.debug('discarded stale Claude ACP session and started fresh', { channelId });
792
883
  await this.clearPersistedSessionBestEffort(channelId);
793
- return this.startFreshSession(channelId, cwd, promptContext, additionalDirectories);
884
+ return this.startFreshSession(channelId, cwd, systemPromptAppend, additionalDirectories);
794
885
  }
795
886
  }
796
- async restoreSession(sessionId, cwd, promptContext, additionalDirectories) {
887
+ async restoreSession(sessionId, cwd, systemPromptAppend, additionalDirectories) {
797
888
  if (!this.connection) {
798
889
  throw new Error('Claude ACP connection is not available');
799
890
  }
@@ -805,7 +896,7 @@ export class ClaudeCliClient {
805
896
  try {
806
897
  const request = {
807
898
  sessionId,
808
- ...buildSessionRequest(cwd, promptContext, additionalDirectories),
899
+ ...buildSessionRequest(cwd, systemPromptAppend, additionalDirectories),
809
900
  };
810
901
  if (this.sessionCapabilities.resumeSession) {
811
902
  await this.connection.agent.request(this.runtime.methods.agent.session.resume, request);
@@ -830,7 +921,7 @@ export class ClaudeCliClient {
830
921
  }
831
922
  }
832
923
  resolveSessionCwd(promptContext) {
833
- return promptContext?.taskWorkspace?.rootPath ?? this.runtime.cwd;
924
+ return promptContext?.resolvedWorkspace?.rootPath ?? this.runtime.cwd;
834
925
  }
835
926
  async recycleSessionIfScopeChanged(channelId, state) {
836
927
  const session = state.session;
@@ -862,7 +953,8 @@ export class ClaudeCliClient {
862
953
  const promptFailure = new Promise((_, reject) => {
863
954
  void promptPromise.catch((error) => reject(markSessionTainted(error)));
864
955
  });
865
- const collector = new ClaudeProgressCollector(options?.onProgress);
956
+ const collector = new AcpProgressCollector(options?.onProgress, readClaudeActivityMetadata);
957
+ const compactionObserver = new ClaudeCompactionObserver(options?.onProgress);
866
958
  for (;;) {
867
959
  let update;
868
960
  try {
@@ -888,6 +980,12 @@ export class ClaudeCliClient {
888
980
  }
889
981
  return output;
890
982
  }
983
+ if (update.update.sessionUpdate === 'agent_message_chunk' && update.update.content.type === 'text') {
984
+ compactionObserver.consumeAnswerChunk(update.update.content.text);
985
+ }
986
+ if (update.update.sessionUpdate === 'usage_update') {
987
+ compactionObserver.consumeUsageUpdate(update.update.used, update.update.size);
988
+ }
891
989
  collector.consume(update);
892
990
  }
893
991
  }
@@ -926,13 +1024,15 @@ export class ClaudeCliClient {
926
1024
  this.shutdownPromise = this.shutdownBackend(error);
927
1025
  }
928
1026
  handlePermissionRequest(params) {
1027
+ const state = this.findChannelStateBySessionId(params.sessionId);
929
1028
  return resolveCopilotPermissionResponse({
930
1029
  gateEnabled: true,
931
1030
  policyMode: 'enforce',
932
1031
  params,
933
1032
  logger: this.logger,
934
1033
  agentId: this.resolveSessionStoreAgentId()?.trim() || undefined,
935
- channelId: this.findChannelIdBySessionId(params.sessionId),
1034
+ channelId: state?.channelId,
1035
+ toolPermissionMode: state?.toolPermissionMode ?? 'default',
936
1036
  });
937
1037
  }
938
1038
  invalidateSession(channelId, state, session) {
@@ -984,10 +1084,10 @@ export class ClaudeCliClient {
984
1084
  }
985
1085
  state.queue.length = 0;
986
1086
  }
987
- findChannelIdBySessionId(sessionId) {
1087
+ findChannelStateBySessionId(sessionId) {
988
1088
  for (const state of this.channels.values()) {
989
1089
  if (state.session?.sessionId === sessionId) {
990
- return state.channelId;
1090
+ return state;
991
1091
  }
992
1092
  }
993
1093
  return undefined;
@@ -108,6 +108,8 @@ export declare class CodexCliClient {
108
108
  private failAll;
109
109
  private closeSession;
110
110
  private clearIdleTimer;
111
+ private handlePermissionRequest;
112
+ private findChannelStateBySessionId;
111
113
  private reconcileIdleChannelState;
112
114
  private ensureSessionStoreLoaded;
113
115
  private readPersistedSessionId;