@pellux/goodvibes-tui 1.17.0 → 1.19.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 (104) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +5025 -1636
  4. package/package.json +7 -7
  5. package/src/cli/endpoints.ts +50 -4
  6. package/src/cli/help.ts +29 -2
  7. package/src/cli/management-commands.ts +99 -23
  8. package/src/cli/management.ts +0 -2
  9. package/src/cli/service-posture.ts +5 -3
  10. package/src/cli/status.ts +27 -6
  11. package/src/cli/surface-command.ts +29 -15
  12. package/src/core/consolidation-receipt.ts +41 -0
  13. package/src/core/conversation-rendering.ts +14 -5
  14. package/src/core/memory-diagnostics-gateway.ts +99 -0
  15. package/src/core/memory-provenance.ts +67 -0
  16. package/src/core/memory-status.ts +118 -0
  17. package/src/core/pairing-devices.ts +49 -0
  18. package/src/core/pairing-handoff.ts +101 -0
  19. package/src/core/pairing-offers.ts +83 -0
  20. package/src/core/pairing-origin.ts +78 -0
  21. package/src/core/pairing-tailscale-gateway.ts +56 -0
  22. package/src/core/power-chip-source.ts +91 -0
  23. package/src/core/power-status.ts +95 -0
  24. package/src/core/stable-host.ts +171 -0
  25. package/src/core/stream-event-wiring.ts +21 -0
  26. package/src/core/turn-budget-outcome.ts +60 -0
  27. package/src/core/turn-cancellation.ts +39 -0
  28. package/src/core/turn-event-wiring.ts +6 -0
  29. package/src/core/voice-provision-gateway.ts +202 -0
  30. package/src/core/voice-provision-status.ts +197 -0
  31. package/src/daemon/cli.ts +127 -60
  32. package/src/input/command-registry.ts +21 -0
  33. package/src/input/commands/acp-runtime.ts +72 -0
  34. package/src/input/commands/devices-runtime.ts +109 -0
  35. package/src/input/commands/experience-runtime.ts +25 -4
  36. package/src/input/commands/git-runtime.ts +28 -4
  37. package/src/input/commands/graph-runtime.ts +45 -0
  38. package/src/input/commands/health-runtime.ts +15 -1
  39. package/src/input/commands/operator-rpc.ts +32 -11
  40. package/src/input/commands/power-runtime.ts +56 -0
  41. package/src/input/commands/qrcode-runtime.ts +14 -19
  42. package/src/input/commands/queue-runtime.ts +70 -0
  43. package/src/input/commands/workstream-attempts.ts +6 -1
  44. package/src/input/commands/workstream-runtime.ts +44 -25
  45. package/src/input/commands/worktree-runtime.ts +35 -8
  46. package/src/input/commands.ts +10 -0
  47. package/src/input/config-modal-types.ts +11 -0
  48. package/src/input/config-modal.ts +65 -2
  49. package/src/input/handler-shortcuts.ts +23 -0
  50. package/src/input/keybindings.ts +19 -1
  51. package/src/input/settings-modal-data.ts +31 -37
  52. package/src/input/settings-modal-types.ts +5 -2
  53. package/src/input/tts-settings-actions.ts +27 -8
  54. package/src/input/voice-local-settings.ts +52 -0
  55. package/src/main.ts +42 -42
  56. package/src/panels/builtin/operations.ts +54 -1
  57. package/src/panels/builtin/shared.ts +12 -0
  58. package/src/panels/builtin-modals.ts +49 -32
  59. package/src/panels/diff-panel.ts +34 -1
  60. package/src/panels/fleet-acts.ts +446 -0
  61. package/src/panels/fleet-gateway.ts +117 -0
  62. package/src/panels/fleet-observed-render.ts +128 -0
  63. package/src/panels/fleet-panel-format.ts +120 -4
  64. package/src/panels/fleet-panel-worktree-detail.ts +14 -0
  65. package/src/panels/fleet-panel.ts +60 -44
  66. package/src/panels/fleet-read-model.ts +34 -3
  67. package/src/panels/fleet-spawn.ts +266 -0
  68. package/src/panels/fleet-stop.ts +38 -2
  69. package/src/panels/fleet-tabs.ts +2 -2
  70. package/src/panels/memory-consolidation-gateway.ts +94 -0
  71. package/src/panels/modals/devices-modal.ts +161 -0
  72. package/src/panels/modals/memory-modal.ts +149 -5
  73. package/src/panels/modals/pairing-modal.ts +198 -68
  74. package/src/panels/modals/security-modal.ts +6 -0
  75. package/src/panels/workstream-graph-render.ts +123 -0
  76. package/src/renderer/composer-fragments.ts +69 -0
  77. package/src/renderer/settings-modal-helpers.ts +9 -0
  78. package/src/renderer/settings-modal.ts +4 -2
  79. package/src/renderer/shell-surface.ts +3 -0
  80. package/src/renderer/tool-call.ts +12 -6
  81. package/src/renderer/ui-factory.ts +32 -0
  82. package/src/runtime/bootstrap-command-context.ts +3 -1
  83. package/src/runtime/bootstrap-command-parts.ts +3 -1
  84. package/src/runtime/bootstrap-core.ts +6 -6
  85. package/src/runtime/bootstrap-shell.ts +7 -0
  86. package/src/runtime/code-index-services.ts +15 -1
  87. package/src/runtime/durability-services.ts +36 -1
  88. package/src/runtime/fleet-services.ts +41 -0
  89. package/src/runtime/idle-power-services.ts +85 -0
  90. package/src/runtime/index.ts +2 -0
  91. package/src/runtime/interaction-seams.ts +109 -0
  92. package/src/runtime/knowledge-services.ts +94 -0
  93. package/src/runtime/legacy-daemon-migration.ts +38 -6
  94. package/src/runtime/legacy-daemon-reconcile.ts +439 -0
  95. package/src/runtime/memory-governance-services.ts +79 -0
  96. package/src/runtime/notification-dispatch.ts +28 -0
  97. package/src/runtime/power-keepawake-remote.ts +75 -0
  98. package/src/runtime/process-lifecycle.ts +3 -1
  99. package/src/runtime/services.ts +105 -107
  100. package/src/runtime/ui-services.ts +3 -0
  101. package/src/runtime/voice-setup-services.ts +104 -0
  102. package/src/shell/blocking-input.ts +21 -141
  103. package/src/shell/recovery-input-helpers.ts +53 -166
  104. package/src/version.ts +11 -2
