@borgee/agents-host 0.2.33 → 0.2.44

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 (60) hide show
  1. package/README.md +28 -7
  2. package/dist/agents-host.d.ts +19 -0
  3. package/dist/agents-host.js +163 -44
  4. package/dist/chat/chat-control-plane.d.ts +9 -0
  5. package/dist/chat/sdk-chat-control-plane.d.ts +10 -1
  6. package/dist/chat/sdk-chat-control-plane.js +9 -0
  7. package/dist/cli-args.d.ts +1 -1
  8. package/dist/cli-args.js +5 -0
  9. package/dist/compatibility-gates.d.ts +1 -0
  10. package/dist/compatibility-gates.js +2 -0
  11. package/dist/config.d.ts +4 -1
  12. package/dist/config.js +21 -3
  13. package/dist/context/injection.d.ts +5 -3
  14. package/dist/context/injection.js +45 -29
  15. package/dist/context/main-session-delegation.d.ts +1 -0
  16. package/dist/context/main-session-delegation.js +6 -0
  17. package/dist/context/prompt.js +25 -35
  18. package/dist/context/skill-manual.d.ts +13 -0
  19. package/dist/context/skill-manual.js +18 -0
  20. package/dist/context/turn-preparation.js +13 -4
  21. package/dist/gateway/localhost-gateway.js +75 -1
  22. package/dist/hosted-turn-content.d.ts +15 -0
  23. package/dist/hosted-turn-content.js +50 -0
  24. package/dist/local-config.js +10 -1
  25. package/dist/managed-daemon.d.ts +3 -2
  26. package/dist/managed-daemon.js +82 -29
  27. package/dist/plugin-sdk.js +58 -1
  28. package/dist/plugin-sdk.js.map +2 -2
  29. package/dist/policy/gateway-authorization.d.ts +18 -3
  30. package/dist/policy/gateway-authorization.js +33 -1
  31. package/dist/providers/claude/adapter.d.ts +3 -1
  32. package/dist/providers/claude/adapter.js +13 -1
  33. package/dist/providers/claude/cli-client.d.ts +36 -3
  34. package/dist/providers/claude/cli-client.js +225 -37
  35. package/dist/providers/codex/adapter.d.ts +3 -1
  36. package/dist/providers/codex/adapter.js +13 -1
  37. package/dist/providers/codex/cli-client.d.ts +35 -3
  38. package/dist/providers/codex/cli-client.js +212 -31
  39. package/dist/providers/codex/project-doc.js +16 -29
  40. package/dist/providers/copilot/adapter.d.ts +3 -1
  41. package/dist/providers/copilot/adapter.js +13 -1
  42. package/dist/providers/copilot/cli-client.d.ts +34 -2
  43. package/dist/providers/copilot/cli-client.js +196 -18
  44. package/dist/providers/create-provider.d.ts +1 -1
  45. package/dist/providers/create-provider.js +30 -14
  46. package/dist/providers/idle-backend-shutdown.d.ts +16 -0
  47. package/dist/providers/idle-backend-shutdown.js +53 -0
  48. package/dist/providers/provider-adapter.d.ts +35 -0
  49. package/dist/providers/provider-adapter.js +44 -1
  50. package/dist/state-paths.d.ts +9 -1
  51. package/dist/state-paths.js +22 -3
  52. package/dist/types.d.ts +40 -2
  53. package/package.json +2 -2
  54. package/skills/borgee-agent/SKILL.md +133 -35
  55. package/skills/borgee-agent/references/errors.md +38 -0
  56. package/skills/borgee-agent/references/task-properties.md +30 -0
  57. package/skills/borgee-agent/scripts/borgee-agent.mjs +553 -0
  58. package/skills/borgee-agent/scripts/borgee-agent.py +547 -0
  59. package/skills/borgee-agent/borgee-agent.mjs +0 -507
  60. package/skills/borgee-agent/borgee-agent.py +0 -438
@@ -505,6 +505,23 @@ class LoopbackLocalhostGatewayController {
505
505
  this.recordAudit('authorized', 200, decision.path, request.method ?? 'GET', decision.binding);
506
506
  return;
507
507
  }
