@borgee/agents-host 0.2.56 → 0.2.65

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 (44) hide show
  1. package/README.md +11 -10
  2. package/dist/agents-host.d.ts +4 -1
  3. package/dist/agents-host.js +229 -20
  4. package/dist/chat/chat-control-plane.d.ts +2 -1
  5. package/dist/chat/sdk-chat-control-plane.d.ts +3 -2
  6. package/dist/chat/sdk-chat-control-plane.js +4 -0
  7. package/dist/compatibility-gates.js +2 -0
  8. package/dist/config.d.ts +2 -0
  9. package/dist/config.js +21 -0
  10. package/dist/context/injection.d.ts +20 -2
  11. package/dist/context/injection.js +146 -103
  12. package/dist/context/prompt.js +11 -7
  13. package/dist/context/resolved-workspace.d.ts +1 -0
  14. package/dist/context/resolved-workspace.js +50 -8
  15. package/dist/context/turn-preparation.js +8 -0
  16. package/dist/gateway/localhost-gateway.js +2 -5
  17. package/dist/local-config.js +11 -1
  18. package/dist/managed-daemon.js +18 -13
  19. package/dist/plugin-sdk.js +0 -17
  20. package/dist/plugin-sdk.js.map +2 -2
  21. package/dist/policy/copilot-permission.d.ts +1 -0
  22. package/dist/policy/copilot-permission.js +18 -0
  23. package/dist/policy/gateway-authorization.js +2 -2
  24. package/dist/providers/claude/adapter.d.ts +1 -0
  25. package/dist/providers/claude/adapter.js +3 -0
  26. package/dist/providers/claude/cli-client.d.ts +2 -1
  27. package/dist/providers/claude/cli-client.js +53 -3
  28. package/dist/providers/codex/adapter.d.ts +1 -0
  29. package/dist/providers/codex/adapter.js +3 -0
  30. package/dist/providers/codex/cli-client.d.ts +3 -0
  31. package/dist/providers/codex/cli-client.js +50 -22
  32. package/dist/providers/copilot/adapter.d.ts +1 -0
  33. package/dist/providers/copilot/adapter.js +3 -0
  34. package/dist/providers/copilot/cli-client.d.ts +2 -1
  35. package/dist/providers/copilot/cli-client.js +30 -3
  36. package/dist/providers/create-provider.d.ts +1 -1
  37. package/dist/providers/create-provider.js +8 -0
  38. package/dist/providers/provider-adapter.d.ts +7 -0
  39. package/dist/providers/provider-adapter.js +8 -0
  40. package/dist/types.d.ts +16 -3
  41. package/package.json +2 -2
  42. package/skills/borgee-agent/SKILL.md +4 -4
  43. package/skills/borgee-agent/references/errors.md +2 -2
  44. package/skills/borgee-agent/references/task-properties.md +4 -3
@@ -49,6 +49,8 @@ export interface ChannelContextStore {
49
49
  missedCollaborationDiagnostic?: MissedCollaborationDiagnostic;
50
50
  incomingMessageType?: string;
51
51
  incomingContent?: string;
52
+ taskAssignmentContextOverride?: TaskAssignmentThreadContext;
53
+ taskAssignmentContextOverridePersistence?: 'persist' | 'ephemeral';
52
54
  }): Promise<PreparedChannelContext>;
53
55
  prepare(channelId: string, options?: {
54
56
  provider?: ProviderKind;
@@ -62,6 +64,8 @@ export interface ChannelContextStore {
62
64
  missedCollaborationDiagnostic?: MissedCollaborationDiagnostic;
63
65
  incomingMessageType?: string;
64
66
  incomingContent?: string;
67
+ taskAssignmentContextOverride?: TaskAssignmentThreadContext;
68
+ taskAssignmentContextOverridePersistence?: 'persist' | 'ephemeral';
65
69
  }): Promise<PreparedChannelContext>;
66
70
  }
67
71
  export interface SkillAssetResolver {
@@ -95,7 +99,10 @@ interface ContextFileSystem {
95
99
  }): Promise<void>;