@@ -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,94 @@
1
+ // ---------------------------------------------------------------------------
2
+ // knowledge-services.ts — the knowledge/wiki + home-graph stack (TUI wiring)
3
+ //
4
+ // Constructs the three KnowledgeStores (regular wiki, agent, home-graph), their
5
+ // semantic services, the ingest services, the home-graph service, and the
6
+ // project-planning + work-plan stores. Extracted into its own module rather
7
+ // than built inline in services.ts, which sits at the architecture check's
8
+ // 800-line cap (scripts/check-architecture.ts) — new/large construction blocks
9
+ // get their own module and a single wiring call there (mirrors
10
+ // createWorkstreamServices / createDurabilityServices / createCodeIndexServices).
11
+ //
12
+ // The semantic services + ingest services receive the memory-governor
13
+ // backpressure seams (isBackgroundPaused for the knowledge-self-improvement job
14
+ // + admitExpensiveWork for the critical-tier refusal), mirroring the SDK's own
15
+ // createRuntimeServices. The web-knowledge gap repairer is wired by the caller
16
+ // AFTER webSearchService exists (it is constructed later in services.ts), using
17
+ // the semantic + ingest handles this returns.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ import {
21
+ HomeGraphService,
22
+ GOODVIBES_AGENT_KNOWLEDGE_DB_FILE,
23
+ HOME_GRAPH_KNOWLEDGE_EXTENSION,
24
+ KnowledgeService,
25
+ KnowledgeSemanticService,
26
+ KnowledgeStore,
27
+ ProjectPlanningService,
28
+ createProviderBackedKnowledgeSemanticLlm,
29
+ projectPlanningProjectIdFromPath,
30
+ } from '@pellux/goodvibes-sdk/platform/knowledge';
31
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
32
+ import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
33
+ import type { ArtifactStore } from '@pellux/goodvibes-sdk/platform/artifacts';
34
+ import type { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
35
+ import type { RuntimeEventBus } from '@/runtime/index.ts';
36
+ import { WorkPlanStore } from '../work-plans/work-plan-store.ts';
37
+
38
+ const REGULAR_KNOWLEDGE_DB_FILE = 'knowledge-wiki.sqlite';
39
+ const HOME_GRAPH_KNOWLEDGE_DB_FILE = 'knowledge-home-graph.sqlite';
40
+
41
+ export interface KnowledgeServicesDeps {
42
+ readonly configManager: ConfigManager;
43
+ readonly providerRegistry: ProviderRegistry;
44
+ readonly artifactStore: ArtifactStore;
45
+ readonly memoryRegistry: MemoryRegistry;
46
+ readonly runtimeBus: RuntimeEventBus;
47
+ readonly workingDirectory: string;
48
+ readonly homeDirectory: string;
49
+ /** True while the 'knowledge-self-improvement' job is governor-paused. */
50
+ readonly isBackgroundPaused: () => boolean;
51
+ /** Critical-tier admission gate for expensive knowledge work. */
52
+ readonly admitExpensiveWork: (label: string) => { allowed: boolean; reason?: string | undefined };
53
+ }
54
+
55
+ export interface KnowledgeServices {
56
+ readonly knowledgeStore: KnowledgeStore;
57
+ readonly agentKnowledgeStore: KnowledgeStore;
58
+ readonly homeGraphKnowledgeStore: KnowledgeStore;
59
+ readonly knowledgeSemanticService: KnowledgeSemanticService;
60
+ readonly homeGraphSemanticService: KnowledgeSemanticService;
61
+ readonly agentKnowledgeSemanticService: KnowledgeSemanticService;
62
+ readonly knowledgeService: KnowledgeService;
63
+ readonly agentKnowledgeService: KnowledgeService;
64
+ readonly homeGraphService: HomeGraphService;
65
+ readonly projectPlanningService: ProjectPlanningService;
66
+ readonly projectPlanningProjectId: string;
67
+ readonly workPlanStore: WorkPlanStore;
68
+ }
69
+
70
+ /** Construct the knowledge/wiki + home-graph stack with governor backpressure wired in. */
71
+ export function createKnowledgeServices(deps: KnowledgeServicesDeps): KnowledgeServices {
72
+ const { configManager, providerRegistry, artifactStore, memoryRegistry, runtimeBus, isBackgroundPaused, admitExpensiveWork } = deps;
73
+ const knowledgeStore = new KnowledgeStore({ configManager, dbFileName: REGULAR_KNOWLEDGE_DB_FILE });
74
+ const agentKnowledgeStore = new KnowledgeStore({ configManager, dbFileName: GOODVIBES_AGENT_KNOWLEDGE_DB_FILE });
75
+ const homeGraphKnowledgeStore = new KnowledgeStore({ configManager, dbFileName: HOME_GRAPH_KNOWLEDGE_DB_FILE });
76
+ const knowledgeSemanticLlm = createProviderBackedKnowledgeSemanticLlm(providerRegistry, { timeoutMs: 20_000, maxConcurrent: 1 });
77
+ const knowledgeSemanticService = new KnowledgeSemanticService(knowledgeStore, { llm: knowledgeSemanticLlm, maxLlmSourcesPerReindex: 3, isBackgroundPaused, admitExpensiveWork });
78
+ const homeGraphSemanticService = new KnowledgeSemanticService(homeGraphKnowledgeStore, { llm: knowledgeSemanticLlm, maxLlmSourcesPerReindex: 3, objectProfiles: HOME_GRAPH_KNOWLEDGE_EXTENSION.objectProfiles, isBackgroundPaused, admitExpensiveWork });
79
+ const agentKnowledgeSemanticService = new KnowledgeSemanticService(agentKnowledgeStore, { llm: knowledgeSemanticLlm, maxLlmSourcesPerReindex: 3, isBackgroundPaused, admitExpensiveWork });
80
+ const knowledgeService = new KnowledgeService(knowledgeStore, artifactStore, undefined, { memoryRegistry, runtimeBus, semanticService: knowledgeSemanticService, admitExpensiveWork });
81
+ knowledgeService.attachRuntimeBus(runtimeBus);
82
+ const agentKnowledgeService = new KnowledgeService(agentKnowledgeStore, artifactStore, undefined, { memoryRegistry, runtimeBus, semanticService: agentKnowledgeSemanticService, admitExpensiveWork });
83
+ agentKnowledgeService.attachRuntimeBus(runtimeBus);
84
+ const homeGraphService = new HomeGraphService(homeGraphKnowledgeStore, artifactStore, { semanticService: homeGraphSemanticService, admitExpensiveWork });
85
+ const projectPlanningProjectId = projectPlanningProjectIdFromPath(deps.workingDirectory);
86
+ const projectPlanningService = new ProjectPlanningService(knowledgeStore, { defaultProjectId: projectPlanningProjectId });
87
+ const workPlanStore = new WorkPlanStore({ homeDirectory: deps.homeDirectory, projectId: projectPlanningProjectId, projectRoot: deps.workingDirectory });
88
+ return {
89
+ knowledgeStore, agentKnowledgeStore, homeGraphKnowledgeStore,
90
+ knowledgeSemanticService, homeGraphSemanticService, agentKnowledgeSemanticService,
91
+ knowledgeService, agentKnowledgeService, homeGraphService,
92
+ projectPlanningService, projectPlanningProjectId, workPlanStore,
93
+ };
94
+ }
@@ -41,7 +41,7 @@
41
41
  * exercised deterministically via fakes and never touches a real running
42
42
  * service in tests.
43
43
  */
44
- import { existsSync, rmSync } from 'node:fs';
44
+ import { existsSync, readFileSync, rmSync } from 'node:fs';
45
45
  import { spawnSync } from 'node:child_process';
46
46
  import net from 'node:net';
47
47
  import { basename, dirname, join } from 'node:path';
@@ -118,12 +118,22 @@ export function buildManagedDaemonServiceManager(params: BuildManagedDaemonServi
118
118
  homeDir: params.homeDir,
119
119
  surfaceRoot: 'tui',
120
120
  });
