@pellux/goodvibes-tui 1.18.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.
- package/CHANGELOG.md +159 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1421 -896
- package/package.json +7 -7
- package/src/cli/endpoints.ts +50 -4
- package/src/cli/help.ts +29 -2
- package/src/cli/management-commands.ts +17 -8
- package/src/cli/service-posture.ts +5 -3
- package/src/cli/status.ts +27 -6
- package/src/cli/surface-command.ts +29 -15
- package/src/core/memory-diagnostics-gateway.ts +99 -0
- package/src/core/memory-status.ts +118 -0
- package/src/core/voice-provision-gateway.ts +202 -0
- package/src/core/voice-provision-status.ts +197 -0
- package/src/daemon/cli.ts +80 -4
- package/src/input/commands/experience-runtime.ts +25 -4
- package/src/input/commands/health-runtime.ts +15 -1
- package/src/input/tts-settings-actions.ts +27 -8
- package/src/input/voice-local-settings.ts +52 -0
- package/src/runtime/code-index-services.ts +15 -1
- package/src/runtime/knowledge-services.ts +94 -0
- package/src/runtime/legacy-daemon-migration.ts +38 -6
- package/src/runtime/legacy-daemon-reconcile.ts +439 -0
- package/src/runtime/memory-governance-services.ts +79 -0
- package/src/runtime/notification-dispatch.ts +28 -0
- package/src/runtime/services.ts +70 -73
- package/src/runtime/voice-setup-services.ts +104 -0
- package/src/version.ts +11 -2
|
@@ -2,6 +2,7 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
3
|
import type { CommandRegistry } from '../command-registry.ts';
|
|
4
4
|
import { requirePanelManager, requireShellPaths } from './runtime-services.ts';
|
|
5
|
+
import { createVoiceProvisionGateway, renderVoiceProvision, runVoiceSetupWithProgress, VOICE_SETUP_ANNOUNCEMENT } from '../../core/voice-provision-gateway.ts';
|
|
5
6
|
|
|
6
7
|
interface VoiceBundle {
|
|
7
8
|
readonly version: 1;
|
|
@@ -222,11 +223,31 @@ export function registerExperienceRuntimeCommands(registry: CommandRegistry): vo
|
|
|
222
223
|
|
|
223
224
|
registry.register({
|
|
224
225
|
name: 'voice',
|
|
225
|
-
description: 'Review or toggle always-speak mode
|
|
226
|
-
usage: '[review|enable|disable|bundle export <path>|bundle inspect <path>]',
|
|
227
|
-
handler(args, ctx) {
|
|
226
|
+
description: 'Review or toggle always-speak mode, provision the managed local voice runtime (setup/status), and package portable voice metadata',
|
|
227
|
+
usage: '[review|status|setup|enable|disable|bundle export <path>|bundle inspect <path>]',
|
|
228
|
+
async handler(args, ctx) {
|
|
228
229
|
const shellPaths = requireShellPaths(ctx);
|
|
229
230
|
const sub = (args[0] ?? 'review').toLowerCase();
|
|
231
|
+
if (sub === 'status' || sub === 'setup') {
|
|
232
|
+
// The one-act managed install returns a final receipt (no per-phase
|
|
233
|
+
// streaming over the verb), so the announcement block prints first and
|
|
234
|
+
// the receipt/failure block follows once the (possibly long,
|
|
235
|
+
// multi-hundred-MB) install resolves.
|
|
236
|
+
const resolution = createVoiceProvisionGateway({
|
|
237
|
+
configManager: ctx.platform.configManager,
|
|
238
|
+
// Lazy: the daemon-disabled / no-base-URL refusals resolve before shell paths.
|
|
239
|
+
homeDirectory: () => requireShellPaths(ctx).homeDirectory,
|
|
240
|
+
});
|
|
241
|
+
if (sub === 'setup') {
|
|
242
|
+
// Print the up-front announcement, then drive the install with live
|
|
243
|
+
// per-component progress polled from voice.local.status.
|
|
244
|
+
if (resolution.available) ctx.print(VOICE_SETUP_ANNOUNCEMENT);
|
|
245
|
+
await runVoiceSetupWithProgress(resolution, { print: (block) => ctx.print(block) });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
ctx.print(await renderVoiceProvision(sub, resolution));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
230
251
|
if (sub === 'review') {
|
|
231
252
|
const enabled = Boolean(ctx.platform.configManager.get('ui.voiceEnabled') ?? false);
|
|
232
253
|
ctx.print([
|
|
@@ -273,7 +294,7 @@ export function registerExperienceRuntimeCommands(registry: CommandRegistry): vo
|
|
|
273
294
|
return;
|
|
274
295
|
}
|
|
275
296
|
}
|
|
276
|
-
ctx.print('Usage: /voice [review|enable|disable|bundle export <path>|bundle inspect <path>]');
|
|
297
|
+
ctx.print('Usage: /voice [review|status|setup|enable|disable|bundle export <path>|bundle inspect <path>]');
|
|
277
298
|
},
|
|
278
299
|
});
|
|
279
300
|
}
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
} from './runtime-services.ts';
|
|
22
22
|
import { getOperatorRpc, describeOperatorRpcError } from './operator-rpc.ts';
|
|
23
23
|
import { formatQuotaSnapshotLine, renderMetricMap, telemetryScopeRefusalLine } from './health-metrics-format.ts';
|
|
24
|
+
import { createMemoryDiagnosticsGateway, renderMemoryDiagnostics } from '../../core/memory-diagnostics-gateway.ts';
|
|
25
|
+
import { requireShellPaths } from './runtime-services.ts';
|
|
24
26
|
|
|
25
27
|
function renderSandboxHealthSummary(configManager: ConfigManager): string[] {
|
|
26
28
|
const backend = String(configManager.get('sandbox.vmBackend') ?? 'local');
|
|
@@ -42,7 +44,7 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
|
|
|
42
44
|
name: 'health',
|
|
43
45
|
aliases: ['doctor'],
|
|
44
46
|
description: 'Health workspace for startup posture, service readiness, sandbox posture, and provider health',
|
|
45
|
-
usage: '[report|review|open|setup|services|sandbox|provider|accounts|auth|settings|intelligence|remote|mcp|metrics|continuity|worktrees|maintenance|term|repair [domain]] — bare and report stay a cross-domain transcript report (see also /health provider for the providers modal)',
|
|
47
|
+
usage: '[report|review|open|setup|services|sandbox|provider|accounts|auth|settings|intelligence|remote|mcp|memory|metrics|continuity|worktrees|maintenance|term|repair [domain]] — bare and report stay a cross-domain transcript report (see also /health provider for the providers modal)',
|
|
46
48
|
async handler(args, ctx) {
|
|
47
49
|
const sub = (args[0] ?? 'review').toLowerCase();
|
|
48
50
|
const readModels = requireReadModels(ctx);
|
|
@@ -52,6 +54,18 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
|
|
|
52
54
|
return;
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
if (sub === 'memory') {
|
|
58
|
+
// The MemoryGovernor snapshot (ops.memory.get): tier, budget vs RSS,
|
|
59
|
+
// per-cache footprints, paused jobs, tripwire — read-only, honest 501
|
|
60
|
+
// when a daemon predates the governor verb.
|
|
61
|
+
const resolution = createMemoryDiagnosticsGateway({
|
|
62
|
+
configManager: ctx.platform.configManager,
|
|
63
|
+
homeDirectory: () => requireShellPaths(ctx).homeDirectory,
|
|
64
|
+
});
|
|
65
|
+
ctx.print(await renderMemoryDiagnostics(resolution));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
if (sub === 'services') {
|
|
56
70
|
const registry = requireServiceRegistry(ctx);
|
|
57
71
|
const all = registry.getAll();
|
|
@@ -2,6 +2,7 @@ import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
|
|
|
2
2
|
import type { CommandContext } from './command-registry.ts';
|
|
3
3
|
import type { SelectionItem } from './selection-modal.ts';
|
|
4
4
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
5
|
+
import { localVoiceSetupOffer } from './voice-local-settings.ts';
|
|
5
6
|
|
|
6
7
|
function getStreamingTtsProviders(ctx: CommandContext): Array<{ id: string; label: string; capabilities: readonly string[] }> {
|
|
7
8
|
const registry = ctx.platform.voiceProviderRegistry;
|
|
@@ -28,14 +29,32 @@ export function openTtsProviderPicker(ctx: CommandContext): boolean {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
const current = String(ctx.platform.configManager.get('tts.provider') ?? '').trim();
|
|
31
|
-
const items: SelectionItem[] = providers.map((provider) =>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
const items: SelectionItem[] = providers.map((provider) => {
|
|
33
|
+
// The managed local engine renders beside ElevenLabs; when it is not yet
|
|
34
|
+
// provisioned, its detail line offers the size-labeled one-act install so
|
|
35
|
+
// the operator sees the download cost before committing to /voice setup.
|
|
36
|
+
if (provider.id === 'local') {
|
|
37
|
+
const offer = localVoiceSetupOffer((key) => ctx.platform.configManager.get(key as Parameters<typeof ctx.platform.configManager.get>[0]));
|
|
38
|
+
if (!offer.provisioned) {
|
|
39
|
+
return {
|
|
40
|
+
id: provider.id,
|
|
41
|
+
label: provider.label,
|
|
42
|
+
detail: provider.id === current ? `${offer.detail} (current)` : offer.detail,
|
|
43
|
+
category: 'streaming TTS providers',
|
|
44
|
+
primaryAction: 'select',
|
|
45
|
+
actions: offer.actions,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
id: provider.id,
|
|
51
|
+
label: provider.label,
|
|
52
|
+
detail: provider.id === current ? `${provider.id} (current)` : provider.id,
|
|
53
|
+
category: 'streaming TTS providers',
|
|
54
|
+
primaryAction: 'select',
|
|
55
|
+
actions: '[Enter] set provider',
|
|
56
|
+
};
|
|
57
|
+
});
|
|
39
58
|
|
|
40
59
|
ctx.openSelection('Choose TTS Provider', items, { preSelectId: current, allowSearch: true }, (result) => {
|
|
41
60
|
if (!result) return;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// voice-local-settings.ts — the managed local-voice setup affordance the voice
|
|
3
|
+
// settings surfaces render beside ElevenLabs.
|
|
4
|
+
//
|
|
5
|
+
// The local engine registers in the voice provider registry regardless of
|
|
6
|
+
// whether it is provisioned yet (its status is honestly "unconfigured", never
|
|
7
|
+
// an error). When it is NOT provisioned, the settings/provider-picker surface
|
|
8
|
+
// offers the one-act install with its download size declared up front — the
|
|
9
|
+
// size is computed synchronously from the pinned piper manifest
|
|
10
|
+
// (piperProvisionBytes) so no daemon round-trip is needed just to label the
|
|
11
|
+
// offer. Platforms with no pinned managed build say so honestly.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
import { currentVoicePlatform, piperProvisionBytes } from '@pellux/goodvibes-sdk/platform/voice';
|
|
15
|
+
import { formatVoiceBytes } from '../core/voice-provision-status.ts';
|
|
16
|
+
|
|
17
|
+
export interface LocalVoiceSetupOffer {
|
|
18
|
+
/** A pinned managed piper build exists for the current platform. */
|
|
19
|
+
readonly supported: boolean;
|
|
20
|
+
/** The local TTS engine + model config keys are set (managed install applied or user-set). */
|
|
21
|
+
readonly provisioned: boolean;
|
|
22
|
+
/** Human size label for the one-act install ("~85 MB" or an honest unavailable note). */
|
|
23
|
+
readonly sizeLabel: string;
|
|
24
|
+
/** The detail line the provider picker renders for the local engine. */
|
|
25
|
+
readonly detail: string;
|
|
26
|
+
/** The picker action hint (advertises /voice setup only where it can actually provision). */
|
|
27
|
+
readonly actions: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Compute the managed local-voice setup offer from config, with the download
|
|
32
|
+
* size declared up front. `provisioned` reflects whether the local TTS engine
|
|
33
|
+
* binary + model keys are set; an unprovisioned-but-supported platform offers
|
|
34
|
+
* the size-labeled /voice setup one-act.
|
|
35
|
+
*/
|
|
36
|
+
export function localVoiceSetupOffer(configGet: (key: string) => unknown): LocalVoiceSetupOffer {
|
|
37
|
+
const platform = currentVoicePlatform();
|
|
38
|
+
const bytes = platform ? piperProvisionBytes(platform) : null;
|
|
39
|
+
const supported = bytes !== null && bytes !== undefined;
|
|
40
|
+
const ttsBinary = String(configGet('voice.local.ttsBinary') ?? '').trim();
|
|
41
|
+
const ttsModel = String(configGet('voice.local.ttsModelPath') ?? '').trim();
|
|
42
|
+
const provisioned = ttsBinary.length > 0 && ttsModel.length > 0;
|
|
43
|
+
const sizeLabel = supported ? `~${formatVoiceBytes(bytes)}` : 'unavailable on this platform';
|
|
44
|
+
|
|
45
|
+
let detail: string;
|
|
46
|
+
if (!supported) detail = 'local voice: no managed build for this platform (set voice.local.* by hand to use it)';
|
|
47
|
+
else if (provisioned) detail = 'local voice: configured (managed engine set)';
|
|
48
|
+
else detail = `local voice: not set up — run /voice setup (${sizeLabel})`;
|
|
49
|
+
|
|
50
|
+
const actions = supported && !provisioned ? '[Enter] select · /voice setup to provision' : '[Enter] set provider';
|
|
51
|
+
return { supported, provisioned, sizeLabel, detail, actions };
|
|
52
|
+
}
|
|
@@ -64,6 +64,15 @@ export interface CodeIndexServicesDeps {
|
|
|
64
64
|
readonly workingDirectory: string;
|
|
65
65
|
readonly configManager: Pick<ConfigManager, 'get'>;
|
|
66
66
|
readonly memoryEmbeddingRegistry: MemoryEmbeddingProviderRegistry;
|
|
67
|
+
/**
|
|
68
|
+
* Memory-governor backpressure (optional): when the governor pauses the
|
|
69
|
+
* 'code-index-reindex' job, the tool-site reindex scheduler stops scheduling
|
|
70
|
+
* new work; the critical tier refuses new reindex work outright via
|
|
71
|
+
* admitExpensiveWork. Absent (most test fixtures) → never paused, always
|
|
72
|
+
* admitted, exactly as before.
|
|
73
|
+
*/
|
|
74
|
+
readonly isReindexPaused?: (() => boolean) | undefined;
|
|
75
|
+
readonly admitExpensiveWork?: ((label: string) => { allowed: boolean; reason?: string | undefined }) | undefined;
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
/**
|
|
@@ -128,7 +137,12 @@ export function createCodeIndexServices(deps: CodeIndexServicesDeps): CodeIndexS
|
|
|
128
137
|
const codeIndexReindexScheduler = new CodeIndexReindexScheduler({
|
|
129
138
|
target: codeIndexStore,
|
|
130
139
|
workingDirectory: deps.workingDirectory,
|
|
131
|
-
|
|
140
|
+
// Honor governor backpressure: a paused code-index reindex job stops
|
|
141
|
+
// scheduling new work until the governor resumes it, and the critical
|
|
142
|
+
// memory tier refuses new reindex work outright (mirrors the SDK).
|
|
143
|
+
isEnabled: () => isCodeInjectionSettingEnabled(deps.configManager)
|
|
144
|
+
&& !(deps.isReindexPaused?.() ?? false)
|
|
145
|
+
&& (deps.admitExpensiveWork?.('code-index reindex').allowed ?? true),
|
|
132
146
|
});
|
|
133
147
|
return { codeIndexStore, codeIndexReindexScheduler };
|
|
134
148
|
}
|
|
@@ -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
|
|
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 }));
|