96
100
  unlink(path: string): Promise<void>;
97
101
  rename(oldPath: string, newPath: string): Promise<void>;
98
- access(path: string): Promise<void>;
102
+ access(path: string, mode?: number): Promise<void>;
103
+ stat?(path: string): Promise<{
104
+ isDirectory(): boolean;
105
+ }>;
99
106
  }
100
107
  interface FileChannelContextStoreOptions {
101
108
  fileSystem?: ContextFileSystem;
@@ -103,8 +110,10 @@ interface FileChannelContextStoreOptions {
103
110
  skillAssetResolver?: SkillAssetResolver;
104
111
  localhostGateway?: LocalhostGatewayContextPublisher;
105
112
  workspaceCollectionRootDir?: string;
106
- taskReader?: Pick<ChatControlPlane, 'getTask' | 'listChannels' | 'listTasks'>;
113
+ taskReader?: Pick<ChatControlPlane, 'getTask' | 'listChannels' | 'listTasks' | 'listUsers'>;
107
114
  taskResolutionTimeoutMs?: number;
115
+ allowCrossAgentIndependentWorkspaceHandoff?: boolean;
116
+ resolveStableAgentId?: () => string | undefined;
108
117
  env?: NodeJS.ProcessEnv;
109
118
  resolvedHomeDir?: string;
110
119
  }
@@ -117,12 +126,15 @@ export declare function extractTaskIdFromTaskAssignmentContent(incomingContent:
117
126
  export declare function encodeChannelPathSegment(channelId: string): string;
118
127
  export declare function resolveChannelContextDirectory(stateRootDir: string, channelId: string): string;
119
128
  export declare function resolveChannelContextPayloadPath(stateRootDir: string, channelId: string): string;
129
+ export declare function resolveTaskAssignmentStateDirectory(stateRootDir: string, channelId: string): string;
130
+ export declare function resolveTaskAssignmentStatePath(stateRootDir: string, channelId: string): string;
120
131
  export declare function resolveTaskWorkspaceRootDirectory(startupWorkspaceRootDir: string): string;
121
132
  export declare function resolveManagedWorkspaceCollectionRoot(workspaceCollectionRootDir: string | undefined, env?: NodeJS.ProcessEnv, resolvedHomeDir?: string): string;
122
133
  export declare function resolveChannelWorkspaceRootDirectory(workspaceCollectionRootDir: string, channelId: string): string;
123
134
  export declare function resolveChannelWorkspaceDirectory(workspaceCollectionRootDir: string, channelId: string): string;
124
135
  export declare function resolveTaskIsolatedWorkspaceDirectory(workspaceCollectionRootDir: string, channelId: string, taskId: string): string;
125
136
  export declare const resolveTaskWorkspaceDirectory: typeof resolveTaskIsolatedWorkspaceDirectory;
137
+ export declare function resolveTaskThreadScratchWorkspaceDirectory(workspaceCollectionRootDir: string, channelId: string): string;
126
138
  export declare function resolveChannelGatewayCredentialPath(stateRootDir: string, channelId: string): string;
127
139
  export declare function resolveGatewayCredentialPathFromPayloadPath(payloadPath: string): string;
128
140
  export declare function resolveClaudeProjectedBriefPathFromPayloadPath(payloadPath: string): string;
@@ -137,6 +149,8 @@ export declare class FileChannelContextStore implements ChannelContextStore {
137
149
  private readonly workspaceCollectionRootDir;
138
150
  private readonly taskReader?;
139
151
  private readonly taskResolutionTimeoutMs;
152
+ private readonly allowCrossAgentIndependentWorkspaceHandoff;
153
+ private readonly resolveStableAgentId?;
140
154
  constructor(stateRootDir: string, options?: FileChannelContextStoreOptions);
141
155
  prepare(inputOrChannelId: {
142
156
  channelId: string;
@@ -151,6 +165,8 @@ export declare class FileChannelContextStore implements ChannelContextStore {
151
165
  missedCollaborationDiagnostic?: MissedCollaborationDiagnostic;
152
166
  incomingMessageType?: string;
153
167
  incomingContent?: string;
168
+ taskAssignmentContextOverride?: TaskAssignmentThreadContext;
169
+ taskAssignmentContextOverridePersistence?: 'persist' | 'ephemeral';
154
170
  } | string, options?: {
155
171
  provider?: ProviderKind;
156
172
  projectionStrategy?: ProjectionStrategy;
@@ -163,6 +179,8 @@ export declare class FileChannelContextStore implements ChannelContextStore {
163
179
  missedCollaborationDiagnostic?: MissedCollaborationDiagnostic;
164
180
  incomingMessageType?: string;
165
181
  incomingContent?: string;
182
+ taskAssignmentContextOverride?: TaskAssignmentThreadContext;
183
+ taskAssignmentContextOverridePersistence?: 'persist' | 'ephemeral';
166
184
  }): Promise<PreparedChannelContext>;
167
185
  private resolveSkillRuntimeBestEffort;
168
186
  }
@@ -1,4 +1,4 @@
1
- import { promises as fs } from 'node:fs';
1
+ import { constants as fsConstants, promises as fs } from 'node:fs';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { homedir } from 'node:os';
4
4
  import { fileURLToPath } from 'node:url';
@@ -7,14 +7,16 @@ import { findTaskForThread } from '../task-thread-resolution.js';
7
7
  import { buildClaudeFileBrief } from './claude-file-brief.js';
8
8
  const CHANNEL_CONTEXT_ROOT_DIRNAME = 'channel-context';
9
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';
10
12
  const CHANNEL_GATEWAY_CREDENTIAL_FILENAME = '.borgee-agent-gateway.json';
11
13
  const CLAUDE_PROJECTED_BRIEF_FILENAME = 'CLAUDE.md';
12
14
  const DEFAULT_HOME_BORGEE_DIRNAME = '.borgee';
13
15
  const CHANNEL_WORKSPACE_COLLECTION_DIRNAME = 'channels';
14
16
  const CHANNEL_WORKSPACE_DIRNAME = 'workspace';
15
17
  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 TASK_THREAD_SCRATCH_WORKSPACE_DIRNAME = 'scratch';
19
+ const TASK_EXECUTION_LOCAL_DIRECTORY_PROPERTY_KEY = 'execution.local_directory';
18
20
  const DEFAULT_TASK_WORKSPACE_RESOLUTION_TIMEOUT_MS = 1_500;
19
21
  export class ChannelContextPreparationError extends Error {
20
22
  partialContext;
@@ -43,8 +45,11 @@ const DEFAULT_FILE_SYSTEM = {
43
45
  async rename(oldPath, newPath) {
44
46
  await fs.rename(oldPath, newPath);
45
47
  },
46
- async access(path) {
47
- 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);
48
53
  },
49
54
  };
50
55
  const BORGEE_AGENT_SKILL_DIRNAME = 'borgee-agent';
@@ -231,9 +236,9 @@ function buildTaskAssignmentContextForTurn(options, existingContext) {
231
236
  }
232
237
  return existingContext;
233
238
  }
234
- async function readExistingTaskAssignmentContext(payloadPath, fileSystem) {
239
+ async function readExistingTaskAssignmentContext(statePath, fileSystem) {
235
240
  try {
236
- const raw = await fileSystem.readFile(payloadPath, { encoding: 'utf8' });
241
+ const raw = await fileSystem.readFile(statePath, { encoding: 'utf8' });
237
242
  const payload = JSON.parse(raw);
238
243
  return sanitizeTaskAssignmentContext(payload.taskAssignmentContext);
239
244
  }
@@ -241,125 +246,124 @@ async function readExistingTaskAssignmentContext(payloadPath, fileSystem) {
241
246
  return undefined;
242
247
  }
243
248
  }
244
- function sanitizeResolvedWorkspaceContext(value) {
245
- if (!isRecord(value) || typeof value.rootPath !== 'string' || !isAbsolute(value.rootPath)) {
246
- return undefined;
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;
247
267
  }
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
- };
268
+ if (params.task.createdBy === selfAgentId) {
269
+ return false;
256
270
  }
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
- };
271
+ if (!params.taskReader) {
272
+ return true;
265
273
  }
266
- return undefined;
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';
267
280
  }
268
- async function readExistingResolvedWorkspaceContext(payloadPath, fileSystem) {
281
+ async function withDeadline(promise, timeoutMs, timeoutMessage) {
282
+ if (timeoutMs <= 0) {
283
+ return await promise;
284
+ }
285
+ let timer;
269
286
  try {
270
- const raw = await fileSystem.readFile(payloadPath, { encoding: 'utf8' });
271
- const payload = JSON.parse(raw);
272
- return sanitizeResolvedWorkspaceContext(payload.resolvedWorkspace);
287
+ return await Promise.race([
288
+ promise,
289
+ new Promise((_, reject) => {
290
+ timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
291
+ }),
292
+ ]);
273
293
  }
274
- catch {
275
- return undefined;
294
+ finally {
295
+ if (timer) {
296
+ clearTimeout(timer);
297
+ }
276
298
  }
277
299
  }
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;
300
+ async function isExistingDirectory(fileSystem, path) {
301
+ if (!isAbsolute(path)) {
302
+ return false;
288
303
  }
289
- const expectedRootPath = resolveChannelWorkspaceDirectory(workspaceCollectionRootDir, resolvedWorkspace.owningChannelId);
290
- if (resolvedWorkspace.rootPath !== expectedRootPath) {
291
- return undefined;
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;
292
313
  }
293
- return {
294
- authority: 'channel',
295
- owningChannelId: resolvedWorkspace.owningChannelId,
296
- rootPath: expectedRootPath,
297
- };
298
314
  }
299
315
  async function resolveTaskThreadWorkspaceContext(params) {
300
316
  const channelWorkspace = buildChannelResolvedWorkspace(params.workspaceCollectionRootDir, params.channelId);
317
+ const discussionOnlyWorkspace = buildTaskThreadScratchResolvedWorkspace(params.workspaceCollectionRootDir, params.channelId);
301
318
  const taskAssignmentContext = params.taskAssignmentContext;
302
319
  const currentTaskId = taskAssignmentContext?.currentTaskId?.trim();
303
- if (taskAssignmentContext?.active !== true
304
- || !currentTaskId
305
- || !params.taskReader) {
320
+ if (taskAssignmentContext?.active !== true) {
306
321
  return { taskAssignmentContext, resolvedWorkspace: channelWorkspace };
307
322
  }
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);
323
+ if (!currentTaskId || !params.taskReader) {
324
+ return { taskAssignmentContext, resolvedWorkspace: discussionOnlyWorkspace };
325
+ }
317
326
  try {
318
327
  const task = await withDeadline(findTaskForThread(params.taskReader, params.channelId, currentTaskId), params.taskResolutionTimeoutMs, `workspace resolution timed out for task ${currentTaskId}`);
319
328
  if (!task) {
320
329
  return {
321
330
  taskAssignmentContext,
322
- resolvedWorkspace: channelWorkspace,
331
+ resolvedWorkspace: discussionOnlyWorkspace,
323
332
  };
324
333
  }
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
- }
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
+ };
343
342
  }
344
- if (task.properties[TASK_WORKSPACE_MODE_PROPERTY_KEY] !== TASK_WORKSPACE_MODE_ISOLATED) {
343
+ if (!params.allowCrossAgentIndependentWorkspaceHandoff
344
+ && await isCrossAgentTaskCreatedByAnotherAgent({
345
+ task,
346
+ taskReader: params.taskReader,
347
+ resolveStableAgentId: params.resolveStableAgentId,
348
+ })) {
345
349
  return {
346
350
  taskAssignmentContext,
347
- resolvedWorkspace: buildChannelResolvedWorkspace(params.workspaceCollectionRootDir, task.channelId),
351
+ resolvedWorkspace: buildTaskThreadScratchResolvedWorkspace(params.workspaceCollectionRootDir, params.channelId, 'cross-agent-independent-workspace-disabled'),
348
352
  };
349
353
  }
350
354
  return {
351
355
  taskAssignmentContext,
352
356
  resolvedWorkspace: {
353
- authority: 'task',
357
+ authority: 'task-execution-target',
354
358
  taskId: task.id,
355
- rootPath: resolveTaskWorkspaceDirectory(params.workspaceCollectionRootDir, params.channelId, task.id),
359
+ rootPath: executionLocalDirectory,
356
360
  },
357
361
  };
358
362
  }
359
363
  catch (error) {
360
364
  return {
361
365
  taskAssignmentContext,
362
- resolvedWorkspace: stickyTaskWorkspace ?? stickyChannelWorkspace ?? channelWorkspace,
366
+ resolvedWorkspace: discussionOnlyWorkspace,
363
367
  };
364
368
  }
365
369
  }
@@ -373,6 +377,12 @@ export function resolveChannelContextDirectory(stateRootDir, channelId) {
373
377
  export function resolveChannelContextPayloadPath(stateRootDir, channelId) {
374
378
  return join(resolveChannelContextDirectory(stateRootDir, channelId), CHANNEL_CONTEXT_PAYLOAD_FILENAME);
375
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
+ }
376
386
  export function resolveTaskWorkspaceRootDirectory(startupWorkspaceRootDir) {
377
387
  return join(resolve(startupWorkspaceRootDir), TASK_ISOLATED_WORKSPACE_COLLECTION_DIRNAME);
378
388
  }
@@ -397,6 +407,9 @@ export function resolveTaskIsolatedWorkspaceDirectory(workspaceCollectionRootDir
397
407
  return join(resolveTaskWorkspaceRootDirectory(resolveChannelWorkspaceRootDirectory(workspaceCollectionRootDir, channelId)), encodeChannelPathSegment(taskId), CHANNEL_WORKSPACE_DIRNAME);
398
408
  }
399
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);
412
+ }
400
413
  export function resolveChannelGatewayCredentialPath(stateRootDir, channelId) {
401
414
  return join(resolveChannelContextDirectory(stateRootDir, channelId), CHANNEL_GATEWAY_CREDENTIAL_FILENAME);
402
415
  }
@@ -499,6 +512,8 @@ export class FileChannelContextStore {
499
512
  workspaceCollectionRootDir;
500
513
  taskReader;
501
514
  taskResolutionTimeoutMs;
515
+ allowCrossAgentIndependentWorkspaceHandoff;
516
+ resolveStableAgentId;
502
517
  constructor(stateRootDir, options = {}) {
503
518
  this.stateRootDir = stateRootDir;
504
519
  this.fileSystem = options.fileSystem ?? DEFAULT_FILE_SYSTEM;
@@ -508,6 +523,9 @@ export class FileChannelContextStore {
508
523
  this.workspaceCollectionRootDir = resolveManagedWorkspaceCollectionRoot(options.workspaceCollectionRootDir, options.env, options.resolvedHomeDir);
509
524
  this.taskReader = options.taskReader;
510
525
  this.taskResolutionTimeoutMs = options.taskResolutionTimeoutMs ?? DEFAULT_TASK_WORKSPACE_RESOLUTION_TIMEOUT_MS;
526
+ this.allowCrossAgentIndependentWorkspaceHandoff
527
+ = options.allowCrossAgentIndependentWorkspaceHandoff ?? true;
528
+ this.resolveStableAgentId = options.resolveStableAgentId;
511
529
  }
512
530
  async prepare(inputOrChannelId, options) {
513
531
  const input = typeof inputOrChannelId === 'string'
@@ -519,10 +537,10 @@ export class FileChannelContextStore {
519
537
  const auxiliaryCollaborationEnabled = collaborationCommandsEnabled && collaborationRoutesAllowedForTurnMode;
520
538
  const directoryPath = resolveChannelContextDirectory(this.stateRootDir, input.channelId);
521
539
  const payloadPath = resolveChannelContextPayloadPath(this.stateRootDir, input.channelId);
540
+ const taskAssignmentStatePath = resolveTaskAssignmentStatePath(this.stateRootDir, input.channelId);
522
541
  const claudeProjectedBriefPath = resolveClaudeProjectedBriefPathFromPayloadPath(payloadPath);
523
542
  const gatewayCredentialPath = resolveGatewayCredentialPathFromPayloadPath(payloadPath);
524
- const existingTaskAssignmentContext = await readExistingTaskAssignmentContext(payloadPath, this.fileSystem);
525
- const existingResolvedWorkspace = await readExistingResolvedWorkspaceContext(payloadPath, this.fileSystem);
543
+ const existingTaskAssignmentContext = await readExistingTaskAssignmentContext(taskAssignmentStatePath, this.fileSystem);
526
544
  const skillRuntime = this.skillRuntimeEnabled
527
545
  ? await this.resolveSkillRuntimeBestEffort()
528
546
  : undefined;
@@ -548,34 +566,40 @@ export class FileChannelContextStore {
548
566
  : {}),
549
567
  }
550
568
  : undefined;
551
- const taskAssignmentContext = buildTaskAssignmentContextForTurn({
552
- incomingMessageType: input.incomingMessageType,
553
- incomingContent: input.incomingContent,
554
- }, existingTaskAssignmentContext);
569
+ const taskAssignmentContext = input.taskAssignmentContextOverride
570
+ ?? buildTaskAssignmentContextForTurn({
571
+ incomingMessageType: input.incomingMessageType,
572
+ incomingContent: input.incomingContent,
573
+ }, existingTaskAssignmentContext);
555
574
  const workspaceResolution = await resolveTaskThreadWorkspaceContext({
556
575
  channelId: input.channelId,
557
576
  workspaceCollectionRootDir: this.workspaceCollectionRootDir,
558
577
  taskAssignmentContext,
559
- existingResolvedWorkspace,
560
578
  taskReader: this.taskReader,
561
579
  taskResolutionTimeoutMs: this.taskResolutionTimeoutMs,
580
+ allowCrossAgentIndependentWorkspaceHandoff: this.allowCrossAgentIndependentWorkspaceHandoff,
581
+ resolveStableAgentId: this.resolveStableAgentId,
582
+ fileSystem: this.fileSystem,
562
583
  });
563
584
  const resolvedWorkspace = workspaceResolution.resolvedWorkspace;
585
+ const runtimeTaskAssignmentContext = workspaceResolution.taskAssignmentContext ?? taskAssignmentContext;
586
+ const persistedTaskAssignmentContext = input.taskAssignmentContextOverridePersistence === 'ephemeral'
587
+ ? existingTaskAssignmentContext
588
+ : runtimeTaskAssignmentContext;
564
589
  const runtimeSurface = buildRuntimeSurfaceForTurn({
565
590
  localhostGateway,
566
591
  collaboration: input.collaboration,
567
- taskAssignmentContext: workspaceResolution.taskAssignmentContext ?? taskAssignmentContext,
592
+ taskAssignmentContext: runtimeTaskAssignmentContext,
568
593
  });
569
- const payload = buildChannelContextPayload(input.channelId, runtimeSurface, input.collaborationOutcome, input.attentionSnapshot, input.compactionSnapshot, input.taskThreadCollaborationContract, input.collaborationCapabilities, input.missedCollaborationDiagnostic, skillRuntime, localhostGateway, workspaceResolution.taskAssignmentContext || resolvedWorkspace
594
+ const payload = buildChannelContextPayload(input.channelId, runtimeSurface, input.collaborationOutcome, input.attentionSnapshot, input.compactionSnapshot, input.taskThreadCollaborationContract, input.collaborationCapabilities, input.missedCollaborationDiagnostic, skillRuntime, localhostGateway, persistedTaskAssignmentContext || resolvedWorkspace
570
595
  ? {
571
- ...(workspaceResolution.taskAssignmentContext
572
- ? { taskAssignmentContext: workspaceResolution.taskAssignmentContext }
596
+ ...(persistedTaskAssignmentContext
597
+ ? { taskAssignmentContext: persistedTaskAssignmentContext }
573
598
  : {}),
574
599
  resolvedWorkspace,
575
600
  }
576
601
  : undefined);
577
602
  const gatewayCredential = buildBorgeeAgentGatewayCredential(input.channelId, issuedLocalhostGateway);
578
- let resolvedWorkspaceMaterialized = false;
579
603
  let claudeProjectedBriefWritten = false;
580
604
  let payloadWritten = false;
581
605
  let gatewayCredentialWritten = false;
@@ -601,14 +625,24 @@ export class FileChannelContextStore {
601
625
  }
602
626
  try {
603
627
  await this.fileSystem.mkdir(directoryPath, { recursive: true, mode: 0o700 });
604
- await this.fileSystem.mkdir(resolvedWorkspace.rootPath, { recursive: true, mode: 0o700 });
605
- resolvedWorkspaceMaterialized = 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 });
631
+ }
606
632
  await pruneGatewayCredentialSidecars(this.fileSystem, directoryPath, gatewayCredential ? CHANNEL_GATEWAY_CREDENTIAL_FILENAME : undefined);
607
633
  if (!claudeProjectedBriefContent) {
608
634
  await unlinkIfPresent(this.fileSystem, claudeProjectedBriefPath);
609
635
  }
610
636
  await this.fileSystem.writeFile(payloadPath, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
611
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
+ }
612
646
  if (gatewayCredential) {
613
647
  // Replaced atomically: a CLI invocation left over from an earlier turn must read either
614
648
  // the old credential or the new one, never a missing or half-written file, because the
@@ -639,14 +673,23 @@ export class FileChannelContextStore {
639
673
  }
640
674
  const partialContext = {
641
675
  directoryPath,
642
- 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
+ },
643
684
  claudeProjectedBriefPath: claudeProjectedBriefWritten ? claudeProjectedBriefPath : undefined,
644
685
  claudeProjectedBriefHash: claudeProjectedBriefWritten ? preparedContext.claudeProjectedBriefHash : undefined,
645
686
  payloadPath: payloadWritten ? payloadPath : undefined,
646
687
  gatewayCredentialPath: gatewayCredentialWritten ? gatewayCredentialPath : undefined,
647
688
  skillRuntime: payloadWritten ? skillRuntime : undefined,
648
689
  localhostGateway: payloadWritten ? localhostGateway : undefined,
649
- resolvedWorkspace: resolvedWorkspaceMaterialized ? resolvedWorkspace : undefined,
690
+ resolvedWorkspace: resolvedWorkspace.authority === 'task-execution-target' && runtimeTaskAssignmentContext?.active === true
691
+ ? buildTaskThreadScratchResolvedWorkspace(this.workspaceCollectionRootDir, input.channelId)
692
+ : resolvedWorkspace,
650
693
  };
651
694
  throw new ChannelContextPreparationError('failed to persist channel context payload', partialContext, { cause: error });
652
695
  }
@@ -39,7 +39,9 @@ function buildLocalhostGatewayPromptLines(context) {
39
39
  if (!context.skillRuntime) {
40
40
  return lines;
41
41
  }
42
- lines.push(context.taskAssignmentContext?.active === true
42
+ const taskThreadActive = context.taskAssignmentContext?.active === true
43
+ || context.runtimeSurface?.task.currentThread === 'task-assignment-thread';
44
+ lines.push(taskThreadActive
43
45
  ? 'This turn runs inside a task assignment thread.'
44
46
  : 'This turn runs in a parent channel, not a task thread.');
45
47
  if (context.runtimeSurface?.task.currentThread === 'parent-channel'
@@ -61,7 +63,7 @@ function buildLocalhostGatewayPromptLines(context) {
61
63
  const collaborationLive = context.localhostGateway.collaboration?.enabled === true
62
64
  && (context.collaborationTurnMode ?? 'ordinary') === 'ordinary';
63
65
  if (!collaborationLive) {
64
- lines.push('Auxiliary collaboration commands (users, draft, send, mention) are not available this turn.');
66
+ lines.push('Auxiliary collaboration commands (draft, send, mention) are not available this turn.');
65
67
  }
66
68
  else if (context.collaborationTurnExecutionId) {
67
69
  lines.push(`Pass --turn-execution-id ${context.collaborationTurnExecutionId} on send, mention and draft.`);
@@ -77,13 +79,15 @@ function buildLocalhostGatewayPromptLines(context) {
77
79
  function buildTaskAssignmentPromptLines(params) {
78
80
  const isTaskAssignmentTurn = params.incomingMessageType?.trim() === 'task_assignment';
79
81
  const taskAssignmentContext = params.promptContext?.taskAssignmentContext;
80
- if (!isTaskAssignmentTurn && taskAssignmentContext?.active !== true) {
82
+ const taskAssignmentThreadActive = taskAssignmentContext?.active === true
83
+ || params.promptContext?.runtimeSurface?.task.currentThread === 'task-assignment-thread';
84
+ if (!isTaskAssignmentTurn && !taskAssignmentThreadActive) {
81
85
  return [];
82
86
  }
83
87
  if (params.provider === 'claude'
84
88
  && params.forceInlineForClaude !== true
85
89
  && !isTaskAssignmentTurn
86
- && taskAssignmentContext?.active === true) {
90
+ && taskAssignmentThreadActive) {
87
91
  return [];
88
92
  }
89
93
  const lines = isTaskAssignmentTurn
@@ -180,7 +184,7 @@ function buildClaudeProjectedBriefTurnDeltaLines(context) {
180
184
  const collaborationLive = context.localhostGateway.collaboration?.enabled === true
181
185
  && (context.collaborationTurnMode ?? 'ordinary') === 'ordinary';
182
186
  if (!collaborationLive) {
183
- lines.push('Auxiliary collaboration commands (users, draft, send, mention) are not available this turn.');
187
+ lines.push('Auxiliary collaboration commands (draft, send, mention) are not available this turn.');
184
188
  }
185
189
  else if (context.collaborationTurnExecutionId) {
186
190
  lines.push(`Pass --turn-execution-id ${context.collaborationTurnExecutionId} on send, mention and draft.`);
@@ -291,8 +295,8 @@ function buildTurnControlAttentionLines(context) {
291
295
  const attentionLines = context?.attentionSnapshot
292
296
  ? [
293
297
  'Attention controls are projection-only host hints. They do not bypass server-side mention policy or widen message delivery.',
294
- `To follow ordinary delivered human messages in this channel, add "attentionUpdate":"follow-channel".`,
295
- `To stop follow-based human wake for this channel, add "attentionUpdate":"unfollow-channel".`,
298
+ `To pin a durable follow hint for ordinary delivered human messages in this channel, add "attentionUpdate":"follow-channel".`,
299
+ `To clear an explicit follow hint and return to server-policy wake for this channel, add "attentionUpdate":"unfollow-channel".`,
296
300
  `To mute ordinary delivered human wake while keeping explicit mentions and task assignments untouched, add "attentionUpdate":"mute-channel".`,
297
301
  `To set a durable channel-level attention hint, add "attentionUpdate":"claim-channel".`,
298
302
  ...(taskThreadAttentionAvailable
@@ -1,2 +1,3 @@
1
1
  import type { PreparedPromptContext, ProviderKind } from '../types.js';
2
2
  export declare function buildResolvedWorkspaceGuidanceLines(context: PreparedPromptContext | undefined, provider?: ProviderKind): string[];
3
+ export declare function isDiscussionOnlyResolvedWorkspace(context: PreparedPromptContext | undefined): boolean;