@pellux/goodvibes-tui 1.22.0 → 1.24.0

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 (36) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +7 -7
  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 +71 -2
  15. package/src/daemon/handlers/inbox/index.ts +63 -3
  16. package/src/daemon/handlers/inbox/poller.ts +54 -8
  17. package/src/input/command-args-hint.ts +2 -0
  18. package/src/input/commands/cluster-runtime.ts +53 -0
  19. package/src/input/commands.ts +2 -0
  20. package/src/input/settings-modal-data.ts +6 -0
  21. package/src/main.ts +3 -0
  22. package/src/renderer/conversation-surface.ts +23 -6
  23. package/src/renderer/conversation-tree.ts +142 -93
  24. package/src/renderer/tool-call.ts +13 -12
  25. package/src/runtime/bootstrap.ts +6 -1
  26. package/src/runtime/cluster-composition.ts +124 -0
  27. package/src/runtime/cluster-group-composition.ts +284 -0
  28. package/src/runtime/daemon-handler-composition.ts +80 -0
  29. package/src/runtime/disposal-wiring.ts +66 -0
  30. package/src/runtime/durability-services.ts +7 -3
  31. package/src/runtime/embedded-service-factories.ts +19 -1
  32. package/src/runtime/process-lifecycle.ts +9 -1
  33. package/src/runtime/remote-execution-composition.ts +62 -0
  34. package/src/runtime/services.ts +50 -50
  35. package/src/verification/verification-ledger.ts +38 -0
  36. package/src/version.ts +1 -1
