@borgee/agents-host 0.2.44 → 0.2.56

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 (56) hide show
  1. package/README.md +23 -27
  2. package/dist/agents-host.d.ts +26 -5
  3. package/dist/agents-host.js +163 -192
  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 +18 -1
  10. package/dist/context/claude-file-brief.d.ts +2 -0
  11. package/dist/context/claude-file-brief.js +83 -0
  12. package/dist/context/compaction.d.ts +20 -0
  13. package/dist/context/compaction.js +59 -0
  14. package/dist/context/injection.d.ts +39 -6
  15. package/dist/context/injection.js +317 -26
  16. package/dist/context/main-session-delegation.d.ts +1 -1
  17. package/dist/context/projection-strategy.d.ts +24 -0
  18. package/dist/context/projection-strategy.js +90 -0
  19. package/dist/context/prompt.d.ts +16 -1
  20. package/dist/context/prompt.js +456 -22
  21. package/dist/context/resolved-workspace.d.ts +2 -0
  22. package/dist/context/resolved-workspace.js +64 -0
  23. package/dist/context/skill-manual.d.ts +1 -0
  24. package/dist/context/skill-manual.js +4 -1
  25. package/dist/context/turn-preparation.d.ts +8 -2
  26. package/dist/context/turn-preparation.js +56 -14
  27. package/dist/gateway/localhost-gateway.js +2 -0
  28. package/dist/managed-daemon.js +122 -9
  29. package/dist/plugin-sdk.js +276 -359
  30. package/dist/plugin-sdk.js.map +4 -4
  31. package/dist/progress-to-activity.d.ts +16 -0
  32. package/dist/progress-to-activity.js +24 -0
  33. package/dist/projection-strategy-values.d.ts +4 -0
  34. package/dist/projection-strategy-values.js +28 -0
  35. package/dist/providers/acp-progress-collector.d.ts +44 -0
  36. package/dist/providers/acp-progress-collector.js +130 -0
  37. package/dist/providers/awaiting-user.d.ts +2 -3
  38. package/dist/providers/awaiting-user.js +5 -7
  39. package/dist/providers/claude/activity-metadata.d.ts +14 -0
  40. package/dist/providers/claude/activity-metadata.js +81 -0
  41. package/dist/providers/claude/cli-client.d.ts +1 -2
  42. package/dist/providers/claude/cli-client.js +190 -117
  43. package/dist/providers/codex/cli-client.js +3 -83
  44. package/dist/providers/codex/project-doc.js +12 -11
  45. package/dist/providers/copilot/cli-client.js +3 -83
  46. package/dist/providers/create-provider.d.ts +2 -0
  47. package/dist/providers/create-provider.js +20 -4
  48. package/dist/state-paths.d.ts +1 -1
  49. package/dist/state-paths.js +3 -3
  50. package/dist/task-thread-resolution.d.ts +3 -2
  51. package/dist/types.d.ts +130 -6
  52. package/package.json +2 -2
  53. package/skills/borgee-agent/SKILL.md +9 -1
  54. package/skills/borgee-agent/references/task-properties.md +5 -2
  55. package/dist/durable-cursor-store.d.ts +0 -5
  56. package/dist/durable-cursor-store.js +0 -7
@@ -1,11 +1,21 @@
1
1
  import { promises as fs } from 'node:fs';
2
- import { randomUUID } from 'node:crypto';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { homedir } from 'node:os';
3
4
  import { fileURLToPath } from 'node:url';
4
- import { dirname, join, resolve } from 'node:path';
5
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
6
+ import { findTaskForThread } from '../task-thread-resolution.js';
7
+ import { buildClaudeFileBrief } from './claude-file-brief.js';
5
8
  const CHANNEL_CONTEXT_ROOT_DIRNAME = 'channel-context';
6
9
  const CHANNEL_CONTEXT_PAYLOAD_FILENAME = 'context.json';
7
10
  const CHANNEL_GATEWAY_CREDENTIAL_FILENAME = '.borgee-agent-gateway.json';
8
- const TASK_WORKSPACE_ROOT_DIRNAME = '.borgee-task-workspaces';
11
+ const CLAUDE_PROJECTED_BRIEF_FILENAME = 'CLAUDE.md';
12
+ const DEFAULT_HOME_BORGEE_DIRNAME = '.borgee';
13
+ const CHANNEL_WORKSPACE_COLLECTION_DIRNAME = 'channels';
14
+ const CHANNEL_WORKSPACE_DIRNAME = 'workspace';
15
+ const TASK_ISOLATED_WORKSPACE_COLLECTION_DIRNAME = 'tasks';
16
+ const TASK_WORKSPACE_MODE_PROPERTY_KEY = 'workspace.mode';
17
+ const TASK_WORKSPACE_MODE_ISOLATED = 'isolated';
18
+ const DEFAULT_TASK_WORKSPACE_RESOLUTION_TIMEOUT_MS = 1_500;
9
19
  export class ChannelContextPreparationError extends Error {
10
20
  partialContext;
11
21
  constructor(message, partialContext, options) {
@@ -62,19 +72,50 @@ function buildBorgeeAgentGatewayCredential(channelId, localhostGateway) {
62
72
  },
63
73
  };
64
74
  }