121
+ // The unit's ExecStart deliberately carries NO endpoint flags
122
+ // (--hostname/--port): the daemon resolves controlPlane.hostMode/host/port
123
+ // from the user's settings at boot, so a later config change (or a host
124
+ // already configured for hostMode=network / a non-default port) keeps its
125
+ // endpoint without a unit rewrite. Baking endpoint values here is what
126
+ // silently re-pinned custom-configured hosts back to the values current at
127
+ // install time. scripts/install.sh writes the same shape — the two paths
128
+ // must produce the identical running daemon (see the installer parity test).
129
+ // `params.host`/`params.port` remain inputs because the migration engine
130
+ // still needs them for its read-only port-liveness probe.
121
131
  const definition: ManagedServiceDefinition = {
122
132
  name: MANAGED_SERVICE_NAME,
123
133
  description: MANAGED_SERVICE_DESCRIPTION,
124
134
  workingDirectory,
125
135
  command: params.binaryPath,
126
- args: ['--daemon-home', params.homeDir, '--hostname', params.host, '--port', String(params.port)],
136
+ args: ['--daemon-home', params.homeDir],
127
137
  env: {},
128
138
  restartOnFailure: true,
129
139
  };
@@ -217,8 +227,7 @@ export function detectLegacyUnit(input: DetectLegacyUnitInput): LegacyUnitInfo {
217
227
  const path = legacyUnitPath(input.homeDir);
218
228
  const fileExists = input.legacyUnitFileExists ?? existsSync;
219
229
  if (!fileExists(path)) return { present: false, active: false, path };
220
- const run: ManagedServiceActionRunner = input.actionRunner
221
- ?? ((command, args) => spawnSync(command, args, { stdio: 'pipe', encoding: 'utf-8' }) as ManagedServiceActionResult);
230
+ const run: ManagedServiceActionRunner = input.actionRunner ?? defaultActionRunner(SYSTEMCTL_TIMEOUT_MS);
222
231
  const result = run('systemctl', ['--user', 'is-active', `${LEGACY_SERVICE_UNIT_NAME}.service`]);
223
232
  const state = (result.stdout ?? '').trim();
224
233
  const active = (result.status ?? 1) === 0 && state === 'active';
@@ -270,6 +279,30 @@ export function legacyUnitNote(legacy: LegacyUnitInfo, trackedServiceName: strin
270
279
  );
271
280
  }