@@ -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';
@@ -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,7 @@ 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';
58
59
  import { BenchmarkStore, CacheHitTracker, FavoritesStore, ModelLimitsService, ProviderCapabilityRegistry, ProviderOptimizer, ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
59
60
  import { KeybindingsManager } from '../input/keybindings.ts';
60
61
  import { AdaptivePlanner, DeterministicReplayEngine, ExecutionPlanManager, SessionLineageTracker, SessionMemoryStore } from '@pellux/goodvibes-sdk/platform/core';
@@ -67,16 +68,10 @@ import { createWorkstreamServices, type OrchestrationEngine, type WorkstreamComm
67
68
  import { wireFleetNeedsInputPush } from './fleet-needs-input-push.ts';
68
69
  import { codeIndexDbPath, createCodeIndexServices, createStoreRerooter, isCodeInjectionSettingEnabled } from './code-index-services.ts';
69
70
  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';
71
+ import type { DaemonHandlerSurfaces } from '../daemon/handlers/index.ts';
72
+ import { createDaemonHandlerComposition } from './daemon-handler-composition.ts';
73
+ import { createClusterServices, startClusterServices, type ClusterGroupComposition } from './cluster-group-composition.ts';
74
+ import type { ClusterCoordinator } from '@pellux/goodvibes-sdk/platform/cluster';
80
75
  import { WorkspaceTrustManager } from './trust/workspace-trust.ts';
81
76
  import { ensureConfiguredModelIsRoutable } from './provider-fallback.ts';
82
77
 
@@ -184,6 +179,12 @@ export interface RuntimeServices {
184
179
  readonly toolLLM: ToolLLM;
185
180
  readonly distributedRuntime: DistributedRuntimeManager;
186
181
  readonly daemonHandlers: DaemonHandlerSurfaces;
182
+ /** 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. */
183
+ readonly clusterCoordinator: ClusterCoordinator;
184
+ /** LAN group membership: identity, keys, roster, and the `cluster` verbs. */
185
+ readonly clusterGroup: ClusterGroupComposition;
186
+ /** Start the group layer and then the election, in that order. Idempotent. */
187
+ readonly startCluster: () => Promise<void>;
187
188
  readonly remoteRunnerRegistry: RemoteRunnerRegistry;
188
189
  readonly remoteSupervisor: RemoteSupervisor;
189
190
  readonly sessionMemoryStore: SessionMemoryStore;
@@ -231,10 +232,11 @@ export interface RuntimeServices {
231
232
  readonly integrationHelpers: IntegrationHelperService;
232
233
  /** 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
234
  rerootStores(newWorkingDir: string): Promise<void>;
235
+ dispose(): void; // Stop every poller this graph started; best-effort, total, idempotent. This surface owns its graph — see disposal-wiring.ts.
234
236
  }
235
237
 
236
238
  export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeServices {
237
- const workingDirectory = options.workingDir;
239
+ const disposalScope = createDisposalScope('RuntimeServices'); const workingDirectory = options.workingDir; // disposal seam: see ./disposal-wiring.ts
238
240
  const homeDirectory = options.homeDirectory;
239
241
  const shellPaths = createShellPathService({
240
242
  workingDirectory,
@@ -382,6 +384,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
382
384
  routeBindings,
383
385
  agentStatusProvider: agentManager,
384
386
  messageSender: agentMessageBus,
387
+ conversationGateConfig: configManager, // without this the gate runs on DEFAULTS: an inbound message landing in a live session takes the handover and starts work whatever conversationGate.mode/gatedSurfaces say
385
388
  });
386
389
  sessionBroker.setContinuationRunner(async ({ task, input }) => {
387
390
  const record = agentManager.spawn({
@@ -418,6 +421,12 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
418
421
  const memorySpine = new MemorySpineClient({ local: createLocalMemoryAccess(memoryRegistry) });
419
422
  const deliveryManager = new AutomationDeliveryManager({
420
423
  configManager,
424
+ // This manager builds the delivery router the daemon actually replies
425
+ // through (bootstrap.ts hands it to the daemon facade). Without the
426
+ // secrets manager it cannot resolve a goodvibes://secrets/... credential,
427
+ // so Telegram accepted every inbound message and dropped every reply with
428
+ // "Missing Telegram bot token" while ntfy — which needs no secret — worked.
429
+ secretsManager,
421
430
  serviceRegistry,
422
431
  runtimeBus: options.runtimeBus,
423
432
  runtimeStore: options.runtimeStore,
@@ -497,43 +506,30 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
497
506
  automationBridge: automationManager,
498
507
  });
499
508
 
500
- // Daemon handler surfaces: attach HOST handlers to the SDK-auto-registered builtin
501
- // gateway descriptors (channels.* / email.* / calendar.*) via catalog.register(descriptor,
502
- // handler, { replace: true }) — the SDK owns every id/descriptor/schema. The remote
503
- // surface reuses the SAME DistributedRuntimeManager the SDK facade injects.
504
- const handlerLogger: HandlerLogger = {
505
- info: (message, meta) => console.info(message, meta ?? ''),
506
- warn: (message, meta) => console.warn(message, meta ?? ''),
507
- error: (message, meta) => console.error(message, meta ?? ''),
508
- };
509
- const handlerContext: HandlerContext = {
510
- catalog: gatewayMethods,
511
- credentials: createDaemonCredentialStore(secretsManager),
509
+ // Which machines on this network are "us", and which of them reads the
510
+ // shared inbox. Both inert until startCluster() — no socket, no key material
511
+ // read; see cluster-group-composition.ts for why they are built together.
512
+ const { clusterGroup, clusterCoordinator } = createClusterServices({
513
+ configManager, shellPaths, secretsManager,
514
+ });
515
+ // Daemon handler surfaces (see daemon-handler-composition.ts); the inbox
516
+ // poller registers itself with the coordinator rather than starting eagerly.
517
+ const daemonHandlers = createDaemonHandlerComposition({
518
+ gatewayMethods,
519
+ secretsManager,
512
520
  configManager,
513
521
  workingDirectory,
514
522
  homeDirectory,
515
- logger: handlerLogger,
516
- };
517
- const daemonHandlers = registerDaemonHandlers(handlerContext, {
518
- registerRouting,
519
- registerInbox: (ctx, routing) =>
520
- registerTriagedInbox(ctx, (inboxCtx) => registerInboxMethods(inboxCtx, routing))
521
- .unregister,
522
- registerDrafts: (ctx) => registerDraftMethods(ctx),
523
- registerCalendar,
524
- registerEmail: (ctx) => registerEmailMethods(ctx),
525
- registerRemote: (ctx) => registerRemoteSurface(ctx, { manager: distributedRuntime }),
523
+ distributedRuntime,
524
+ clusterCoordinator,
526
525
  });
527
526
 
528
- const remoteRunnerRegistry = new RemoteRunnerRegistry(agentManager);
529
- const remoteSupervisor = new RemoteSupervisor(remoteRunnerRegistry);
530
- const sandboxSessionRegistry = new SandboxSessionRegistry(workingDirectory);
531
- const mcpRegistry = new McpRegistry({
532
- hookDispatcher,
533
- sandboxSessions: sandboxSessionRegistry,
534
- });
535
- mcpRegistry.setRuntimeBus(options.runtimeBus);
536
- mcpRegistry.setSandboxRuntime(configManager, sandboxSessionRegistry);
527
+ // Remote runners and the sandboxes tool calls are confined to; see
528
+ // remote-execution-composition.ts for why the four are built as one.
529
+ const { remoteRunnerRegistry, remoteSupervisor, sandboxSessionRegistry, mcpRegistry }
530
+ = createRemoteExecutionServices({
531
+ agentManager, workingDirectory, hookDispatcher, configManager, runtimeBus: options.runtimeBus,
532
+ });
537
533
  const tokenAuditor = new ApiTokenAuditor({ managed: false });
538
534
  const componentHealthMonitor = new ComponentHealthMonitor();
539
535
  const worktreeRegistry = new WorktreeRegistry(workingDirectory);
@@ -544,8 +540,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
544
540
  bindProviderOptimizerFeatureFlag(featureFlags, providerOptimizer);
545
541
  applyProviderOptimizerConfigMode(configManager, providerOptimizer);
546
542
  const sessionMemoryStore = new SessionMemoryStore();
547
- const sessionLineageTracker = new SessionLineageTracker();
548
- const sessionChangeTracker = new SessionChangeTracker();
543
+ const sessionLineageTracker = new SessionLineageTracker(); const sessionChangeTracker = new SessionChangeTracker();
549
544
  const planManager = new ExecutionPlanManager(workingDirectory);
550
545
  const adaptivePlanner = new AdaptivePlanner();
551
546
  const idempotencyStore = new IdempotencyStore();
@@ -572,7 +567,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
572
567
  // code-index-services.ts's header doc.
573
568
  const { codeIndexStore, codeIndexReindexScheduler } = createCodeIndexServices({ workingDirectory, configManager, memoryEmbeddingRegistry, isReindexPaused: () => pauseController.isPaused('code-index-reindex'), admitExpensiveWork });
574
569
  // 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({
570
+ const { storeSnapshotScheduler, appendOnlyRetentionScheduler, userPermissionRuleStore, stopDurabilityHousekeeping, stopConfigWatch } = createDurabilityServices({
576
571
  configManager, secretsManager, providerRegistry, memoryDbPath, codeIndexDbPath: codeIndexDbPath(workingDirectory), surface, shellPaths, // + retention-sweep roots & live config watch (mirrors the SDK)
577
572
  ...(options.currentSessionId ? { currentSessionId: options.currentSessionId } : {}), // exempts the running session from crash-residue reaping
578
573
  });
@@ -618,7 +613,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
618
613
  const { voiceSetup } = wireVoiceSetup({ configManager, shellPaths, voiceProviders, admitExpensiveWork });
619
614
 
620
615
  // 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 }) });
616
+ 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
617
  const integrationHelpers = new IntegrationHelperService({
623
618
  surface, // surface-scoped: continuity's recovery-file check must read the SAME paths the app writes with, not the unscoped legacy pair.
624
619
  runtimeStore: options.runtimeStore,
@@ -684,7 +679,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
684
679
  // bridge (the high-churn 'ops' domain stays out of the wholesale allowlist).
685
680
  wireMemoryPressureNotice(options.runtimeBus, notificationDispatcher);
686
681
 
687
- return {
682
+ const services: RuntimeServices = {
688
683
  workingDirectory,
689
684
  homeDirectory,
690
685
  surface,
@@ -762,6 +757,9 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
762
757
  toolLLM,
763
758
  distributedRuntime,
764
759
  daemonHandlers,
760
+ clusterCoordinator,
761
+ clusterGroup,
762
+ startCluster: () => startClusterServices({ clusterGroup, clusterCoordinator }),
765
763
  remoteRunnerRegistry,
766
764
  remoteSupervisor,
767
765
  sessionMemoryStore,
@@ -796,5 +794,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
796
794
  workspaceCheckpointManager,
797
795
  integrationHelpers,
798
796
  rerootStores: createStoreRerooter({ codeIndexStore, projectIndex }),
797
+ dispose: (): void => disposalScope.dispose(),
799
798
  };
799
+ registerSurfaceRuntimePollers(disposalScope.registry, services, { stopConfigWatch }); return services;
800
800
  }
@@ -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.22.0';
9
+ let _version = '1.24.0';
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