@pellux/goodvibes-tui 1.23.0 → 1.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +3 -3
  5. package/src/cli/bundle-command.ts +4 -0
  6. package/src/cli/help.ts +5 -0
  7. package/src/cli/management-utils.ts +4 -0
  8. package/src/cluster/commands.ts +382 -0
  9. package/src/cluster/remote-daemon-target.ts +225 -0
  10. package/src/cluster/render.ts +268 -0
  11. package/src/core/conversation-line-cache.ts +11 -6
  12. package/src/core/conversation-render-context.ts +122 -0
  13. package/src/core/conversation-rendering.ts +86 -89
  14. package/src/daemon/cli.ts +73 -2
  15. package/src/daemon/handlers/inbox/cursor-store.ts +12 -1
  16. package/src/daemon/handlers/inbox/index.ts +63 -3
  17. package/src/daemon/handlers/inbox/poller.ts +66 -10
  18. package/src/daemon/handlers/index.ts +9 -0
  19. package/src/input/command-args-hint.ts +2 -0
  20. package/src/input/commands/cluster-runtime.ts +53 -0
  21. package/src/input/commands.ts +2 -0
  22. package/src/input/settings-modal-data.ts +6 -0
  23. package/src/main.ts +3 -0
  24. package/src/renderer/conversation-surface.ts +23 -6
  25. package/src/renderer/conversation-tree.ts +142 -93
  26. package/src/renderer/tool-call.ts +13 -12
  27. package/src/runtime/agent-graph-composition.ts +78 -0
  28. package/src/runtime/bootstrap.ts +6 -1
  29. package/src/runtime/cluster-composition.ts +124 -0
  30. package/src/runtime/cluster-group-composition.ts +284 -0
  31. package/src/runtime/daemon-handler-composition.ts +80 -0
  32. package/src/runtime/disposal-wiring.ts +83 -0
  33. package/src/runtime/durability-services.ts +7 -3
  34. package/src/runtime/embedded-service-factories.ts +19 -1
  35. package/src/runtime/process-lifecycle.ts +9 -1
  36. package/src/runtime/remote-execution-composition.ts +62 -0
  37. package/src/runtime/services.ts +73 -76
  38. package/src/verification/verification-ledger.ts +38 -0
  39. package/src/version.ts +1 -1