508
+ case 'current-task-property': {
509
+ const task = await this.loadCurrentThreadTask(binding);
510
+ const updatedTask = await this.applyTaskPropertyChange(request, task.id, decision.route.key);
511
+ this.sendTaskPropertyResult(response, decision, binding.channelId, updatedTask, request.method);
512
+ return;
513
+ }
514
+ case 'task-property': {
515
+ // Load first: the write must be refused for a task outside the bound
516
+ // channel BEFORE it lands, not reported as not-found afterwards.
517
+ const task = await this.loadAuthorizedTask(binding.channelId, {
518
+ taskId: decision.route.taskId,
519
+ resource: 'task',
520
+ });
521
+ const updatedTask = await this.applyTaskPropertyChange(request, task.id, decision.route.key);
522
+ this.sendTaskPropertyResult(response, decision, binding.channelId, updatedTask, request.method);
523
+ return;
524
+ }
508
525
  case 'task-history': {
509
526
  // The thread this reads is the one the server recorded on the task: thread_id is
510
527
  // written once by task creation and is not part of the task update whitelist, so no
@@ -612,9 +629,40 @@ class LoopbackLocalhostGatewayController {
612
629
  }
613
630
  return task;
614
631
  }
632
+ /**
633
+ * PUT writes the key, DELETE removes it. The method has already been
634
+ * narrowed to those two by the route's allowed-method table.
635
+ */
636
+ async applyTaskPropertyChange(request, taskId, key) {
637
+ try {
638
+ if (request.method === 'DELETE') {
639
+ return await this.controlPlane.deleteTaskProperty({ taskId, key });
640
+ }
641
+ const body = await readGatewayJsonBody(request);
642
+ return await this.controlPlane.setTaskProperty({ taskId, key, value: parseTaskPropertyValue(body) });
643
+ }
644
+ catch (error) {
645
+ throw mapGatewayControlPlaneError(error, 'authorized');
646
+ }
647
+ }
648
+ sendTaskPropertyResult(response, decision, boundChannelId, task, method) {
649
+ if (!isTaskInAuthorizedScope(boundChannelId, task)) {
650
+ this.sendJson(response, 404, { error: 'not_found' });
651
+ this.recordAudit('not-found', 404, decision.path, method ?? 'PUT', decision.binding);
652
+ return;
653
+ }
654
+ this.sendJson(response, 200, task);
655
+ this.recordAudit('authorized', 200, decision.path, method ?? 'PUT', decision.binding);
656
+ }
615
657
  async loadCurrentThreadTask(binding) {
658
+ // Only a task-thread binding has a current task at all. Without that context the scan below
659
+ // would list every visible channel's tasks to conclude what the binding already says, so the
660
+ // parent-channel answer is given here instead of after an O(channels x tasks) sweep.
661
+ if (binding.payload?.taskAssignmentContext?.active !== true) {
662
+ throw new GatewayHttpError(404, { error: 'not_found' }, 'not-found');
663
+ }
616
664
  let task;
617
- const preferredTaskId = binding.payload?.taskAssignmentContext?.currentTaskId;
665
+ const preferredTaskId = binding.payload.taskAssignmentContext.currentTaskId;
618
666
  try {
619
667
  task = await findTaskForThread(this.controlPlane, binding.channelId, preferredTaskId);
620
668
  }
@@ -816,6 +864,17 @@ function parseUpdateTaskInput(taskId, body) {
816
864
  }
817
865
  return input;
818
866
  }
867
+ /**
868
+ * The value is required and must be a string. An absent or non-string value is
869
+ * a caller bug, not an instruction to store the empty string — storing that
870
+ * would silently blank a key the caller meant to set.
871
+ */
872
+ function parseTaskPropertyValue(body) {
873
+ if (!isRecord(body) || typeof body.value !== 'string') {
874
+ throw new GatewayHttpError(400, { error: 'invalid_property_value' }, 'bad-request');
875
+ }
876
+ return body.value;
877
+ }
819
878
  function isTaskInAuthorizedScope(boundChannelId, task) {
820
879
  return task.channelId === boundChannelId || task.threadId === boundChannelId;
821
880
  }
@@ -846,6 +905,21 @@ function mapGatewayControlPlaneError(error, fallbackReason) {
846
905
  case 'bpp.channel_id_required':
847
906
  case 'bpp.task_id_required':
848
907
  return new GatewayHttpError(400, { error: 'bad_request' }, 'bad-request');
908
+ // An unregistered key, an oversized value or a status outside the enum is the
909
+ // caller's own mistake. Falling through to 502 would tell the agent the server
910
+ // is broken, and it would retry the same bad call instead of correcting it.
911
+ case 'bpp.task_property_key_unknown':
912
+ return new GatewayHttpError(400, { error: 'unknown_property_key' }, 'bad-request');
913
+ case 'bpp.task_property_value_too_long':
914
+ return new GatewayHttpError(400, { error: 'property_value_too_long' }, 'bad-request');
915
+ case 'bpp.task_invalid_status':
916
+ return new GatewayHttpError(400, { error: 'invalid_status' }, 'bad-request');
917
+ // A distinct body, not the bare `not_found` the other routes use: on a
918
+ // current-task property request `not_found` means the thread's own task
919
+ // could not be resolved, and "pass the task id" is the wrong correction
920
+ // for a key that simply was not set.
921
+ case 'bpp.task_property_not_found':
922
+ return new GatewayHttpError(404, { error: 'property_not_found' }, 'not-found');
849
923
  default:
850
924
  return new GatewayHttpError(502, { error: 'upstream_error' }, fallbackReason);
851
925
  }