65
- function buildChannelContextPayload(channelId, collaborationOutcome, attentionSnapshot, taskThreadCollaborationContract, collaborationCapabilities, missedCollaborationDiagnostic, skillRuntime, localhostGateway, options) {
75
+ function buildChannelContextPayload(channelId, runtimeSurface, collaborationOutcome, attentionSnapshot, compactionSnapshot, taskThreadCollaborationContract, collaborationCapabilities, missedCollaborationDiagnostic, skillRuntime, localhostGateway, options) {
66
76
  return {
67
77
  schemaVersion: 1,
68
78
  channelId,
79
+ runtimeSurface,
69
80
  ...(collaborationOutcome ? { collaborationOutcome } : {}),
70
81
  ...(attentionSnapshot ? { attentionSnapshot } : {}),
82
+ ...(compactionSnapshot ? { compactionSnapshot } : {}),
71
83
  ...(taskThreadCollaborationContract ? { taskThreadCollaborationContract } : {}),
72
84
  ...(collaborationCapabilities ? { collaborationCapabilities } : {}),
73
85
  ...(missedCollaborationDiagnostic ? { missedCollaborationDiagnostic } : {}),
74
86
  ...(skillRuntime ? { skillRuntime: toSkillRuntimePayload(skillRuntime) } : {}),
75
87
  ...(localhostGateway ? { localhostGateway } : {}),
76
88
  ...(options?.taskAssignmentContext ? { taskAssignmentContext: options.taskAssignmentContext } : {}),
77
- ...(options?.taskWorkspace ? { taskWorkspace: options.taskWorkspace } : {}),
89
+ ...(options?.resolvedWorkspace ? { resolvedWorkspace: options.resolvedWorkspace } : {}),
90
+ };
91
+ }
92
+ function hashUtf8Content(content) {
93
+ return createHash('sha256').update(content).digest('hex');
94
+ }
95
+ async function unlinkIfPresent(fileSystem, path) {
96
+ await fileSystem.unlink(path).catch((error) => {
97
+ if (error.code !== 'ENOENT') {
98
+ throw error;
99
+ }
100
+ });
101
+ }
102
+ function buildPromptContextForClaudeProjectedBrief(preparedContext) {
103
+ return {
104
+ channelContextPayloadPath: preparedContext.payloadPath,
105
+ claudeProjectedBriefPath: preparedContext.claudeProjectedBriefPath,
106
+ claudeProjectedBriefHash: preparedContext.claudeProjectedBriefHash,
107
+ gatewayCredentialPath: preparedContext.gatewayCredentialPath,
108
+ skillRuntime: preparedContext.skillRuntime,
109
+ localhostGateway: preparedContext.localhostGateway,
110
+ taskAssignmentContext: preparedContext.payload.taskAssignmentContext,
111
+ resolvedWorkspace: preparedContext.resolvedWorkspace,
112
+ runtimeSurface: preparedContext.payload.runtimeSurface,
113
+ collaborationOutcome: preparedContext.payload.collaborationOutcome,
114
+ attentionSnapshot: preparedContext.payload.attentionSnapshot,
115
+ compactionSnapshot: preparedContext.payload.compactionSnapshot,
116
+ collaborationCapabilities: preparedContext.payload.collaborationCapabilities,
117
+ missedCollaborationDiagnostic: preparedContext.payload.missedCollaborationDiagnostic,
118
+ taskThreadCollaborationContract: preparedContext.payload.taskThreadCollaborationContract,
78
119
  };
79
120
  }
80
121
  function isRecord(value) {
@@ -96,6 +137,73 @@ function isUsableTaskId(value) {
96
137
  && !/[\u0000-\u001f\u007f\s]/u.test(value)
97
138
  && canRoundTripThroughUriComponentEncoding(value);
98
139
  }
140
+ function normalizeTaskAssignmentCurrentTaskId(taskAssignmentContext) {
141
+ const currentTaskId = typeof taskAssignmentContext?.currentTaskId === 'string'
142
+ ? taskAssignmentContext.currentTaskId.trim()
143
+ : '';
144
+ return currentTaskId && isUsableTaskId(currentTaskId)
145
+ ? currentTaskId
146
+ : undefined;
147
+ }
148
+ export function buildRuntimeSurfaceForTurn(options) {
149
+ const turnMode = options?.collaboration?.turnMode ?? 'ordinary';
150
+ const gatewayAvailable = options?.localhostGateway !== undefined;
151
+ const currentTaskId = normalizeTaskAssignmentCurrentTaskId(options?.taskAssignmentContext);
152
+ const taskAssignmentThreadActive = options?.taskAssignmentContext?.active === true;
153
+ const collaborationGatewayEnabled = gatewayAvailable && options.localhostGateway?.collaboration?.enabled === true;
154
+ const auxiliarySendAvailable = turnMode === 'ordinary'
155
+ && collaborationGatewayEnabled
156
+ && options?.collaboration?.sendRoutesAllowed === true;
157
+ return {
158
+ schemaVersion: 1,
159
+ turnMode,
160
+ gateway: {
161
+ available: gatewayAvailable,
162
+ readOnlyRoutesAuthorized: gatewayAvailable,
163
+ inspectBeforeAnswer: gatewayAvailable,
164
+ },
165
+ task: {
166
+ currentThread: taskAssignmentThreadActive ? 'task-assignment-thread' : 'parent-channel',
167
+ parentChannelTaskCollection: taskAssignmentThreadActive ? 'parent-channel-only' : 'available',
168
+ currentTaskShorthand: currentTaskId
169
+ ? 'persisted-current-task'
170
+ : taskAssignmentThreadActive
171
+ ? 'fallback-current-task'
172
+ : 'requires-task-id',
173
+ currentTaskIdPersisted: currentTaskId !== undefined,
174
+ currentTaskFallback: !currentTaskId && taskAssignmentThreadActive
175
+ ? 'visible-parent-channel-thread-scan'
176
+ : 'disallowed',
177
+ },
178
+ collaboration: gatewayAvailable
179
+ ? turnMode === 'ordinary'
180
+ ? {
181
+ participantLookup: collaborationGatewayEnabled ? 'available' : 'unavailable',
182
+ draftRead: auxiliarySendAvailable ? 'available' : 'unavailable',
183
+ auxiliarySend: auxiliarySendAvailable ? 'available' : 'unavailable',
184
+ }
185
+ : {
186
+ participantLookup: 'unavailable',
187
+ draftRead: 'unavailable',
188
+ auxiliarySend: 'unavailable',
189
+ }
190
+ : {
191
+ participantLookup: 'unavailable',
192
+ draftRead: 'unavailable',
193
+ auxiliarySend: 'unavailable',
194
+ },
195
+ response: {
196
+ mainVisibleReply: turnMode === 'ordinary'
197
+ ? 'model-visible-reply'
198
+ : turnMode === 'protocol-managed'
199
+ ? 'host-visible-protocol-reply'
200
+ : 'no-visible-reply',
201
+ auxiliaryMessageUsage: auxiliarySendAvailable
202
+ ? 'targeted-escalation-only'
203
+ : 'unavailable',
204
+ },
205
+ };
206
+ }
99
207
  function sanitizeTaskAssignmentContext(value) {
100
208
  if (!isRecord(value) || value.active !== true) {
101
209
  return undefined;
@@ -133,6 +241,128 @@ async function readExistingTaskAssignmentContext(payloadPath, fileSystem) {
133
241
  return undefined;
134
242
  }
135
243
  }
244
+ function sanitizeResolvedWorkspaceContext(value) {
245
+ if (!isRecord(value) || typeof value.rootPath !== 'string' || !isAbsolute(value.rootPath)) {
246
+ return undefined;
247
+ }
248
+ if (value.authority === 'channel'
249
+ && typeof value.owningChannelId === 'string'
250
+ && value.owningChannelId.length > 0) {
251
+ return {
252
+ authority: 'channel',
253
+ owningChannelId: value.owningChannelId,
254
+ rootPath: value.rootPath,
255
+ };
256
+ }
257
+ if (value.authority === 'task'
258
+ && typeof value.taskId === 'string'
259
+ && isUsableTaskId(value.taskId.trim())) {
260
+ return {
261
+ authority: 'task',
262
+ taskId: value.taskId.trim(),
263
+ rootPath: value.rootPath,
264
+ };
265
+ }
266
+ return undefined;
267
+ }
268
+ async function readExistingResolvedWorkspaceContext(payloadPath, fileSystem) {
269
+ try {
270
+ const raw = await fileSystem.readFile(payloadPath, { encoding: 'utf8' });
271
+ const payload = JSON.parse(raw);
272
+ return sanitizeResolvedWorkspaceContext(payload.resolvedWorkspace);
273
+ }
274
+ catch {
275
+ return undefined;
276
+ }
277
+ }
278
+ function buildChannelResolvedWorkspace(workspaceCollectionRootDir, channelId) {
279
+ return {
280
+ authority: 'channel',
281
+ owningChannelId: channelId,
282
+ rootPath: resolveChannelWorkspaceDirectory(workspaceCollectionRootDir, channelId),
283
+ };
284
+ }
285
+ function buildStickyChannelResolvedWorkspace(workspaceCollectionRootDir, resolvedWorkspace) {
286
+ if (resolvedWorkspace?.authority !== 'channel') {
287
+ return undefined;
288
+ }
289
+ const expectedRootPath = resolveChannelWorkspaceDirectory(workspaceCollectionRootDir, resolvedWorkspace.owningChannelId);
290
+ if (resolvedWorkspace.rootPath !== expectedRootPath) {
291
+ return undefined;
292
+ }
293
+ return {
294
+ authority: 'channel',
295
+ owningChannelId: resolvedWorkspace.owningChannelId,
296
+ rootPath: expectedRootPath,
297
+ };
298
+ }
299
+ async function resolveTaskThreadWorkspaceContext(params) {
300
+ const channelWorkspace = buildChannelResolvedWorkspace(params.workspaceCollectionRootDir, params.channelId);
301
+ const taskAssignmentContext = params.taskAssignmentContext;
302
+ const currentTaskId = taskAssignmentContext?.currentTaskId?.trim();
303
+ if (taskAssignmentContext?.active !== true
304
+ || !currentTaskId
305
+ || !params.taskReader) {
306
+ return { taskAssignmentContext, resolvedWorkspace: channelWorkspace };
307
+ }
308
+ const stickyTaskWorkspace = params.existingResolvedWorkspace?.authority === 'task'
309
+ && params.existingResolvedWorkspace.taskId === currentTaskId
310
+ ? {
311
+ authority: 'task',
312
+ taskId: currentTaskId,
313
+ rootPath: resolveTaskWorkspaceDirectory(params.workspaceCollectionRootDir, params.channelId, currentTaskId),
314
+ }
315
+ : undefined;
316
+ const stickyChannelWorkspace = buildStickyChannelResolvedWorkspace(params.workspaceCollectionRootDir, params.existingResolvedWorkspace);
317
+ try {
318
+ const task = await withDeadline(findTaskForThread(params.taskReader, params.channelId, currentTaskId), params.taskResolutionTimeoutMs, `workspace resolution timed out for task ${currentTaskId}`);
319
+ if (!task) {
320
+ return {
321
+ taskAssignmentContext,
322
+ resolvedWorkspace: channelWorkspace,
323
+ };
324
+ }
325
+ async function withDeadline(promise, timeoutMs, timeoutMessage) {
326
+ if (timeoutMs <= 0) {
327
+ return await promise;
328
+ }
329
+ let timer;
330
+ try {
331
+ return await Promise.race([
332
+ promise,
333
+ new Promise((_, reject) => {
334
+ timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
335
+ }),
336
+ ]);
337
+ }
338
+ finally {
339
+ if (timer) {
340
+ clearTimeout(timer);
341
+ }
342
+ }
343
+ }
344
+ if (task.properties[TASK_WORKSPACE_MODE_PROPERTY_KEY] !== TASK_WORKSPACE_MODE_ISOLATED) {
345
+ return {
346
+ taskAssignmentContext,
347
+ resolvedWorkspace: buildChannelResolvedWorkspace(params.workspaceCollectionRootDir, task.channelId),
348
+ };
349
+ }
350
+ return {
351
+ taskAssignmentContext,
352
+ resolvedWorkspace: {
353
+ authority: 'task',
354
+ taskId: task.id,
355
+ rootPath: resolveTaskWorkspaceDirectory(params.workspaceCollectionRootDir, params.channelId, task.id),
356
+ },
357
+ };
358
+ }
359
+ catch (error) {
360
+ return {
361
+ taskAssignmentContext,
362
+ resolvedWorkspace: stickyTaskWorkspace ?? stickyChannelWorkspace ?? channelWorkspace,
363
+ };
364
+ }
365
+ }
136
366
  export function encodeChannelPathSegment(channelId) {
137
367
  const encoded = Buffer.from(channelId, 'utf8').toString('hex');
138
368
  return encoded.length > 0 ? encoded : 'empty';
@@ -144,17 +374,38 @@ export function resolveChannelContextPayloadPath(stateRootDir, channelId) {
144
374
  return join(resolveChannelContextDirectory(stateRootDir, channelId), CHANNEL_CONTEXT_PAYLOAD_FILENAME);
145
375
  }
146
376
  export function resolveTaskWorkspaceRootDirectory(startupWorkspaceRootDir) {
147
- return join(resolve(startupWorkspaceRootDir), TASK_WORKSPACE_ROOT_DIRNAME);
377
+ return join(resolve(startupWorkspaceRootDir), TASK_ISOLATED_WORKSPACE_COLLECTION_DIRNAME);
378
+ }
379
+ export function resolveManagedWorkspaceCollectionRoot(workspaceCollectionRootDir, env = process.env, resolvedHomeDir = homedir()) {
380
+ const explicitRoot = workspaceCollectionRootDir?.trim();
381
+ if (explicitRoot) {
382
+ return resolve(explicitRoot);
383
+ }
384
+ const home = env.HOME?.trim() || resolvedHomeDir.trim();
385
+ if (!home) {
386
+ throw new Error('Unable to resolve a user home directory for agents-host workspaces');
387
+ }
388
+ return join(resolve(home), DEFAULT_HOME_BORGEE_DIRNAME, CHANNEL_WORKSPACE_COLLECTION_DIRNAME);
389
+ }
390
+ export function resolveChannelWorkspaceRootDirectory(workspaceCollectionRootDir, channelId) {
391
+ return join(resolve(workspaceCollectionRootDir), encodeChannelPathSegment(channelId));
148
392
  }
149
- export function resolveTaskWorkspaceDirectory(startupWorkspaceRootDir, channelId, taskId) {
150
- return join(resolveTaskWorkspaceRootDirectory(startupWorkspaceRootDir), encodeChannelPathSegment(channelId), encodeChannelPathSegment(taskId));
393
+ export function resolveChannelWorkspaceDirectory(workspaceCollectionRootDir, channelId) {
394
+ return join(resolveChannelWorkspaceRootDirectory(workspaceCollectionRootDir, channelId), CHANNEL_WORKSPACE_DIRNAME);
151
395
  }
396
+ export function resolveTaskIsolatedWorkspaceDirectory(workspaceCollectionRootDir, channelId, taskId) {
397
+ return join(resolveTaskWorkspaceRootDirectory(resolveChannelWorkspaceRootDirectory(workspaceCollectionRootDir, channelId)), encodeChannelPathSegment(taskId), CHANNEL_WORKSPACE_DIRNAME);
398
+ }
399
+ export const resolveTaskWorkspaceDirectory = resolveTaskIsolatedWorkspaceDirectory;
152
400
  export function resolveChannelGatewayCredentialPath(stateRootDir, channelId) {
153
401
  return join(resolveChannelContextDirectory(stateRootDir, channelId), CHANNEL_GATEWAY_CREDENTIAL_FILENAME);
154
402
  }
155
403
  export function resolveGatewayCredentialPathFromPayloadPath(payloadPath) {
156
404
  return join(dirname(payloadPath), CHANNEL_GATEWAY_CREDENTIAL_FILENAME);
157
405
  }
406
+ export function resolveClaudeProjectedBriefPathFromPayloadPath(payloadPath) {
407
+ return join(dirname(payloadPath), CLAUDE_PROJECTED_BRIEF_FILENAME);
408
+ }
158
409
  /**
159
410
  * Every basename a gateway credential can carry in a channel context directory, including the ones
160
411
  * this host no longer writes. The directory holds at most one live credential — the token is
@@ -245,14 +496,18 @@ export class FileChannelContextStore {
245
496
  skillRuntimeEnabled;
246
497
  skillAssetResolver;
247
498
  localhostGateway;
248
- startupWorkspaceRootDir;
499
+ workspaceCollectionRootDir;
500
+ taskReader;
501
+ taskResolutionTimeoutMs;
249
502
  constructor(stateRootDir, options = {}) {
250
503
  this.stateRootDir = stateRootDir;
251
504
  this.fileSystem = options.fileSystem ?? DEFAULT_FILE_SYSTEM;
252
505
  this.skillRuntimeEnabled = options.skillRuntimeEnabled ?? false;
253
506
  this.skillAssetResolver = options.skillAssetResolver ?? new PackageSkillAssetResolver(import.meta.url, this.fileSystem);
254
507
  this.localhostGateway = options.localhostGateway;
255
- this.startupWorkspaceRootDir = resolve(options.taskWorkspaceRootDir ?? process.cwd());
508
+ this.workspaceCollectionRootDir = resolveManagedWorkspaceCollectionRoot(options.workspaceCollectionRootDir, options.env, options.resolvedHomeDir);
509
+ this.taskReader = options.taskReader;
510
+ this.taskResolutionTimeoutMs = options.taskResolutionTimeoutMs ?? DEFAULT_TASK_WORKSPACE_RESOLUTION_TIMEOUT_MS;
256
511
  }
257
512
  async prepare(inputOrChannelId, options) {
258
513
  const input = typeof inputOrChannelId === 'string'
@@ -264,8 +519,10 @@ export class FileChannelContextStore {
264
519
  const auxiliaryCollaborationEnabled = collaborationCommandsEnabled && collaborationRoutesAllowedForTurnMode;
265
520
  const directoryPath = resolveChannelContextDirectory(this.stateRootDir, input.channelId);
266
521
  const payloadPath = resolveChannelContextPayloadPath(this.stateRootDir, input.channelId);
522
+ const claudeProjectedBriefPath = resolveClaudeProjectedBriefPathFromPayloadPath(payloadPath);
267
523
  const gatewayCredentialPath = resolveGatewayCredentialPathFromPayloadPath(payloadPath);
268
524
  const existingTaskAssignmentContext = await readExistingTaskAssignmentContext(payloadPath, this.fileSystem);
525
+ const existingResolvedWorkspace = await readExistingResolvedWorkspaceContext(payloadPath, this.fileSystem);
269
526
  const skillRuntime = this.skillRuntimeEnabled
270
527
  ? await this.resolveSkillRuntimeBestEffort()
271
528
  : undefined;
@@ -295,20 +552,31 @@ export class FileChannelContextStore {
295
552
  incomingMessageType: input.incomingMessageType,
296
553
  incomingContent: input.incomingContent,
297
554
  }, existingTaskAssignmentContext);
298
- const taskWorkspace = taskAssignmentContext?.currentTaskId
299
- ? {
300
- currentTaskId: taskAssignmentContext.currentTaskId,
301
- rootPath: resolveTaskWorkspaceDirectory(this.startupWorkspaceRootDir, input.channelId, taskAssignmentContext.currentTaskId),
302
- }
303
- : undefined;
304
- const payload = buildChannelContextPayload(input.channelId, input.collaborationOutcome, input.attentionSnapshot, input.taskThreadCollaborationContract, input.collaborationCapabilities, input.missedCollaborationDiagnostic, skillRuntime, localhostGateway, taskAssignmentContext || taskWorkspace
555
+ const workspaceResolution = await resolveTaskThreadWorkspaceContext({
556
+ channelId: input.channelId,
557
+ workspaceCollectionRootDir: this.workspaceCollectionRootDir,
558
+ taskAssignmentContext,
559
+ existingResolvedWorkspace,
560
+ taskReader: this.taskReader,
561
+ taskResolutionTimeoutMs: this.taskResolutionTimeoutMs,
562
+ });
563
+ const resolvedWorkspace = workspaceResolution.resolvedWorkspace;
564
+ const runtimeSurface = buildRuntimeSurfaceForTurn({
565
+ localhostGateway,
566
+ collaboration: input.collaboration,
567
+ taskAssignmentContext: workspaceResolution.taskAssignmentContext ?? taskAssignmentContext,
568
+ });
569
+ const payload = buildChannelContextPayload(input.channelId, runtimeSurface, input.collaborationOutcome, input.attentionSnapshot, input.compactionSnapshot, input.taskThreadCollaborationContract, input.collaborationCapabilities, input.missedCollaborationDiagnostic, skillRuntime, localhostGateway, workspaceResolution.taskAssignmentContext || resolvedWorkspace
305
570
  ? {
306
- ...(taskAssignmentContext ? { taskAssignmentContext } : {}),
307
- ...(taskWorkspace ? { taskWorkspace } : {}),
571
+ ...(workspaceResolution.taskAssignmentContext
572
+ ? { taskAssignmentContext: workspaceResolution.taskAssignmentContext }
573
+ : {}),
574
+ resolvedWorkspace,
308
575
  }
309
576
  : undefined);
310
577
  const gatewayCredential = buildBorgeeAgentGatewayCredential(input.channelId, issuedLocalhostGateway);
311
- let taskWorkspaceMaterialized = false;
578
+ let resolvedWorkspaceMaterialized = false;
579
+ let claudeProjectedBriefWritten = false;
312
580
  let payloadWritten = false;
313
581
  let gatewayCredentialWritten = false;
314
582
  const preparedContext = {
@@ -318,15 +586,27 @@ export class FileChannelContextStore {
318
586
  gatewayCredentialPath: gatewayCredential ? gatewayCredentialPath : undefined,
319
587
  skillRuntime,
320
588
  localhostGateway,
321
- taskWorkspace,
589
+ resolvedWorkspace,
322
590
  };
591
+ const shouldProjectClaudeBrief = input.provider === 'claude' && input.projectionStrategy === 'file-brief';
592
+ const claudeProjectedBriefContent = shouldProjectClaudeBrief
593
+ ? buildClaudeFileBrief(buildPromptContextForClaudeProjectedBrief({
594
+ ...preparedContext,
595
+ claudeProjectedBriefPath,
596
+ }))
597
+ : undefined;
598
+ if (claudeProjectedBriefContent) {
599
+ preparedContext.claudeProjectedBriefPath = claudeProjectedBriefPath;
600
+ preparedContext.claudeProjectedBriefHash = hashUtf8Content(claudeProjectedBriefContent);
601
+ }
323
602
  try {
324
603
  await this.fileSystem.mkdir(directoryPath, { recursive: true, mode: 0o700 });
325
- if (taskWorkspace) {
326
- await this.fileSystem.mkdir(taskWorkspace.rootPath, { recursive: true, mode: 0o700 });
327
- taskWorkspaceMaterialized = true;
328
- }
604
+ await this.fileSystem.mkdir(resolvedWorkspace.rootPath, { recursive: true, mode: 0o700 });
605
+ resolvedWorkspaceMaterialized = true;
329
606
  await pruneGatewayCredentialSidecars(this.fileSystem, directoryPath, gatewayCredential ? CHANNEL_GATEWAY_CREDENTIAL_FILENAME : undefined);
607
+ if (!claudeProjectedBriefContent) {
608
+ await unlinkIfPresent(this.fileSystem, claudeProjectedBriefPath);
609
+ }
330
610
  await this.fileSystem.writeFile(payloadPath, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
331
611
  payloadWritten = true;
332
612
  if (gatewayCredential) {
@@ -342,20 +622,31 @@ export class FileChannelContextStore {
342
622
  else {
343
623
  this.localhostGateway?.clearChannel(input.channelId);
344
624
  }
625
+ if (claudeProjectedBriefContent) {
626
+ const stagingProjectedBriefPath = join(directoryPath, `.claude-file-brief.${randomUUID()}.md`);
627
+ await this.fileSystem.writeFile(stagingProjectedBriefPath, claudeProjectedBriefContent, { encoding: 'utf8', mode: 0o600 });
628
+ await this.fileSystem.rename(stagingProjectedBriefPath, claudeProjectedBriefPath);
629
+ claudeProjectedBriefWritten = true;
630
+ }
345
631
  if (issuedLocalhostGateway) {
346
632
  this.localhostGateway?.publishPayload(input.channelId, payloadPath, payload);
347
633
  }
348
634
  return preparedContext;
349
635
  }
350
636
  catch (error) {
637
+ if (claudeProjectedBriefContent && !claudeProjectedBriefWritten) {
638
+ await unlinkIfPresent(this.fileSystem, claudeProjectedBriefPath);
639
+ }
351
640
  const partialContext = {
352
641
  directoryPath,
353
642
  payload,
643
+ claudeProjectedBriefPath: claudeProjectedBriefWritten ? claudeProjectedBriefPath : undefined,
644
+ claudeProjectedBriefHash: claudeProjectedBriefWritten ? preparedContext.claudeProjectedBriefHash : undefined,
354
645
  payloadPath: payloadWritten ? payloadPath : undefined,
355
646
  gatewayCredentialPath: gatewayCredentialWritten ? gatewayCredentialPath : undefined,
356
647
  skillRuntime: payloadWritten ? skillRuntime : undefined,
357
648
  localhostGateway: payloadWritten ? localhostGateway : undefined,
358
- taskWorkspace: taskWorkspaceMaterialized ? taskWorkspace : undefined,
649
+ resolvedWorkspace: resolvedWorkspaceMaterialized ? resolvedWorkspace : undefined,
359
650
  };
360
651
  throw new ChannelContextPreparationError('failed to persist channel context payload', partialContext, { cause: error });
361
652
  }
@@ -1 +1 @@
1
- export declare const MAIN_SESSION_DELEGATION_LINES: readonly string[];
1
+ export declare const MAIN_SESSION_DELEGATION_LINES: readonly ["This session is your coordination surface: read the channel, answer the human, break work down, and keep task state current here.", "Delegate the actual work to subagents — research, code edits, long builds and test runs, and broad repository sweeps belong in a delegated worker instead of running inline here.", "Delegation changes who executes, not who owns: the work still belongs to this thread, and you still report the result here yourself.", "Stay responsive while delegated work runs, so a new incoming message never waits behind a long local operation."];
@@ -0,0 +1,24 @@
1
+ import type { DebugLogger } from '../debug.js';
2
+ import type { ChatControlPlane } from '../chat/chat-control-plane.js';
3
+ import type { ProjectionStrategy } from '../types.js';
4
+ interface ProjectionStrategyResolverOptions {
5
+ baseUrl?: string;
6
+ agentApiKey?: string;
7
+ resolveStableAgentId?: () => string | undefined;
8
+ fetch?: typeof fetch;
9
+ fallback?: () => ProjectionStrategy;
10
+ logger?: Pick<DebugLogger, 'error'>;
11
+ taskReader?: Pick<ChatControlPlane, 'getTask' | 'listChannels' | 'listTasks'>;
12
+ }
13
+ export declare class ServerProjectionStrategyResolver {
14
+ private readonly options;
15
+ private readonly strategyCache;
16
+ private readonly channelAliases;
17
+ private readonly fetchImpl;
18
+ private readonly fallback;
19
+ constructor(options: ProjectionStrategyResolverOptions);
20
+ resolve(channelId: string): Promise<ProjectionStrategy>;
21
+ private resolveCachedStrategy;
22
+ private resolveEffectiveCacheKey;
23
+ }
24
+ export {};
@@ -0,0 +1,90 @@
1
+ import { DEFAULT_PROJECTION_STRATEGY, normalizeProjectionStrategy, } from '../projection-strategy-values.js';
2
+ import { findTaskForThread } from '../task-thread-resolution.js';
3
+ export class ServerProjectionStrategyResolver {
4
+ options;
5
+ strategyCache = new Map();
6
+ channelAliases = new Map();
7
+ fetchImpl;
8
+ fallback;
9
+ constructor(options) {
10
+ this.options = options;
11
+ this.fetchImpl = options.fetch ?? fetch;
12
+ this.fallback = options.fallback ?? (() => DEFAULT_PROJECTION_STRATEGY);
13
+ }
14
+ async resolve(channelId) {
15
+ const baseUrl = this.options.baseUrl?.trim();
16
+ const agentApiKey = this.options.agentApiKey?.trim();
17
+ const agentId = this.options.resolveStableAgentId?.()?.trim();
18
+ if (!baseUrl || !agentApiKey || !agentId) {
19
+ return this.fallback();
20
+ }
21
+ const cacheKey = `${agentId}::${channelId}`;
22
+ const url = new URL(`/api/v1/agents/${encodeURIComponent(agentId)}/channels/${encodeURIComponent(channelId)}/projection-strategy`, baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);
23
+ try {
24
+ const response = await this.fetchImpl(url.toString(), {
25
+ headers: {
26
+ Authorization: `Bearer ${agentApiKey}`,
27
+ },
28
+ });
29
+ if (response.status === 404) {
30
+ const effectiveCacheKey = await this.resolveEffectiveCacheKey(agentId, channelId, cacheKey);
31
+ this.channelAliases.set(cacheKey, effectiveCacheKey);
32
+ this.strategyCache.set(effectiveCacheKey, DEFAULT_PROJECTION_STRATEGY);
33
+ return DEFAULT_PROJECTION_STRATEGY;
34
+ }
35
+ if (!response.ok) {
36
+ throw new Error(`server returned ${response.status} ${response.statusText}`.trim());
37
+ }
38
+ const body = await response.json();
39
+ const strategy = normalizeProjectionStrategy(typeof body.projection_strategy === 'string' ? body.projection_strategy : undefined);
40
+ if (!strategy) {
41
+ throw new Error('server returned an invalid projection strategy');
42
+ }
43
+ const effectiveChannelId = typeof body.channel_id === 'string' && body.channel_id.length > 0
44
+ ? body.channel_id
45
+ : channelId;
46
+ const effectiveCacheKey = `${agentId}::${effectiveChannelId}`;
47
+ this.channelAliases.set(cacheKey, effectiveCacheKey);
48
+ this.strategyCache.set(effectiveCacheKey, strategy);
49
+ return strategy;
50
+ }
51
+ catch (error) {
52
+ const cached = await this.resolveCachedStrategy(agentId, channelId, cacheKey);
53
+ if (cached) {
54
+ return cached;
55
+ }
56
+ const message = error instanceof Error ? error.message : String(error);
57
+ this.options.logger?.error('failed to resolve server-backed projection strategy; using fallback', {
58
+ agentId,
59
+ channelId,
60
+ error: message,
61
+ });
62
+ return DEFAULT_PROJECTION_STRATEGY;
63
+ }
64
+ }
65
+ async resolveCachedStrategy(agentId, channelId, cacheKey) {
66
+ const direct = this.strategyCache.get(this.channelAliases.get(cacheKey) ?? cacheKey);
67
+ if (direct) {
68
+ return direct;
69
+ }
70
+ if (!this.options.taskReader) {
71
+ return undefined;
72
+ }
73
+ const effectiveCacheKey = await this.resolveEffectiveCacheKey(agentId, channelId, cacheKey);
74
+ if (effectiveCacheKey === cacheKey) {
75
+ return undefined;
76
+ }
77
+ this.channelAliases.set(cacheKey, effectiveCacheKey);
78
+ return this.strategyCache.get(effectiveCacheKey);
79
+ }
80
+ async resolveEffectiveCacheKey(agentId, channelId, cacheKey) {
81
+ if (!this.options.taskReader) {
82
+ return cacheKey;
83
+ }
84
+ const task = await findTaskForThread(this.options.taskReader, channelId);
85
+ if (!task?.channelId) {
86
+ return cacheKey;
87
+ }
88
+ return `${agentId}::${task.channelId}`;
89
+ }
90
+ }
@@ -1,4 +1,4 @@
1
- import type { PreparedPromptContext, ProviderKind } from '../types.js';
1
+ import type { ProjectionStrategy, PreparedPromptContext, ProviderKind } from '../types.js';
2
2
  /**
3
3
  * Builds the per-turn prompt. Conversation memory is intentionally NOT
4
4
  * assembled here: each provider's CLI client resumes a native per-channel
@@ -16,3 +16,18 @@ export declare function buildPrompt(params: {
16
16
  incomingMessageType?: string;
17
17
  promptContext?: PreparedPromptContext;
18
18
  }): string;
19
+ export declare function buildClaudeSessionPromptAppend(params: {
20
+ agentName: string;
21
+ promptContext?: PreparedPromptContext;
22
+ promptStrategy?: ProjectionStrategy;
23
+ }): string | undefined;
24
+ export declare function buildClaudeTurnPrompt(params: {
25
+ agentName?: string;
26
+ channelId: string;
27
+ incomingAuthorId: string;
28
+ incomingContent: string;
29
+ incomingEventKind?: string;
30
+ incomingMessageType?: string;
31
+ promptContext?: PreparedPromptContext;
32
+ promptStrategy?: ProjectionStrategy;
33
+ }): string;