@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,11 +1,23 @@
1
- import { promises as fs } from 'node:fs';
2
- import { randomUUID } from 'node:crypto';
1
+ import { constants as fsConstants, promises as fs } from 'node:fs';
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';
10
+ const TASK_ASSIGNMENT_STATE_ROOT_DIRNAME = 'task-assignment-state';
11
+ const TASK_ASSIGNMENT_STATE_FILENAME = 'context.json';
7
12
  const CHANNEL_GATEWAY_CREDENTIAL_FILENAME = '.borgee-agent-gateway.json';
8
- const TASK_WORKSPACE_ROOT_DIRNAME = '.borgee-task-workspaces';
13
+ const CLAUDE_PROJECTED_BRIEF_FILENAME = 'CLAUDE.md';
14
+ const DEFAULT_HOME_BORGEE_DIRNAME = '.borgee';
15
+ const CHANNEL_WORKSPACE_COLLECTION_DIRNAME = 'channels';
16
+ const CHANNEL_WORKSPACE_DIRNAME = 'workspace';
17
+ const TASK_ISOLATED_WORKSPACE_COLLECTION_DIRNAME = 'tasks';
18
+ const TASK_THREAD_SCRATCH_WORKSPACE_DIRNAME = 'scratch';
19
+ const TASK_EXECUTION_LOCAL_DIRECTORY_PROPERTY_KEY = 'execution.local_directory';
20
+ const DEFAULT_TASK_WORKSPACE_RESOLUTION_TIMEOUT_MS = 1_500;
9
21
  export class ChannelContextPreparationError extends Error {
10
22
  partialContext;
11
23
  constructor(message, partialContext, options) {
@@ -33,8 +45,11 @@ const DEFAULT_FILE_SYSTEM = {
33
45
  async rename(oldPath, newPath) {
34
46
  await fs.rename(oldPath, newPath);
35
47
  },
36
- async access(path) {
37
- await fs.access(path);
48
+ async access(path, mode) {
49
+ await fs.access(path, mode);
50
+ },
51
+ async stat(path) {
52
+ return await fs.stat(path);
38
53
  },
39
54
  };
40
55
  const BORGEE_AGENT_SKILL_DIRNAME = 'borgee-agent';
@@ -62,19 +77,50 @@ function buildBorgeeAgentGatewayCredential(channelId, localhostGateway) {
62
77
  },
63
78
  };
64
79
  }
65
- function buildChannelContextPayload(channelId, collaborationOutcome, attentionSnapshot, taskThreadCollaborationContract, collaborationCapabilities, missedCollaborationDiagnostic, skillRuntime, localhostGateway, options) {
80
+ function buildChannelContextPayload(channelId, runtimeSurface, collaborationOutcome, attentionSnapshot, compactionSnapshot, taskThreadCollaborationContract, collaborationCapabilities, missedCollaborationDiagnostic, skillRuntime, localhostGateway, options) {
66
81
  return {
67
82
  schemaVersion: 1,
68
83
  channelId,
84
+ runtimeSurface,
69
85
  ...(collaborationOutcome ? { collaborationOutcome } : {}),
70
86
  ...(attentionSnapshot ? { attentionSnapshot } : {}),
87
+ ...(compactionSnapshot ? { compactionSnapshot } : {}),
71
88
  ...(taskThreadCollaborationContract ? { taskThreadCollaborationContract } : {}),
72
89
  ...(collaborationCapabilities ? { collaborationCapabilities } : {}),
73
90
  ...(missedCollaborationDiagnostic ? { missedCollaborationDiagnostic } : {}),
74
91
  ...(skillRuntime ? { skillRuntime: toSkillRuntimePayload(skillRuntime) } : {}),
75
92
  ...(localhostGateway ? { localhostGateway } : {}),
76
93
  ...(options?.taskAssignmentContext ? { taskAssignmentContext: options.taskAssignmentContext } : {}),
77
- ...(options?.taskWorkspace ? { taskWorkspace: options.taskWorkspace } : {}),
94
+ ...(options?.resolvedWorkspace ? { resolvedWorkspace: options.resolvedWorkspace } : {}),
95
+ };
96
+ }
97
+ function hashUtf8Content(content) {
98
+ return createHash('sha256').update(content).digest('hex');
99
+ }
100
+ async function unlinkIfPresent(fileSystem, path) {
101
+ await fileSystem.unlink(path).catch((error) => {
102
+ if (error.code !== 'ENOENT') {
103
+ throw error;
104
+ }
105
+ });
106
+ }
107
+ function buildPromptContextForClaudeProjectedBrief(preparedContext) {
108
+ return {
109
+ channelContextPayloadPath: preparedContext.payloadPath,
110
+ claudeProjectedBriefPath: preparedContext.claudeProjectedBriefPath,
111
+ claudeProjectedBriefHash: preparedContext.claudeProjectedBriefHash,
112
+ gatewayCredentialPath: preparedContext.gatewayCredentialPath,
113
+ skillRuntime: preparedContext.skillRuntime,
114
+ localhostGateway: preparedContext.localhostGateway,
115
+ taskAssignmentContext: preparedContext.payload.taskAssignmentContext,
116
+ resolvedWorkspace: preparedContext.resolvedWorkspace,
117
+ runtimeSurface: preparedContext.payload.runtimeSurface,
118
+ collaborationOutcome: preparedContext.payload.collaborationOutcome,
119
+ attentionSnapshot: preparedContext.payload.attentionSnapshot,
120
+ compactionSnapshot: preparedContext.payload.compactionSnapshot,
121
+ collaborationCapabilities: preparedContext.payload.collaborationCapabilities,
122
+ missedCollaborationDiagnostic: preparedContext.payload.missedCollaborationDiagnostic,
123
+ taskThreadCollaborationContract: preparedContext.payload.taskThreadCollaborationContract,
78
124
  };
79
125
  }
80
126
  function isRecord(value) {
@@ -96,6 +142,73 @@ function isUsableTaskId(value) {
96
142
  && !/[\u0000-\u001f\u007f\s]/u.test(value)
97
143
  && canRoundTripThroughUriComponentEncoding(value);
98
144
  }
145
+ function normalizeTaskAssignmentCurrentTaskId(taskAssignmentContext) {
146
+ const currentTaskId = typeof taskAssignmentContext?.currentTaskId === 'string'
147
+ ? taskAssignmentContext.currentTaskId.trim()
148
+ : '';
149
+ return currentTaskId && isUsableTaskId(currentTaskId)
150
+ ? currentTaskId
151
+ : undefined;
152
+ }
153
+ export function buildRuntimeSurfaceForTurn(options) {
154
+ const turnMode = options?.collaboration?.turnMode ?? 'ordinary';
155
+ const gatewayAvailable = options?.localhostGateway !== undefined;
156
+ const currentTaskId = normalizeTaskAssignmentCurrentTaskId(options?.taskAssignmentContext);
157
+ const taskAssignmentThreadActive = options?.taskAssignmentContext?.active === true;
158
+ const collaborationGatewayEnabled = gatewayAvailable && options.localhostGateway?.collaboration?.enabled === true;
159
+ const auxiliarySendAvailable = turnMode === 'ordinary'
160
+ && collaborationGatewayEnabled
161
+ && options?.collaboration?.sendRoutesAllowed === true;
162
+ return {
163
+ schemaVersion: 1,
164
+ turnMode,
165
+ gateway: {
166
+ available: gatewayAvailable,
167
+ readOnlyRoutesAuthorized: gatewayAvailable,
168
+ inspectBeforeAnswer: gatewayAvailable,
169
+ },
170
+ task: {
171
+ currentThread: taskAssignmentThreadActive ? 'task-assignment-thread' : 'parent-channel',
172
+ parentChannelTaskCollection: taskAssignmentThreadActive ? 'parent-channel-only' : 'available',
173
+ currentTaskShorthand: currentTaskId
174
+ ? 'persisted-current-task'
175
+ : taskAssignmentThreadActive
176
+ ? 'fallback-current-task'
177
+ : 'requires-task-id',
178
+ currentTaskIdPersisted: currentTaskId !== undefined,
179
+ currentTaskFallback: !currentTaskId && taskAssignmentThreadActive
180
+ ? 'visible-parent-channel-thread-scan'
181
+ : 'disallowed',
182
+ },
183
+ collaboration: gatewayAvailable
184
+ ? turnMode === 'ordinary'
185
+ ? {
186
+ participantLookup: collaborationGatewayEnabled ? 'available' : 'unavailable',
187
+ draftRead: auxiliarySendAvailable ? 'available' : 'unavailable',
188
+ auxiliarySend: auxiliarySendAvailable ? 'available' : 'unavailable',
189
+ }
190
+ : {
191
+ participantLookup: 'unavailable',
192
+ draftRead: 'unavailable',
193
+ auxiliarySend: 'unavailable',
194
+ }
195
+ : {
196
+ participantLookup: 'unavailable',
197
+ draftRead: 'unavailable',
198
+ auxiliarySend: 'unavailable',
199
+ },
200
+ response: {
201
+ mainVisibleReply: turnMode === 'ordinary'
202
+ ? 'model-visible-reply'
203
+ : turnMode === 'protocol-managed'
204
+ ? 'host-visible-protocol-reply'
205
+ : 'no-visible-reply',
206
+ auxiliaryMessageUsage: auxiliarySendAvailable
207
+ ? 'targeted-escalation-only'
208
+ : 'unavailable',
209
+ },
210
+ };
211
+ }
99
212
  function sanitizeTaskAssignmentContext(value) {
100
213
  if (!isRecord(value) || value.active !== true) {
101
214
  return undefined;
@@ -123,9 +236,9 @@ function buildTaskAssignmentContextForTurn(options, existingContext) {
123
236
  }
124
237
  return existingContext;
125
238
  }
126
- async function readExistingTaskAssignmentContext(payloadPath, fileSystem) {
239
+ async function readExistingTaskAssignmentContext(statePath, fileSystem) {
127
240
  try {
128
- const raw = await fileSystem.readFile(payloadPath, { encoding: 'utf8' });
241
+ const raw = await fileSystem.readFile(statePath, { encoding: 'utf8' });
129
242
  const payload = JSON.parse(raw);
130
243
  return sanitizeTaskAssignmentContext(payload.taskAssignmentContext);
131
244
  }
@@ -133,6 +246,127 @@ async function readExistingTaskAssignmentContext(payloadPath, fileSystem) {
133
246
  return undefined;
134
247
  }
135
248
  }
249
+ function buildChannelResolvedWorkspace(workspaceCollectionRootDir, channelId) {
250
+ return {
251
+ authority: 'channel',
252
+ owningChannelId: channelId,
253
+ rootPath: resolveChannelWorkspaceDirectory(workspaceCollectionRootDir, channelId),
254
+ };
255
+ }
256
+ function buildTaskThreadScratchResolvedWorkspace(workspaceCollectionRootDir, channelId, reason = 'missing-or-invalid-execution-target') {
257
+ return {
258
+ authority: 'task-thread-scratch',
259
+ rootPath: resolveTaskThreadScratchWorkspaceDirectory(workspaceCollectionRootDir, channelId),
260
+ reason,
261
+ };
262
+ }
263
+ async function isCrossAgentTaskCreatedByAnotherAgent(params) {
264
+ const selfAgentId = params.resolveStableAgentId?.()?.trim();
265
+ if (!selfAgentId) {
266
+ return true;
267
+ }
268
+ if (params.task.createdBy === selfAgentId) {
269
+ return false;
270
+ }
271
+ if (!params.taskReader) {
272
+ return true;
273
+ }
274
+ const users = await params.taskReader.listUsers();
275
+ const creator = users.find((user) => user.id === params.task.createdBy);
276
+ if (!creator) {
277
+ return true;
278
+ }
279
+ return creator.kind === 'agent';
280
+ }
281
+ async function withDeadline(promise, timeoutMs, timeoutMessage) {
282
+ if (timeoutMs <= 0) {
283
+ return await promise;
284
+ }
285
+ let timer;
286
+ try {
287
+ return await Promise.race([
288
+ promise,
289
+ new Promise((_, reject) => {
290
+ timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
291
+ }),
292
+ ]);
293
+ }
294
+ finally {
295
+ if (timer) {
296
+ clearTimeout(timer);
297
+ }
298
+ }
299
+ }
300
+ async function isExistingDirectory(fileSystem, path) {
301
+ if (!isAbsolute(path)) {
302
+ return false;
303
+ }
304
+ try {
305
+ await fileSystem.access(path, fsConstants.R_OK | fsConstants.W_OK | fsConstants.X_OK);
306
+ if (typeof fileSystem.stat !== 'function') {
307
+ return true;
308
+ }
309
+ return (await fileSystem.stat(path)).isDirectory();
310
+ }
311
+ catch {
312
+ return false;
313
+ }
314
+ }
315
+ async function resolveTaskThreadWorkspaceContext(params) {
316
+ const channelWorkspace = buildChannelResolvedWorkspace(params.workspaceCollectionRootDir, params.channelId);
317
+ const discussionOnlyWorkspace = buildTaskThreadScratchResolvedWorkspace(params.workspaceCollectionRootDir, params.channelId);
318
+ const taskAssignmentContext = params.taskAssignmentContext;
319
+ const currentTaskId = taskAssignmentContext?.currentTaskId?.trim();
320
+ if (taskAssignmentContext?.active !== true) {
321
+ return { taskAssignmentContext, resolvedWorkspace: channelWorkspace };
322
+ }
323
+ if (!currentTaskId || !params.taskReader) {
324
+ return { taskAssignmentContext, resolvedWorkspace: discussionOnlyWorkspace };
325
+ }
326
+ try {
327
+ const task = await withDeadline(findTaskForThread(params.taskReader, params.channelId, currentTaskId), params.taskResolutionTimeoutMs, `workspace resolution timed out for task ${currentTaskId}`);
328
+ if (!task) {
329
+ return {
330
+ taskAssignmentContext,
331
+ resolvedWorkspace: discussionOnlyWorkspace,
332
+ };
333
+ }
334
+ const executionLocalDirectory = task.properties[TASK_EXECUTION_LOCAL_DIRECTORY_PROPERTY_KEY];
335
+ if (typeof executionLocalDirectory !== 'string'
336
+ || executionLocalDirectory.length === 0
337
+ || !(await isExistingDirectory(params.fileSystem, executionLocalDirectory))) {
338
+ return {
339
+ taskAssignmentContext,
340
+ resolvedWorkspace: discussionOnlyWorkspace,
341
+ };
342
+ }
343
+ if (!params.allowCrossAgentIndependentWorkspaceHandoff
344
+ && await isCrossAgentTaskCreatedByAnotherAgent({
345
+ task,
346
+ taskReader: params.taskReader,
347
+ resolveStableAgentId: params.resolveStableAgentId,
348
+ })) {
349
+ return {
350
+ taskAssignmentContext,
351
+ resolvedWorkspace: buildTaskThreadScratchResolvedWorkspace(params.workspaceCollectionRootDir, params.channelId, 'cross-agent-independent-workspace-disabled'),
352
+ };
353
+ }
354
+ return {
355
+ taskAssignmentContext,
356
+ resolvedWorkspace: {
357
+ authority: 'task-execution-target',
358
+ taskId: task.id,
359
+ rootPath: executionLocalDirectory,
360
+ },
361
+ };
362
+ }
363
+ catch (error) {
364
+ return {
365
+ taskAssignmentContext,
366
+ resolvedWorkspace: discussionOnlyWorkspace,
367
+ };
368
+ }
369
+ }
136
370
  export function encodeChannelPathSegment(channelId) {
137
371
  const encoded = Buffer.from(channelId, 'utf8').toString('hex');
138
372
  return encoded.length > 0 ? encoded : 'empty';
@@ -143,11 +377,38 @@ export function resolveChannelContextDirectory(stateRootDir, channelId) {
143
377
  export function resolveChannelContextPayloadPath(stateRootDir, channelId) {
144
378
  return join(resolveChannelContextDirectory(stateRootDir, channelId), CHANNEL_CONTEXT_PAYLOAD_FILENAME);
145
379
  }
380
+ export function resolveTaskAssignmentStateDirectory(stateRootDir, channelId) {
381
+ return resolve(stateRootDir, TASK_ASSIGNMENT_STATE_ROOT_DIRNAME, encodeChannelPathSegment(channelId));
382
+ }
383
+ export function resolveTaskAssignmentStatePath(stateRootDir, channelId) {
384
+ return join(resolveTaskAssignmentStateDirectory(stateRootDir, channelId), TASK_ASSIGNMENT_STATE_FILENAME);
385
+ }
146
386
  export function resolveTaskWorkspaceRootDirectory(startupWorkspaceRootDir) {
147
- return join(resolve(startupWorkspaceRootDir), TASK_WORKSPACE_ROOT_DIRNAME);
387
+ return join(resolve(startupWorkspaceRootDir), TASK_ISOLATED_WORKSPACE_COLLECTION_DIRNAME);
148
388
  }
149
- export function resolveTaskWorkspaceDirectory(startupWorkspaceRootDir, channelId, taskId) {
150
- return join(resolveTaskWorkspaceRootDirectory(startupWorkspaceRootDir), encodeChannelPathSegment(channelId), encodeChannelPathSegment(taskId));
389
+ export function resolveManagedWorkspaceCollectionRoot(workspaceCollectionRootDir, env = process.env, resolvedHomeDir = homedir()) {
390
+ const explicitRoot = workspaceCollectionRootDir?.trim();
391
+ if (explicitRoot) {
392
+ return resolve(explicitRoot);
393
+ }
394
+ const home = env.HOME?.trim() || resolvedHomeDir.trim();
395
+ if (!home) {
396
+ throw new Error('Unable to resolve a user home directory for agents-host workspaces');
397
+ }
398
+ return join(resolve(home), DEFAULT_HOME_BORGEE_DIRNAME, CHANNEL_WORKSPACE_COLLECTION_DIRNAME);
399
+ }
400
+ export function resolveChannelWorkspaceRootDirectory(workspaceCollectionRootDir, channelId) {
401
+ return join(resolve(workspaceCollectionRootDir), encodeChannelPathSegment(channelId));
402
+ }
403
+ export function resolveChannelWorkspaceDirectory(workspaceCollectionRootDir, channelId) {
404
+ return join(resolveChannelWorkspaceRootDirectory(workspaceCollectionRootDir, channelId), CHANNEL_WORKSPACE_DIRNAME);
405
+ }
406
+ export function resolveTaskIsolatedWorkspaceDirectory(workspaceCollectionRootDir, channelId, taskId) {
407
+ return join(resolveTaskWorkspaceRootDirectory(resolveChannelWorkspaceRootDirectory(workspaceCollectionRootDir, channelId)), encodeChannelPathSegment(taskId), CHANNEL_WORKSPACE_DIRNAME);
408
+ }
409
+ export const resolveTaskWorkspaceDirectory = resolveTaskIsolatedWorkspaceDirectory;
410
+ export function resolveTaskThreadScratchWorkspaceDirectory(workspaceCollectionRootDir, channelId) {
411
+ return join(resolveChannelWorkspaceRootDirectory(workspaceCollectionRootDir, channelId), TASK_THREAD_SCRATCH_WORKSPACE_DIRNAME, CHANNEL_WORKSPACE_DIRNAME);
151
412
  }
152
413
  export function resolveChannelGatewayCredentialPath(stateRootDir, channelId) {
153
414
  return join(resolveChannelContextDirectory(stateRootDir, channelId), CHANNEL_GATEWAY_CREDENTIAL_FILENAME);
@@ -155,6 +416,9 @@ export function resolveChannelGatewayCredentialPath(stateRootDir, channelId) {
155
416
  export function resolveGatewayCredentialPathFromPayloadPath(payloadPath) {
156
417
  return join(dirname(payloadPath), CHANNEL_GATEWAY_CREDENTIAL_FILENAME);
157
418
  }
419
+ export function resolveClaudeProjectedBriefPathFromPayloadPath(payloadPath) {
420
+ return join(dirname(payloadPath), CLAUDE_PROJECTED_BRIEF_FILENAME);
421
+ }
158
422
  /**
159
423
  * Every basename a gateway credential can carry in a channel context directory, including the ones
160
424
  * this host no longer writes. The directory holds at most one live credential — the token is
@@ -245,14 +509,23 @@ export class FileChannelContextStore {
245
509
  skillRuntimeEnabled;
246
510
  skillAssetResolver;
247
511
  localhostGateway;
248
- startupWorkspaceRootDir;
512
+ workspaceCollectionRootDir;
513
+ taskReader;
514
+ taskResolutionTimeoutMs;
515
+ allowCrossAgentIndependentWorkspaceHandoff;
516
+ resolveStableAgentId;
249
517
  constructor(stateRootDir, options = {}) {
250
518
  this.stateRootDir = stateRootDir;
251
519
  this.fileSystem = options.fileSystem ?? DEFAULT_FILE_SYSTEM;
252
520
  this.skillRuntimeEnabled = options.skillRuntimeEnabled ?? false;
253
521
  this.skillAssetResolver = options.skillAssetResolver ?? new PackageSkillAssetResolver(import.meta.url, this.fileSystem);
254
522
  this.localhostGateway = options.localhostGateway;
255
- this.startupWorkspaceRootDir = resolve(options.taskWorkspaceRootDir ?? process.cwd());
523
+ this.workspaceCollectionRootDir = resolveManagedWorkspaceCollectionRoot(options.workspaceCollectionRootDir, options.env, options.resolvedHomeDir);
524
+ this.taskReader = options.taskReader;
525
+ this.taskResolutionTimeoutMs = options.taskResolutionTimeoutMs ?? DEFAULT_TASK_WORKSPACE_RESOLUTION_TIMEOUT_MS;
526
+ this.allowCrossAgentIndependentWorkspaceHandoff
527
+ = options.allowCrossAgentIndependentWorkspaceHandoff ?? true;
528
+ this.resolveStableAgentId = options.resolveStableAgentId;
256
529
  }
257
530
  async prepare(inputOrChannelId, options) {
258
531
  const input = typeof inputOrChannelId === 'string'
@@ -264,8 +537,10 @@ export class FileChannelContextStore {
264
537
  const auxiliaryCollaborationEnabled = collaborationCommandsEnabled && collaborationRoutesAllowedForTurnMode;
265
538
  const directoryPath = resolveChannelContextDirectory(this.stateRootDir, input.channelId);
266
539
  const payloadPath = resolveChannelContextPayloadPath(this.stateRootDir, input.channelId);
540
+ const taskAssignmentStatePath = resolveTaskAssignmentStatePath(this.stateRootDir, input.channelId);
541
+ const claudeProjectedBriefPath = resolveClaudeProjectedBriefPathFromPayloadPath(payloadPath);
267
542
  const gatewayCredentialPath = resolveGatewayCredentialPathFromPayloadPath(payloadPath);
268
- const existingTaskAssignmentContext = await readExistingTaskAssignmentContext(payloadPath, this.fileSystem);
543
+ const existingTaskAssignmentContext = await readExistingTaskAssignmentContext(taskAssignmentStatePath, this.fileSystem);
269
544
  const skillRuntime = this.skillRuntimeEnabled
270
545
  ? await this.resolveSkillRuntimeBestEffort()
271
546
  : undefined;
@@ -291,24 +566,41 @@ export class FileChannelContextStore {
291
566
  : {}),
292
567
  }
293
568
  : undefined;
294
- const taskAssignmentContext = buildTaskAssignmentContextForTurn({
295
- incomingMessageType: input.incomingMessageType,
296
- incomingContent: input.incomingContent,
297
- }, 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
569
+ const taskAssignmentContext = input.taskAssignmentContextOverride
570
+ ?? buildTaskAssignmentContextForTurn({
571
+ incomingMessageType: input.incomingMessageType,
572
+ incomingContent: input.incomingContent,
573
+ }, existingTaskAssignmentContext);
574
+ const workspaceResolution = await resolveTaskThreadWorkspaceContext({
575
+ channelId: input.channelId,
576
+ workspaceCollectionRootDir: this.workspaceCollectionRootDir,
577
+ taskAssignmentContext,
578
+ taskReader: this.taskReader,
579
+ taskResolutionTimeoutMs: this.taskResolutionTimeoutMs,
580
+ allowCrossAgentIndependentWorkspaceHandoff: this.allowCrossAgentIndependentWorkspaceHandoff,
581
+ resolveStableAgentId: this.resolveStableAgentId,
582
+ fileSystem: this.fileSystem,
583
+ });
584
+ const resolvedWorkspace = workspaceResolution.resolvedWorkspace;
585
+ const runtimeTaskAssignmentContext = workspaceResolution.taskAssignmentContext ?? taskAssignmentContext;
586
+ const persistedTaskAssignmentContext = input.taskAssignmentContextOverridePersistence === 'ephemeral'
587
+ ? existingTaskAssignmentContext
588
+ : runtimeTaskAssignmentContext;
589
+ const runtimeSurface = buildRuntimeSurfaceForTurn({
590
+ localhostGateway,
591
+ collaboration: input.collaboration,
592
+ taskAssignmentContext: runtimeTaskAssignmentContext,
593
+ });
594
+ const payload = buildChannelContextPayload(input.channelId, runtimeSurface, input.collaborationOutcome, input.attentionSnapshot, input.compactionSnapshot, input.taskThreadCollaborationContract, input.collaborationCapabilities, input.missedCollaborationDiagnostic, skillRuntime, localhostGateway, persistedTaskAssignmentContext || resolvedWorkspace
305
595
  ? {
306
- ...(taskAssignmentContext ? { taskAssignmentContext } : {}),
307
- ...(taskWorkspace ? { taskWorkspace } : {}),
596
+ ...(persistedTaskAssignmentContext
597
+ ? { taskAssignmentContext: persistedTaskAssignmentContext }
598
+ : {}),
599
+ resolvedWorkspace,
308
600
  }
309
601
  : undefined);
310
602
  const gatewayCredential = buildBorgeeAgentGatewayCredential(input.channelId, issuedLocalhostGateway);
311
- let taskWorkspaceMaterialized = false;
603
+ let claudeProjectedBriefWritten = false;
312
604
  let payloadWritten = false;
313
605
  let gatewayCredentialWritten = false;
314
606
  const preparedContext = {
@@ -318,17 +610,39 @@ export class FileChannelContextStore {
318
610
  gatewayCredentialPath: gatewayCredential ? gatewayCredentialPath : undefined,
319
611
  skillRuntime,
320
612
  localhostGateway,
321
- taskWorkspace,
613
+ resolvedWorkspace,
322
614
  };
615
+ const shouldProjectClaudeBrief = input.provider === 'claude' && input.projectionStrategy === 'file-brief';
616
+ const claudeProjectedBriefContent = shouldProjectClaudeBrief
617
+ ? buildClaudeFileBrief(buildPromptContextForClaudeProjectedBrief({
618
+ ...preparedContext,
619
+ claudeProjectedBriefPath,
620
+ }))
621
+ : undefined;
622
+ if (claudeProjectedBriefContent) {
623
+ preparedContext.claudeProjectedBriefPath = claudeProjectedBriefPath;
624
+ preparedContext.claudeProjectedBriefHash = hashUtf8Content(claudeProjectedBriefContent);
625
+ }
323
626
  try {
324
627
  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;
628
+ await this.fileSystem.mkdir(resolveTaskAssignmentStateDirectory(this.stateRootDir, input.channelId), { recursive: true, mode: 0o700 });
629
+ if (resolvedWorkspace.authority !== 'task-execution-target') {
630
+ await this.fileSystem.mkdir(resolvedWorkspace.rootPath, { recursive: true, mode: 0o700 });
328
631
  }
329
632
  await pruneGatewayCredentialSidecars(this.fileSystem, directoryPath, gatewayCredential ? CHANNEL_GATEWAY_CREDENTIAL_FILENAME : undefined);
633
+ if (!claudeProjectedBriefContent) {
634
+ await unlinkIfPresent(this.fileSystem, claudeProjectedBriefPath);
635
+ }
330
636
  await this.fileSystem.writeFile(payloadPath, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
331
637
  payloadWritten = true;
638
+ if (persistedTaskAssignmentContext) {
639
+ const stagingTaskAssignmentStatePath = join(resolveTaskAssignmentStateDirectory(this.stateRootDir, input.channelId), `.task-assignment-state.${randomUUID()}.json`);
640
+ await this.fileSystem.writeFile(stagingTaskAssignmentStatePath, `${JSON.stringify({ taskAssignmentContext: persistedTaskAssignmentContext }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
641
+ await this.fileSystem.rename(stagingTaskAssignmentStatePath, taskAssignmentStatePath);
642
+ }
643
+ else {
644
+ await unlinkIfPresent(this.fileSystem, taskAssignmentStatePath);
645
+ }
332
646
  if (gatewayCredential) {
333
647
  // Replaced atomically: a CLI invocation left over from an earlier turn must read either
334
648
  // the old credential or the new one, never a missing or half-written file, because the
@@ -342,20 +656,40 @@ export class FileChannelContextStore {
342
656
  else {
343
657
  this.localhostGateway?.clearChannel(input.channelId);
344
658
  }
659
+ if (claudeProjectedBriefContent) {
660
+ const stagingProjectedBriefPath = join(directoryPath, `.claude-file-brief.${randomUUID()}.md`);
661
+ await this.fileSystem.writeFile(stagingProjectedBriefPath, claudeProjectedBriefContent, { encoding: 'utf8', mode: 0o600 });
662
+ await this.fileSystem.rename(stagingProjectedBriefPath, claudeProjectedBriefPath);
663
+ claudeProjectedBriefWritten = true;
664
+ }
345
665
  if (issuedLocalhostGateway) {
346
666
  this.localhostGateway?.publishPayload(input.channelId, payloadPath, payload);
347
667
  }
348
668
  return preparedContext;
349
669
  }
350
670
  catch (error) {
671
+ if (claudeProjectedBriefContent && !claudeProjectedBriefWritten) {
672
+ await unlinkIfPresent(this.fileSystem, claudeProjectedBriefPath);
673
+ }
351
674
  const partialContext = {
352
675
  directoryPath,
353
- payload,
676
+ payload: {
677
+ ...payload,
678
+ ...(resolvedWorkspace.authority === 'task-execution-target' && runtimeTaskAssignmentContext?.active === true
679
+ ? {
680
+ resolvedWorkspace: buildTaskThreadScratchResolvedWorkspace(this.workspaceCollectionRootDir, input.channelId),
681
+ }
682
+ : {}),
683
+ },
684
+ claudeProjectedBriefPath: claudeProjectedBriefWritten ? claudeProjectedBriefPath : undefined,
685
+ claudeProjectedBriefHash: claudeProjectedBriefWritten ? preparedContext.claudeProjectedBriefHash : undefined,
354
686
  payloadPath: payloadWritten ? payloadPath : undefined,
355
687
  gatewayCredentialPath: gatewayCredentialWritten ? gatewayCredentialPath : undefined,
356
688
  skillRuntime: payloadWritten ? skillRuntime : undefined,
357
689
  localhostGateway: payloadWritten ? localhostGateway : undefined,
358
- taskWorkspace: taskWorkspaceMaterialized ? taskWorkspace : undefined,
690
+ resolvedWorkspace: resolvedWorkspace.authority === 'task-execution-target' && runtimeTaskAssignmentContext?.active === true
691
+ ? buildTaskThreadScratchResolvedWorkspace(this.workspaceCollectionRootDir, input.channelId)
692
+ : resolvedWorkspace,
359
693
  };
360
694
  throw new ChannelContextPreparationError('failed to persist channel context payload', partialContext, { cause: error });
361
695
  }
@@ -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
+ }