272
281
 
282
+ /**
283
+ * Hard ceiling on every systemctl invocation made through a DEFAULT action
284
+ * runner in this module. The reconcile below runs on the daemon's own startup
285
+ * path, and `spawnSync` without a timeout blocks the single JS event loop for
286
+ * as long as the child runs — a wedged user D-Bus (a real incident class on
287
+ * this host) would freeze an already-listening daemon indefinitely. A timed-out
288
+ * call reports `status: null`, which every status check in this module treats
289
+ * as failure, so a wedge degrades to an honest refusal instead of a hang.
290
+ */
291
+ export const SYSTEMCTL_TIMEOUT_MS = 5_000;
292
+
293
+ export function defaultActionRunner(timeoutMs: number): ManagedServiceActionRunner {
294
+ return (command, args) =>
295
+ spawnSync(command, args, { stdio: 'pipe', encoding: 'utf-8', timeout: timeoutMs }) as ManagedServiceActionResult;
296
+ }
297
+
298
+ /** Parse a `systemctl show -p MainPID --value` reply: a positive integer pid, or undefined when absent/unparseable/0. */
299
+ export function parseMainPid(result: { status?: number | null; stdout?: string | null | undefined }): number | undefined {
300
+ if ((result.status ?? 1) !== 0) return undefined;
301
+ const parsed = Number.parseInt((result.stdout ?? '').trim(), 10);
302
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
303
+ }
304
+
305
+
273
306
  /**
274
307
  * Read-only, best-effort TCP connect probe used ONLY by the legacy-absent
275
308
  * branch to tell "nothing is listening on this port" apart from "an
@@ -490,8 +523,7 @@ export async function runLegacyDaemonMigration(
490
523
  }
491
524
 
492
525
  // New unit verified healthy — now, and only now, retire the legacy unit.
493
- const run: ManagedServiceActionRunner = params.actionRunner
494
- ?? ((command, args) => spawnSync(command, args, { stdio: 'pipe', encoding: 'utf-8' }) as ManagedServiceActionResult);
526
+ const run: ManagedServiceActionRunner = params.actionRunner ?? defaultActionRunner(SYSTEMCTL_TIMEOUT_MS);
495
527
  const stopResult = run('systemctl', ['--user', 'stop', `${LEGACY_SERVICE_UNIT_NAME}.service`]);
496
528
  const disableResult = run('systemctl', ['--user', 'disable', `${LEGACY_SERVICE_UNIT_NAME}.service`]);
497
529
  const removeFile = params.legacyUnitFileRemove ?? ((path: string) => rmSync(path, { force: true }));