@@ -0,0 +1,62 @@
1
+ /**
2
+ * remote-execution-composition.ts — running work somewhere other than here.
3
+ *
4
+ * Four collaborators that only make sense together, so they are built together
5
+ * rather than as four unrelated lines in the middle of the runtime composition:
6
+ *
7
+ * - RemoteRunnerRegistry knows which remote runners exist and how to reach
8
+ * them; RemoteSupervisor drives them and is meaningless without it.
9
+ *
10
+ * - SandboxSessionRegistry owns the sandboxes a tool call can be confined to,
11
+ * and McpRegistry is the thing that confines them — an MCP server started
12
+ * without the sandbox registry runs its tools directly on the host.
13
+ *
14
+ * The pairing in each case is the point: handing the supervisor a registry it
15
+ * did not build, or the MCP registry a sandbox runtime it does not share with
16
+ * the session registry, produces a runtime that looks wired and quietly is not.
17
+ */
18
+ import { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
19
+ import {
20
+ RemoteRunnerRegistry,
21
+ RemoteSupervisor,
22
+ SandboxSessionRegistry,
23
+ type RuntimeEventBus,
24
+ } from '@/runtime/index.ts';
25
+ import type { AgentManager } from '@pellux/goodvibes-sdk/platform/tools';
26
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
27
+ import type { HookDispatcher } from '@pellux/goodvibes-sdk/platform/hooks';
28
+
29
+ export interface RemoteExecutionServices {
30
+ readonly remoteRunnerRegistry: RemoteRunnerRegistry;
31
+ readonly remoteSupervisor: RemoteSupervisor;
32
+ readonly sandboxSessionRegistry: SandboxSessionRegistry;
33
+ readonly mcpRegistry: McpRegistry;
34
+ }
35
+
36
+ /**
37
+ * Build the remote-execution and sandboxing seam.
38
+ *
39
+ * Constructing these opens no connection and starts no server: a runner is
40
+ * reached when something asks for it, and an MCP server starts when a session
41
+ * requests it. Composing a runtime in a test therefore executes nothing.
42
+ */
43
+ export function createRemoteExecutionServices(options: {
44
+ readonly agentManager: AgentManager;
45
+ readonly workingDirectory: string;
46
+ readonly hookDispatcher: HookDispatcher;
47
+ readonly configManager: ConfigManager;
48
+ readonly runtimeBus: RuntimeEventBus;
49
+ }): RemoteExecutionServices {
50
+ const remoteRunnerRegistry = new RemoteRunnerRegistry(options.agentManager);
51
+ const remoteSupervisor = new RemoteSupervisor(remoteRunnerRegistry);
52
+ const sandboxSessionRegistry = new SandboxSessionRegistry(options.workingDirectory);
53
+ const mcpRegistry = new McpRegistry({
54
+ hookDispatcher: options.hookDispatcher,
55
+ sandboxSessions: sandboxSessionRegistry,
56
+ });
57
+ mcpRegistry.setRuntimeBus(options.runtimeBus);
58
+ // The SAME sandbox registry the session registry above owns, so a tool call
59
+ // confined by one is confined by the other.
60
+ mcpRegistry.setSandboxRuntime(options.configManager, sandboxSessionRegistry);
61
+ return { remoteRunnerRegistry, remoteSupervisor, sandboxSessionRegistry, mcpRegistry };
62
+ }
@@ -11,6 +11,7 @@ import { StepUpService } from '@pellux/goodvibes-sdk/daemon';
11
11
  import { PairingTokenManager } from '@pellux/goodvibes-sdk/platform/pairing';
12
12
  import { resolvePairingWebOrigin } from '../core/pairing-origin.ts';
13
13
  import { attachWsOnlyGatewayVerbHandlers } from '@pellux/goodvibes-terminal-shell';
14
+ import { createDisposalScope, registerSurfaceRuntimePollers } from './disposal-wiring.ts';
14
15
  import { WatcherRegistry } from '@pellux/goodvibes-sdk/platform/watchers';
15
16
  import { ArtifactStore } from '@pellux/goodvibes-sdk/platform/artifacts';
16
17
  import { createWebKnowledgeGapRepairer } from '@pellux/goodvibes-sdk/platform/knowledge';
@@ -19,7 +20,7 @@ import { createKnowledgeServices } from './knowledge-services.ts';
19
20
  import { MediaProviderRegistry, ensureBuiltinMediaProviders } from '@pellux/goodvibes-sdk/platform/media';
20
21
  import { MultimodalService } from '@pellux/goodvibes-sdk/platform/multimodal';
21
22
  import { AgentMessageBus, AgentOrchestrator, ArchetypeLoader, WrfcController } from '@pellux/goodvibes-sdk/platform/agents';
22
- import { AgentManager, ContextAccountingHolder, OverflowHandler, ProcessManager, createWorkflowServices, type WorkflowServices } from '@pellux/goodvibes-sdk/platform/tools';
23
+ import { AgentManager, ContextAccountingHolder, OverflowHandler, ProcessManager, cancelAllAgentRuns, createWorkflowServices, type WorkflowServices } from '@pellux/goodvibes-sdk/platform/tools';
23
24
  import { FileStateCache, FileUndoManager, MemoryConsolidationScheduler, MemoryEmbeddingProviderRegistry, MemoryRegistry, MemoryStore, ModeManager, ProjectIndex, resolveCanonicalMemoryDbPath, type CodeIndexStore, type CodeIndexReindexScheduler } from '@pellux/goodvibes-sdk/platform/state';
24
25
  import type { StoreSnapshotScheduler } from '@pellux/goodvibes-sdk/platform/state/store-snapshots';
25
26
  import type { UserPermissionRuleStore } from '@pellux/goodvibes-sdk/platform/permissions';
@@ -36,8 +37,7 @@ import type { DomainDispatch, RuntimeStore } from './store/index.ts';
36
37
  import {
37
38
  type RuntimeEventBus, DistributedRuntimeManager, RemoteRunnerRegistry, RemoteSupervisor, IntegrationHelperService,
38
39
  IdempotencyStore, ComponentHealthMonitor, WorktreeRegistry, SandboxSessionRegistry, createShellPathService,
39
- type ShellPathService, type FeatureFlagManager, createFeatureFlagManager, PolicyRuntimeState,
40
- type SessionSurface,
40
+ type ShellPathService, type FeatureFlagManager, createFeatureFlagManager, PolicyRuntimeState, type SessionSurface,
41
41
  } from '@/runtime/index.ts';
42
42
  import { createSessionStorageServices } from './session-storage-services.ts';
43
43
  import { VoiceProviderRegistry, VoiceService, ensureBuiltinVoiceProviders } from '@pellux/goodvibes-sdk/platform/voice';
@@ -55,6 +55,8 @@ import { SessionManager, CrossSessionTaskRegistry, SessionChangeTracker } from '
55
55
  import { ApiTokenAuditor, UserAuthManager } from '@pellux/goodvibes-sdk/platform/security';
56
56
  import { WebhookNotifier } from '@pellux/goodvibes-sdk/platform/integrations';
57
57
  import { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
58
+ import { createRemoteExecutionServices } from './remote-execution-composition.ts';
59
+ import { createAgentGraph } from './agent-graph-composition.ts';
58
60
  import { BenchmarkStore, CacheHitTracker, FavoritesStore, ModelLimitsService, ProviderCapabilityRegistry, ProviderOptimizer, ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
59
61
  import { KeybindingsManager } from '../input/keybindings.ts';
60
62
  import { AdaptivePlanner, DeterministicReplayEngine, ExecutionPlanManager, SessionLineageTracker, SessionMemoryStore } from '@pellux/goodvibes-sdk/platform/core';
@@ -67,16 +69,10 @@ import { createWorkstreamServices, type OrchestrationEngine, type WorkstreamComm
67
69
  import { wireFleetNeedsInputPush } from './fleet-needs-input-push.ts';
68
70
  import { codeIndexDbPath, createCodeIndexServices, createStoreRerooter, isCodeInjectionSettingEnabled } from './code-index-services.ts';
69
71
  import type { WorkPlanStore } from '../work-plans/work-plan-store.ts';
70
- import { registerDaemonHandlers, type DaemonHandlerSurfaces } from '../daemon/handlers/index.ts';
71
- import type { HandlerContext, HandlerLogger } from '../daemon/handlers/context.ts';
72
- import { createDaemonCredentialStore } from '../daemon/handlers/credentials.ts';
73
- import { registerRouting } from '../daemon/handlers/routing/index.ts';
74
- import { registerInboxMethods } from '../daemon/handlers/inbox/index.ts';
75
- import { registerTriagedInbox } from '../daemon/handlers/triage/index.ts';
76
- import { registerDraftMethods } from '../daemon/handlers/drafts/index.ts';
77
- import { registerCalendar } from '../daemon/handlers/calendar/index.ts';
78
- import { registerEmailMethods } from '../daemon/handlers/email/index.ts';
79
- import { registerRemoteSurface } from '../daemon/handlers/remote/index.ts';
72
+ import type { DaemonHandlerSurfaces } from '../daemon/handlers/index.ts';
73
+ import { createDaemonHandlerComposition } from './daemon-handler-composition.ts';
74
+ import { createClusterServices, startClusterServices, type ClusterGroupComposition } from './cluster-group-composition.ts';
75
+ import type { ClusterCoordinator } from '@pellux/goodvibes-sdk/platform/cluster';
80
76
  import { WorkspaceTrustManager } from './trust/workspace-trust.ts';
81
77
  import { ensureConfiguredModelIsRoutable } from './provider-fallback.ts';
82
78
 
@@ -184,6 +180,12 @@ export interface RuntimeServices {
184
180
  readonly toolLLM: ToolLLM;
185
181
  readonly distributedRuntime: DistributedRuntimeManager;
186
182
  readonly daemonHandlers: DaemonHandlerSurfaces;
183
+ /** Elects the one node on this network that consumes inbound messages; hand it to the DaemonServer so its consumers share this leadership instead of holding a second election. */
184
+ readonly clusterCoordinator: ClusterCoordinator;
185
+ /** LAN group membership: identity, keys, roster, and the `cluster` verbs. */
186
+ readonly clusterGroup: ClusterGroupComposition;
187
+ /** Start the group layer and then the election, in that order. Idempotent. */
188
+ readonly startCluster: () => Promise<void>;
187
189
  readonly remoteRunnerRegistry: RemoteRunnerRegistry;
188
190
  readonly remoteSupervisor: RemoteSupervisor;
189
191
  readonly sessionMemoryStore: SessionMemoryStore;
@@ -231,10 +233,20 @@ export interface RuntimeServices {
231
233
  readonly integrationHelpers: IntegrationHelperService;
232
234
  /** Re-root path-bound stores (MemoryStore, ProjectIndex) to a new working directory, called by WorkspaceSwapManager after verification; stores needing a process restart just warn-log and keep serving the old path until the daemon restarts with the new --working-dir. */
233
235
  rerootStores(newWorkingDir: string): Promise<void>;
236
+ /**
237
+ * Cancel the agent runs this graph is hosting, returning how many.
238
+ *
239
+ * Required by the SDK's RuntimePollerOwners: by dispose() time the fleet
240
+ * registry, orchestration engine, process registry and bus these runs report
241
+ * through are already down, so a run still described as "running" is orphaned
242
+ * rather than preserved.
243
+ */
244
+ cancelHostedAgentRuns(): number;
245
+ dispose(): void; // Stop every poller this graph started; best-effort, total, idempotent. This surface owns its graph — see disposal-wiring.ts.
234
246
  }
235
247
 
236
248
  export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeServices {
237
- const workingDirectory = options.workingDir;
249
+ const disposalScope = createDisposalScope('RuntimeServices'); const workingDirectory = options.workingDir; // disposal seam: see ./disposal-wiring.ts
238
250
  const homeDirectory = options.homeDirectory;
239
251
  const shellPaths = createShellPathService({
240
252
  workingDirectory,
@@ -340,34 +352,14 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
340
352
  runtimeStore: options.runtimeStore,
341
353
  runtimeBus: options.runtimeBus,
342
354
  });
343
- const agentMessageBus = new AgentMessageBus();
344
- agentMessageBus.setRuntimeBus(options.runtimeBus);
345
- const archetypeLoader = new ArchetypeLoader(join(workingDirectory, '.goodvibes', 'agents'));
346
- const agentOrchestrator = new AgentOrchestrator({
347
- messageBus: agentMessageBus,
348
- });
349
- agentOrchestrator.setRuntimeBus(options.runtimeBus);
350
- const agentManager = new AgentManager({
351
- archetypeLoader,
352
- messageBus: agentMessageBus,
353
- executor: agentOrchestrator,
354
- configManager,
355
- // The live registry lets a bare model id in a spawn() override resolve
356
- // through the shared resolver instead of being rejected as unqualified.
357
- providerRegistry,
358
- });
359
- const contextAccountingHolder = new ContextAccountingHolder();
360
- agentOrchestrator.setConversationSink({ // Conversation-snapshot bridge (mirrors the SDK's own createRuntimeServices)
361
- register: (agentId, source) => agentManager.registerConversationSource(agentId, source),
362
- release: (agentId) => agentManager.releaseConversationSource(agentId),
363
- });
364
- agentManager.setRuntimeBus(options.runtimeBus);
365
- const wrfcController = new WrfcController(options.runtimeBus, agentMessageBus, {
366
- agentManager,
367
- configManager,
368
- projectRoot: workingDirectory,
355
+ // The agent-execution graph, wired in both directions; see
356
+ // agent-graph-composition.ts for why the six are built as one.
357
+ const {
358
+ agentMessageBus, archetypeLoader, agentOrchestrator,
359
+ agentManager, contextAccountingHolder, wrfcController,
360
+ } = createAgentGraph({
361
+ runtimeBus: options.runtimeBus, workingDirectory, configManager, providerRegistry,
369
362
  });
370
- agentManager.setWrfcController(wrfcController);
371
363
  const hookDispatcher = new HookDispatcher({ agentManager, toolLLM, projectRoot: workingDirectory }, hookActivityTracker);
372
364
  configManager.attachHookDispatcher(hookDispatcher);
373
365
  const hookWorkbench = createHookWorkbench({
@@ -419,6 +411,12 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
419
411
  const memorySpine = new MemorySpineClient({ local: createLocalMemoryAccess(memoryRegistry) });
420
412
  const deliveryManager = new AutomationDeliveryManager({
421
413
  configManager,
414
+ // This manager builds the delivery router the daemon actually replies
415
+ // through (bootstrap.ts hands it to the daemon facade). Without the
416
+ // secrets manager it cannot resolve a goodvibes://secrets/... credential,
417
+ // so Telegram accepted every inbound message and dropped every reply with
418
+ // "Missing Telegram bot token" while ntfy — which needs no secret — worked.
419
+ secretsManager,
422
420
  serviceRegistry,
423
421
  runtimeBus: options.runtimeBus,
424
422
  runtimeStore: options.runtimeStore,
@@ -498,43 +496,30 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
498
496
  automationBridge: automationManager,
499
497
  });
500
498
 
501
- // Daemon handler surfaces: attach HOST handlers to the SDK-auto-registered builtin
502
- // gateway descriptors (channels.* / email.* / calendar.*) via catalog.register(descriptor,
503
- // handler, { replace: true }) — the SDK owns every id/descriptor/schema. The remote
504
- // surface reuses the SAME DistributedRuntimeManager the SDK facade injects.
505
- const handlerLogger: HandlerLogger = {
506
- info: (message, meta) => console.info(message, meta ?? ''),
507
- warn: (message, meta) => console.warn(message, meta ?? ''),
508
- error: (message, meta) => console.error(message, meta ?? ''),
509
- };
510
- const handlerContext: HandlerContext = {
511
- catalog: gatewayMethods,
512
- credentials: createDaemonCredentialStore(secretsManager),
499
+ // Which machines on this network are "us", and which of them reads the
500
+ // shared inbox. Both inert until startCluster() — no socket, no key material
501
+ // read; see cluster-group-composition.ts for why they are built together.
502
+ const { clusterGroup, clusterCoordinator } = createClusterServices({
503
+ configManager, shellPaths, secretsManager,
504
+ });
505
+ // Daemon handler surfaces (see daemon-handler-composition.ts); the inbox
506
+ // poller registers itself with the coordinator rather than starting eagerly.
507
+ const daemonHandlers = createDaemonHandlerComposition({
508
+ gatewayMethods,
509
+ secretsManager,
513
510
  configManager,
514
511
  workingDirectory,
515
512
  homeDirectory,
516
- logger: handlerLogger,
517
- };
518
- const daemonHandlers = registerDaemonHandlers(handlerContext, {
519
- registerRouting,
520
- registerInbox: (ctx, routing) =>
521
- registerTriagedInbox(ctx, (inboxCtx) => registerInboxMethods(inboxCtx, routing))
522
- .unregister,
523
- registerDrafts: (ctx) => registerDraftMethods(ctx),
524
- registerCalendar,
525
- registerEmail: (ctx) => registerEmailMethods(ctx),
526
- registerRemote: (ctx) => registerRemoteSurface(ctx, { manager: distributedRuntime }),
513
+ distributedRuntime,
514
+ clusterCoordinator,
527
515
  });
528
516
 
529
- const remoteRunnerRegistry = new RemoteRunnerRegistry(agentManager);
530
- const remoteSupervisor = new RemoteSupervisor(remoteRunnerRegistry);
531
- const sandboxSessionRegistry = new SandboxSessionRegistry(workingDirectory);
532
- const mcpRegistry = new McpRegistry({
533
- hookDispatcher,
534
- sandboxSessions: sandboxSessionRegistry,
535
- });
536
- mcpRegistry.setRuntimeBus(options.runtimeBus);
537
- mcpRegistry.setSandboxRuntime(configManager, sandboxSessionRegistry);
517
+ // Remote runners and the sandboxes tool calls are confined to; see
518
+ // remote-execution-composition.ts for why the four are built as one.
519
+ const { remoteRunnerRegistry, remoteSupervisor, sandboxSessionRegistry, mcpRegistry }
520
+ = createRemoteExecutionServices({
521
+ agentManager, workingDirectory, hookDispatcher, configManager, runtimeBus: options.runtimeBus,
522
+ });
538
523
  const tokenAuditor = new ApiTokenAuditor({ managed: false });
539
524
  const componentHealthMonitor = new ComponentHealthMonitor();
540
525
  const worktreeRegistry = new WorktreeRegistry(workingDirectory);
@@ -572,7 +557,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
572
557
  // code-index-services.ts's header doc.
573
558
  const { codeIndexStore, codeIndexReindexScheduler } = createCodeIndexServices({ workingDirectory, configManager, memoryEmbeddingRegistry, isReindexPaused: () => pauseController.isPaused('code-index-reindex'), admitExpensiveWork });
574
559
  // Store snapshots, the periodic append-only sweep, durable remembered-approval rules + the live credential chain — see durability-services.ts.
575
- const { storeSnapshotScheduler, appendOnlyRetentionScheduler, userPermissionRuleStore, stopDurabilityHousekeeping } = createDurabilityServices({
560
+ const { storeSnapshotScheduler, appendOnlyRetentionScheduler, userPermissionRuleStore, stopDurabilityHousekeeping, stopConfigWatch } = createDurabilityServices({
576
561
  configManager, secretsManager, providerRegistry, memoryDbPath, codeIndexDbPath: codeIndexDbPath(workingDirectory), surface, shellPaths, // + retention-sweep roots & live config watch (mirrors the SDK)
577
562
  ...(options.currentSessionId ? { currentSessionId: options.currentSessionId } : {}), // exempts the running session from crash-residue reaping
578
563
  });
@@ -618,7 +603,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
618
603
  const { voiceSetup } = wireVoiceSetup({ configManager, shellPaths, voiceProviders, admitExpensiveWork });
619
604
 
620
605
  // Terminal-shell wrapper over the SDK registerGatewayVerbGroups (gateway-verbs.ts); checkin.*/fleet-needs-input/pairing.* register only when their deps are present. memoryGovernor lights up ops.memory.get; voiceSetup lights up voice.local.status/install.
621
- attachWsOnlyGatewayVerbHandlers(gatewayMethods, { processRegistry, workspaceCheckpointManager, conversationRewindPort: createSessionConversationRewindPort(), sessionBroker, secretsManager, stepUpService, approvalBroker, requestApproval: (input) => approvalBroker.requestApproval(input), watcherRegistry, userPermissionRuleStore, shellPaths, configManager, runtimeStore: options.runtimeStore, channelDeliveryRouter, providerRegistry, automationManager, sessionLister: sessionBroker, sessionIntake: sessionBroker, workingDirectory, memoryRegistry, pairingTokens, sessionLiveTurnControls, powerManager, memoryGovernor, voiceSetup, relayAvailable: () => configManager.get('relay.enabled') === true, pairingWebOrigin: () => resolvePairingWebOrigin(configManager).origin, ...wireFleetNeedsInputPush({ registry: processRegistry, runtimeBus: options.runtimeBus, sessionBroker }) });
606
+ attachWsOnlyGatewayVerbHandlers(gatewayMethods, { processRegistry, workspaceCheckpointManager, conversationRewindPort: createSessionConversationRewindPort(), sessionBroker, secretsManager, stepUpService, approvalBroker, requestApproval: (input) => approvalBroker.requestApproval(input), watcherRegistry, userPermissionRuleStore, shellPaths, configManager, runtimeStore: options.runtimeStore, channelDeliveryRouter, providerRegistry, automationManager, sessionLister: sessionBroker, sessionIntake: sessionBroker, workingDirectory, memoryRegistry, pairingTokens, sessionLiveTurnControls, powerManager, memoryGovernor, voiceSetup, relayAvailable: () => configManager.get('relay.enabled') === true, pairingWebOrigin: () => resolvePairingWebOrigin(configManager).origin, disposal: disposalScope.registry, ...wireFleetNeedsInputPush({ registry: processRegistry, runtimeBus: options.runtimeBus, sessionBroker }) });
622
607
  const integrationHelpers = new IntegrationHelperService({
623
608
  surface, // surface-scoped: continuity's recovery-file check must read the SAME paths the app writes with, not the unscoped legacy pair.
624
609
  runtimeStore: options.runtimeStore,
@@ -684,7 +669,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
684
669
  // bridge (the high-churn 'ops' domain stays out of the wholesale allowlist).
685
670
  wireMemoryPressureNotice(options.runtimeBus, notificationDispatcher);
686
671
 
687
- return {
672
+ const services: RuntimeServices = {
688
673
  workingDirectory,
689
674
  homeDirectory,
690
675
  surface,
@@ -762,6 +747,9 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
762
747
  toolLLM,
763
748
  distributedRuntime,
764
749
  daemonHandlers,
750
+ clusterCoordinator,
751
+ clusterGroup,
752
+ startCluster: () => startClusterServices({ clusterGroup, clusterCoordinator }),
765
753
  remoteRunnerRegistry,
766
754
  remoteSupervisor,
767
755
  sessionMemoryStore,
@@ -796,5 +784,14 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
796
784
  workspaceCheckpointManager,
797
785
  integrationHelpers,
798
786
  rerootStores: createStoreRerooter({ codeIndexStore, projectIndex }),
787
+ // Cancels the agent runs this graph was hosting. By dispose() time the
788
+ // fleet registry, orchestration engine, process registry and bus these runs
789
+ // report through are already down, so a run still described as "running" is
790
+ // orphaned rather than preserved — and this is the only shutdown-reachable
791
+ // way to abort its in-flight provider call instead of letting it sleep out
792
+ // a retry backoff nobody is waiting on.
793
+ cancelHostedAgentRuns: () => cancelAllAgentRuns(agentManager),
794
+ dispose: (): void => disposalScope.dispose(),
799
795
  };
796
+ registerSurfaceRuntimePollers(disposalScope.registry, services, { stopConfigWatch }); return services;
800
797
  }
@@ -206,9 +206,47 @@ const ENABLEMENT_KEYS_BEHAVIOR_VERIFIED = new Set(
206
206
  ).size;
207
207
 
208
208
  /** Settings with real local behavior verification: the authored baseline plus the persistence-tested feature-knob, device/trigger, and enablement keys. */
209
+ /**
210
+ * Config keys added by the LAN group-key layer: the automatic group-key
211
+ * rotation interval, the dual-generation acceptance window around a rotation,
212
+ * the discovery beacon interval, and the roster gossip interval.
213
+ *
214
+ * THIS CONSTANT IS NOT A DIAL, for the same reason as the two above. Each key
215
+ * is counted because a test that exercises it was actually written:
216
+ * `src/test/verification/cluster-group-settings-persistence.test.ts` runs the
217
+ * same four-part persistence contract — schema default exposure, `set()`
218
+ * through the key's own validator to disk, reload into a fresh ConfigManager
219
+ * with read-back equality, and reset-to-default that survives reload.
220
+ *
221
+ * PER-KEY EVIDENCE. All four have a LIVE consumer in this build, which is more
222
+ * than several of the keys counted above can claim:
223
+ * cluster.keyRotationHours — read by resolveClusterGroupSettings and
224
+ * compared against the current key's age in
225
+ * the group runtime's rotation check.
226
+ * cluster.keyRotationGraceMinutes — sets how long the previous generation
227
+ * stays accepted after a scheduled
228
+ * rotation; drives the keyring's accepted
229
+ * generation set.
230
+ * cluster.beaconSeconds — the discovery beacon timer interval, and
231
+ * the basis of the "recently heard from"
232
+ * window that decides which member mints a
233
+ * rotation.
234
+ * cluster.rosterGossipSeconds — how often the member list is shared.
235
+ *
236
+ * `cluster.enabled` is NOT here: it is a feature enablement key and the ledger
237
+ * counts those in its own set.
238
+ */
239
+ export const CLUSTER_GROUP_LOCAL_SETTINGS = [
240
+ 'cluster.keyRotationHours',
241
+ 'cluster.keyRotationGraceMinutes',
242
+ 'cluster.beaconSeconds',
243
+ 'cluster.rosterGossipSeconds',
244
+ ] as const;
245
+
209
246
  const SETTINGS_BEHAVIOR_VERIFIED = SETTINGS_BEHAVIOR_BASELINE
210
247
  + FEATURE_KNOB_LOCAL_SETTINGS.length
211
248
  + DEVICE_AND_TRIGGER_LOCAL_SETTINGS.length
249
+ + CLUSTER_GROUP_LOCAL_SETTINGS.length
212
250
  + ENABLEMENT_KEYS_BEHAVIOR_VERIFIED;
213
251
 
214
252
  const EXTERNAL_SLASH_COMMANDS = new Set([
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '1.23.0';
9
+ let _version = '1.24.1';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  // Only trust a version read from OUR OWN package.json. A Bun single-file