@@ -0,0 +1,15 @@
1
+ import type { HostedMessageAttachment, HostedTurnContentPart } from './types.js';
2
+ type CanonicalHostedMessageAttachment = HostedMessageAttachment & {
3
+ kind: NonNullable<HostedMessageAttachment['kind']>;
4
+ };
5
+ export declare function normalizeHostedMessageAttachment(attachment: HostedMessageAttachment): CanonicalHostedMessageAttachment;
6
+ export declare function classifyHostedAttachmentKind(contentType: string | undefined): Extract<HostedTurnContentPart, {
7
+ type: 'image' | 'file';
8
+ }>['type'];
9
+ export declare function buildHostedTurnContentParts(input: {
10
+ text?: string;
11
+ attachments?: readonly HostedMessageAttachment[];
12
+ }): HostedTurnContentPart[];
13
+ export declare function extractHostedMessageAttachments(parts: readonly HostedTurnContentPart[]): HostedMessageAttachment[];
14
+ export declare function buildHostedIncomingContentText(parts: readonly HostedTurnContentPart[]): string;
15
+ export {};
@@ -0,0 +1,50 @@
1
+ export function normalizeHostedMessageAttachment(attachment) {
2
+ return {
3
+ url: attachment.url,
4
+ ...(attachment.filename ? { filename: attachment.filename } : {}),
5
+ contentType: attachment.contentType,
6
+ kind: attachment.kind ?? classifyHostedAttachmentKind(attachment.contentType),
7
+ ...(attachment.sizeBytes !== undefined ? { sizeBytes: attachment.sizeBytes } : {}),
8
+ };
9
+ }
10
+ export function classifyHostedAttachmentKind(contentType) {
11
+ return contentType?.toLowerCase().startsWith('image/') ? 'image' : 'file';
12
+ }
13
+ export function buildHostedTurnContentParts(input) {
14
+ const text = input.text?.trim() ?? '';
15
+ const attachments = input.attachments ?? [];
16
+ const parts = [];
17
+ if (text.length > 0) {
18
+ parts.push({ type: 'text', text });
19
+ }
20
+ for (const attachment of attachments) {
21
+ const normalizedAttachment = normalizeHostedMessageAttachment(attachment);
22
+ parts.push({
23
+ type: normalizedAttachment.kind,
24
+ attachment: normalizedAttachment,
25
+ });
26
+ }
27
+ return parts;
28
+ }
29
+ export function extractHostedMessageAttachments(parts) {
30
+ return parts.flatMap((part) => ('attachment' in part ? [normalizeHostedMessageAttachment(part.attachment)] : []));
31
+ }
32
+ export function buildHostedIncomingContentText(parts) {
33
+ const text = parts
34
+ .flatMap((part) => (part.type === 'text' ? [part.text] : []))
35
+ .join('\n')
36
+ .trim();
37
+ const nonTextParts = parts.filter((part) => part.type === 'image' || part.type === 'file');
38
+ if (nonTextParts.length === 0) {
39
+ return text;
40
+ }
41
+ const summaryLines = [
42
+ `[Attachment metadata was received for ${nonTextParts.length} attachment${nonTextParts.length === 1 ? '' : 's'}]`,
43
+ ...nonTextParts.map((part, index) => {
44
+ const filename = part.attachment.filename?.trim();
45
+ return `${index + 1}. ${part.type};${filename ? ` filename: ${filename};` : ''} content type: ${part.attachment.contentType}`;
46
+ }),
47
+ ];
48
+ const summary = summaryLines.join('\n');
49
+ return text ? `${text}\n\n${summary}` : summary;
50
+ }
@@ -2,7 +2,7 @@ import { promises as fs } from 'node:fs';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
4
4
  import { parseDocument, stringify } from 'yaml';
5
- import { assertProviderCompatibility, assertProviderCommandCompatibility, optionalNonEmptyString, optionalStringArray, parseCopilotSessionTtlMinutesValue, requireNonEmptyString, resolveProvider, resolveProviderCommandConfig, } from './config.js';
5
+ import { assertProviderCompatibility, assertProviderCommandCompatibility, optionalNonEmptyString, optionalStringArray, parseCopilotSessionTtlMinutesValue, parseProviderIdleShutdownMinutesValue, requireNonEmptyString, resolveProvider, resolveProviderCommandConfig, } from './config.js';
6
6
  import { resolveLocalConfigAgentStateRoot } from './state-paths.js';
