@pellux/goodvibes-tui 1.17.0 → 1.18.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 (84) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +4596 -1732
  4. package/package.json +7 -7
  5. package/src/cli/management-commands.ts +82 -15
  6. package/src/cli/management.ts +0 -2
  7. package/src/core/consolidation-receipt.ts +41 -0
  8. package/src/core/conversation-rendering.ts +14 -5
  9. package/src/core/memory-provenance.ts +67 -0
  10. package/src/core/pairing-devices.ts +49 -0
  11. package/src/core/pairing-handoff.ts +101 -0
  12. package/src/core/pairing-offers.ts +83 -0
  13. package/src/core/pairing-origin.ts +78 -0
  14. package/src/core/pairing-tailscale-gateway.ts +56 -0
  15. package/src/core/power-chip-source.ts +91 -0
  16. package/src/core/power-status.ts +95 -0
  17. package/src/core/stable-host.ts +171 -0
  18. package/src/core/stream-event-wiring.ts +21 -0
  19. package/src/core/turn-budget-outcome.ts +60 -0
  20. package/src/core/turn-cancellation.ts +39 -0
  21. package/src/core/turn-event-wiring.ts +6 -0
  22. package/src/daemon/cli.ts +47 -56
  23. package/src/input/command-registry.ts +21 -0
  24. package/src/input/commands/acp-runtime.ts +72 -0
  25. package/src/input/commands/devices-runtime.ts +109 -0
  26. package/src/input/commands/git-runtime.ts +28 -4
  27. package/src/input/commands/graph-runtime.ts +45 -0
  28. package/src/input/commands/operator-rpc.ts +32 -11
  29. package/src/input/commands/power-runtime.ts +56 -0
  30. package/src/input/commands/qrcode-runtime.ts +14 -19
  31. package/src/input/commands/queue-runtime.ts +70 -0
  32. package/src/input/commands/workstream-attempts.ts +6 -1
  33. package/src/input/commands/workstream-runtime.ts +44 -25
  34. package/src/input/commands/worktree-runtime.ts +35 -8
  35. package/src/input/commands.ts +10 -0
  36. package/src/input/config-modal-types.ts +11 -0
  37. package/src/input/config-modal.ts +65 -2
  38. package/src/input/handler-shortcuts.ts +23 -0
  39. package/src/input/keybindings.ts +19 -1
  40. package/src/input/settings-modal-data.ts +31 -37
  41. package/src/input/settings-modal-types.ts +5 -2
  42. package/src/main.ts +42 -42
  43. package/src/panels/builtin/operations.ts +54 -1
  44. package/src/panels/builtin/shared.ts +12 -0
  45. package/src/panels/builtin-modals.ts +49 -32
  46. package/src/panels/diff-panel.ts +34 -1
  47. package/src/panels/fleet-acts.ts +446 -0
  48. package/src/panels/fleet-gateway.ts +117 -0
  49. package/src/panels/fleet-observed-render.ts +128 -0
  50. package/src/panels/fleet-panel-format.ts +120 -4
  51. package/src/panels/fleet-panel-worktree-detail.ts +14 -0
  52. package/src/panels/fleet-panel.ts +60 -44
  53. package/src/panels/fleet-read-model.ts +34 -3
  54. package/src/panels/fleet-spawn.ts +266 -0
  55. package/src/panels/fleet-stop.ts +38 -2
  56. package/src/panels/fleet-tabs.ts +2 -2
  57. package/src/panels/memory-consolidation-gateway.ts +94 -0
  58. package/src/panels/modals/devices-modal.ts +161 -0
  59. package/src/panels/modals/memory-modal.ts +149 -5
  60. package/src/panels/modals/pairing-modal.ts +198 -68
  61. package/src/panels/modals/security-modal.ts +6 -0
  62. package/src/panels/workstream-graph-render.ts +123 -0
  63. package/src/renderer/composer-fragments.ts +69 -0
  64. package/src/renderer/settings-modal-helpers.ts +9 -0
  65. package/src/renderer/settings-modal.ts +4 -2
  66. package/src/renderer/shell-surface.ts +3 -0
  67. package/src/renderer/tool-call.ts +12 -6
  68. package/src/renderer/ui-factory.ts +32 -0
  69. package/src/runtime/bootstrap-command-context.ts +3 -1
  70. package/src/runtime/bootstrap-command-parts.ts +3 -1
  71. package/src/runtime/bootstrap-core.ts +6 -6
  72. package/src/runtime/bootstrap-shell.ts +7 -0
  73. package/src/runtime/durability-services.ts +36 -1
  74. package/src/runtime/fleet-services.ts +41 -0
  75. package/src/runtime/idle-power-services.ts +85 -0
  76. package/src/runtime/index.ts +2 -0
  77. package/src/runtime/interaction-seams.ts +109 -0
  78. package/src/runtime/power-keepawake-remote.ts +75 -0
  79. package/src/runtime/process-lifecycle.ts +3 -1
  80. package/src/runtime/services.ts +38 -37
  81. package/src/runtime/ui-services.ts +3 -0
  82. package/src/shell/blocking-input.ts +21 -141
  83. package/src/shell/recovery-input-helpers.ts +53 -166
  84. package/src/version.ts +1 -1
