@borgee/agents-host 0.2.26 → 0.2.29

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.
@@ -5,7 +5,7 @@ import { createConnection, createServer } from 'node:net';
5
5
  import { dirname, join, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { AgentsHostSupervisor } from './agents-host-supervisor.js';
8
- import { COMPATIBILITY_GATES_ENV, createManagedRuntimeSettingsFingerprint, INTERNAL_POLICY_MODE_ENV, INTERNAL_PROVIDER_IMPLEMENTATIONS_ENV, MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE, MANAGED_RUNTIME_SETTINGS_SCHEMA_VERSION, resolveManagedRuntimeSettingsSnapshot, normalizeInternalProviderImplementationOverrides, } from './compatibility-gates.js';
8
+ import { 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 { loadConfigFromEnv } from './config.js';
10
10
  import { loadLocalConfigGenerateSpec, materializeLocalConfig, parseGenerateConfigSpec, resolveLocalConfigLayout, } from './local-config.js';
11
11
  import { normalizeManagedRuntimeKey, resolveManagedBootstrapLockPath, resolveManagedDaemonLogPath, resolveManagedDaemonSocketPath, resolveManagedRuntimeRoot, resolveManagedRuntimeSettingsPath, } from './state-paths.js';
@@ -222,15 +222,45 @@ function buildManagedRuntimeSettingsEnv(baseEnv, snapshot) {
222
222
  return {
223
223
  ...baseEnv,
224
224
  [COMPATIBILITY_GATES_ENV]: snapshot.compatibilityGates.join(','),
225
+ [INTERNAL_DISABLED_COMPATIBILITY_GATES_ENV]: resolveDisabledDefaultCompatibilityGates(snapshot.compatibilityGates).join(','),
225
226
  [INTERNAL_PROVIDER_IMPLEMENTATIONS_ENV]: snapshot.providerImplementationOverrides.join(','),
226
227
  [INTERNAL_POLICY_MODE_ENV]: snapshot.internalPolicyMode,
227
228
  };
228
229
  }
229
- function getManagedRuntimeFingerprintMismatch(status, desiredSettings) {
230
- if (!desiredSettings.convergenceEnabled) {
231
- return status.managedRuntimeFingerprint != null;
230
+ function buildDesiredManagedRuntimeSettingsFromSnapshot(snapshot) {
231
+ return {
232
+ snapshot,
233
+ fingerprint: createManagedRuntimeSettingsFingerprint(snapshot),
234
+ convergenceEnabled: snapshot.compatibilityGates.includes(MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE),
235
+ };
236
+ }
237
+ async function getManagedRuntimeFingerprintMismatch(rootPath, status, desiredSettings) {
238
+ if (status.managedRuntimeFingerprint != null) {
239
+ return status.managedRuntimeFingerprint !== desiredSettings.fingerprint;
240
+ }
241
+ if (desiredSettings.convergenceEnabled) {
242
+ return true;
243
+ }
244
+ const persistedSettingsSnapshot = await loadManagedRuntimeSettingsSnapshot(rootPath);
245
+ if (!persistedSettingsSnapshot) {
246
+ return true;
247
+ }
248
+ return createManagedRuntimeSettingsFingerprint(persistedSettingsSnapshot) !== desiredSettings.fingerprint;
249
+ }
250
+ async function resolveAuthoritativeManagedRuntimeSettings(rootPath, effectiveProcessEnv) {
251
+ const persistedSettingsSnapshot = await loadManagedRuntimeSettingsSnapshot(rootPath);
252
+ if (!persistedSettingsSnapshot) {
253
+ return {
254
+ desiredSettings: resolveDesiredManagedRuntimeSettings(effectiveProcessEnv),
255
+ processEnv: effectiveProcessEnv,
256
+ persistedSettingsSnapshot: null,
257
+ };
232
258
  }
233
- return status.managedRuntimeFingerprint !== desiredSettings.fingerprint;
259
+ return {
260
+ desiredSettings: buildDesiredManagedRuntimeSettingsFromSnapshot(persistedSettingsSnapshot),
261
+ processEnv: buildManagedRuntimeSettingsEnv(effectiveProcessEnv, persistedSettingsSnapshot),
262
+ persistedSettingsSnapshot,
263
+ };
234
264
  }
235
265
  function cloneLocalAgentConfig(agent) {
236
266
  return {
@@ -311,6 +341,57 @@ export function buildManagedLocalAgentConfig(options) {
311
341
  },
312
342
  };
313
343
  }
344
+ function mergeManagedLocalAgentConfig(existingAgent, generatedAgent, envOverrides) {
345
+ const overrides = envOverrides ?? {};
346
+ const hasOverride = (key) => Object.prototype.hasOwnProperty.call(overrides, key);
347
+ return {
348
+ ...cloneLocalAgentConfig(existingAgent),
349
+ key: generatedAgent.key,
350
+ apiKey: generatedAgent.apiKey,
351
+ enabled: true,
352
+ name: hasOverride('BORGEE_AGENT_NAME') ? generatedAgent.name : existingAgent.name,
353
+ provider: hasOverride('RUNTIME_PROVIDER') ? generatedAgent.provider : existingAgent.provider,
354
+ claudeCommand: hasOverride('CLAUDE_COMMAND') ? generatedAgent.claudeCommand : existingAgent.claudeCommand,
355
+ claudeArgs: hasOverride('CLAUDE_ARGS')
356
+ ? [...(generatedAgent.claudeArgs ?? [])]
357
+ : [...(existingAgent.claudeArgs ?? [])],
358
+ codexCommand: hasOverride('CODEX_COMMAND') ? generatedAgent.codexCommand : existingAgent.codexCommand,
359
+ codexArgs: hasOverride('CODEX_ARGS')
360
+ ? [...(generatedAgent.codexArgs ?? [])]
361
+ : [...(existingAgent.codexArgs ?? [])],
362
+ copilotCommand: hasOverride('COPILOT_COMMAND')
363
+ ? generatedAgent.copilotCommand
364
+ : existingAgent.copilotCommand,
365
+ copilotArgs: hasOverride('COPILOT_ARGS')
366
+ ? [...(generatedAgent.copilotArgs ?? [])]
367
+ : [...(existingAgent.copilotArgs ?? [])],
368
+ copilotSessionTtlMinutes: hasOverride('COPILOT_SESSION_TTL_MINUTES')
369
+ ? generatedAgent.copilotSessionTtlMinutes
370
+ : existingAgent.copilotSessionTtlMinutes,
371
+ };
372
+ }
373
+ function collectExplicitManagedAgentEnvOverrides(processEnv, optionEnv) {
374
+ const explicitOverrides = {};
375
+ for (const key of [
376
+ 'BORGEE_AGENT_NAME',
377
+ 'RUNTIME_PROVIDER',
378
+ 'CLAUDE_COMMAND',
379
+ 'CLAUDE_ARGS',
380
+ 'CODEX_COMMAND',
381
+ 'CODEX_ARGS',
382
+ 'COPILOT_COMMAND',
383
+ 'COPILOT_ARGS',
384
+ 'COPILOT_SESSION_TTL_MINUTES',
385
+ ]) {
386
+ if (processEnv[key] !== undefined) {
387
+ explicitOverrides[key] = processEnv[key];
388
+ }
389
+ if (optionEnv && Object.prototype.hasOwnProperty.call(optionEnv, key)) {
390
+ explicitOverrides[key] = optionEnv[key];
391
+ }
392
+ }
393
+ return explicitOverrides;
394
+ }
314
395
  function resolveManagedRuntimeLayoutForServerUrl(serverUrl, processEnv = process.env) {
315
396
  const rootPath = resolveManagedRuntimeRoot(serverUrl, processEnv);
316
397
  return {
@@ -884,8 +965,6 @@ export async function bootstrapManagedDaemonStart(options, deps = {}) {
884
965
  ...(options.processEnv ?? process.env),
885
966
  ...options.env,
886
967
  };
887
- const loadSpec = deps.loadSpec
888
- ?? ((hostConfigPath) => loadLocalConfigGenerateSpec(hostConfigPath, { env: effectiveProcessEnv }));
889
968
  const materialize = deps.materialize ?? materializeLocalConfig;
890
969
  const sendRequest = deps.sendRequest ?? sendManagedDaemonRequest;
891
970
  const spawnDaemonProcess = deps.spawnDaemon ?? spawnManagedDaemonProcess;
@@ -895,19 +974,51 @@ export async function bootstrapManagedDaemonStart(options, deps = {}) {
895
974
  ?? (async (rootPath) => {
896
975
  await waitForManagedDaemonShutdown(rootPath, canConnectToSocket);
897
976
  });
898
- const desiredSettings = resolveDesiredManagedRuntimeSettings(effectiveProcessEnv);
899
- const resolvedManagedAgent = options.apiKey === undefined ? null : buildManagedLocalAgentConfig(options);
900
- const resolved = resolvedManagedAgent ??
901
- resolveManagedRuntimeLayoutForServerUrl(options.serverUrl, options.processEnv);
977
+ const requestedSettings = resolveDesiredManagedRuntimeSettings(effectiveProcessEnv);
978
+ const resolved = resolveManagedRuntimeLayoutForServerUrl(options.serverUrl, options.processEnv);
902
979
  const releaseBootstrapLock = await acquireLock(resolved.rootPath);
903
980
  try {
981
+ const currentRuntimeSettings = await resolveAuthoritativeManagedRuntimeSettings(resolved.rootPath, effectiveProcessEnv);
982
+ const loadManagedSpecWithCurrentRuntimeEnv = (hostConfigPath) => deps.loadSpec
983
+ ? deps.loadSpec(hostConfigPath)
984
+ : loadLocalConfigGenerateSpec(hostConfigPath, {
985
+ env: currentRuntimeSettings.processEnv,
986
+ });
987
+ if (options.apiKey !== undefined
988
+ && currentRuntimeSettings.persistedSettingsSnapshot == null
989
+ && await fs.access(resolved.hostConfigPath).then(() => true).catch(() => false)) {
990
+ throw new Error(`Managed runtime root ${resolved.rootPath} predates managed-runtime-settings.json; run \`agents-host start-managed ${options.serverUrl}\` once to migrate it before updating an individual agent`);
991
+ }
992
+ const explicitManagedAgentOverrides = collectExplicitManagedAgentEnvOverrides(options.processEnv ?? process.env, options.env);
993
+ const currentManagedSpec = options.apiKey === undefined
994
+ ? undefined
995
+ : await loadManagedSpecOrUndefined(resolved.hostConfigPath, loadManagedSpecWithCurrentRuntimeEnv);
996
+ const resolvedManagedAgent = options.apiKey === undefined
997
+ ? null
998
+ : (() => {
999
+ const generatedManagedAgent = buildManagedLocalAgentConfig({
1000
+ ...options,
1001
+ processEnv: currentRuntimeSettings.processEnv,
1002
+ env: options.env,
1003
+ });
1004
+ const existingAgent = currentManagedSpec?.agents.find((agent) => agent.key === generatedManagedAgent.agent.key);
1005
+ return existingAgent
1006
+ ? {
1007
+ ...generatedManagedAgent,
1008
+ agent: mergeManagedLocalAgentConfig(existingAgent, generatedManagedAgent.agent, explicitManagedAgentOverrides),
1009
+ }
1010
+ : generatedManagedAgent;
1011
+ })();
1012
+ const desiredBootstrapSettings = options.apiKey === undefined ? requestedSettings : currentRuntimeSettings.desiredSettings;
1013
+ const desiredBootstrapProcessEnv = options.apiKey === undefined ? effectiveProcessEnv : currentRuntimeSettings.processEnv;
904
1014
  let statusResponse = await readReadyManagedDaemonStatus(resolved.rootPath, sendRequest);
905
- if (statusResponse && getManagedRuntimeFingerprintMismatch(statusResponse, desiredSettings)) {
1015
+ if (statusResponse
1016
+ && await getManagedRuntimeFingerprintMismatch(resolved.rootPath, statusResponse, desiredBootstrapSettings)) {
906
1017
  await recycleManagedDaemonWithDesiredSettings({
907
1018
  rootPath: resolved.rootPath,
908
1019
  debug: options.debug === true,
909
- processEnv: effectiveProcessEnv,
910
- desiredSettings,
1020
+ processEnv: desiredBootstrapProcessEnv,
1021
+ desiredSettings: desiredBootstrapSettings,
911
1022
  sendRequest,
912
1023
  waitForShutdown,
913
1024
  spawnDaemonProcess,
@@ -917,11 +1028,17 @@ export async function bootstrapManagedDaemonStart(options, deps = {}) {
917
1028
  }
918
1029
  if (!statusResponse) {
919
1030
  if (options.apiKey === undefined) {
920
- await ensurePersistedManagedAgentsExist(options.serverUrl, resolved.hostConfigPath, loadSpec);
1031
+ await ensurePersistedManagedAgentsExist(options.serverUrl, resolved.hostConfigPath, (hostConfigPath) => options.apiKey === undefined
1032
+ ? (deps.loadSpec
1033
+ ? deps.loadSpec(hostConfigPath)
1034
+ : loadLocalConfigGenerateSpec(hostConfigPath, {
1035
+ env: desiredBootstrapProcessEnv,
1036
+ }))
1037
+ : loadManagedSpecWithCurrentRuntimeEnv(hostConfigPath));
921
1038
  }
922
1039
  else {
923
1040
  try {
924
- await loadSpec(resolved.hostConfigPath);
1041
+ await loadManagedSpecWithCurrentRuntimeEnv(resolved.hostConfigPath);
925
1042
  }
926
1043
  catch (error) {
927
1044
  if (isNotFoundError(error)) {
@@ -938,8 +1055,8 @@ export async function bootstrapManagedDaemonStart(options, deps = {}) {
938
1055
  await startManagedDaemonWithPersistedSettings({
939
1056
  rootPath: resolved.rootPath,
940
1057
  debug: options.debug === true,
941
- processEnv: effectiveProcessEnv,
942
- desiredSettings,
1058
+ processEnv: desiredBootstrapProcessEnv,
1059
+ desiredSettings: desiredBootstrapSettings,
943
1060
  spawnDaemonProcess,
944
1061
  waitForDaemon,
945
1062
  sendRequest,
@@ -948,7 +1065,11 @@ export async function bootstrapManagedDaemonStart(options, deps = {}) {
948
1065
  }
949
1066
  if (options.apiKey === undefined) {
950
1067
  if (!statusResponse) {
951
- await ensurePersistedManagedAgentsExist(options.serverUrl, resolved.hostConfigPath, loadSpec);
1068
+ await ensurePersistedManagedAgentsExist(options.serverUrl, resolved.hostConfigPath, (hostConfigPath) => deps.loadSpec
1069
+ ? deps.loadSpec(hostConfigPath)
1070
+ : loadLocalConfigGenerateSpec(hostConfigPath, {
1071
+ env: desiredBootstrapProcessEnv,
1072
+ }));
952
1073
  }
953
1074
  else {
954
1075
  assertManagedRuntimeBinding(statusResponse.borgeeBaseUrl, options.serverUrl);
@@ -1079,10 +1200,11 @@ async function terminateChildProcessAndWait(child) {
1079
1200
  }
1080
1201
  await waitForChildExit(child, DAEMON_FORCE_KILL_WAIT_MS);
1081
1202
  }
1082
- async function rollbackManagedApplyFailure(rootPath, previousSpec, materialize, clearRuntimeImpl, applyError) {
1203
+ async function rollbackManagedApplyFailure(rootPath, previousSpec, previousSettingsSnapshot, materialize, clearRuntimeImpl, applyError) {
1083
1204
  const original = toManagedErrorPayload(applyError, 'MANAGED_RELOAD_FAILED');
1084
1205
  try {
1085
1206
  if (previousSpec) {
1207
+ await restoreManagedRuntimeSettingsSnapshot(rootPath, previousSettingsSnapshot);
1086
1208
  await materialize(rootPath, cloneLocalConfigGenerateSpec(previousSpec));
1087
1209
  }
1088
1210
  else {
@@ -1139,8 +1261,6 @@ function validateDaemonUpsertAgentInput(currentSpec, serverUrl, agent, env = pro
1139
1261
  }
1140
1262
  export async function describeManagedSpec(options, deps = {}) {
1141
1263
  const effectiveProcessEnv = options.processEnv ?? process.env;
1142
- const loadSpec = deps.loadSpec
1143
- ?? ((hostConfigPath) => loadLocalConfigGenerateSpec(hostConfigPath, { env: effectiveProcessEnv }));
1144
1264
  const sendRequest = deps.sendRequest ?? sendManagedDaemonRequest;
1145
1265
  const resolved = resolveManagedRuntimeLayoutForServerUrl(options.serverUrl, options.processEnv);
1146
1266
  try {
@@ -1167,7 +1287,12 @@ export async function describeManagedSpec(options, deps = {}) {
1167
1287
  }
1168
1288
  }
1169
1289
  try {
1170
- const persistedSpec = await loadManagedSpecOrUndefined(resolved.hostConfigPath, loadSpec);
1290
+ const currentRuntimeSettings = await resolveAuthoritativeManagedRuntimeSettings(resolved.rootPath, effectiveProcessEnv);
1291
+ const persistedSpec = await loadManagedSpecOrUndefined(resolved.hostConfigPath, (hostConfigPath) => deps.loadSpec
1292
+ ? deps.loadSpec(hostConfigPath)
1293
+ : loadLocalConfigGenerateSpec(hostConfigPath, {
1294
+ env: currentRuntimeSettings.processEnv,
1295
+ }));
1171
1296
  if (!persistedSpec) {
1172
1297
  return { ok: true, present: false };
1173
1298
  }
@@ -1207,27 +1332,13 @@ async function applySpecToRunningManagedDaemon(rootPath, borgeeBaseUrl, spec, se
1207
1332
  }
1208
1333
  export async function applyManagedSpec(options, deps = {}) {
1209
1334
  const effectiveProcessEnv = options.processEnv ?? process.env;
1210
- const loadSpec = deps.loadSpec
1211
- ?? ((hostConfigPath) => loadLocalConfigGenerateSpec(hostConfigPath, { env: effectiveProcessEnv }));
1212
- const materialize = deps.materialize
1213
- ?? ((rootPath, spec) => materializeLocalConfig(rootPath, spec, { env: effectiveProcessEnv }));
1335
+ const desiredSettings = resolveDesiredManagedRuntimeSettings(effectiveProcessEnv);
1214
1336
  const sendRequest = deps.sendRequest ?? sendManagedDaemonRequest;
1215
1337
  const spawnDaemonProcess = deps.spawnDaemon ?? spawnManagedDaemonProcess;
1216
1338
  const waitForDaemon = deps.waitForDaemonReady ?? waitForManagedDaemonReady;
1217
1339
  const acquireLock = deps.acquireBootstrapLock ?? acquireBootstrapLock;
1218
1340
  const clearManagedRuntimeImpl = deps.clearManagedRuntime ?? clearManagedRuntime;
1219
1341
  const resolved = resolveManagedRuntimeLayoutForServerUrl(options.serverUrl, options.processEnv);
1220
- let spec;
1221
- try {
1222
- spec = validateApplyManagedInput(options.spec, options.serverUrl, effectiveProcessEnv);
1223
- }
1224
- catch (error) {
1225
- if (isManagedOperationError(error)) {
1226
- return toManagedErrorResponse(error);
1227
- }
1228
- return toManagedErrorResponse(managedOperationError('MANAGED_SPEC_INVALID', `Invalid managed spec: ${toErrorMessage(error)}`));
1229
- }
1230
- const desiredSettings = resolveDesiredManagedRuntimeSettings(effectiveProcessEnv);
1231
1342
  const waitForShutdown = deps.waitForShutdown
1232
1343
  ?? (async (rootPath) => {
1233
1344
  await waitForManagedDaemonShutdown(rootPath, canConnectToSocket);
@@ -1240,6 +1351,32 @@ export async function applyManagedSpec(options, deps = {}) {
1240
1351
  return toApplyManagedColdStartErrorResponse(error);
1241
1352
  }
1242
1353
  try {
1354
+ const currentRuntimeSettings = await resolveAuthoritativeManagedRuntimeSettings(resolved.rootPath, effectiveProcessEnv);
1355
+ const loadSpecWithCurrentRuntimeEnv = (hostConfigPath) => deps.loadSpec
1356
+ ? deps.loadSpec(hostConfigPath)
1357
+ : loadLocalConfigGenerateSpec(hostConfigPath, {
1358
+ env: currentRuntimeSettings.processEnv,
1359
+ });
1360
+ const materializeWithDesiredEnv = (rootPath, nextSpec) => deps.materialize
1361
+ ? deps.materialize(rootPath, nextSpec)
1362
+ : materializeLocalConfig(rootPath, nextSpec, {
1363
+ env: effectiveProcessEnv,
1364
+ });
1365
+ const materializeWithCurrentRuntimeEnv = (rootPath, nextSpec) => deps.materialize
1366
+ ? deps.materialize(rootPath, nextSpec)
1367
+ : materializeLocalConfig(rootPath, nextSpec, {
1368
+ env: currentRuntimeSettings.processEnv,
1369
+ });
1370
+ let spec;
1371
+ try {
1372
+ spec = validateApplyManagedInput(options.spec, options.serverUrl, effectiveProcessEnv);
1373
+ }
1374
+ catch (error) {
1375
+ if (isManagedOperationError(error)) {
1376
+ return toManagedErrorResponse(error);
1377
+ }
1378
+ return toManagedErrorResponse(managedOperationError('MANAGED_SPEC_INVALID', `Invalid managed spec: ${toErrorMessage(error)}`));
1379
+ }
1243
1380
  let statusResponse;
1244
1381
  try {
1245
1382
  statusResponse = await readReadyManagedDaemonStatus(resolved.rootPath, sendRequest);
@@ -1250,7 +1387,8 @@ export async function applyManagedSpec(options, deps = {}) {
1250
1387
  }
1251
1388
  throw error;
1252
1389
  }
1253
- if (statusResponse && getManagedRuntimeFingerprintMismatch(statusResponse, desiredSettings)) {
1390
+ if (statusResponse
1391
+ && await getManagedRuntimeFingerprintMismatch(resolved.rootPath, statusResponse, desiredSettings)) {
1254
1392
  try {
1255
1393
  await recycleManagedDaemonWithDesiredSettings({
1256
1394
  rootPath: resolved.rootPath,
@@ -1273,7 +1411,7 @@ export async function applyManagedSpec(options, deps = {}) {
1273
1411
  }
1274
1412
  let previousSpec;
1275
1413
  try {
1276
- previousSpec = await loadManagedSpecOrUndefined(resolved.hostConfigPath, loadSpec);
1414
+ previousSpec = await loadManagedSpecOrUndefined(resolved.hostConfigPath, loadSpecWithCurrentRuntimeEnv);
1277
1415
  if (previousSpec) {
1278
1416
  assertManagedSpecBinding(previousSpec, options.serverUrl);
1279
1417
  }
@@ -1282,10 +1420,10 @@ export async function applyManagedSpec(options, deps = {}) {
1282
1420
  return toApplyManagedColdStartErrorResponse(error);
1283
1421
  }
1284
1422
  try {
1285
- await materialize(resolved.rootPath, cloneLocalConfigGenerateSpec(spec));
1423
+ await materializeWithDesiredEnv(resolved.rootPath, cloneLocalConfigGenerateSpec(spec));
1286
1424
  }
1287
1425
  catch (error) {
1288
- return rollbackManagedApplyFailure(resolved.rootPath, previousSpec, materialize, clearManagedRuntimeImpl, error);
1426
+ return rollbackManagedApplyFailure(resolved.rootPath, previousSpec, currentRuntimeSettings.persistedSettingsSnapshot ?? undefined, materializeWithCurrentRuntimeEnv, clearManagedRuntimeImpl, error);
1289
1427
  }
1290
1428
  try {
1291
1429
  await startManagedDaemonWithPersistedSettings({
@@ -1300,7 +1438,7 @@ export async function applyManagedSpec(options, deps = {}) {
1300
1438
  const status = await sendRequest(resolved.rootPath, { type: 'status' });
1301
1439
  if (!status.ok) {
1302
1440
  const managedError = getManagedErrorPayload(status);
1303
- return rollbackManagedApplyFailure(resolved.rootPath, previousSpec, materialize, clearManagedRuntimeImpl, managedOperationError(managedError.code, managedError.message));
1441
+ return rollbackManagedApplyFailure(resolved.rootPath, previousSpec, currentRuntimeSettings.persistedSettingsSnapshot ?? undefined, materializeWithCurrentRuntimeEnv, clearManagedRuntimeImpl, managedOperationError(managedError.code, managedError.message));
1304
1442
  }
1305
1443
  if (status.type !== 'status') {
1306
1444
  throw new Error('Unexpected managed daemon response for status');
@@ -1314,7 +1452,7 @@ export async function applyManagedSpec(options, deps = {}) {
1314
1452
  };
1315
1453
  }
1316
1454
  catch (error) {
1317
- return rollbackManagedApplyFailure(resolved.rootPath, previousSpec, materialize, clearManagedRuntimeImpl, error);
1455
+ return rollbackManagedApplyFailure(resolved.rootPath, previousSpec, currentRuntimeSettings.persistedSettingsSnapshot ?? undefined, materializeWithCurrentRuntimeEnv, clearManagedRuntimeImpl, error);
1318
1456
  }
1319
1457
  }
1320
1458
  finally {
@@ -1350,9 +1488,7 @@ export class ManagedAgentsHostDaemon {
1350
1488
  this.loadSpec = deps.loadSpec ?? loadLocalConfigGenerateSpec;
1351
1489
  this.materialize = deps.materialize ?? materializeLocalConfig;
1352
1490
  const managedRuntimeSettings = resolveDesiredManagedRuntimeSettings(process.env);
1353
- this.managedRuntimeFingerprint = managedRuntimeSettings.convergenceEnabled
1354
- ? managedRuntimeSettings.fingerprint
1355
- : null;
1491
+ this.managedRuntimeFingerprint = managedRuntimeSettings.fingerprint;
1356
1492
  this.logger = deps.logger ?? console;
1357
1493
  this.logPath = deps.logPath ?? null;
1358
1494
  this.server = createServer((socket) => {
@@ -1,5 +1,17 @@
1
1
  export const HOST_CONTROL_PREFIX = '[[BORGEE_CONTROL]] ';
2
2
  export const AWAITING_USER_CONTROL_PREFIX = HOST_CONTROL_PREFIX;
3
+ const VALID_ATTENTION_UPDATES = new Set([
4
+ 'follow-channel',
5
+ 'unfollow-channel',
6
+ 'mute-channel',
7
+ 'claim-channel',
8
+ 'claim-task-thread',
9
+ 'unclaim-channel',
10
+ 'unclaim-task-thread',
11
+ ]);
12
+ function isProviderAttentionUpdate(value) {
13
+ return typeof value === 'string' && VALID_ATTENTION_UPDATES.has(value);
14
+ }
3
15
  function hasVisibleText(value) {
4
16
  return value.trim().length > 0;
5
17
  }
@@ -50,13 +62,26 @@ function parseControlPayload(payload) {
50
62
  if (typeof record.kind !== 'string') {
51
63
  return null;
52
64
  }
65
+ const attentionUpdate = (() => {
66
+ if (record.attentionUpdate === undefined) {
67
+ return undefined;
68
+ }
69
+ return isProviderAttentionUpdate(record.attentionUpdate) ? record.attentionUpdate : null;
70
+ })();
71
+ if (attentionUpdate === null) {
72
+ return null;
73
+ }
53
74
  if (record.kind === 'continue-to-peer' || record.kind === 'conclude-locally') {
54
- return Object.keys(record).length === 1
55
- ? { kind: record.kind }
75
+ return Object.keys(record).every((key) => key === 'kind' || key === 'attentionUpdate')
76
+ ? {
77
+ kind: record.kind,
78
+ ...(attentionUpdate ? { attentionUpdate } : {}),
79
+ }
56
80
  : null;
57
81
  }
58
82
  if (record.kind === 'start-protocol') {
59
- return Object.keys(record).length === 2
83
+ return attentionUpdate === undefined
84
+ && Object.keys(record).length === 2
60
85
  && typeof record.rounds === 'number'
61
86
  && Number.isInteger(record.rounds)
62
87
  && record.rounds > 0
@@ -68,7 +93,7 @@ function parseControlPayload(payload) {
68
93
  }
69
94
  if (record.kind === 'awaiting-user') {
70
95
  const keys = Object.keys(record);
71
- if (keys.some((key) => key !== 'kind' && key !== 'question' && key !== 'reason')) {
96
+ if (keys.some((key) => key !== 'kind' && key !== 'question' && key !== 'reason' && key !== 'attentionUpdate')) {
72
97
  return null;
73
98
  }
74
99
  if (typeof record.question !== 'string' || record.question.trim().length === 0) {
@@ -82,8 +107,17 @@ function parseControlPayload(payload) {
82
107
  kind: 'awaiting-user',
83
108
  question: record.question.trim(),
84
109
  ...(typeof record.reason === 'string' ? { reason: record.reason.trim() } : {}),
110
+ ...(attentionUpdate ? { attentionUpdate } : {}),
85
111
  };
86
112
  }
113
+ if (record.kind === 'attention-only') {
114
+ return Object.keys(record).length === 2 && attentionUpdate
115
+ ? {
116
+ kind: 'attention-only',
117
+ attentionUpdate,
118
+ }
119
+ : null;
120
+ }
87
121
  return null;
88
122
  }
89
123
  export function parseProviderReply(text) {
@@ -98,8 +132,15 @@ export function parseProviderReply(text) {
98
132
  controlMalformed: true,
99
133
  };
100
134
  }
135
+ const textWithoutFooter = stripTrailingCarriageReturn(match.analyzedText.slice(0, match.removalStart));
136
+ if (control.kind === 'attention-only' && !hasVisibleText(textWithoutFooter)) {
137
+ return {
138
+ text: textWithoutFooter,
139
+ controlMalformed: true,
140
+ };
141
+ }
101
142
  return {
102
- text: stripTrailingCarriageReturn(match.analyzedText.slice(0, match.removalStart)),
143
+ text: textWithoutFooter,
103
144
  control,
104
145
  ...(control.kind === 'awaiting-user'
105
146
  ? {
@@ -1,4 +1,8 @@
1
1
  import { basename } from 'node:path';
2
+ import { buildAttentionSummaryLines } from '../../context/attention.js';
3
+ import { buildCollaborationCapabilityDeclarationSummaryLines, buildMissedCollaborationDiagnosticSummaryLines, } from '../../context/collaboration-capabilities-diagnostics.js';
4
+ import { buildCollaborationOutcomeSummaryLines } from '../../context/collaboration-outcome.js';
5
+ import { buildTaskThreadCollaborationSummaryLines } from '../../context/task-thread-collaboration.js';
2
6
  const PROJECT_DOC_MAX_BYTES = 32 * 1024;
3
7
  function buildSkillRuntimeLines(context) {
4
8
  if (!context?.skillRuntime || !context.channelContextPayloadPath) {
@@ -47,6 +51,26 @@ export function buildCodexProjectDocument(context) {
47
51
  'That writable task workspace is local to agents-host for the current task thread and does not imply that the target repository has already been checked out there.',
48
52
  ]
49
53
  : []),
54
+ ...(() => {
55
+ const collaborationOutcomeLines = buildCollaborationOutcomeSummaryLines(context?.collaborationOutcome);
56
+ return collaborationOutcomeLines.length > 0 ? ['', ...collaborationOutcomeLines] : [];
57
+ })(),
58
+ ...(() => {
59
+ const attentionLines = buildAttentionSummaryLines(context?.attentionSnapshot);
60
+ return attentionLines.length > 0 ? ['', ...attentionLines] : [];
61
+ })(),
62
+ ...(() => {
63
+ const capabilityLines = buildCollaborationCapabilityDeclarationSummaryLines(context?.collaborationCapabilities);
64
+ return capabilityLines.length > 0 ? ['', ...capabilityLines] : [];
65
+ })(),
66
+ ...(() => {
67
+ const diagnosticLines = buildMissedCollaborationDiagnosticSummaryLines(context?.missedCollaborationDiagnostic);
68
+ return diagnosticLines.length > 0 ? ['', ...diagnosticLines] : [];
69
+ })(),
70
+ ...(() => {
71
+ const taskThreadLines = buildTaskThreadCollaborationSummaryLines(context?.taskThreadCollaborationContract);
72
+ return taskThreadLines.length > 0 ? ['', ...taskThreadLines] : [];
73
+ })(),
50
74
  ...(() => {
51
75
  const skillRuntimeLines = buildSkillRuntimeLines(context);
52
76
  return skillRuntimeLines.length > 0 ? ['', ...skillRuntimeLines] : [];
@@ -11,6 +11,7 @@ export declare function resolveAgentCursorPath(stateRootDir: string, agentId: st
11
11
  export declare function resolveSharedProtocolKickoffDecisionPath(stateRootDir: string, channelId: string, anchorMessageId: string): string;
12
12
  export declare function resolveSharedProtocolStatusPath(stateRootDir: string, channelId: string, anchorMessageId: string): string;
13
13
  export declare function resolveConnectionsStatePath(stateRootDir: string): string;
14
+ export declare function resolveAttentionStatePath(stateRootDir: string, channelId: string): string;
14
15
  export declare function resolveAuthorizationAuditPath(stateRootDir: string): string;
15
16
  export declare function resolvePreviousAuthorizationAuditPath(stateRootDir: string): string;
16
17
  export declare function resolveClaudeSessionMapPath(stateRootDir: string, agentId: string): string;
@@ -110,6 +110,9 @@ export function resolveSharedProtocolStatusPath(stateRootDir, channelId, anchorM
110
110
  export function resolveConnectionsStatePath(stateRootDir) {
111
111
  return join(stateRootDir, 'connections-state.sqlite');
112
112
  }
113
+ export function resolveAttentionStatePath(stateRootDir, channelId) {
114
+ return join(stateRootDir, 'attention-state', `${encodeSegment(channelId)}.json`);
115
+ }
113
116
  export function resolveAuthorizationAuditPath(stateRootDir) {
114
117
  return join(stateRootDir, 'authorization-audit.jsonl');
115
118
  }
package/dist/types.d.ts CHANGED
@@ -140,6 +140,83 @@ export interface CollaborationDraftSnapshot {
140
140
  finalMessageId?: string;
141
141
  }
142
142
  export type ProviderCollaborationTurnMode = 'ordinary' | 'silent-kickoff' | 'protocol-managed';
143
+ export type CollaborationOutcomeResponseState = 'responded' | 'blocked' | 'superseded' | 'failed';
144
+ export type CollaborationOutcomeDeliveryState = 'posted' | 'draft-finalized' | 'not-delivered' | 'delivery-failed';
145
+ export type CollaborationOutcomeWakeState = 'waiting' | 'suppressed-agent' | 'resumed-human';
146
+ export interface CollaborationOutcomeBlockedDetails {
147
+ question: string;
148
+ reason?: string;
149
+ }
150
+ /**
151
+ * Shared host-side projection only. This is the latest host-observed snapshot
152
+ * that adapters may surface back to providers; it is not provider-authoritative
153
+ * truth about collaboration state or recovery.
154
+ */
155
+ export interface CollaborationOutcomeSnapshot {
156
+ source: 'host-observed';
157
+ responseState: CollaborationOutcomeResponseState;
158
+ deliveryState: CollaborationOutcomeDeliveryState;
159
+ wakeState?: CollaborationOutcomeWakeState;
160
+ blockedDetails?: CollaborationOutcomeBlockedDetails;
161
+ observedAt: number;
162
+ turnExecutionId?: string;
163
+ }
164
+ export type AttentionDeliveryMode = 'default' | 'follow' | 'muted';
165
+ export type AttentionClaimState = 'unclaimed' | 'claimed';
166
+ export type AttentionClaimActivation = 'inactive' | 'active' | 'dormant';
167
+ export type AttentionWakeMode = 'current-policy' | 'follow-visible-human' | 'mute-non-mention';
168
+ export type ProviderAttentionUpdate = 'follow-channel' | 'unfollow-channel' | 'mute-channel' | 'claim-channel' | 'claim-task-thread' | 'unclaim-channel' | 'unclaim-task-thread';
169
+ export interface AttentionClaimContext {
170
+ scope: 'channel' | 'task-thread';
171
+ taskId: string;
172
+ }
173
+ /**
174
+ * Shared host-side projection only. This is the latest host-observed snapshot
175
+ * that adapters may surface back to providers; it is not server-authoritative
176
+ * truth about delivery policy, fanout, or ownership.
177
+ */
178
+ export interface AttentionSnapshot {
179
+ source: 'host-observed';
180
+ deliveryMode: AttentionDeliveryMode;
181
+ claimState: AttentionClaimState;
182
+ claimActivation: AttentionClaimActivation;
183
+ wakeMode: AttentionWakeMode;
184
+ observedAt: number;
185
+ turnExecutionId?: string;
186
+ claimContext?: AttentionClaimContext;
187
+ }
188
+ export type TaskThreadCollaborationTurnRole = 'assignment' | 'continuation';
189
+ export type TaskThreadMainResultContract = 'ordinary-final-reply-in-thread';
190
+ export type TaskThreadAuxiliaryRouteContract = 'optional-auxiliary-send-or-escalation-only';
191
+ export type TaskThreadCheckInContract = 'status-update-intent-only';
192
+ export type TaskThreadBlockedWorkContract = 'in-thread-disclosure-only';
193
+ /**
194
+ * Shared host-side projection only. This is a narrow adapter-facing summary of
195
+ * task-thread collaboration meaning; it is not runtime-authoritative truth
196
+ * about task ownership, task routes, heartbeat execution, or blocked state.
197
+ */
198
+ export interface TaskThreadCollaborationContract {
199
+ source: 'host-projected';
200
+ turnRole: TaskThreadCollaborationTurnRole;
201
+ taskThreadContextActive: true;
202
+ mainResult: TaskThreadMainResultContract;
203
+ auxiliaryRoute: TaskThreadAuxiliaryRouteContract;
204
+ checkIn: TaskThreadCheckInContract;
205
+ blockedWork: TaskThreadBlockedWorkContract;
206
+ }
207
+ export interface CollaborationCapabilityDeclaration {
208
+ collaborationOutcome?: true;
209
+ attentionSnapshot?: true;
210
+ taskThreadCollaborationContract?: true;
211
+ missedCollaborationDiagnostic?: true;
212
+ recoveryExplanation?: 'runtime-local-only';
213
+ }
214
+ export type MissedCollaborationDiagnosticStage = 'delivery' | 'wake' | 'response';
215
+ export type MissedCollaborationDiagnosticReason = 'delivery-failed' | 'not-delivered' | 'wake-suppressed-attention-policy' | 'blocked-awaiting-user';
216
+ export interface MissedCollaborationDiagnostic {
217
+ stage: MissedCollaborationDiagnosticStage;
218
+ reason: MissedCollaborationDiagnosticReason;
219
+ }
143
220
  export interface ProviderCollaborationContext {
144
221
  enabled: boolean;
145
222
  turnExecutionId?: string;
@@ -158,6 +235,11 @@ export interface ProviderInput {
158
235
  incomingEventKind?: string;
159
236
  incomingMessageType?: string;
160
237
  collaboration?: ProviderCollaborationContext;
238
+ collaborationOutcome?: CollaborationOutcomeSnapshot;
239
+ attentionSnapshot?: AttentionSnapshot;
240
+ taskThreadCollaborationContract?: TaskThreadCollaborationContract;
241
+ collaborationCapabilities?: CollaborationCapabilityDeclaration;
242
+ missedCollaborationDiagnostic?: MissedCollaborationDiagnostic;
161
243
  }
162
244
  export interface ProviderProtocolContext {
163
245
  anchorMessageId: string;
@@ -210,6 +292,11 @@ export interface PreparedPromptContext {
210
292
  gatewayAuthPath?: string;
211
293
  collaborationTurnExecutionId?: string;
212
294
  collaborationTurnMode?: ProviderCollaborationTurnMode;
295
+ collaborationOutcome?: CollaborationOutcomeSnapshot;
296
+ attentionSnapshot?: AttentionSnapshot;
297
+ taskThreadCollaborationContract?: TaskThreadCollaborationContract;
298
+ collaborationCapabilities?: CollaborationCapabilityDeclaration;
299
+ missedCollaborationDiagnostic?: MissedCollaborationDiagnostic;
213
300
  kickoff?: ProviderProtocolKickoffContext;
214
301
  protocol?: ProviderProtocolContext;
215
302
  grounding?: ProviderTurnGroundingContext;
@@ -234,16 +321,22 @@ export interface ProviderAwaitingUser {
234
321
  question: string;
235
322
  reason?: string;
236
323
  }
237
- export type ProviderTurnControl = {
324
+ export interface ProviderTurnControlBase {
325
+ attentionUpdate?: ProviderAttentionUpdate;
326
+ }
327
+ export type ProviderTurnControl = ({
238
328
  kind: 'continue-to-peer';
239
- } | {
329
+ } & ProviderTurnControlBase) | ({
240
330
  kind: 'conclude-locally';
241
- } | {
331
+ } & ProviderTurnControlBase) | {
242
332
  kind: 'start-protocol';
243
333
  rounds: number;
244
334
  } | ({
245
335
  kind: 'awaiting-user';
246
- } & ProviderAwaitingUser);
336
+ } & ProviderAwaitingUser & ProviderTurnControlBase) | ({
337
+ kind: 'attention-only';
338
+ attentionUpdate: ProviderAttentionUpdate;
339
+ } & ProviderTurnControlBase);
247
340
  export interface ProviderReply {
248
341
  text: string;
249
342
  awaitingUser?: ProviderAwaitingUser;