7
7
  const SUPPORTED_CONFIG_EXTENSIONS = new Set(['.json', '.yaml', '.yml']);
8
8
  export const DEFAULT_LOCAL_HOST_CONFIG_FILENAME = 'agents-host.yaml';
@@ -124,6 +124,9 @@ function parseProviderCommandOverrides(value, sourceLabel) {
124
124
  if (value.copilotSessionTtlMinutes !== undefined && value.copilotSessionTtlMinutes !== null) {
125
125
  overrides.copilotSessionTtlMinutes = parseCopilotSessionTtlMinutesValue(value.copilotSessionTtlMinutes, sourceLabel);
126
126
  }
127
+ if (value.providerIdleShutdownMinutes !== undefined && value.providerIdleShutdownMinutes !== null) {
128
+ overrides.providerIdleShutdownMinutes = parseProviderIdleShutdownMinutesValue(value.providerIdleShutdownMinutes, sourceLabel);
129
+ }
127
130
  return overrides;
128
131
  }
129
132
  function toAgentsDir(hostConfigPath, rawAgentsDir) {
@@ -593,6 +596,9 @@ function renderAgentConfigYaml(agent) {
593
596
  if (agent.copilotSessionTtlMinutes !== undefined) {
594
597
  config.copilotSessionTtlMinutes = agent.copilotSessionTtlMinutes;
595
598
  }
599
+ if (agent.providerIdleShutdownMinutes !== undefined) {
600
+ config.providerIdleShutdownMinutes = agent.providerIdleShutdownMinutes;
601
+ }
596
602
  return stringify(config);
597
603
  }
598
604
  function buildManagedAgentSnapshot(host, stateRootBaseDir, sourcePath, agent) {
@@ -629,6 +635,9 @@ function resolveAgentProviderCommandConfig(hostDefaults, agent) {
629
635
  ...(agent.copilotSessionTtlMinutes !== undefined
630
636
  ? { copilotSessionTtlMinutes: agent.copilotSessionTtlMinutes }
631
637
  : {}),
638
+ ...(agent.providerIdleShutdownMinutes !== undefined
639
+ ? { providerIdleShutdownMinutes: agent.providerIdleShutdownMinutes }
640
+ : {}),
632
641
  });
633
642
  }
