agent-transport-system 0.3.2 → 0.3.3

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.
package/dist/ats.js CHANGED
@@ -24,11 +24,11 @@ import wrapAnsi from "wrap-ansi";
24
24
  import { Box, Container, Editor, Key, ProcessTerminal, TUI, Text, getEditorKeybindings, matchesKey } from "@mariozechner/pi-tui";
25
25
 
26
26
  //#region package.json
27
- var version$1 = "0.3.2";
27
+ var version$1 = "0.3.3";
28
28
  var package_default = {
29
29
  name: "agent-transport-system",
30
30
  version: version$1,
31
- atsReleaseDate: "2026-04-16",
31
+ atsReleaseDate: "2026-04-17",
32
32
  description: "Agent Transport System CLI - https://ats.sh",
33
33
  license: "MIT",
34
34
  type: "module",
@@ -28220,7 +28220,28 @@ function resolveBootstrapCommandUnavailableSummary(message) {
28220
28220
 
28221
28221
  //#endregion
28222
28222
  //#region src/system/reply-readiness.ts
28223
- const REPLY_READINESS_REASON_PRIORITY = [...REPLY_READINESS_REASON_CODE_VALUES];
28223
+ const REPLY_READINESS_REASON_PRIORITY = [
28224
+ "profile.inactive",
28225
+ "profile.unbound",
28226
+ "controller.binding.disabled",
28227
+ "projection.missing",
28228
+ "controller.registry.disabled",
28229
+ "controller.launch.choice_required",
28230
+ "controller.launch.needs_repair",
28231
+ "workspace.missing",
28232
+ "workspace.invalid",
28233
+ "runtime.adapter.unsupported",
28234
+ "controller.bootstrap.failed",
28235
+ "controller.gateway.unhealthy",
28236
+ "service.not_installed",
28237
+ "service.drifted",
28238
+ "service.not_running",
28239
+ "dispatch.storage_not_ready",
28240
+ "service.gateway_chain_unhealthy",
28241
+ "local_agents.none_needed",
28242
+ "route.offline",
28243
+ "route.not_registered"
28244
+ ];
28224
28245
  const MANUAL_SPACE_PARTICIPATION_REASON = "You can still join the space and send messages yourself";
28225
28246
  async function resolveDeviceReplyReadinessStatus(input = {}) {
28226
28247
  const status = input.status ?? await getDaemonStatus();
@@ -47467,7 +47488,10 @@ async function runDaemon(input) {
47467
47488
  return;
47468
47489
  }
47469
47490
  if (canUseInteractivePrompts(runtime)) {
47470
- await runInteractiveDaemonMenu({ view: runtime.resolvedView });
47491
+ await runInteractiveDaemonMenu({
47492
+ gatewayUrl: input.gatewayUrl,
47493
+ view: runtime.resolvedView
47494
+ });
47471
47495
  return;
47472
47496
  }
47473
47497
  presenter.line({
@@ -48693,6 +48717,7 @@ async function runDaemonStatusFlow(input, options = {}) {
48693
48717
  await runDaemonStatus(input);
48694
48718
  if (!(options.offerStartPrompt && interactiveHuman)) return "completed";
48695
48719
  return await maybeOfferDaemonStartAfterStatus({
48720
+ gatewayUrl: input.gatewayUrl,
48696
48721
  view: "human",
48697
48722
  ownerUserId
48698
48723
  });
@@ -49855,6 +49880,7 @@ async function runDaemonBackgroundStartForDecision(input) {
49855
49880
  const previousExitCode = process.exitCode;
49856
49881
  const result = await runDaemonRun({
49857
49882
  agentOverviewHandled: true,
49883
+ gatewayUrl: input.gatewayUrl,
49858
49884
  mode: "background",
49859
49885
  view: input.view
49860
49886
  });
@@ -50937,7 +50963,10 @@ async function handleInteractiveDaemonMenuChoice(input, choice) {
50937
50963
  }
50938
50964
  if (choice === DAEMON_MENU_CHOICE.reinstall) {
50939
50965
  const result = await runDaemonReinstall(input, { suppressInstallOutput: true });
50940
- if (isDaemonReinstallCompletedAndStopped(result)) return await maybeOfferDaemonStartAfterStatus({ view: input.view }) === "cancelled";
50966
+ if (isDaemonReinstallCompletedAndStopped(result)) return await maybeOfferDaemonStartAfterStatus({
50967
+ gatewayUrl: input.gatewayUrl,
50968
+ view: input.view
50969
+ }) === "cancelled";
50941
50970
  return result.result === "cancelled";
50942
50971
  }
50943
50972
  return await runDaemonUninstall(input) === "cancelled";
@@ -50948,7 +50977,10 @@ async function maybeOfferDaemonStartAfterStatus(input) {
50948
50977
  localDemand: input.localDemand
50949
50978
  })).offerStartPrompt) return "completed";
50950
50979
  const presenter = createPresenter(await resolveRuntimeContext({ view: input.view }));
50951
- const decision = await runDaemonStartDecisionPrompt({ startService: async () => await runDaemonBackgroundStartForDecision({ view: input.view }) });
50980
+ const decision = await runDaemonStartDecisionPrompt({ startService: async () => await runDaemonBackgroundStartForDecision({
50981
+ gatewayUrl: input.gatewayUrl,
50982
+ view: input.view
50983
+ }) });
50952
50984
  if (decision === "cancelled") return "cancelled";
50953
50985
  if (decision === "deferred_with_card") return "completed";
50954
50986
  if (decision === "not_now") {
@@ -51363,12 +51395,18 @@ async function runDaemonServiceParticipationInstallStage(input) {
51363
51395
  return { status: refreshedStatus };
51364
51396
  }
51365
51397
  async function runDaemonServiceParticipationActionStage(input) {
51366
- if (input.status.kind === "needs_repair") return mapDaemonBackgroundStartResult(await runDaemonBackgroundStartForDecision({ view: input.input.view }), {
51398
+ if (input.status.kind === "needs_repair") return mapDaemonBackgroundStartResult(await runDaemonBackgroundStartForDecision({
51399
+ gatewayUrl: input.input.gatewayUrl,
51400
+ view: input.input.view
51401
+ }), {
51367
51402
  presenter: input.input.presenter,
51368
51403
  codePrefix: input.codePrefix
51369
51404
  });
51370
51405
  if (input.status.kind !== "not_running") return { status: "aligned" };
51371
- return mapDaemonBackgroundStartResult(await runDaemonStartDecisionPrompt({ startService: async () => await runDaemonBackgroundStartForDecision({ view: input.input.view }) }), {
51406
+ return mapDaemonBackgroundStartResult(await runDaemonStartDecisionPrompt({ startService: async () => await runDaemonBackgroundStartForDecision({
51407
+ gatewayUrl: input.input.gatewayUrl,
51408
+ view: input.input.view
51409
+ }) }), {
51372
51410
  presenter: input.input.presenter,
51373
51411
  codePrefix: input.codePrefix
51374
51412
  });
@@ -51495,7 +51533,10 @@ async function runServiceCheck(input) {
51495
51533
  return { status: "cancelled" };
51496
51534
  }
51497
51535
  if (selected === CONTINUE_WITHOUT_SERVICE) return { status: "continue" };
51498
- if (!await fixServiceStatus(serviceStatus)) return { status: "continue" };
51536
+ if (!await fixServiceStatus({
51537
+ gatewayUrl: input.context.gatewayUrl,
51538
+ serviceStatus
51539
+ })) return { status: "continue" };
51499
51540
  return { status: "continue" };
51500
51541
  }
51501
51542
  async function resolveServiceStatus(input) {
@@ -51549,16 +51590,16 @@ async function resolveServiceStatus(input) {
51549
51590
  };
51550
51591
  });
51551
51592
  }
51552
- async function fixServiceStatus(serviceStatus) {
51593
+ async function fixServiceStatus(input) {
51553
51594
  const expectedVersion = resolveCurrentDaemonExpectedVersion();
51554
- if (serviceStatus.kind === "missing") {
51595
+ if (input.serviceStatus.kind === "missing") {
51555
51596
  await ensureDaemonInstalledForCliStartup({
51556
51597
  daemonVersion: expectedVersion,
51557
51598
  mode: "always"
51558
51599
  });
51559
51600
  return true;
51560
51601
  }
51561
- if (serviceStatus.kind === "outdated") {
51602
+ if (input.serviceStatus.kind === "outdated") {
51562
51603
  const daemonStatus = await getDaemonStatus();
51563
51604
  await ensureDaemonVersionForCliStartup({
51564
51605
  daemonVersion: expectedVersion,
@@ -51567,9 +51608,15 @@ async function fixServiceStatus(serviceStatus) {
51567
51608
  });
51568
51609
  return true;
51569
51610
  }
51570
- if (serviceStatus.kind === "not_running") return await runDaemonBackgroundStartForDecision({ view: "human" }) === "started";
51571
- if (serviceStatus.kind === "needs_repair") return await runDaemonBackgroundStartForDecision({ view: "human" }) === "started";
51572
- if (serviceStatus.kind === "repair_blocked" || serviceStatus.kind === "repair_failed") {
51611
+ if (input.serviceStatus.kind === "not_running") return await runDaemonBackgroundStartForDecision({
51612
+ gatewayUrl: input.gatewayUrl ?? void 0,
51613
+ view: "human"
51614
+ }) === "started";
51615
+ if (input.serviceStatus.kind === "needs_repair") return await runDaemonBackgroundStartForDecision({
51616
+ gatewayUrl: input.gatewayUrl ?? void 0,
51617
+ view: "human"
51618
+ }) === "started";
51619
+ if (input.serviceStatus.kind === "repair_blocked" || input.serviceStatus.kind === "repair_failed") {
51573
51620
  await runDaemonStatusSafely();
51574
51621
  return false;
51575
51622
  }
@@ -60988,7 +61035,9 @@ function buildReplyReadinessNextSteps(input) {
60988
61035
  formatAtsCliCommand("ats service run")
60989
61036
  ];
60990
61037
  case "profile.inactive": return [formatAtsCliCommand("ats profile list"), formatAtsCliCommand("ats profile set <agent-profile-id>")];
60991
- case "profile.unbound": return [formatAtsCliCommand(`ats profile update ${profileId} --controller-kind builtin --controller-ref ${controllerRef ?? "<controller-ref>"} --controller-enabled true`), formatAtsCliCommand(`ats space join <space-id> --profile ${profileId}`)];
61038
+ case "profile.unbound":
61039
+ if (controllerRef) return [formatAtsCliCommand(`ats profile update ${profileId} --controller-kind builtin --controller-ref ${controllerRef} --controller-enabled true`), formatAtsCliCommand(`ats space join <space-id> --profile ${profileId}`)];
61040
+ return [formatAtsCliCommand("ats start"), formatAtsCliCommand("ats agents list")];
60992
61041
  case "controller.binding.disabled": return [formatAtsCliCommand(`ats profile update ${profileId} --controller-enabled true`)];
60993
61042
  case "projection.missing": return [formatAtsCliCommand("ats doctor --repair")];
60994
61043
  case "controller.registry.disabled": return [formatAtsCliCommand(`ats agents enable --agent ${agentCommandId}`), formatAtsCliCommand("ats doctor --repair")];
@@ -64257,41 +64306,20 @@ async function runStartSkillsStep(input) {
64257
64306
  }
64258
64307
  }
64259
64308
  async function runStartAgentsStep(input) {
64260
- const builtinCandidates = (await listStartLocalBuiltinAgentCandidates()).sort(compareStartAgentCandidates);
64261
- const selectableCandidates = builtinCandidates.filter((candidate) => candidate.selectable);
64262
- const payload = buildStartAgentsPayload({
64263
- builtinCandidates,
64264
- selectableCandidates
64265
- });
64266
- const onboardingState = await getConfiguredStartV1OnboardingState().catch(() => null);
64267
- const preconfiguredAgentSelection = resolvePreconfiguredStartAgentSelection({
64268
- builtinCandidates,
64269
- localAgentsSetupCompleted: typeof onboardingState?.localAgentsSetupCompletedAt === "string" && onboardingState.localAgentsSetupCompletedAt.length > 0,
64270
- selectableCandidates
64271
- });
64309
+ const agentSelectionState = await resolveStartAgentSelectionState();
64272
64310
  if (input.runtime.resolvedView === "agent") {
64273
64311
  input.presenter.data({
64274
64312
  code: "start.agents",
64275
- payload
64313
+ payload: agentSelectionState.payload
64314
+ });
64315
+ return createStartAgentsStepResult(agentSelectionState.preconfiguredAgentSelection ?? {
64316
+ mode: "unresolved",
64317
+ selectedAgentIds: []
64276
64318
  });
64277
- if (preconfiguredAgentSelection) return {
64278
- agentSelection: preconfiguredAgentSelection,
64279
- allowBackFromProfile: false
64280
- };
64281
- return {
64282
- agentSelection: {
64283
- mode: "unresolved",
64284
- selectedAgentIds: []
64285
- },
64286
- allowBackFromProfile: false
64287
- };
64288
64319
  }
64289
- if (preconfiguredAgentSelection) return {
64290
- agentSelection: preconfiguredAgentSelection,
64291
- allowBackFromProfile: false
64292
- };
64320
+ if (agentSelectionState.preconfiguredAgentSelection) return createStartAgentsStepResult(agentSelectionState.preconfiguredAgentSelection);
64293
64321
  if (!input.interactive) {
64294
- if (selectableCandidates.length > 0) input.presenter.line({
64322
+ if (agentSelectionState.selectableCandidates.length > 0) input.presenter.line({
64295
64323
  code: "start.agents.non_interactive",
64296
64324
  text: formatInlineAtsCliCommands("⚠️ Agent selection still needs a decision. Run `ats start` interactively or use `ats agents enable --all`, `ats agents enable --agent <id...>`, or `ats agents enable --agent <id> --install <install-id>` before continuing.")
64297
64325
  });
@@ -64303,69 +64331,135 @@ async function runStartAgentsStep(input) {
64303
64331
  allowBackFromProfile: false
64304
64332
  };
64305
64333
  }
64334
+ return await runInteractiveStartAgentsSelectionLoop({
64335
+ builtinCandidates: agentSelectionState.builtinCandidates,
64336
+ profile: input.profile,
64337
+ presenter: input.presenter,
64338
+ view: input.view
64339
+ });
64340
+ }
64341
+ async function resolveStartAgentSelectionState() {
64342
+ const builtinCandidates = (await listStartLocalBuiltinAgentCandidates()).sort(compareStartAgentCandidates);
64343
+ const selectableCandidates = builtinCandidates.filter((candidate) => candidate.selectable);
64344
+ const onboardingState = await getConfiguredStartV1OnboardingState().catch(() => null);
64345
+ const localAgentsSetupCompleted = typeof onboardingState?.localAgentsSetupCompletedAt === "string" && onboardingState.localAgentsSetupCompletedAt.length > 0;
64346
+ return {
64347
+ builtinCandidates,
64348
+ selectableCandidates,
64349
+ payload: buildStartAgentsPayload({
64350
+ builtinCandidates,
64351
+ selectableCandidates
64352
+ }),
64353
+ preconfiguredAgentSelection: resolvePreconfiguredStartAgentSelection({
64354
+ builtinCandidates,
64355
+ localAgentsSetupCompleted,
64356
+ selectableCandidates
64357
+ })
64358
+ };
64359
+ }
64360
+ function createStartAgentsStepResult(agentSelection, allowBackFromProfile = false) {
64361
+ return {
64362
+ agentSelection,
64363
+ allowBackFromProfile
64364
+ };
64365
+ }
64366
+ async function runInteractiveStartAgentsSelectionLoop(input) {
64306
64367
  for (;;) {
64307
64368
  const localAgentDecision = await promptStartLocalAgentDecision();
64308
64369
  if (localAgentDecision === "cancelled" || localAgentDecision === "exit") {
64309
64370
  cancel("Cancelled.");
64310
64371
  return "cancelled";
64311
64372
  }
64312
- if (localAgentDecision === "no_agents") {
64313
- await syncStartLocalAgentSelection({
64314
- builtinCandidates,
64315
- selectedAgentIds: [],
64316
- profile: input.profile,
64317
- view: input.view
64373
+ if (localAgentDecision === "no_agents") return await completeStartLocalAgentSelection({
64374
+ builtinCandidates: input.builtinCandidates,
64375
+ profile: input.profile,
64376
+ presenter: input.presenter,
64377
+ selectedAgentIds: [],
64378
+ view: input.view
64379
+ });
64380
+ const repairedBuiltinCandidates = await maybeRepairStartLocalAgentsBeforeSelection({
64381
+ builtinCandidates: input.builtinCandidates,
64382
+ profile: input.profile,
64383
+ view: input.view
64384
+ });
64385
+ if (repairedBuiltinCandidates === "cancelled") {
64386
+ cancel("Cancelled.");
64387
+ return "cancelled";
64388
+ }
64389
+ if (repairedBuiltinCandidates.filter((candidate) => candidate.selectable).length === 0) {
64390
+ input.presenter.line({
64391
+ code: "start.agents.none_ready_after_repair",
64392
+ text: formatInlineAtsCliCommands("No local agents are ready to use on this device yet. Repair one with `ats agents repair --agent <id>` or keep local agents off for now.")
64318
64393
  });
64319
- await persistStartLocalAgentsSetupCompleted();
64320
- emitStartLocalAgentsSetupSaved(input.presenter);
64321
- return {
64322
- agentSelection: {
64323
- mode: "no_agents",
64324
- selectedAgentIds: []
64325
- },
64326
- allowBackFromProfile: true
64327
- };
64394
+ continue;
64328
64395
  }
64329
- const selectedAgentIds = await promptStartAgentSelection(builtinCandidates);
64396
+ const selectedAgentIds = await promptStartAgentSelection(repairedBuiltinCandidates);
64330
64397
  if (selectedAgentIds === "cancelled") {
64331
64398
  cancel("Cancelled.");
64332
64399
  return "cancelled";
64333
64400
  }
64334
64401
  if (selectedAgentIds === "back") continue;
64335
- if (selectedAgentIds.length === 0) {
64336
- await syncStartLocalAgentSelection({
64337
- builtinCandidates,
64338
- selectedAgentIds,
64339
- profile: input.profile,
64340
- view: input.view
64341
- });
64342
- await persistStartLocalAgentsSetupCompleted();
64343
- emitStartLocalAgentsSetupSaved(input.presenter);
64344
- return {
64345
- agentSelection: {
64346
- mode: "no_agents",
64347
- selectedAgentIds: []
64348
- },
64349
- allowBackFromProfile: true
64350
- };
64351
- }
64352
- await syncStartLocalAgentSelection({
64353
- builtinCandidates,
64354
- selectedAgentIds,
64402
+ if (selectedAgentIds.length === 0) return await completeStartLocalAgentSelection({
64403
+ builtinCandidates: input.builtinCandidates,
64355
64404
  profile: input.profile,
64405
+ presenter: input.presenter,
64406
+ selectedAgentIds: [],
64407
+ view: input.view
64408
+ });
64409
+ return await completeStartLocalAgentSelection({
64410
+ builtinCandidates: repairedBuiltinCandidates,
64411
+ profile: input.profile,
64412
+ presenter: input.presenter,
64413
+ selectedAgentIds,
64356
64414
  view: input.view
64357
64415
  });
64358
- await persistStartLocalAgentsSetupCompleted();
64359
- emitStartLocalAgentsSetupSaved(input.presenter);
64360
- return {
64361
- agentSelection: {
64362
- mode: "selected_agents",
64363
- selectedAgentIds
64364
- },
64365
- allowBackFromProfile: true
64366
- };
64367
64416
  }
64368
64417
  }
64418
+ async function completeStartLocalAgentSelection(input) {
64419
+ await syncStartLocalAgentSelection({
64420
+ builtinCandidates: input.builtinCandidates,
64421
+ selectedAgentIds: input.selectedAgentIds,
64422
+ profile: input.profile,
64423
+ view: input.view
64424
+ });
64425
+ await persistStartLocalAgentsSetupCompleted();
64426
+ emitStartLocalAgentsSetupSaved(input.presenter);
64427
+ return createStartAgentsStepResult(input.selectedAgentIds.length === 0 ? {
64428
+ mode: "no_agents",
64429
+ selectedAgentIds: []
64430
+ } : {
64431
+ mode: "selected_agents",
64432
+ selectedAgentIds: [...input.selectedAgentIds]
64433
+ }, true);
64434
+ }
64435
+ async function maybeRepairStartLocalAgentsBeforeSelection(input) {
64436
+ const repairableCandidates = input.builtinCandidates.filter((candidate) => candidate.detected && !candidate.selectable && candidate.selectableReason === "needs_repair");
64437
+ if (repairableCandidates.length === 0) return input.builtinCandidates;
64438
+ const repairDecision = await select({
64439
+ message: buildStartLocalAgentRepairPromptMessage(repairableCandidates),
64440
+ initialValue: "repair_now",
64441
+ options: [{
64442
+ value: "repair_now",
64443
+ label: "Repair now"
64444
+ }, {
64445
+ value: "skip_for_now",
64446
+ label: "Skip for now"
64447
+ }]
64448
+ });
64449
+ if (isCancel(repairDecision)) return "cancelled";
64450
+ if (repairDecision !== "repair_now") return input.builtinCandidates;
64451
+ await runAgentsRepair({
64452
+ agent: repairableCandidates.map((candidate) => candidate.agentId),
64453
+ profile: input.profile,
64454
+ view: input.view
64455
+ });
64456
+ return await listStartLocalBuiltinAgentCandidates();
64457
+ }
64458
+ function buildStartLocalAgentRepairPromptMessage(candidates) {
64459
+ const displayNames = candidates.map((candidate) => candidate.displayName);
64460
+ if (displayNames.length === 1) return `${displayNames[0]} needs repair on this device before ATS can use it. Repair it now?`;
64461
+ return `${displayNames.join(", ")} need repair on this device before ATS can use them. Repair them now?`;
64462
+ }
64369
64463
  function buildStartAgentsPayload(input) {
64370
64464
  return {
64371
64465
  candidates: input.builtinCandidates.map((candidate) => ({
@@ -64397,11 +64491,17 @@ function resolveSavedStartAgentSelection(selectableCandidates) {
64397
64491
  };
64398
64492
  }
64399
64493
  function resolvePreconfiguredStartAgentSelection(input) {
64400
- if (input.builtinCandidates.length === 0 || input.selectableCandidates.length === 0) return {
64494
+ const hasRepairableCandidates = input.builtinCandidates.some((candidate) => candidate.detected && !candidate.selectable && candidate.selectableReason === "needs_repair");
64495
+ if (input.builtinCandidates.length === 0) return {
64401
64496
  mode: "no_agents",
64402
64497
  selectedAgentIds: []
64403
64498
  };
64499
+ if (input.selectableCandidates.length === 0 && hasRepairableCandidates) return null;
64404
64500
  if (!input.localAgentsSetupCompleted) return null;
64501
+ if (input.selectableCandidates.length === 0) return {
64502
+ mode: "no_agents",
64503
+ selectedAgentIds: []
64504
+ };
64405
64505
  return resolveSavedStartAgentSelection([...input.selectableCandidates]);
64406
64506
  }
64407
64507
  async function persistStartLocalAgentsSetupCompleted() {
@@ -64733,6 +64833,7 @@ async function runStartServiceStep(input) {
64733
64833
  presenter: input.presenter,
64734
64834
  resolvedView: input.runtime.resolvedView,
64735
64835
  allowPrompt: input.interactive,
64836
+ gatewayUrl: input.gatewayUrl,
64736
64837
  localDemand,
64737
64838
  forcePrompt: selectedLocalAgentsRequireService || currentProfileIsExplicitLocalAgent,
64738
64839
  view: input.view
@@ -64882,6 +64983,7 @@ async function runStartHandoff(input) {
64882
64983
  const serviceStepResult = await runHandoffLocalStepWithFailedProgressSync({
64883
64984
  run: async () => await runStartServiceStep({
64884
64985
  foundation,
64986
+ gatewayUrl,
64885
64987
  runtime: input.runtime,
64886
64988
  presenter: input.presenter,
64887
64989
  interactive: true,
@@ -79622,6 +79724,26 @@ const LOCAL_SERVICE_ONLY_JOIN_BLOCK_REASON_CODES = new Set([
79622
79724
  "service.not_installed",
79623
79725
  "service.not_running"
79624
79726
  ]);
79727
+ const LOCAL_PARTICIPATION_ONLY_JOIN_BLOCK_REASON_CODES = new Set([
79728
+ "service.not_installed",
79729
+ "service.drifted",
79730
+ "service.not_running",
79731
+ "dispatch.storage_not_ready",
79732
+ "service.gateway_chain_unhealthy",
79733
+ "profile.unbound",
79734
+ "controller.binding.disabled",
79735
+ "projection.missing",
79736
+ "controller.registry.disabled",
79737
+ "controller.launch.choice_required",
79738
+ "controller.launch.needs_repair",
79739
+ "workspace.missing",
79740
+ "workspace.invalid",
79741
+ "runtime.adapter.unsupported",
79742
+ "controller.bootstrap.failed",
79743
+ "controller.gateway.unhealthy",
79744
+ "route.offline",
79745
+ "route.not_registered"
79746
+ ]);
79625
79747
  async function prepareSpaceJoinEntryContext(input) {
79626
79748
  const runtime = await resolveRuntimeContext({
79627
79749
  profile: input.profile,
@@ -79724,19 +79846,18 @@ async function prepareSpaceJoinServiceState(input) {
79724
79846
  };
79725
79847
  }
79726
79848
  async function assertSpaceJoinTargetPreflightReady(input) {
79727
- const serviceRequirement = input.resolvedView === "human" ? resolveCliServiceParticipationRequirement({
79728
- intent: input.serviceIntent ?? "space_join",
79729
- humanOnlyParticipation: !isExplicitLocalAgentProfile(input.atsProfile) && input.localDemand?.decision !== "keep_running",
79730
- explicitSelectedLocalAgents: isExplicitLocalAgentProfile(input.atsProfile),
79731
- selectedAgentProfilesHaveRunnableLocalSetup: hasRunnableLocalAgentSetup(input.atsProfile),
79732
- localDemand: input.localDemand ?? null
79733
- }) : "not_needed";
79849
+ const { explicitLocalAgentProfile, serviceRequirement } = resolveSpaceJoinLocalParticipationState(input);
79850
+ let emittedMembershipOnlyGuidance = false;
79851
+ let emittedServiceGuidance = false;
79734
79852
  if (serviceRequirement === "agent_setup_needed") {
79735
79853
  input.presenter.line({
79736
- code: "space.join.agent_setup_needed",
79737
- text: `Finish local agent setup before you bring this profile into a space. Use \`ats profile ready --profile ${input.atsProfile.atsProfileId}\` or rerun \`ats start\`.`
79854
+ code: "space.join.local_participation.pending",
79855
+ text: buildSpaceJoinMembershipOnlyMessage({
79856
+ atsProfile: input.atsProfile,
79857
+ reasonCodes: ["profile.unbound"]
79858
+ })
79738
79859
  });
79739
- return "cancelled";
79860
+ emittedMembershipOnlyGuidance = true;
79740
79861
  }
79741
79862
  const shouldManageServiceParticipation = serviceRequirement === "offer_service_gate";
79742
79863
  const runPreflight = async () => await emitSpaceCommandPreflight({
@@ -79749,12 +79870,14 @@ async function assertSpaceJoinTargetPreflightReady(input) {
79749
79870
  });
79750
79871
  let preflight = await runPreflight();
79751
79872
  if (shouldManageServiceParticipation) {
79752
- const gateResult = await runDaemonServiceParticipationGate({
79753
- intent: input.serviceIntent ?? "space_join",
79873
+ const gateResult = await runSpaceJoinServiceParticipationGate({
79874
+ allowPrompt: input.allowPrompt,
79875
+ explicitLocalAgentProfile,
79876
+ gatewayUrl: input.baseUrl,
79877
+ localDemand: input.localDemand,
79754
79878
  presenter: input.presenter,
79755
79879
  resolvedView: input.resolvedView,
79756
- allowPrompt: input.allowPrompt ?? true,
79757
- localDemand: input.localDemand ?? null,
79880
+ serviceIntent: input.serviceIntent,
79758
79881
  view: input.view
79759
79882
  });
79760
79883
  if (gateResult.status === "cancelled") return "cancelled";
@@ -79767,24 +79890,90 @@ async function assertSpaceJoinTargetPreflightReady(input) {
79767
79890
  humanServiceGuidanceMode: "suppress",
79768
79891
  emitOutput: false
79769
79892
  });
79770
- if (gateResult.status === "declined") input.presenter.line({
79771
- code: "space.join.service.declined",
79772
- text: "ATS will continue joining this space without wakeable local agent participation on this device. To enable background replies later, rerun `ats start` or use `ats service install` and `ats service run`."
79893
+ if (gateResult.status === "declined") {
79894
+ input.presenter.line({
79895
+ code: "space.join.service.declined",
79896
+ text: "ATS will continue joining this space without wakeable local agent participation on this device. To enable background replies later, rerun `ats start` or use `ats service install` and `ats service run`."
79897
+ });
79898
+ emittedServiceGuidance = true;
79899
+ }
79900
+ }
79901
+ const blockedJoinResult = handleBlockedSpaceJoinLocalParticipation({
79902
+ atsProfile: input.atsProfile,
79903
+ explicitLocalAgentProfile,
79904
+ preflight,
79905
+ presenter: input.presenter,
79906
+ serviceRequirement,
79907
+ emittedMembershipOnlyGuidance,
79908
+ emittedServiceGuidance
79909
+ });
79910
+ if (blockedJoinResult) return blockedJoinResult;
79911
+ if (preflight.deviceReplyReadiness.replyReadiness === "blocked") throw new Error(preflight.deviceReplyReadiness.reasonText);
79912
+ return "continue";
79913
+ }
79914
+ function resolveSpaceJoinLocalParticipationState(input) {
79915
+ const explicitLocalAgentProfile = isExplicitLocalAgentProfile(input.atsProfile);
79916
+ return {
79917
+ explicitLocalAgentProfile,
79918
+ serviceRequirement: resolveCliServiceParticipationRequirement({
79919
+ intent: input.serviceIntent ?? "space_join",
79920
+ humanOnlyParticipation: !explicitLocalAgentProfile && input.localDemand?.decision !== "keep_running",
79921
+ explicitSelectedLocalAgents: explicitLocalAgentProfile,
79922
+ selectedAgentProfilesHaveRunnableLocalSetup: hasRunnableLocalAgentSetup(input.atsProfile),
79923
+ localDemand: input.localDemand ?? null
79924
+ })
79925
+ };
79926
+ }
79927
+ async function runSpaceJoinServiceParticipationGate(input) {
79928
+ return await runDaemonServiceParticipationGate({
79929
+ intent: input.serviceIntent ?? "space_join",
79930
+ presenter: input.presenter,
79931
+ resolvedView: input.resolvedView,
79932
+ allowPrompt: input.allowPrompt ?? true,
79933
+ gatewayUrl: input.gatewayUrl,
79934
+ localDemand: input.localDemand ?? null,
79935
+ forcePrompt: input.explicitLocalAgentProfile,
79936
+ view: input.view
79937
+ });
79938
+ }
79939
+ function handleBlockedSpaceJoinLocalParticipation(input) {
79940
+ if (input.preflight.deviceReplyReadiness.replyReadiness !== "blocked" || !isBlockingLocalParticipationJoinAttention(input.preflight)) return null;
79941
+ if (input.explicitLocalAgentProfile) {
79942
+ if (!input.emittedMembershipOnlyGuidance) input.presenter.line({
79943
+ code: "space.join.local_participation.pending",
79944
+ text: buildSpaceJoinMembershipOnlyMessage({
79945
+ atsProfile: input.atsProfile,
79946
+ reasonCodes: input.preflight.deviceReplyReadiness.reasonCodes
79947
+ })
79773
79948
  });
79949
+ return "continue";
79774
79950
  }
79775
- if (preflight.deviceReplyReadiness.replyReadiness === "blocked" && (shouldManageServiceParticipation || serviceRequirement === "not_needed") && isBlockingLocalServiceJoinAttention(preflight)) {
79776
- if (serviceRequirement === "not_needed") input.presenter.line({
79951
+ if (input.serviceRequirement === "not_needed") {
79952
+ input.presenter.line({
79777
79953
  code: "space.join.service.not_needed",
79778
79954
  text: "ATS will keep joining this space without wakeable local agent participation on this device. If you decide to bring local agents later, rerun `ats start` or use `ats service install` and `ats service run`."
79779
79955
  });
79780
79956
  return "continue";
79781
79957
  }
79782
- if (preflight.deviceReplyReadiness.replyReadiness === "blocked") throw new Error(preflight.deviceReplyReadiness.reasonText);
79958
+ if (!input.emittedServiceGuidance) input.presenter.line({
79959
+ code: "space.join.service.pending",
79960
+ text: "ATS will keep joining this space without wakeable local agent participation on this device until ATS Service is ready. To enable background replies later, rerun `ats start` or use `ats service install` and `ats service run`."
79961
+ });
79783
79962
  return "continue";
79784
79963
  }
79785
- function isBlockingLocalServiceJoinAttention(preflight) {
79964
+ function isBlockingLocalParticipationJoinAttention(preflight) {
79786
79965
  const { deviceReplyReadiness } = preflight;
79787
- return deviceReplyReadiness.replyReadiness === "blocked" && deviceReplyReadiness.reasonCodes.length > 0 && deviceReplyReadiness.reasonCodes.every((reasonCode) => LOCAL_SERVICE_ONLY_JOIN_BLOCK_REASON_CODES.has(reasonCode));
79966
+ return deviceReplyReadiness.replyReadiness === "blocked" && deviceReplyReadiness.reasonCodes.length > 0 && deviceReplyReadiness.reasonCodes.every((reasonCode) => LOCAL_PARTICIPATION_ONLY_JOIN_BLOCK_REASON_CODES.has(reasonCode));
79967
+ }
79968
+ function buildSpaceJoinMembershipOnlyMessage(input) {
79969
+ const profileName = input.atsProfile.profileName;
79970
+ const profileId = input.atsProfile.atsProfileId;
79971
+ const reasonCodes = input.reasonCodes;
79972
+ const setupCommand = `ats profile ready --profile ${profileId}`;
79973
+ if (reasonCodes.includes("profile.unbound")) return `${profileName} will join this space, but it is not connected to a local agent on this device yet. It can appear here now, but it will not wake or reply from this device until you finish setup. Use \`${setupCommand}\` or rerun \`ats start\`.`;
79974
+ if (reasonCodes.some((reasonCode) => LOCAL_SERVICE_ONLY_JOIN_BLOCK_REASON_CODES.has(reasonCode)) || reasonCodes.includes("dispatch.storage_not_ready") || reasonCodes.includes("service.gateway_chain_unhealthy") || reasonCodes.includes("route.offline") || reasonCodes.includes("route.not_registered")) return `${profileName} will join this space, but it will not wake or reply from this device until ATS Service is ready. Use \`ats service install\`, \`ats service run\`, or rerun \`ats start\`.`;
79975
+ if (reasonCodes.includes("controller.launch.needs_repair") || reasonCodes.includes("controller.launch.choice_required") || reasonCodes.includes("workspace.missing") || reasonCodes.includes("workspace.invalid")) return `${profileName} will join this space, but it is not ready to wake or reply from this device yet. Use \`${setupCommand}\` or rerun \`ats start\`.`;
79976
+ return `${profileName} will join this space, but local setup on this device is still incomplete. It can appear here now, but it will not wake or reply from this device until you finish setup. Use \`${setupCommand}\` or rerun \`ats start\`.`;
79788
79977
  }
79789
79978
  function buildJoinTargetMissingMessage(input) {
79790
79979
  return formatMessage(input.resolvedView, "space.target.missing", { command: "join" });
@@ -80144,7 +80333,7 @@ function resolveSpaceJoinMembershipProfileKind(value) {
80144
80333
  return value === "human" || value === "agent" ? value : null;
80145
80334
  }
80146
80335
  function normalizeOptionalString$2(value) {
80147
- return typeof value === "string" ? value : null;
80336
+ return typeof value === "string" ? value : void 0;
80148
80337
  }
80149
80338
  function buildLocallyCreatedPendingSpaceJoinMembershipCandidate(profile) {
80150
80339
  return {
@@ -80153,8 +80342,8 @@ function buildLocallyCreatedPendingSpaceJoinMembershipCandidate(profile) {
80153
80342
  profileName: profile.profileName,
80154
80343
  profileKind: profile.profileKind === "agent" ? "agent" : "human",
80155
80344
  ownerUserId: profile.ownerUserId,
80156
- ownerName: typeof profile.ownerName === "string" ? profile.ownerName : null,
80157
- controllerRef: profile.controllerBinding?.controllerRef ?? null,
80345
+ ownerName: typeof profile.ownerName === "string" ? profile.ownerName : void 0,
80346
+ controllerRef: profile.controllerBinding?.controllerRef,
80158
80347
  controllerEnabled: profile.controllerBinding?.controllerEnabled === true,
80159
80348
  alreadyJoined: false,
80160
80349
  needsActivation: false
@@ -80367,15 +80556,16 @@ async function applySelectedSpaceAddMembersBeforeJoin(input, dependencies) {
80367
80556
  });
80368
80557
  }
80369
80558
  if (confirmedCandidates.candidates.length === 0) return "completed";
80370
- if (await dependencies.prepareSpaceMemberCandidatesLocalParticipation({
80559
+ const servicePreparationResult = await dependencies.prepareSpaceMemberCandidatesLocalParticipation({
80371
80560
  actorProfile: input.actorProfile,
80372
80561
  candidates: confirmedCandidates.candidates,
80373
80562
  presenter: input.presenter,
80374
80563
  resolvedView: input.resolvedView
80375
- }) === "cancelled") return input.cancelResult;
80564
+ });
80565
+ if (servicePreparationResult.status === "cancelled") return input.cancelResult;
80376
80566
  const result = await dependencies.applySpaceAddMembers({
80377
80567
  actorProfile: input.actorProfile,
80378
- candidates: confirmedCandidates.candidates,
80568
+ candidates: servicePreparationResult.candidates,
80379
80569
  space: input.space,
80380
80570
  baseUrl: input.baseUrl,
80381
80571
  password: input.password
@@ -80391,7 +80581,7 @@ async function applySelectedSpaceAddMembersBeforeJoin(input, dependencies) {
80391
80581
  presenter: input.presenter,
80392
80582
  resolvedView: input.resolvedView,
80393
80583
  space: input.space,
80394
- selectedCount: confirmedCandidates.candidates.length,
80584
+ selectedCount: servicePreparationResult.candidates.length,
80395
80585
  result,
80396
80586
  joinNoticeSummary
80397
80587
  });
@@ -82032,20 +82222,22 @@ async function runSpaceAddMembersForTarget(input) {
82032
82222
  selectedProfileIds
82033
82223
  });
82034
82224
  if (selectedCandidates.length === 0) return { status: "completed" };
82035
- if (await ensureSpaceMemberCandidatesLocalParticipationReady({
82225
+ const servicePreparationResult = await ensureSpaceMemberCandidatesLocalParticipationReady({
82036
82226
  atsProfile: input.atsProfile,
82037
82227
  candidates: selectedCandidates,
82038
82228
  presenter: input.presenter,
82039
82229
  resolvedView: input.runtime.resolvedView,
82040
82230
  allowPrompt: input.allowPrompt,
82041
82231
  view: input.view
82042
- }) === "cancelled") return input.allowBackToCaller ? {
82232
+ });
82233
+ if (servicePreparationResult.status === "cancelled") return input.allowBackToCaller ? {
82043
82234
  status: "repeat_target",
82044
82235
  authenticatedGatewayUrl
82045
82236
  } : { status: "cancelled" };
82237
+ const preparedCandidates = servicePreparationResult.candidates;
82046
82238
  const result = await applySpaceAddMembers({
82047
82239
  actorProfile: input.atsProfile,
82048
- candidates: selectedCandidates,
82240
+ candidates: preparedCandidates,
82049
82241
  space: input.target.space,
82050
82242
  baseUrl: targetBaseUrl,
82051
82243
  password: input.password
@@ -82061,7 +82253,7 @@ async function runSpaceAddMembersForTarget(input) {
82061
82253
  presenter: input.presenter,
82062
82254
  resolvedView: input.runtime.resolvedView,
82063
82255
  space: input.target.space,
82064
- selectedCount: selectedCandidates.length,
82256
+ selectedCount: preparedCandidates.length,
82065
82257
  result,
82066
82258
  joinNoticeSummary
82067
82259
  });
@@ -82150,44 +82342,157 @@ function resolveSelectedSpaceMemberCandidates(input) {
82150
82342
  return input.selectedProfileIds.map((profileId) => input.candidates.find((candidate) => candidate.profileId === profileId)).filter((candidate) => Boolean(candidate));
82151
82343
  }
82152
82344
  async function ensureSpaceMemberCandidatesLocalParticipationReady(input) {
82153
- const explicitSelectedLocalAgents = hasAnyExplicitLocalAgentCandidates(input.candidates);
82154
- const serviceRequirement = resolveCliServiceParticipationRequirement({
82345
+ let preparedCandidates = await refreshSpaceMemberCandidatesLocalReadiness(input.candidates);
82346
+ const repairableCandidates = preparedCandidates.filter(isSpaceMemberCandidateRepairableLocalParticipationCandidate);
82347
+ if (repairableCandidates.length > 0 && input.resolvedView === "human" && input.allowPrompt) {
82348
+ const repairNow = await confirm({
82349
+ message: buildSpaceMemberRepairPromptMessage(repairableCandidates),
82350
+ active: "Repair now",
82351
+ inactive: "Not now",
82352
+ initialValue: true
82353
+ });
82354
+ if (isCancel(repairNow)) {
82355
+ cancel("Cancelled.");
82356
+ return { status: "cancelled" };
82357
+ }
82358
+ if (repairNow) {
82359
+ await repairSpaceMemberCandidatesLocalParticipation({
82360
+ atsProfile: input.atsProfile,
82361
+ candidates: repairableCandidates,
82362
+ view: input.view
82363
+ });
82364
+ preparedCandidates = await refreshSpaceMemberCandidatesLocalReadiness(preparedCandidates);
82365
+ }
82366
+ }
82367
+ const wakeableLocalCandidates = preparedCandidates.filter(isSpaceMemberCandidateRunnableLocalParticipationCandidate);
82368
+ const explicitSelectedLocalAgents = hasAnyExplicitLocalAgentCandidates(wakeableLocalCandidates);
82369
+ if (resolveCliServiceParticipationRequirement({
82155
82370
  intent: "space_add_members",
82156
82371
  humanOnlyParticipation: !explicitSelectedLocalAgents,
82157
82372
  explicitSelectedLocalAgents,
82158
- selectedAgentProfilesHaveRunnableLocalSetup: haveAllExplicitLocalAgentCandidatesRunnable(input.candidates),
82373
+ selectedAgentProfilesHaveRunnableLocalSetup: haveAllExplicitLocalAgentCandidatesRunnable(wakeableLocalCandidates),
82159
82374
  localDemand: input.resolvedView === "human" ? await resolveDaemonLocalServiceDemand({ ownerUserId: input.atsProfile.ownerUserId }).catch(() => null) : null
82160
- });
82161
- if (serviceRequirement === "not_needed") return "continue";
82162
- if (serviceRequirement === "agent_setup_needed") {
82163
- input.presenter.line({
82164
- code: "space.add_members.agent_setup_needed",
82165
- text: "At least one selected agent profile is not set up on this device yet. Finish agent setup first, then retry. Use `ats start` or `ats profile ready --profile <agent-profile-id>`."
82166
- });
82167
- return "cancelled";
82168
- }
82375
+ }) === "not_needed") return {
82376
+ status: "continue",
82377
+ candidates: preparedCandidates
82378
+ };
82169
82379
  if (!(input.resolvedView === "human" && input.allowPrompt)) {
82170
82380
  input.presenter.line({
82171
82381
  code: "space.add_members.service.deferred",
82172
82382
  text: "ATS will continue without wakeable local agent participation on this device. To enable background replies later, rerun `ats start` or use `ats service install` and `ats service run`."
82173
82383
  });
82174
- return "continue";
82384
+ return {
82385
+ status: "continue",
82386
+ candidates: preparedCandidates
82387
+ };
82175
82388
  }
82176
82389
  const gateResult = await runDaemonServiceParticipationGate({
82177
82390
  intent: "space_add_members",
82178
82391
  presenter: input.presenter,
82179
82392
  resolvedView: input.resolvedView,
82180
82393
  allowPrompt: input.allowPrompt,
82394
+ gatewayUrl: input.gatewayUrl,
82181
82395
  localDemand: await resolveDaemonLocalServiceDemand({ ownerUserId: input.atsProfile.ownerUserId }).catch(() => null),
82182
82396
  forcePrompt: explicitSelectedLocalAgents,
82183
82397
  view: input.view
82184
82398
  });
82185
- if (gateResult.status === "cancelled") return "cancelled";
82399
+ if (gateResult.status === "cancelled") return { status: "cancelled" };
82186
82400
  if (gateResult.status !== "aligned") input.presenter.line({
82187
82401
  code: "space.add_members.service.declined",
82188
82402
  text: "ATS will continue adding these profiles without wakeable local agent participation on this device. To enable background replies later, rerun `ats start` or use `ats service install` and `ats service run`."
82189
82403
  });
82190
- return "continue";
82404
+ return {
82405
+ status: "continue",
82406
+ candidates: preparedCandidates
82407
+ };
82408
+ }
82409
+ async function repairSpaceMemberCandidatesLocalParticipation(input) {
82410
+ if (input.candidates.some((candidate) => candidate.localParticipationReasonCodes?.some((reasonCode) => reasonCode === "projection.missing" || reasonCode === "controller.launch.needs_repair"))) await syncDeviceRuntimeStateProjection({ mode: "repair" });
82411
+ const repairableAgentIds = [...new Set(input.candidates.map((candidate) => candidate.localTargetLaunchStatus === "needs_repair" ? candidate.localTargetAgentId ?? null : null).filter((value) => Boolean(value)))];
82412
+ if (repairableAgentIds.length > 0) await runAgentsRepair({
82413
+ agent: repairableAgentIds,
82414
+ profile: input.atsProfile.atsProfileId,
82415
+ view: input.view
82416
+ });
82417
+ if (input.candidates.some((candidate) => candidate.localParticipationReasonCodes?.some((reasonCode) => reasonCode === "workspace.missing" || reasonCode === "workspace.invalid"))) await syncProfileWorkspaceState({ mode: "repair" });
82418
+ if (input.candidates.some((candidate) => candidate.localParticipationReasonCodes?.some((reasonCode) => reasonCode === "dispatch.storage_not_ready" || reasonCode === "service.gateway_chain_unhealthy"))) await runDoctor({
82419
+ profile: input.atsProfile.atsProfileId,
82420
+ repair: true,
82421
+ view: input.view,
82422
+ agentOverviewHandled: true
82423
+ });
82424
+ }
82425
+ async function refreshSpaceMemberCandidatesLocalReadiness(candidates) {
82426
+ const localTargetStates = await listAgentTargetStates().catch(() => []);
82427
+ if (localTargetStates.length === 0) return await Promise.all(candidates.map(async (candidate) => {
82428
+ return await enrichSpaceMemberCandidateLocalParticipationReadiness({
82429
+ ...candidate,
82430
+ localTargetDisplayName: candidate.localTargetDisplayName ?? null,
82431
+ localTargetLaunchStatus: candidate.localTargetLaunchStatus ?? null,
82432
+ localTargetSelectable: candidate.localTargetSelectable ?? false
82433
+ });
82434
+ }));
82435
+ const localTargetStateById = new Map(localTargetStates.map((state) => [state.agentId, state]));
82436
+ return await Promise.all(candidates.map(async (candidate) => {
82437
+ const localTargetAgentId = resolveSpaceMemberCandidateLocalTargetAgentId({
82438
+ profileKind: candidate.profileKind,
82439
+ controllerRef: candidate.controllerRef,
82440
+ controllerEnabled: candidate.controllerEnabled
82441
+ });
82442
+ const localTargetState = localTargetAgentId ? localTargetStateById.get(localTargetAgentId) ?? null : null;
82443
+ return await enrichSpaceMemberCandidateLocalParticipationReadiness({
82444
+ ...candidate,
82445
+ localTargetAgentId,
82446
+ localTargetDisplayName: localTargetState?.displayName ?? null,
82447
+ localTargetLaunchStatus: localTargetState?.launchStatus ?? null,
82448
+ localTargetSelectable: localTargetState?.selectable ?? false
82449
+ });
82450
+ }));
82451
+ }
82452
+ async function enrichSpaceMemberCandidateLocalParticipationReadiness(candidate) {
82453
+ if (candidate.profileKind !== "agent" || !candidate.atsProfile || candidate.controllerEnabled !== true || typeof candidate.controllerRef !== "string" || candidate.controllerRef.trim().length === 0) return {
82454
+ ...candidate,
82455
+ localParticipationReadiness: null,
82456
+ localParticipationReasonCodes: [],
82457
+ localParticipationReasonText: null
82458
+ };
82459
+ let snapshot = null;
82460
+ try {
82461
+ snapshot = await resolveCurrentReplyReadinessSnapshot({
82462
+ checkGatewayChain: false,
82463
+ ownerUserId: candidate.atsProfile.ownerUserId,
82464
+ profile: candidate.atsProfile
82465
+ });
82466
+ } catch {
82467
+ snapshot = null;
82468
+ }
82469
+ if (!snapshot) return {
82470
+ ...candidate,
82471
+ localParticipationReadiness: null,
82472
+ localParticipationReasonCodes: [],
82473
+ localParticipationReasonText: null
82474
+ };
82475
+ const readinessSummary = resolveServiceParticipationReadiness({
82476
+ deviceReplyReadiness: snapshot.deviceReplyReadiness,
82477
+ runtimeStatus: snapshot.displayRuntimeStatus ?? snapshot.runtimeStatus
82478
+ });
82479
+ return {
82480
+ ...candidate,
82481
+ localParticipationReadiness: readinessSummary.serviceReadiness,
82482
+ localParticipationReasonCodes: [...snapshot.deviceReplyReadiness.reasonCodes],
82483
+ localParticipationReasonText: snapshot.deviceReplyReadiness.reasonText
82484
+ };
82485
+ }
82486
+ function isSpaceMemberCandidateRunnableLocalParticipationCandidate(candidate) {
82487
+ return candidate.profileKind === "agent" && candidate.controllerEnabled === true && typeof candidate.controllerRef === "string" && candidate.controllerRef.trim().length > 0 && candidate.localTargetSelectable === true && (candidate.localParticipationReadiness === "ready" || candidate.localParticipationReadiness === "not_running" || candidate.localParticipationReadiness === "not_installed");
82488
+ }
82489
+ function isSpaceMemberCandidateRepairableLocalParticipationCandidate(candidate) {
82490
+ return candidate.profileKind === "agent" && candidate.controllerEnabled === true && typeof candidate.controllerRef === "string" && candidate.controllerRef.trim().length > 0 && (typeof candidate.localTargetAgentId === "string" && candidate.localTargetAgentId.trim().length > 0 && candidate.localTargetLaunchStatus === "needs_repair" || candidate.localParticipationReadiness === "needs_repair");
82491
+ }
82492
+ function buildSpaceMemberRepairPromptMessage(candidates) {
82493
+ const displayNames = candidates.map((candidate) => candidate.localTargetDisplayName || candidate.localTargetAgentId || candidate.profileName);
82494
+ if (displayNames.length === 1) return `${displayNames[0]} needs repair on this device before ATS can use it for background replies. Repair it now?`;
82495
+ return `${displayNames.join(", ")} need repair on this device before ATS can use them for background replies. Repair them now?`;
82191
82496
  }
82192
82497
  async function runSpaceJoin(input) {
82193
82498
  let failureRuntime = null;
@@ -82344,6 +82649,7 @@ async function runSpaceJoinTargetLoop(input) {
82344
82649
  prepareSpaceMemberCandidatesLocalParticipation: async ({ actorProfile, candidates, presenter, resolvedView }) => await ensureSpaceMemberCandidatesLocalParticipationReady({
82345
82650
  atsProfile: actorProfile,
82346
82651
  candidates,
82652
+ gatewayUrl: targetBaseUrl,
82347
82653
  presenter,
82348
82654
  resolvedView,
82349
82655
  allowPrompt: input.canPromptPreJoinActions,
@@ -83360,6 +83666,8 @@ async function resolveSpaceRemoveMembersTarget(input) {
83360
83666
  }
83361
83667
  async function resolveSpaceMemberCandidatesForAdd(input) {
83362
83668
  const profiles = await listAtsProfiles();
83669
+ const localTargetStates = await listAgentTargetStates().catch(() => []);
83670
+ const localTargetStateById = new Map(localTargetStates.map((state) => [state.agentId, state]));
83363
83671
  const ownerProfileNameByOwnerUserId = buildOwnerProfileNameByOwnerUserId(profiles);
83364
83672
  const candidateProfiles = profiles.filter((profile) => profile.status === "active" && isSpaceMembershipProfileKind(profile.profileKind) && profile.atsProfileId !== input.currentProfile.atsProfileId).filter((profile) => typeof input.currentOwnerUserId !== "string" || input.currentOwnerUserId.trim().length === 0 || profile.ownerUserId === input.currentOwnerUserId);
83365
83673
  if (candidateProfiles.length === 0) return [];
@@ -83369,25 +83677,55 @@ async function resolveSpaceMemberCandidatesForAdd(input) {
83369
83677
  ...input.password === void 0 ? {} : { spacePassword: input.password }
83370
83678
  });
83371
83679
  const memberProfileIds = new Set([...membersSnapshot.humans, ...membersSnapshot.agents].map((member) => member.profileId));
83372
- return candidateProfiles.map((profile) => {
83373
- return {
83374
- profileId: profile.atsProfileId,
83375
- profileHandle: profile.profileHandle,
83376
- profileName: profile.profileName,
83377
- profileKind: profile.profileKind,
83378
- ownerProfileName: ownerProfileNameByOwnerUserId.get(profile.ownerUserId),
83379
- ownerUserId: profile.ownerUserId,
83380
- ownerName: profile.ownerName,
83381
- controllerRef: profile.controllerBinding?.controllerRef,
83382
- controllerEnabled: profile.controllerBinding?.controllerEnabled,
83383
- alreadyJoined: memberProfileIds.has(profile.atsProfileId),
83384
- needsActivation: false
83385
- };
83386
- }).sort((left, right) => {
83680
+ return await refreshSpaceMemberCandidatesLocalReadiness(candidateProfiles.map((profile) => buildSpaceMemberCandidateFromProfile({
83681
+ profile,
83682
+ localTargetStateById,
83683
+ ownerProfileName: ownerProfileNameByOwnerUserId.get(profile.ownerUserId) ?? void 0,
83684
+ alreadyJoined: memberProfileIds.has(profile.atsProfileId)
83685
+ })).sort((left, right) => {
83387
83686
  const byName = left.profileName.localeCompare(right.profileName);
83388
83687
  if (byName !== 0) return byName;
83389
83688
  return left.profileId.localeCompare(right.profileId);
83689
+ }));
83690
+ }
83691
+ function buildSpaceMemberCandidateFromProfile(input) {
83692
+ const controllerRef = input.profile.controllerBinding?.controllerRef;
83693
+ const controllerEnabled = input.profile.controllerBinding?.controllerEnabled;
83694
+ const localTargetAgentId = resolveSpaceMemberCandidateLocalTargetAgentId({
83695
+ profileKind: input.profile.profileKind,
83696
+ controllerRef,
83697
+ controllerEnabled
83390
83698
  });
83699
+ const localTargetState = localTargetAgentId ? input.localTargetStateById.get(localTargetAgentId) ?? null : null;
83700
+ return {
83701
+ atsProfile: input.profile,
83702
+ profileId: input.profile.atsProfileId,
83703
+ profileHandle: input.profile.profileHandle,
83704
+ profileName: input.profile.profileName,
83705
+ profileKind: input.profile.profileKind,
83706
+ bindingState: input.profile.bindingState,
83707
+ ownerProfileName: input.ownerProfileName,
83708
+ ownerUserId: input.profile.ownerUserId,
83709
+ ownerName: input.profile.ownerName,
83710
+ controllerRef,
83711
+ controllerEnabled,
83712
+ localTargetAgentId,
83713
+ localTargetDisplayName: localTargetState?.displayName ?? null,
83714
+ localTargetLaunchStatus: localTargetState?.launchStatus ?? null,
83715
+ localTargetSelectable: localTargetState?.selectable ?? false,
83716
+ localParticipationReadiness: null,
83717
+ localParticipationReasonCodes: [],
83718
+ localParticipationReasonText: null,
83719
+ alreadyJoined: input.alreadyJoined,
83720
+ needsActivation: false
83721
+ };
83722
+ }
83723
+ function resolveSpaceMemberCandidateLocalTargetAgentId(input) {
83724
+ if (input.profileKind !== "agent" || input.controllerEnabled !== true || typeof input.controllerRef !== "string") return null;
83725
+ const controllerRef = input.controllerRef.trim();
83726
+ if (!controllerRef) return null;
83727
+ if (controllerRef.startsWith("builtin:")) return normalizeBuiltinControllerAgentId(controllerRef) || null;
83728
+ return controllerRef;
83391
83729
  }
83392
83730
  async function resolveSpaceMemberCandidatesForRemove(input) {
83393
83731
  const membersSnapshot = await createCliSpaceApi(input.baseUrl).getMembersSnapshot({
@@ -83737,6 +84075,7 @@ async function applySpaceAddMembers(input) {
83737
84075
  return result;
83738
84076
  }
83739
84077
  function emitSpaceAddMembersResult(input) {
84078
+ const localParticipationFollowUps = input.result.added.map((candidate) => buildSpaceMemberJoinedFollowUp(candidate)).filter((followUp) => followUp !== null);
83740
84079
  if (input.resolvedView === "agent") {
83741
84080
  input.presenter.data({
83742
84081
  code: "space.add_members",
@@ -83757,7 +84096,11 @@ function emitSpaceAddMembersResult(input) {
83757
84096
  recordedCount: input.joinNoticeSummary.recordedCount,
83758
84097
  failedCount: input.joinNoticeSummary.failedCount,
83759
84098
  ...input.joinNoticeSummary.firstFailure ? { firstFailure: input.joinNoticeSummary.firstFailure } : {}
83760
- }
84099
+ },
84100
+ localParticipationFollowUps: localParticipationFollowUps.map((followUp) => ({
84101
+ profileId: followUp.profileId,
84102
+ message: followUp.text
84103
+ }))
83761
84104
  }
83762
84105
  });
83763
84106
  return;
@@ -83775,6 +84118,10 @@ function emitSpaceAddMembersResult(input) {
83775
84118
  sanitize: true,
83776
84119
  minContentWidth: 56
83777
84120
  });
84121
+ for (const followUp of localParticipationFollowUps) input.presenter.line({
84122
+ code: "space.add_members.local_participation_follow_up",
84123
+ text: followUp.text
84124
+ });
83778
84125
  if (input.result.failed.length > 0) for (const failure of input.result.failed) input.presenter.line({
83779
84126
  code: "space.add_members.failed",
83780
84127
  text: `${failure.candidate.profileId}: ${toErrorMessage$2(failure.error)}`
@@ -83794,6 +84141,22 @@ function emitSpaceAddMembersResult(input) {
83794
84141
  firstFailure: input.joinNoticeSummary.firstFailure
83795
84142
  });
83796
84143
  }
84144
+ function buildSpaceMemberJoinedFollowUp(candidate) {
84145
+ if (candidate.profileKind !== "agent") return null;
84146
+ if (isSpaceMemberCandidateRepairableLocalParticipationCandidate(candidate)) {
84147
+ const agentId = candidate.localTargetAgentId;
84148
+ const repairCommand = agentId ? `ats agents repair --agent ${agentId}` : "ats start";
84149
+ return {
84150
+ profileId: candidate.profileId,
84151
+ text: `${candidate.profileName} joined this space, but ${(candidate.localTargetDisplayName || candidate.localTargetAgentId || "this local agent").trim()} still needs repair on this device before it can wake or reply in the background. Use \`${repairCommand}\` or rerun \`ats start\`.`
84152
+ };
84153
+ }
84154
+ if (!isSpaceMemberCandidateRunnableLocalParticipationCandidate(candidate)) return {
84155
+ profileId: candidate.profileId,
84156
+ text: `${candidate.profileName} joined this space, but it is not connected to a local agent on this device yet. It can appear here now, but it will not wake or reply from this device until you finish setup. Use \`ats profile ready --profile ${candidate.profileId}\` or rerun \`ats start\`.`
84157
+ };
84158
+ return null;
84159
+ }
83797
84160
  async function applySpaceRemoveMembers(input) {
83798
84161
  const result = {
83799
84162
  removed: [],
@@ -84134,7 +84497,7 @@ function formatSpaceMemberCandidateLabel(candidate) {
84134
84497
  function formatSpaceMemberCandidateHint(candidate) {
84135
84498
  if (candidate.alreadyJoined) return "Already in this space";
84136
84499
  if (candidate.needsActivation) return "Needs mention setup";
84137
- return "Selectable";
84500
+ return resolveSpaceMemberCandidateStatusLabel(candidate) ?? "Selectable";
84138
84501
  }
84139
84502
  function buildHumanSpaceMemberCandidateSearchItem(candidate) {
84140
84503
  return {
@@ -84146,10 +84509,12 @@ function buildHumanSpaceMemberCandidateSearchItem(candidate) {
84146
84509
  };
84147
84510
  }
84148
84511
  function buildHumanSpaceMemberCandidateDetail(candidate) {
84149
- let status = null;
84150
- if (candidate.alreadyJoined === true) status = "already in this space";
84151
- else if (candidate.needsActivation) status = "needs mention setup";
84152
- return status ? `ID ${sanitizeSingleLinePromptText(candidate.profileId)} · ${sanitizeSingleLinePromptText(status)}` : `ID ${sanitizeSingleLinePromptText(candidate.profileId)}`;
84512
+ const statuses = [];
84513
+ if (candidate.alreadyJoined === true) statuses.push("already in this space");
84514
+ else if (candidate.needsActivation) statuses.push("needs mention setup");
84515
+ const agentStatuses = resolveSpaceMemberCandidateStatusBadges(candidate);
84516
+ statuses.push(...agentStatuses.map((status) => status.toLowerCase()));
84517
+ return statuses.length > 0 ? `ID ${sanitizeSingleLinePromptText(candidate.profileId)} · ${statuses.map((status) => sanitizeSingleLinePromptText(status)).join(" · ")}` : `ID ${sanitizeSingleLinePromptText(candidate.profileId)}`;
84153
84518
  }
84154
84519
  function buildHumanSpaceMemberCandidateSearchText(candidate) {
84155
84520
  return [
@@ -84160,10 +84525,26 @@ function buildHumanSpaceMemberCandidateSearchText(candidate) {
84160
84525
  sanitizeSingleLinePromptText(normalizeOptionalString$1(candidate.ownerName)),
84161
84526
  sanitizeSingleLinePromptText(normalizeOptionalString$1(candidate.ownerUserId)),
84162
84527
  sanitizeSingleLinePromptText(normalizeOptionalString$1(candidate.controllerRef)),
84528
+ ...resolveSpaceMemberCandidateStatusBadges(candidate),
84163
84529
  candidate.alreadyJoined ? "already in this space" : null,
84164
84530
  candidate.needsActivation ? "needs mention setup" : null
84165
84531
  ].filter((value) => typeof value === "string").join("\n");
84166
84532
  }
84533
+ function resolveSpaceMemberCandidateStatusLabel(candidate) {
84534
+ const statuses = resolveSpaceMemberCandidateStatusBadges(candidate);
84535
+ if (statuses.length === 0) return null;
84536
+ return statuses.join(" · ");
84537
+ }
84538
+ function resolveSpaceMemberCandidateStatusBadges(candidate) {
84539
+ if (candidate.profileKind !== "agent") return [];
84540
+ if (isSpaceMemberCandidateBackgroundReplyReady(candidate)) return ["Ready for background replies"];
84541
+ if (isSpaceMemberCandidateRepairableLocalParticipationCandidate(candidate)) return ["Needs repair"];
84542
+ if (candidate.controllerEnabled === true && typeof candidate.controllerRef === "string" && candidate.controllerRef.trim().length > 0) return ["Can join only"];
84543
+ return ["Not connected", "Can join only"];
84544
+ }
84545
+ function isSpaceMemberCandidateBackgroundReplyReady(candidate) {
84546
+ return candidate.profileKind === "agent" && candidate.controllerEnabled === true && typeof candidate.controllerRef === "string" && candidate.controllerRef.trim().length > 0 && candidate.localTargetSelectable === true && candidate.localParticipationReadiness === "ready";
84547
+ }
84167
84548
  function normalizeRequestedMemberIds(value) {
84168
84549
  if (!Array.isArray(value)) return [];
84169
84550
  const ids = [];
@@ -87469,6 +87850,7 @@ async function runStart(input) {
87469
87850
  });
87470
87851
  const serviceStepResult = await runStartServiceStep({
87471
87852
  foundation,
87853
+ gatewayUrl,
87472
87854
  runtime,
87473
87855
  presenter,
87474
87856
  interactive,