@@ -0,0 +1,85 @@
1
+ // ---------------------------------------------------------------------------
2
+ // idle-power-services.ts — the idle-time / sleep-ownership / live-turn slice of
3
+ // the runtime composition, extracted from services.ts (file-size hygiene).
4
+ //
5
+ // Mirrors the SDK's own createRuntimeServices wiring: idle-time memory
6
+ // consolidation (learning.consolidation.*), host sleep ownership (power.*), and
7
+ // the per-session live-turn control holder that backs the
8
+ // sessions.toolCalls.cancel / sessions.queuedMessages.* wire verbs. Consolidation
9
+ // receipts ride the SAME attach-time queue every other receipt uses — a run that
10
+ // changed something records a one-line notice into the file-backed
11
+ // feature-announcement store, drained on the next surface attach; a quiet run
12
+ // records nothing.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
16
+ import type { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
17
+ import { MemoryConsolidationScheduler } from '@pellux/goodvibes-sdk/platform/state';
18
+ import { SessionLiveTurnControlsHolder } from '@pellux/goodvibes-sdk/platform/control-plane';
19
+ import { PowerManager, wireRuntimePower, createUnavailablePowerSeam } from '@pellux/goodvibes-sdk/platform/power';
20
+ import { FeatureAnnouncementStore, featureAnnouncementsPath } from '@pellux/goodvibes-sdk/platform/runtime/feature-announcements';
21
+ import type { RuntimeEventBus } from '@pellux/goodvibes-sdk/platform/runtime/state';
22
+ import { formatConsolidationReceipt } from '../core/consolidation-receipt.ts';
23
+
24
+ export interface IdlePowerServicesDeps {
25
+ readonly configManager: ConfigManager;
26
+ readonly memoryRegistry: MemoryRegistry;
27
+ readonly runtimeBus: RuntimeEventBus;
28
+ /** True when no session is busy — the consolidation scheduler's idle gate. */
29
+ readonly isIdle: () => boolean;
30
+ /** Sleep-edge checkpoint + a wake catch-up tick (store snapshots). */
31
+ readonly snapshotTick: () => void;
32
+ /** Wake catch-up: fire the automation heartbeat. */
33
+ readonly heartbeat: () => Promise<void>;
34
+ /**
35
+ * Host power seam. SDK 1.9.0 makes wireRuntimePower default an absent seam to
36
+ * the REAL host seam (createHostPowerSeam — which spawns systemd-inhibit and a
37
+ * dbus-monitor sleep-edge watcher). That spawn is a host-level side effect, so
38
+ * this fork mirrors the SDK's own createRuntimeServices: when no seam is passed
39
+ * we default to the NON-spawning unavailable seam, keeping test-constructed
40
+ * runtimes deterministic. Only the real long-lived compositions (the standalone
41
+ * daemon and the embedded interactive runtime, which itself IS the daemon) pass
42
+ * createHostPowerSeam() to opt into live OS keep-awake/idle-inhibit.
43
+ */
44
+ readonly powerSeam?: Parameters<typeof wireRuntimePower>[0]['seam'];
45
+ }
46
+
47
+ export interface IdlePowerServices {
48
+ readonly memoryConsolidationScheduler: MemoryConsolidationScheduler;
49
+ readonly powerManager: PowerManager;
50
+ readonly sessionLiveTurnControls: SessionLiveTurnControlsHolder;
51
+ }
52
+
53
+ /** Construct (and start) the idle-consolidation scheduler, power manager, and live-turn holder. */
54
+ export function wireIdlePowerAndLiveTurn(deps: IdlePowerServicesDeps): IdlePowerServices {
55
+ const consolidationReceiptStore = new FeatureAnnouncementStore(featureAnnouncementsPath(deps.configManager));
56
+ const memoryConsolidationScheduler = new MemoryConsolidationScheduler({
57
+ memoryRegistry: deps.memoryRegistry,
58
+ configSource: deps.configManager,
59
+ isIdle: deps.isIdle,
60
+ onReceipt: (receipt) => {
61
+ const text = formatConsolidationReceipt(receipt);
62
+ if (text) consolidationReceiptStore.record(receipt.runId, text);
63
+ },
64
+ });
65
+ memoryConsolidationScheduler.start();
66
+ const sessionLiveTurnControls = new SessionLiveTurnControlsHolder();
67
+ const powerManager = wireRuntimePower({
68
+ readConfig: (key) => deps.configManager.get(key as Parameters<typeof deps.configManager.get>[0]),
69
+ writeConfig: (key, value) => deps.configManager.setDynamic(key as Parameters<typeof deps.configManager.setDynamic>[0], value),
70
+ // Live config subscription: an external edit to power.keepAwake (a settings-
71
+ // modal toggle, a hand-edited settings file) applies LIVE to the in-process
72
+ // manager — acquires/releases the inhibitor and lights the chip with no
73
+ // restart. This is the config-apply path for the EMBEDDED topology + the
74
+ // local chip; the external daemon is reached separately over the verb.
75
+ subscribeConfig: (key, cb) => (deps.configManager.subscribe as unknown as (k: string, c: (v: unknown) => void) => () => void)(key, (newValue) => cb(newValue)),
76
+ runtimeBus: deps.runtimeBus,
77
+ sleepCheckpoint: deps.snapshotTick,
78
+ wakeCatchUp: [() => memoryConsolidationScheduler.tick(), deps.snapshotTick, deps.heartbeat],
79
+ // Non-spawning default (see powerSeam doc above): without an explicit opt-in
80
+ // the seam is the honest unavailable one, NOT the spawning host seam that
81
+ // wireRuntimePower would otherwise pick for an undefined seam.
82
+ seam: deps.powerSeam ?? createUnavailablePowerSeam('runtime services constructed without a host power seam'),
83
+ });
84
+ return { memoryConsolidationScheduler, powerManager, sessionLiveTurnControls };
85
+ }
@@ -279,8 +279,10 @@ export const loadLastConversation = operations.loadLastConversation;
279
279
  export const loadRecoveryConversation = operations.loadRecoveryConversation;
280
280
  export const writeRecoveryFile = operations.writeRecoveryFile;
281
281
  export const deleteRecoveryFile = operations.deleteRecoveryFile;
282
+ export const autoRestoreRecovery = operations.autoRestoreRecovery;
282
283
  export const checkRecoveryFile = operations.checkRecoveryFile;
283
284
  export const getRecoveryFilePath = operations.getRecoveryFilePath;
285
+ export const getRecoveryDir = operations.getRecoveryDir;
284
286
  export const getLastSessionPointerPath = operations.getLastSessionPointerPath;
285
287
  export const writeLastSessionPointer = operations.writeLastSessionPointer;
286
288
  export const readLastSessionPointer = operations.readLastSessionPointer;
@@ -0,0 +1,109 @@
1
+ // ---------------------------------------------------------------------------
2
+ // interaction-seams.ts — the command-context interaction seams and the memory-
3
+ // provenance UI controller, extracted from main.ts (file-size hygiene).
4
+ //
5
+ // The seams expose the in-process orchestrator's per-tool cancel and mid-turn
6
+ // queue edit/delete, the power toggle/status, and the provenance-chip drill-in
7
+ // to the command layer. The memory-provenance controller owns the "used N
8
+ // memories" chip's state (the latest turn's metadata.memory.recordIds and the
9
+ // expand flag), its per-turn capture, and its render.
10
+ // ---------------------------------------------------------------------------
11
+
12
+ import type { CommandContext } from '../input/command-registry.ts';
13
+ import type { PowerManager } from '@pellux/goodvibes-sdk/platform/power';
14
+ import type { Line } from '../types/grid.ts';
15
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
16
+ import { createCancelToolCall, type ToolCancelOrchestrator } from '../core/turn-cancellation.ts';
17
+ import { powerSurfaceFromState } from '../core/power-status.ts';
18
+ import { memoryRecordIdsFromTurn, readMemoryShowProvenance } from '../core/memory-provenance.ts';
19
+ import { UIFactory } from '../renderer/ui-factory.ts';
20
+
21
+ /** The orchestrator surface the interaction seams need. */
22
+ export interface InteractionOrchestrator extends ToolCancelOrchestrator {
23
+ listQueuedMessages(): ReadonlyArray<{ id: string; queuedAt: number; text: string }>;
24
+ editQueuedMessage(id: string, text: string): boolean;
25
+ deleteQueuedMessage(id: string): boolean;
26
+ }
27
+
28
+ export interface InteractionSeamDeps {
29
+ readonly orchestrator: InteractionOrchestrator;
30
+ readonly powerManager: PowerManager;
31
+ /** Topology-aware power projection (the chip source): the DAEMON's state in adopted-external mode. Falls back to the local manager when absent. */
32
+ readonly readPowerSurface?: (() => ReturnType<typeof powerSurfaceFromState>) | undefined;
33
+ readonly render: () => void;
34
+ readonly notify: (message: string) => void;
35
+ readonly getActiveToolCallId: () => string | undefined;
36
+ readonly toggleMemoryProvenance: () => void;
37
+ }
38
+
39
+ /** Wire the per-tool cancel, queue edit/delete, power, and memory-provenance seams onto the command context. */
40
+ export function wireInteractionSeams(cc: CommandContext, deps: InteractionSeamDeps): void {
41
+ cc.cancelToolCall = createCancelToolCall(deps.orchestrator, deps.getActiveToolCallId, () => {
42
+ deps.notify('[Tool] Cancelled the running tool call — the turn continues.');
43
+ deps.render();
44
+ });
45
+ cc.listQueuedMessages = () => deps.orchestrator.listQueuedMessages();
46
+ cc.editQueuedMessage = (id, text) => { const ok = deps.orchestrator.editQueuedMessage(id, text); if (ok) deps.render(); return ok; };
47
+ cc.deleteQueuedMessage = (id) => { const ok = deps.orchestrator.deleteQueuedMessage(id); if (ok) deps.render(); return ok; };
48
+ cc.toggleMemoryProvenance = deps.toggleMemoryProvenance;
49
+ cc.getPowerState = () => (deps.readPowerSurface ? deps.readPowerSurface() : powerSurfaceFromState(deps.powerManager.getState()));
50
+ cc.setKeepAwake = async (enabled) => { const next = powerSurfaceFromState(await deps.powerManager.setKeepAwake(enabled)); deps.render(); return next; };
51
+ }
52
+
53
+ /** The memory-provenance chip UI: per-turn capture, drill-in state, and render. */
54
+ export interface MemoryProvenanceUi {
55
+ /** Toggle the drill-in expand state (Alt+M). */
56
+ toggle(): void;
57
+ /** Capture the memory record ids from a completed turn's payload (metadata.memory.recordIds). */
58
+ onTurnCompleted(evt: unknown): void;
59
+ /** The chip lines for the current turn — empty when the setting is off or the turn used no memories. */
60
+ renderChip(width: number, configManager: Pick<ConfigManager, 'get'>): Line[];
61
+ }
62
+
63
+ /** A resolved provenance record summary (null = the record is no longer available). */
64
+ type ResolvedRecord = { readonly summary: string; readonly cls: string } | null;
65
+
66
+ /**
67
+ * Create the memory-provenance chip controller (default OFF; reads nothing when
68
+ * off). When the drill-in is expanded it RESOLVES each memory record id to its
69
+ * summary through the memory spine (the same host-or-daemon access the recall
70
+ * commands use, so an adopted daemon resolves over the wire) — per-id and
71
+ * lazily, so one forgotten record never blanks the rest and a collapsed chip
72
+ * costs nothing. Mirrors the webui's provenance detail fetch.
73
+ */
74
+ export function createMemoryProvenanceUi(deps: {
75
+ readonly render: () => void;
76
+ readonly memorySpine: { get(id: string): Promise<{ summary: string; cls: string } | null> };
77
+ }): MemoryProvenanceUi {
78
+ let latestRecordIds: readonly string[] = [];
79
+ let expanded = false;
80
+ // id -> resolved record (null = not found). Absent = not yet fetched.
81
+ const resolved = new Map<string, ResolvedRecord>();
82
+ const inFlight = new Set<string>();
83
+
84
+ const resolveExpanded = (): void => {
85
+ for (const id of latestRecordIds) {
86
+ if (resolved.has(id) || inFlight.has(id)) continue;
87
+ inFlight.add(id);
88
+ void deps.memorySpine.get(id)
89
+ .then((record) => { resolved.set(id, record ? { summary: record.summary, cls: record.cls } : null); })
90
+ .catch(() => { resolved.set(id, null); })
91
+ .finally(() => { inFlight.delete(id); deps.render(); });
92
+ }
93
+ };
94
+
95
+ return {
96
+ toggle() {
97
+ expanded = !expanded;
98
+ if (expanded) resolveExpanded();
99
+ deps.render();
100
+ },
101
+ onTurnCompleted(evt) { latestRecordIds = memoryRecordIdsFromTurn(evt); expanded = false; },
102
+ renderChip(width, configManager) {
103
+ if (!readMemoryShowProvenance(configManager) || latestRecordIds.length === 0) return [];
104
+ if (expanded) resolveExpanded(); // lazy: fetch any still-unresolved ids (idempotent)
105
+ const entries = latestRecordIds.map((id) => ({ id, record: resolved.has(id) ? resolved.get(id) : undefined }));
106
+ return UIFactory.createMemoryProvenanceChip(width, latestRecordIds.length, entries, expanded);
107
+ },
108
+ };
109
+ }
@@ -0,0 +1,75 @@
1
+ // ---------------------------------------------------------------------------
2
+ // power-keepawake-remote.ts — forward the keep-awake toggle to an adopted
3
+ // EXTERNAL daemon over the power.keepAwake.set operator verb.
4
+ //
5
+ // In the external/adopted-daemon topology the in-process PowerManager is NOT
6
+ // the daemon's, so a keep-awake toggle applied only locally dies when the TUI
7
+ // closes. The owner ruling is that keep-awake is daemon-held: it must survive
8
+ // the TUI closing. The config file does NOT carry it across (power.keepAwake is
9
+ // not a cross-surface shared key), so the toggle is forwarded to the daemon
10
+ // over the verb instead — best-effort, gated on reachability. Mirrors the
11
+ // agent's own power-keep-awake-remote seam.
12
+ //
13
+ // A single config subscriber carries ALL THREE toggle paths (/power, Alt+A, the
14
+ // settings modal): each lands on the power.keepAwake config key — the local
15
+ // PowerManager persists it on set, and the settings modal writes it directly —
16
+ // so subscribing to that one key catches every origin. In the EMBEDDED topology
17
+ // this is a no-op: the in-process manager already IS the daemon.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
21
+ import type { PowerState } from '@pellux/goodvibes-sdk/platform/power';
22
+ import { resolveOperatorRpc } from '../input/commands/operator-rpc.ts';
23
+
24
+ export interface KeepAwakeRemoteDeps {
25
+ readonly configManager: ConfigManager;
26
+ readonly homeDirectory: string | (() => string);
27
+ /** True only in the external/adopted-daemon topology (the local manager is not the daemon's). */
28
+ readonly isExternalDaemon: () => boolean;
29
+ /** Injectable rpc resolution (tests); defaults to the shared resolveOperatorRpc. */
30
+ readonly resolveRpc?: typeof resolveOperatorRpc;
31
+ }
32
+
33
+ /**
34
+ * Best-effort forward of the keep-awake toggle to the adopted external daemon so
35
+ * the DAEMON holds the inhibitor (surviving the TUI closing). A no-op in the
36
+ * embedded topology and quiet when the daemon is unreachable — a transient
37
+ * daemon hiccup never breaks the local toggle.
38
+ */
39
+ export async function forwardKeepAwakeToDaemon(enabled: boolean, deps: KeepAwakeRemoteDeps): Promise<void> {
40
+ if (!deps.isExternalDaemon()) return;
41
+ const rpc = (deps.resolveRpc ?? resolveOperatorRpc)({ configManager: deps.configManager, homeDirectory: deps.homeDirectory });
42
+ if (!rpc.available) return;
43
+ try {
44
+ await rpc.sdk.operator.invoke('power.keepAwake.set', { enabled });
45
+ } catch {
46
+ // Best-effort by design; the local toggle already applied.
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Install a config subscriber that forwards every power.keepAwake change to the
52
+ * adopted external daemon. Returns an unsubscribe handle. Because all three
53
+ * toggle origins land on the same config key, this one seam carries them all.
54
+ */
55
+ export function installKeepAwakeRemoteForward(deps: KeepAwakeRemoteDeps): () => void {
56
+ return deps.configManager.subscribe('power.keepAwake', (newValue) => {
57
+ void forwardKeepAwakeToDaemon(newValue === true, deps);
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Fetch the adopted DAEMON's power.status.get (null when unreachable or not in
63
+ * the external topology). The chip's external-mode poll source — the adopted
64
+ * wire is plain HTTP with no event bridge, so the chip syncs by polling.
65
+ */
66
+ export async function fetchDaemonPowerState(deps: KeepAwakeRemoteDeps): Promise<PowerState | null> {
67
+ if (!deps.isExternalDaemon()) return null;
68
+ const rpc = (deps.resolveRpc ?? resolveOperatorRpc)({ configManager: deps.configManager, homeDirectory: deps.homeDirectory });
69
+ if (!rpc.available) return null;
70
+ try {
71
+ return await rpc.sdk.operator.invoke('power.status.get', {}) as unknown as PowerState;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
@@ -219,8 +219,10 @@ export function installProcessLifecycle(deps: ProcessLifecycleDeps): ProcessLife
219
219
  }
220
220
  // Only remove the recovery fallback once the durable shutdown save actually completed,
221
221
  // so an interrupted or timed-out shutdown leaves the snapshot for the next launch.
222
+ // Scoped to THIS session's id: without it, one session's clean exit wipes
223
+ // every session's crash snapshot (the concurrent-session isolation bug).
222
224
  if (shutdownOk) {
223
- deleteRecoveryFile({ homeDirectory: ctx.services.homeDirectory });
225
+ deleteRecoveryFile({ homeDirectory: ctx.services.homeDirectory }, ctx.runtime.sessionId);
224
226
  }
225
227
  process.exit(0);
226
228
  };
@@ -4,9 +4,13 @@ import { ConfigManager, ServiceRegistry, SubscriptionManager, ToolLLM } from '@p
4
4
  import { SecretsManager } from '../config/secrets.ts';
5
5
  import { AutomationDeliveryManager, AutomationManager } from '@pellux/goodvibes-sdk/platform/automation';
6
6
  import { ChannelDeliveryRouter, ChannelPolicyManager, type ChannelPluginRegistry, type RouteBindingManager, type SurfaceRegistry } from '@pellux/goodvibes-sdk/platform/channels';
7
- import { ApprovalBroker, GatewayMethodCatalog, SharedSessionBroker } from '@pellux/goodvibes-sdk/platform/control-plane';
7
+ import { ApprovalBroker, GatewayMethodCatalog, SessionLiveTurnControlsHolder, SharedSessionBroker } from '@pellux/goodvibes-sdk/platform/control-plane';
8
+ import type { PowerManager } from '@pellux/goodvibes-sdk/platform/power';
9
+ import { wireIdlePowerAndLiveTurn } from './idle-power-services.ts';
8
10
  import { StepUpService } from '@pellux/goodvibes-sdk/daemon';
9
- import { attachWsOnlyGatewayVerbHandlers, createArchivableFleetRegistry } from '@pellux/goodvibes-terminal-shell';
11
+ import { PairingTokenManager } from '@pellux/goodvibes-sdk/platform/pairing';
12
+ import { resolvePairingWebOrigin } from '../core/pairing-origin.ts';
13
+ import { attachWsOnlyGatewayVerbHandlers } from '@pellux/goodvibes-terminal-shell';
10
14
  import { WatcherRegistry } from '@pellux/goodvibes-sdk/platform/watchers';
11
15
  import { ArtifactStore } from '@pellux/goodvibes-sdk/platform/artifacts';
12
16
  import {
@@ -25,7 +29,7 @@ import { MediaProviderRegistry, ensureBuiltinMediaProviders } from '@pellux/good
25
29
  import { MultimodalService } from '@pellux/goodvibes-sdk/platform/multimodal';
26
30
  import { AgentMessageBus, AgentOrchestrator, ArchetypeLoader, WrfcController } from '@pellux/goodvibes-sdk/platform/agents';
27
31
  import { AgentManager, ContextAccountingHolder, OverflowHandler, ProcessManager, createWorkflowServices, type WorkflowServices } from '@pellux/goodvibes-sdk/platform/tools';
28
- import { FileStateCache, FileUndoManager, MemoryEmbeddingProviderRegistry, MemoryRegistry, MemoryStore, ModeManager, ProjectIndex, resolveCanonicalMemoryDbPath, type CodeIndexStore, type CodeIndexReindexScheduler } from '@pellux/goodvibes-sdk/platform/state';
32
+ import { FileStateCache, FileUndoManager, MemoryConsolidationScheduler, MemoryEmbeddingProviderRegistry, MemoryRegistry, MemoryStore, ModeManager, ProjectIndex, resolveCanonicalMemoryDbPath, type CodeIndexStore, type CodeIndexReindexScheduler } from '@pellux/goodvibes-sdk/platform/state';
29
33
  import type { StoreSnapshotScheduler } from '@pellux/goodvibes-sdk/platform/state/store-snapshots';
30
34
  import type { UserPermissionRuleStore } from '@pellux/goodvibes-sdk/platform/permissions';
31
35
  import { buildExecPromptAnswerHandler } from '@pellux/goodvibes-sdk/platform/runtime/permissions/exec-prompt-wiring';
@@ -55,13 +59,14 @@ import { SessionManager, CrossSessionTaskRegistry, SessionChangeTracker } from '
55
59
  import { ApiTokenAuditor, UserAuthManager } from '@pellux/goodvibes-sdk/platform/security';
56
60
  import { WebhookNotifier } from '@pellux/goodvibes-sdk/platform/integrations';
57
61
  import { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
58
- import { BenchmarkStore, CacheHitTracker, computeUsageCostUsd, FavoritesStore, ModelLimitsService, ProviderCapabilityRegistry, ProviderOptimizer, ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
62
+ import { BenchmarkStore, CacheHitTracker, FavoritesStore, ModelLimitsService, ProviderCapabilityRegistry, ProviderOptimizer, ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
59
63
  import { KeybindingsManager } from '../input/keybindings.ts';
60
64
  import { AdaptivePlanner, DeterministicReplayEngine, ExecutionPlanManager, SessionLineageTracker, SessionMemoryStore } from '@pellux/goodvibes-sdk/platform/core';
61
65
  import { deriveFeatureStates, bindFeatureSettingsBridge } from '@pellux/goodvibes-sdk/platform/runtime/state';
62
66
  import { createChannelComposition } from './channel-composition.ts';
63
67
  import { applyProviderOptimizerConfigMode, bindProviderOptimizerFeatureFlag } from './provider-optimizer-wiring.ts';
64
68
  import { type ArchivableProcessRegistry } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
69
+ import { createFleetServices } from './fleet-services.ts';
65
70
  import { createWorkstreamServices, type OrchestrationEngine, type WorkstreamCommandService } from './workstream-services.ts';
66
71
  import { wireFleetNeedsInputPush } from './fleet-needs-input-push.ts';
67
72
  import { codeIndexDbPath, createCodeIndexServices, isCodeInjectionSettingEnabled } from './code-index-services.ts';
@@ -90,6 +95,13 @@ export interface RuntimeServicesOptions {
90
95
  readonly getConversationTitle?: () => string | undefined;
91
96
  readonly workingDir: string;
92
97
  readonly homeDirectory: string;
98
+ /** Opt-in (daemon-side only): fold host-observed external coding-agent sessions
99
+ * into the fleet as 'observed-external' rows. Interactive leaves it off and reads
100
+ * the daemon snapshot. Mirrors the SDK's own createRuntimeServices option. */
101
+ readonly observeExternalAgents?: boolean | undefined;
102
+ /** Host power seam opt-in. Fork mirrors the SDK: non-spawning unavailable-seam
103
+ * default (idle-power-services.ts); daemon + embedded runtime pass createHostPowerSeam(). */
104
+ readonly powerSeam?: Parameters<typeof wireIdlePowerAndLiveTurn>[0]['powerSeam'];
93
105
  }
94
106
 
95
107
  export interface RuntimeServices {
@@ -135,6 +147,7 @@ export interface RuntimeServices {
135
147
  readonly serviceRegistry: ServiceRegistry;
136
148
  readonly secretsManager: SecretsManager;
137
149
  readonly stepUpService: StepUpService;
150
+ readonly pairingTokens: PairingTokenManager; // backs pairing.tokens.* verbs + the settings device surface (mirrors the SDK composition)
138
151
  readonly subscriptionManager: SubscriptionManager;
139
152
  readonly localUserAuthManager: UserAuthManager;
140
153
  readonly profileManager: ProfileManager;
@@ -198,6 +211,9 @@ export interface RuntimeServices {
198
211
  readonly codeIndexReindexScheduler: CodeIndexReindexScheduler; // tool-site reindex
199
212
  /** Daily snapshots of every SQLite store this runtime writes, with bounded retention; unref'd timers (mirrors the SDK composition — hosts that tear down a runtime stop() it themselves). */
200
213
  readonly storeSnapshotScheduler: StoreSnapshotScheduler;
214
+ readonly memoryConsolidationScheduler: MemoryConsolidationScheduler;
215
+ readonly powerManager: PowerManager;
216
+ readonly sessionLiveTurnControls: SessionLiveTurnControlsHolder;
201
217
  /** Unified live process registry (agents, WRFC chains, workflows, watchers, background processes) backing the Fleet panel; archive-aware — finished subtrees can be moved to the session archive view. */
202
218
  readonly processRegistry: ArchivableProcessRegistry;
203
219
  readonly modeManager: ModeManager;
@@ -249,6 +265,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
249
265
  });
250
266
  // Step-up (WebAuthn) ceremony service, shared between the ceremony gateway verbs and the relay gate's verifier.
251
267
  const stepUpService = new StepUpService({ secrets: secretsManager });
268
+ const pairingTokens = new PairingTokenManager(shellPaths.resolveUserPath('control-plane', 'pairing-tokens.json'));
252
269
  const subscriptionManager = new SubscriptionManager(shellPaths.resolveUserPath('tui', 'subscriptions.json'));
253
270
  const serviceRegistry = new ServiceRegistry(shellPaths.resolveProjectPath('tui', 'services.json'), {
254
271
  secretsManager,
@@ -468,7 +485,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
468
485
  projectRoot: workingDirectory,
469
486
  });
470
487
  const voiceProviders = new VoiceProviderRegistry();
471
- ensureBuiltinVoiceProviders(voiceProviders);
488
+ ensureBuiltinVoiceProviders(voiceProviders, { readConfig: (key) => configManager.get(key as Parameters<typeof configManager.get>[0]) });
472
489
  const voiceService = new VoiceService(voiceProviders);
473
490
  const webSearchProviders = new WebSearchProviderRegistry({
474
491
  env: process.env,
@@ -478,18 +495,9 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
478
495
  serviceRegistry,
479
496
  featureFlags,
480
497
  });
481
- knowledgeSemanticService.setGapRepairer(createWebKnowledgeGapRepairer({
482
- searchService: webSearchService,
483
- ingestService: knowledgeService,
484
- }));
485
- agentKnowledgeSemanticService.setGapRepairer(createWebKnowledgeGapRepairer({
486
- searchService: webSearchService,
487
- ingestService: agentKnowledgeService,
488
- }));
489
- homeGraphSemanticService.setGapRepairer(createWebKnowledgeGapRepairer({
490
- searchService: webSearchService,
491
- ingestService: homeGraphService,
492
- }));
498
+ for (const [semantic, ingest] of [[knowledgeSemanticService, knowledgeService], [agentKnowledgeSemanticService, agentKnowledgeService], [homeGraphSemanticService, homeGraphService]] as const) {
499
+ semantic.setGapRepairer(createWebKnowledgeGapRepairer({ searchService: webSearchService, ingestService: ingest }));
500
+ }
493
501
  const mediaProviders = new MediaProviderRegistry();
494
502
  ensureBuiltinMediaProviders(mediaProviders, artifactStore, providerRegistry);
495
503
  const multimodalService = new MultimodalService(artifactStore, mediaProviders, voiceService, knowledgeService);
@@ -591,36 +599,25 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
591
599
  // Store snapshots + durable remembered-approval rules + the live credential
592
600
  // chain, mirroring the SDK composition — see durability-services.ts.
593
601
  const { storeSnapshotScheduler, userPermissionRuleStore } = createDurabilityServices({
594
- configManager, secretsManager, providerRegistry, memoryDbPath, codeIndexDbPath: codeIndexDbPath(workingDirectory),
602
+ configManager, secretsManager, providerRegistry, memoryDbPath, codeIndexDbPath: codeIndexDbPath(workingDirectory), workingDirectory, surfaceRoot: 'tui', homeDirectory, shellPaths, // + retention-sweep roots & live config watch (mirrors the SDK)
595
603
  });
596
604
  const codeInjectionOrchestratorDeps = { codeIndex: codeIndexStore, isCodeInjectionSettingEnabled: () => isCodeInjectionSettingEnabled(configManager), codeIndexReindexScheduler }; // Code-injection seam (agent here; main via orchestrator-core-services.ts)
597
- // Shared, archive-aware fleet registry — see gateway-verbs.ts's factory doc.
598
- const processRegistry = createArchivableFleetRegistry({
599
- agentManager,
600
- wrfcController,
605
+ const { processRegistry } = createFleetServices({ // Shared archive-aware fleet registry (+ daemon observed rows) — see fleet-services.ts
606
+ agentManager, wrfcController,
601
607
  orchestrationEngine, // Folds workstream/phase/work-item nodes into the fleet
602
608
  codeIndexService: codeIndexStore, // Folds a single 'code-index' node into the fleet
603
- processManager,
604
- watcherRegistry,
605
- workflow,
606
- approvalBroker,
607
- sessionBroker,
609
+ processManager, watcherRegistry, workflow, approvalBroker, sessionBroker,
608
610
  messageBus: agentMessageBus, // Backs steer()/`steerable` (the Fleet steer composer builds on top)
609
611
  automationManager, // Folds /schedule AutomationJobs into the fleet as 'schedule' nodes
610
612
  runtimeBus: options.runtimeBus,
611
- // Honest-unpriced through the ONE pricing resolver (manual -> registration -> provider-served
612
- // -> catalog -> unknown); unknown/subscription yields null, never $0. Mirrors the SDK composition.
613
- priceUsage: (model, usage) => (model ? computeUsageCostUsd(providerRegistry.resolveModelPricing(model), usage) : null),
613
+ observeExternalAgents: options.observeExternalAgents, providerRegistry, // observeExternalAgents is daemon-side only
614
614
  });
615
- const modeManager = new ModeManager();
616
- const fileUndoManager = new FileUndoManager();
615
+ const modeManager = new ModeManager(); const fileUndoManager = new FileUndoManager();
617
616
  const workspaceCheckpointManager = createWorkspaceCheckpointing({ workspaceRoot: workingDirectory, runtimeBus: options.runtimeBus, configManager });
617
+ const { memoryConsolidationScheduler, powerManager, sessionLiveTurnControls } = wireIdlePowerAndLiveTurn({ configManager, memoryRegistry, runtimeBus: options.runtimeBus, isIdle: () => sessionBroker.countBusySessions() === 0, snapshotTick: () => storeSnapshotScheduler.tick(), heartbeat: async () => { await automationManager.triggerHeartbeat({ source: 'wake-catchup' }); }, powerSeam: options.powerSeam });
618
618
 
619
- // Shared terminal-shell wrapper over the SDK's registerGatewayVerbGroups (see gateway-verbs.ts): 501s the ws-only
620
- // family without it. principals.*/channels.profiles.*/ci.* register unconditionally; checkin.* only with
621
- // channelDeliveryRouter/providerRegistry/automationManager/sessionLister ALL present; fleet needs-input push gated by
622
- // runtimeBus/sessionPresence (fleet-needs-input-push.ts). conversationRewindPort serves conversation-scope rewind live.
623
- 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, ...wireFleetNeedsInputPush({ registry: processRegistry, runtimeBus: options.runtimeBus, sessionBroker }) });
619
+ // Terminal-shell wrapper over the SDK registerGatewayVerbGroups (gateway-verbs.ts); checkin.*/fleet-needs-input/pairing.* register only when their deps are present.
620
+ 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, relayAvailable: () => configManager.get('relay.enabled') === true, pairingWebOrigin: () => resolvePairingWebOrigin(configManager).origin, ...wireFleetNeedsInputPush({ registry: processRegistry, runtimeBus: options.runtimeBus, sessionBroker }) });
624
621
  const integrationHelpers = new IntegrationHelperService({
625
622
  workingDirectory,
626
623
  homeDirectory,
@@ -723,6 +720,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
723
720
  serviceRegistry,
724
721
  secretsManager,
725
722
  stepUpService,
723
+ pairingTokens,
726
724
  subscriptionManager,
727
725
  localUserAuthManager,
728
726
  profileManager,
@@ -782,6 +780,9 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
782
780
  codeIndexStore,
783
781
  codeIndexReindexScheduler,
784
782
  storeSnapshotScheduler,
783
+ memoryConsolidationScheduler,
784
+ powerManager,
785
+ sessionLiveTurnControls,
785
786
  processRegistry,
786
787
  modeManager,
787
788
  fileUndoManager,
@@ -59,6 +59,8 @@ export interface UiPlatformServices {
59
59
  readonly surfaceRegistry: RuntimeServices['surfaceRegistry'];
60
60
  readonly subscriptionManager: RuntimeServices['subscriptionManager'];
61
61
  readonly secretsManager: SecretsManager;
62
+ /** Per-device pairing tokens — backs the pairing modal QR and the settings device surface. */
63
+ readonly pairingTokens: RuntimeServices['pairingTokens'];
62
64
  readonly tokenAuditor: RuntimeServices['tokenAuditor'];
63
65
  readonly replayEngine: RuntimeServices['replayEngine'];
64
66
  readonly webhookNotifier: RuntimeServices['webhookNotifier'];
@@ -188,6 +190,7 @@ export function createUiRuntimeServices(
188
190
  surfaceRegistry: runtimeServices.surfaceRegistry,
189
191
  subscriptionManager: runtimeServices.subscriptionManager,
190
192
  secretsManager: runtimeServices.secretsManager,
193
+ pairingTokens: runtimeServices.pairingTokens,
191
194
  tokenAuditor: runtimeServices.tokenAuditor,
192
195
  replayEngine: runtimeServices.replayEngine,
193
196
  webhookNotifier: runtimeServices.webhookNotifier,