634
643
  export async function loadLocalConfigSnapshot(hostConfigPath, deps = {}) {
@@ -95,6 +95,7 @@ export interface ManagedAgentsHostDaemonDeps {
95
95
  materialize?: (rootPath: string, spec: LocalConfigGenerateSpec) => Promise<unknown>;
96
96
  logger?: Pick<Console, 'log' | 'error'>;
97
97
  logPath?: string;
98
+ platform?: NodeJS.Platform;
98
99
  }
99
100
  export interface DescribeManagedSpecOptions {
100
101
  serverUrl: string;
@@ -192,7 +193,7 @@ export declare function applyManagedSpec(options: ApplyManagedSpecOptions, deps?
192
193
  export declare class ManagedAgentsHostDaemon {
193
194
  private readonly rootPath;
194
195
  private readonly hostConfigPath;
195
- private readonly socketPath;
196
+ private readonly endpoint;
196
197
  private readonly createSupervisor;
197
198
  private readonly supervisor;
198
199
  private readonly loadSpec;
@@ -204,7 +205,7 @@ export declare class ManagedAgentsHostDaemon {
204
205
  private requestQueue;
205
206
  private started;
206
207
  private ready;
207
- private ownsSocket;
208
+ private ownsEndpoint;
208
209
  private shutdownRequested;
209
210
  private ownedSocketInode;
210
211
  constructor(rootPath: string, debug?: boolean, deps?: ManagedAgentsHostDaemonDeps);
@@ -8,7 +8,7 @@ import { AgentsHostSupervisor } from './agents-host-supervisor.js';
8
8
  import { CLAUDE_PROVIDER_V2_COMPATIBILITY_GATE, COMPATIBILITY_GATES_ENV, createManagedRuntimeSettingsFingerprint, INTERNAL_DISABLED_COMPATIBILITY_GATES_ENV, INTERNAL_POLICY_MODE_ENV, INTERNAL_PROVIDER_IMPLEMENTATIONS_ENV, MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE, MANAGED_RUNTIME_SETTINGS_SCHEMA_VERSION, resolveDisabledDefaultCompatibilityGates, resolveManagedRuntimeSettingsSnapshot, normalizeInternalProviderImplementationOverrides, } from './compatibility-gates.js';
9
9
  import { DEFAULT_PROVIDER_COMMAND_CONFIG, hasLegacyClaudeOneShotArgs, loadConfigFromEnv, } from './config.js';
10
10
  import { loadLocalConfigGenerateSpec, materializeLocalConfig, parseGenerateConfigSpec, resolveLocalConfigLayout, } from './local-config.js';
11
- import { normalizeManagedRuntimeKey, resolveManagedBootstrapLockPath, resolveManagedDaemonLogPath, resolveManagedDaemonSocketPath, resolveManagedRuntimeRoot, resolveManagedRuntimeSettingsPath, } from './state-paths.js';
11
+ import { normalizeManagedRuntimeKey, resolveManagedBootstrapLockPath, resolveManagedDaemonEndpoint, resolveManagedDaemonLogPath, resolveManagedRuntimeRoot, resolveManagedRuntimeSettingsPath, } from './state-paths.js';
12
12
  const MANAGED_ROOT_MODE = 0o700;
13
13
  const MANAGED_CONFIG_MODE = 0o600;
14
14
  const BOOTSTRAP_LOCK_WAIT_MS = 10_000;
@@ -339,6 +339,7 @@ export function buildManagedLocalAgentConfig(options) {
339
339
  copilotCommand: config.copilotCommand,
340
340
  copilotArgs: [...config.copilotArgs],
341
341
  copilotSessionTtlMinutes: config.copilotSessionTtlMinutes,
342
+ providerIdleShutdownMinutes: config.providerIdleShutdownMinutes,
342
343
  },
343
344
  };
344
345
  }
@@ -382,6 +383,9 @@ function mergeManagedLocalAgentConfig(hostDefaults, existingAgent, generatedAgen
382
383
  copilotSessionTtlMinutes: hasOverride('COPILOT_SESSION_TTL_MINUTES')
383
384
  ? generatedAgent.copilotSessionTtlMinutes
384
385
  : existingAgent.copilotSessionTtlMinutes,
386
+ providerIdleShutdownMinutes: hasOverride('PROVIDER_IDLE_SHUTDOWN_MINUTES')
387
+ ? generatedAgent.providerIdleShutdownMinutes
388
+ : existingAgent.providerIdleShutdownMinutes,
385
389
  };
386
390
  }
387
391
  function collectExplicitManagedAgentEnvOverrides(processEnv, optionEnv) {
@@ -396,6 +400,7 @@ function collectExplicitManagedAgentEnvOverrides(processEnv, optionEnv) {
396
400
  'COPILOT_COMMAND',
397
401
  'COPILOT_ARGS',
398
402
  'COPILOT_SESSION_TTL_MINUTES',
403
+ 'PROVIDER_IDLE_SHUTDOWN_MINUTES',
399
404
  ]) {
400
405
  if (processEnv[key] !== undefined) {
401
406
  explicitOverrides[key] = processEnv[key];
@@ -594,8 +599,11 @@ async function canConnectToSocket(socketPath) {
594
599
  socket.once('error', () => finalize(false));
595
600
  });
596
601
  }
597
- async function removeStaleControlSocket(socketPath) {
598
- const stats = await fs.lstat(socketPath).catch((error) => {
602
+ async function removeStaleControlSocket(endpoint) {
603
+ if (endpoint.kind === 'named-pipe') {
604
+ return false;
605
+ }
606
+ const stats = await fs.lstat(endpoint.address).catch((error) => {
599
607
  if (isNotFoundError(error)) {
600
608
  return undefined;
601
609
  }
@@ -605,15 +613,15 @@ async function removeStaleControlSocket(socketPath) {
605
613
  return false;
606
614
  }
607
615
  if (stats.isDirectory() || stats.isSymbolicLink()) {
608
- throw new Error(`Managed control socket path has an unexpected value: ${socketPath}`);
616
+ throw new Error(`Managed control socket path has an unexpected value: ${endpoint.address}`);
609
617
  }
610
- if (await canConnectToSocket(socketPath)) {
618
+ if (await canConnectToSocket(endpoint.address)) {
611
619
  return false;
612
620
  }
613
621
  if (!stats.isSocket() && !stats.isFile()) {
614
- throw new Error(`Managed control socket path has an unexpected value: ${socketPath}`);
622
+ throw new Error(`Managed control socket path has an unexpected value: ${endpoint.address}`);
615
623
  }
616
- await fs.rm(socketPath, { force: true });
624
+ await fs.rm(endpoint.address, { force: true });
617
625
  return true;
618
626
  }
619
627
  function parseDaemonResponse(raw) {
@@ -650,12 +658,12 @@ function isManagedDaemonConnectionError(error) {
650
658
  isNodeErrorWithCode(error, 'ECONNRESET'));
651
659
  }
652
660
  export async function sendManagedDaemonRequest(rootPath, request) {
653
- const socketPath = resolveManagedDaemonSocketPath(rootPath);
661
+ const endpoint = resolveManagedDaemonEndpoint(rootPath);
654
662
  const requestTimeoutMs = request.type === 'upsertAgent' || request.type === 'applySpec'
655
663
  ? DAEMON_RECONCILE_WAIT_MS
656
664
  : DAEMON_READY_WAIT_MS;
657
665
  return new Promise((resolveRequest, rejectRequest) => {
658
- const socket = createConnection(socketPath);
666
+ const socket = createConnection(endpoint.address);
659
667
  let settled = false;
660
668
  let responseBuffer = '';
661
669
  const rejectOnce = (error) => {
@@ -676,7 +684,7 @@ export async function sendManagedDaemonRequest(rootPath, request) {
676
684
  };
677
685
  socket.setEncoding('utf8');
678
686
  socket.setTimeout(requestTimeoutMs);
679
- socket.once('timeout', () => rejectOnce(new Error(`Timed out talking to managed daemon: ${socketPath}`)));
687
+ socket.once('timeout', () => rejectOnce(new Error(`Timed out talking to managed daemon: ${endpoint.address}`)));
680
688
  socket.once('error', rejectOnce);
681
689
  socket.on('data', (chunk) => {
682
690
  responseBuffer += chunk;
@@ -785,9 +793,9 @@ async function readReadyManagedDaemonStatus(rootPath, sendRequest) {
785
793
  }
786
794
  async function waitForManagedDaemonShutdown(rootPath, canConnect) {
787
795
  const deadline = Date.now() + DAEMON_READY_WAIT_MS;
788
- const socketPath = resolveManagedDaemonSocketPath(rootPath);
796
+ const endpoint = resolveManagedDaemonEndpoint(rootPath);
789
797
  while (Date.now() < deadline) {
790
- const listening = await canConnect(socketPath).catch((error) => {
798
+ const listening = await canConnect(endpoint.address).catch((error) => {
791
799
  if (isNodeErrorWithCode(error, 'ENOENT')) {
792
800
  return false;
793
801
  }
@@ -1476,7 +1484,7 @@ export async function applyManagedSpec(options, deps = {}) {
1476
1484
  export class ManagedAgentsHostDaemon {
1477
1485
  rootPath;
1478
1486
  hostConfigPath;
1479
- socketPath;
1487
+ endpoint;
1480
1488
  createSupervisor;
1481
1489
  supervisor;
1482
1490
  loadSpec;
@@ -1488,14 +1496,14 @@ export class ManagedAgentsHostDaemon {
1488
1496
  requestQueue = Promise.resolve();
1489
1497
  started = false;
1490
1498
  ready = false;
1491
- ownsSocket = false;
1499
+ ownsEndpoint = false;
1492
1500
  shutdownRequested = false;
1493
1501
  ownedSocketInode = null;
1494
1502
  constructor(rootPath, debug = false, deps = {}) {
1495
1503
  const layout = resolveLocalConfigLayout(rootPath);
1496
1504
  this.rootPath = layout.root;
1497
1505
  this.hostConfigPath = layout.hostConfigPath;
1498
- this.socketPath = resolveManagedDaemonSocketPath(this.rootPath);
1506
+ this.endpoint = resolveManagedDaemonEndpoint(this.rootPath, deps.platform);
1499
1507
  this.createSupervisor =
1500
1508
  deps.createSupervisor ??
1501
1509
  ((configPath, daemonDebug) => new AgentsHostSupervisor(configPath, { debug: daemonDebug }));
@@ -1530,22 +1538,34 @@ export class ManagedAgentsHostDaemon {
1530
1538
  this.logger.log('[agents-host] managed daemon ready', {
1531
1539
  rootPath: this.rootPath,
1532
1540
  hostConfigPath: this.hostConfigPath,
1533
- socketPath: this.socketPath,
1541
+ socketPath: this.endpoint.address,
1534
1542
  });
1535
1543
  }
1536
1544
  catch (error) {
1537
1545
  this.started = false;
1538
1546
  this.ready = false;
1539
- await this.stop().catch(() => undefined);
1547
+ try {
1548
+ await this.stop();
1549
+ }
1550
+ catch (cleanupError) {
1551
+ this.logger.error('[agents-host] managed daemon startup cleanup failed', {
1552
+ rootPath: this.rootPath,
1553
+ error: cleanupError,
1554
+ });
1555
+ }
1540
1556
  throw error;
1541
1557
  }
1542
1558
  }
1543
1559
  async stop() {
1544
1560
  this.ready = false;
1561
+ let stopError;
1545
1562
  try {
1546
1563
  await this.supervisor.stop();
1547
1564
  }
1548
- finally {
1565
+ catch (error) {
1566
+ stopError = error;
1567
+ }
1568
+ try {
1549
1569
  await new Promise((resolveStop) => {
1550
1570
  if (!this.server.listening) {
1551
1571
  resolveStop();
@@ -1553,17 +1573,38 @@ export class ManagedAgentsHostDaemon {
1553
1573
  }
1554
1574
  this.server.close(() => resolveStop());
1555
1575
  });
1556
- if (this.ownsSocket) {
1557
- const socketStats = await fs.lstat(this.socketPath).catch(() => undefined);
1576
+ if (this.ownsEndpoint && this.endpoint.kind === 'unix-socket') {
1577
+ const socketStats = await fs.lstat(this.endpoint.address).catch((error) => {
1578
+ if (isNotFoundError(error)) {
1579
+ return undefined;
1580
+ }
1581
+ throw error;
1582
+ });
1558
1583
  if (socketStats && socketStats.ino === this.ownedSocketInode) {
1559
- await fs.rm(this.socketPath, { force: true }).catch(() => undefined);
1584
+ await fs.rm(this.endpoint.address, { force: true });
1560
1585
  }
1561
- this.ownsSocket = false;
1562
- this.ownedSocketInode = null;
1563
1586
  }
1587
+ }
1588
+ catch (cleanupError) {
1589
+ if (stopError) {
1590
+ this.logger.error('[agents-host] managed daemon control endpoint cleanup failed', {
1591
+ rootPath: this.rootPath,
1592
+ error: cleanupError,
1593
+ });
1594
+ }
1595
+ else {
1596
+ stopError = cleanupError;
1597
+ }
1598
+ }
1599
+ finally {
1600
+ this.ownsEndpoint = false;
1601
+ this.ownedSocketInode = null;
1564
1602
  this.started = false;
1565
1603
  this.shutdownRequested = false;
1566
1604
  }
1605
+ if (stopError) {
1606
+ throw stopError;
1607
+ }
1567
1608
  }
1568
1609
  async listen() {
1569
1610
  for (let attempt = 0; attempt < 2; attempt++) {
@@ -1579,20 +1620,32 @@ export class ManagedAgentsHostDaemon {
1579
1620
  };
1580
1621
  this.server.once('error', onError);
1581
1622
  this.server.once('listening', onListening);
1582
- this.server.listen(this.socketPath);
1623
+ this.server.listen(this.endpoint.address);
1583
1624
  });
1584
- await fs.chmod(this.socketPath, MANAGED_CONFIG_MODE);
1585
- this.ownedSocketInode = (await fs.lstat(this.socketPath)).ino;
1586
- this.ownsSocket = true;
1625
+ if (this.endpoint.kind === 'unix-socket') {
1626
+ await fs.chmod(this.endpoint.address, MANAGED_CONFIG_MODE);
1627
+ this.ownedSocketInode = (await fs.lstat(this.endpoint.address)).ino;
1628
+ }
1629
+ this.ownsEndpoint = true;
1587
1630
  return;
1588
1631
  }
1589
1632
  catch (error) {
1590
- if (!isSocketBusyError(error) || !(await removeStaleControlSocket(this.socketPath))) {
1633
+ if (!isSocketBusyError(error)) {
1634
+ throw error;
1635
+ }
1636
+ if (this.endpoint.kind === 'named-pipe') {
1637
+ if (await canConnectToSocket(this.endpoint.address)) {
1638
+ throw error;
1639
+ }
1640
+ await delay(DAEMON_READY_RETRY_MS);
1641
+ continue;
1642
+ }
1643
+ if (!(await removeStaleControlSocket(this.endpoint))) {
1591
1644
  throw error;
1592
1645
  }
1593
1646
  }
1594
1647
  }
1595
- throw new Error(`Unable to bind managed daemon control socket: ${this.socketPath}`);
1648
+ throw new Error(`Unable to bind managed daemon control endpoint: ${this.endpoint.address}`);
1596
1649
  }
1597
1650
  async handleSocket(socket) {
1598
1651
  let buffer = '';
@@ -4729,6 +4729,12 @@ function encodeActionPayload(action) {
4729
4729
  ...action.heartbeat_interval_ms !== void 0 ? { heartbeat_interval_ms: action.heartbeat_interval_ms } : {},
4730
4730
  ...action.heartbeat_prompt !== void 0 ? { heartbeat_prompt: action.heartbeat_prompt } : {}
4731
4731
  };
4732
+ case "set_task_property":
4733
+ return { task_id: action.taskId, key: action.key, value: action.value };
4734
+ case "delete_task_property":
4735
+ return { task_id: action.taskId, key: action.key };
4736
+ case "list_task_property_definitions":
4737
+ return {};
4732
4738
  default:
4733
4739
  return {};
4734
4740
  }
@@ -4795,7 +4801,17 @@ function decodeActionResult(op, payloadJSON, frameCursor) {
4795
4801
  case "create_task":
4796
4802
  case "get_task":
4797
4803
  case "update_task":
4804
+ case "set_task_property":
4805
+ case "delete_task_property":
4798
4806
  return decodeTask(p);
4807
+ case "list_task_property_definitions": {
4808
+ const arr = Array.isArray(p) ? p : [];
4809
+ return arr.map((d) => ({
4810
+ key: String(d.key ?? ""),
4811
+ label: String(d.label ?? ""),
4812
+ render: d.render === "url" || d.render === "identifier" ? d.render : "text"
4813
+ }));
4814
+ }
4799
4815
  case "list_tasks": {
4800
4816
  const arr = Array.isArray(p) ? p : [];
4801
4817
  return arr.map(decodeTask);
@@ -4809,6 +4825,7 @@ function decodeTask(t) {
4809
4825
  id: String(t.id ?? ""),
4810
4826
  channelId: String(t.channel_id ?? ""),
4811
4827
  guildId: String(t.guild_id ?? ""),
4828
+ seq: Number(t.seq ?? 0),
4812
4829
  title: String(t.title ?? ""),
4813
4830
  description: String(t.description ?? ""),
4814
4831
  status: String(t.status ?? ""),
@@ -4818,9 +4835,23 @@ function decodeTask(t) {
4818
4835
  messageId: t.message_id != null ? String(t.message_id) : null,
4819
4836
  threadId: t.thread_id != null ? String(t.thread_id) : null,
4820
4837
  createdAt: Number(t.created_at ?? 0),
4821
- updatedAt: Number(t.updated_at ?? 0)
4838
+ updatedAt: Number(t.updated_at ?? 0),
4839
+ heartbeatIntervalMs: Number(t.heartbeat_interval_ms ?? 0),
4840
+ heartbeatPrompt: String(t.heartbeat_prompt ?? ""),
4841
+ participants: Array.isArray(t.participants) ? t.participants.map((p) => String(p)) : [],
4842
+ properties: decodeTaskProperties(t.properties)
4822
4843
  };
4823
4844
  }
4845
+ function decodeTaskProperties(raw) {
4846
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw))
4847
+ return {};
4848
+ const out = {};
4849
+ for (const [key, value] of Object.entries(raw)) {
4850
+ if (typeof value === "string")
4851
+ out[key] = value;
4852
+ }
4853
+ return out;
4854
+ }
4824
4855
  function mapInbound(frame) {
4825
4856
  const kind = frame.kind;
4826
4857
  const base = {
@@ -5068,6 +5099,32 @@ var Client = class {
5068
5099
  heartbeat_prompt: input.heartbeatPrompt
5069
5100
  });
5070
5101
  }
5102
+ // ─── Task properties ────────────────────────────────────
5103
+ // One key per call. The server has no whole-object property write, so two
5104
+ // agents writing different keys of the same task never overwrite each other.
5105
+ async setTaskProperty(input) {
5106
+ return await this.t.perform({
5107
+ op: "set_task_property",
5108
+ taskId: input.taskId,
5109
+ key: input.key,
5110
+ value: input.value
5111
+ });
5112
+ }
5113
+ async deleteTaskProperty(input) {
5114
+ return await this.t.perform({
5115
+ op: "delete_task_property",
5116
+ taskId: input.taskId,
5117
+ key: input.key
5118
+ });
5119
+ }
5120
+ /**
5121
+ * The registered property keys with their labels and render kinds. Read this
5122
+ * rather than hardcoding keys — the server can register a new property
5123
+ * without an SDK release.
5124
+ */
5125
+ async listTaskPropertyDefinitions() {
5126
+ return await this.t.perform({ op: "list_task_property_definitions" });
5127
+ }
5071
5128
  startTyping(channelId) {
5072
5129
  const emit = () => {
5073
5130
  void this.t.perform({ op: "report_typing", channelId }).catch((err) => this.logger.warn("startTyping failed", err));