@pellux/goodvibes-tui 1.26.0 → 1.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +3756 -1110
- package/package.json +8 -7
- package/scripts/postinstall.js +57 -1
- package/src/audio/activation-sound.ts +117 -0
- package/src/audio/capture.ts +112 -0
- package/src/audio/onnxruntime-assets.d.ts +24 -0
- package/src/audio/voice-capture-wiring.ts +119 -0
- package/src/audio/voice-input-session.ts +181 -0
- package/src/audio/wake-inference.ts +182 -0
- package/src/audio/wake-runtime.ts +341 -0
- package/src/cli/bundle-command.ts +4 -1
- package/src/cli/management-utils.ts +4 -1
- package/src/cli/tui-startup.ts +10 -1
- package/src/core/voice-capture-status.ts +53 -0
- package/src/core/voice-stt-gateway.ts +75 -0
- package/src/core/wake-provision-runner.ts +86 -0
- package/src/core/wake-provision-status.ts +170 -0
- package/src/daemon/cli.ts +117 -28
- package/src/daemon/fatal-boot-report.ts +111 -0
- package/src/daemon/provision-wake-model.ts +81 -0
- package/src/input/command-registry.ts +6 -0
- package/src/input/commands/experience-runtime.ts +37 -3
- package/src/input/handler-shortcuts.ts +8 -0
- package/src/input/keybindings.ts +10 -1
- package/src/input/onboarding/onboarding-feature-units.ts +5 -5
- package/src/input/settings-modal-types.ts +14 -1
- package/src/main.ts +12 -14
- package/src/renderer/process-indicator.ts +66 -0
- package/src/renderer/settings-modal-helpers.ts +4 -2
- package/src/renderer/shell-surface.ts +22 -3
- package/src/runtime/bootstrap-core.ts +7 -7
- package/src/runtime/bootstrap.ts +3 -3
- package/src/runtime/device-posture-composition.ts +110 -0
- package/src/runtime/disposal-wiring.ts +16 -0
- package/src/runtime/embedded-service-factories.ts +16 -0
- package/src/runtime/legacy-daemon-migration.ts +12 -1
- package/src/runtime/runtime-services-types.ts +17 -0
- package/src/runtime/services.ts +81 -4
- package/src/runtime/voice-setup-services.ts +50 -4
- package/src/shell/voice-capture-shell.ts +69 -0
- package/src/verification/verification-ledger.ts +59 -20
- package/src/version.ts +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import { renderProcessIndicator } from './process-indicator.ts';
|
|
2
|
+
import { renderProcessIndicator, renderVoiceCaptureIndicator } from './process-indicator.ts';
|
|
3
3
|
import { UIFactory } from './ui-factory.ts';
|
|
4
|
+
import { voiceCaptureRowVisible, type VoiceCaptureIndicatorState } from '../core/voice-capture-status.ts';
|
|
4
5
|
|
|
5
6
|
export interface ShellFooterBuildOptions {
|
|
6
7
|
readonly width: number;
|
|
@@ -84,6 +85,14 @@ export interface ShellFooterBuildOptions {
|
|
|
84
85
|
readonly webSurfaceUrl?: string;
|
|
85
86
|
/** True while power.keepAwake holds — renders the always-visible "sleep disabled" chip. */
|
|
86
87
|
readonly powerKeepAwake?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Live microphone state — a push-to-talk recording, or the wake detector for
|
|
90
|
+
* as long as it runs. Rendered as a persistent row beside the process
|
|
91
|
+
* indicator, because a capture device held open with nothing on screen saying
|
|
92
|
+
* so is the one state a voice feature must never be in. Null (or a wake state
|
|
93
|
+
* with `voice.wake.indicator: off`) renders nothing.
|
|
94
|
+
*/
|
|
95
|
+
readonly voiceCapture?: VoiceCaptureIndicatorState | null;
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
export interface ShellFooterBuildResult {
|
|
@@ -98,6 +107,8 @@ export interface ShellFooterBuildResult {
|
|
|
98
107
|
const FOOTER_BASE_ROWS = 5;
|
|
99
108
|
const CONTEXT_PROGRESS_ROWS = 1;
|
|
100
109
|
const PROCESS_INDICATOR_ROWS = 1;
|
|
110
|
+
/** The live-microphone row, when one is showing (see renderVoiceCaptureIndicator). */
|
|
111
|
+
const VOICE_CAPTURE_ROWS = 1;
|
|
101
112
|
// Compact posture drops the context-info line (and never shows the context
|
|
102
113
|
// bar or process indicator), leaving just: prompt-box top border, bottom
|
|
103
114
|
// border, token-usage line, and the help/exit line.
|
|
@@ -151,6 +162,7 @@ export function estimateShellFooterHeight(
|
|
|
151
162
|
promptLineCount: number,
|
|
152
163
|
contextWindow?: number,
|
|
153
164
|
compact = false,
|
|
165
|
+
voiceCapture: VoiceCaptureIndicatorState | null = null,
|
|
154
166
|
): number {
|
|
155
167
|
if (lastRenderedFooterHeight !== null && lastRenderedFooterHeight.compact === compact) {
|
|
156
168
|
return lastRenderedFooterHeight.height;
|
|
@@ -160,7 +172,11 @@ export function estimateShellFooterHeight(
|
|
|
160
172
|
return COMPACT_FOOTER_BASE_ROWS + safePromptLines;
|
|
161
173
|
}
|
|
162
174
|
const progressRows = contextWindow && contextWindow > 0 ? CONTEXT_PROGRESS_ROWS : 0;
|
|
163
|
-
|
|
175
|
+
// Counted on the cold-start path too: a shell launched with the wake detector
|
|
176
|
+
// already listening renders that row in its very first frame, and a viewport
|
|
177
|
+
// sized one row too tall would draw the transcript's last line under it.
|
|
178
|
+
const voiceRows = voiceCaptureRowVisible(voiceCapture) ? VOICE_CAPTURE_ROWS : 0;
|
|
179
|
+
return FOOTER_BASE_ROWS + safePromptLines + progressRows + PROCESS_INDICATOR_ROWS + voiceRows;
|
|
164
180
|
}
|
|
165
181
|
|
|
166
182
|
export function buildShellFooter(
|
|
@@ -206,7 +222,10 @@ export function buildShellFooter(
|
|
|
206
222
|
options.runningAgentProgress,
|
|
207
223
|
);
|
|
208
224
|
const inputBoxRows = Math.max(1, options.promptLineCount) + 2;
|
|
209
|
-
|
|
225
|
+
// The voice row sits directly under the prompt box, ABOVE the process
|
|
226
|
+
// indicator: an open microphone is a live condition the user is acting inside,
|
|
227
|
+
// while the process indicator is a background summary.
|
|
228
|
+
lines.splice(inputBoxRows, 0, ...renderVoiceCaptureIndicator(options.width, options.voiceCapture ?? null), ...processIndicator);
|
|
210
229
|
// Scriptable status line — dim informational line above the prompt. Unshifted
|
|
211
230
|
// before the context hint so the context hint (if any) sits above it.
|
|
212
231
|
if (options.scriptableStatusLine) {
|
|
@@ -20,11 +20,11 @@ import { createRuntimeStore, createDomainDispatch, type RuntimeStore } from './s
|
|
|
20
20
|
import {
|
|
21
21
|
type MutableRuntimeState, type SessionEvent, bindFeatureSettingsBridge, createFeatureFlagManager, deriveFeatureStates, RuntimeEventBus, ForensicsCollector,
|
|
22
22
|
ForensicsRegistry, generateUserSessionId, loadBootstrapSystemPrompt, syncConfiguredServices,
|
|
23
|
-
registerBootstrapHookBridge, registerBootstrapRuntimeEvents,
|
|
23
|
+
registerBootstrapHookBridge, registerBootstrapRuntimeEvents, configureRuntimeEventBusDefaults, runtimeEventBusOptionsFrom,
|
|
24
24
|
} from '@/runtime/index.ts';
|
|
25
25
|
import { readExecEnvScrubAllowlist } from '../input/exec-env-scrub-config.ts';
|
|
26
26
|
import { createSandboxExecAsk, sandboxExecAskDepsFromRuntime } from '../permissions/sandbox-exec-gate.ts';
|
|
27
|
-
import { createRuntimeServices, type RuntimeServices } from './services.ts';
|
|
27
|
+
import { createRuntimeServices, installDevicePosture, type RuntimeServices } from './services.ts';
|
|
28
28
|
import { createHostPowerSeam } from '@pellux/goodvibes-sdk/platform/power';
|
|
29
29
|
import { runBootMemoryFold } from './memory-fold.ts';
|
|
30
30
|
import { wireCostPricing } from '../export/cost-utils.ts';
|
|
@@ -238,7 +238,7 @@ export async function initializeBootstrapCore(
|
|
|
238
238
|
bindFeatureSettingsBridge(configManager, featureFlags);
|
|
239
239
|
|
|
240
240
|
const userSessionId = `user-${generateUserSessionId()}`;
|
|
241
|
-
const runtimeBus = new RuntimeEventBus();
|
|
241
|
+
configureRuntimeEventBusDefaults(runtimeEventBusOptionsFrom((key) => configManager.get(key))); const runtimeBus = new RuntimeEventBus(); // cap from runtime.eventBus.maxListeners before the first bus exists
|
|
242
242
|
const store = createRuntimeStore();
|
|
243
243
|
const domainDispatch = createDomainDispatch(store);
|
|
244
244
|
let getConversationTitle = (): string | undefined => undefined;
|
|
@@ -249,9 +249,9 @@ export async function initializeBootstrapCore(
|
|
|
249
249
|
runtimeStore: store,
|
|
250
250
|
getConversationTitle: () => getConversationTitle(),
|
|
251
251
|
workingDir, homeDirectory, ...(options.daemonHomeDirectory === undefined ? {} : { daemonHomeDirectory: options.daemonHomeDirectory }),
|
|
252
|
-
// Embedded topology: the in-process runtime IS the daemon
|
|
253
|
-
//
|
|
254
|
-
powerSeam: createHostPowerSeam(),
|
|
252
|
+
// Embedded topology: the in-process runtime IS the daemon, so it holds its own OS inhibitor (fork
|
|
253
|
+
// default is non-spawning) and owns the wake-model boot retry a standalone daemon would. See services.ts.
|
|
254
|
+
powerSeam: createHostPowerSeam(), provisionWakeModelsAtBoot: true,
|
|
255
255
|
});
|
|
256
256
|
await services.workspaceTrustManager.load(); // settle any already-persisted trust decision before any tool runs
|
|
257
257
|
const providerRegistry = services.providerRegistry;
|
|
@@ -381,7 +381,7 @@ export async function initializeBootstrapCore(
|
|
|
381
381
|
contextAccountingHolder: services.contextAccountingHolder,
|
|
382
382
|
// First contained (sandboxed) command run announces "commands now run contained" once — recorded and surfaced now.
|
|
383
383
|
onSandboxedRun: createSandboxContainmentNotice({ configManager, notify: (text) => conversation.log(`[Sandbox] ${text}`, { fg: '135' }) }),
|
|
384
|
-
});
|
|
384
|
+
}); installDevicePosture(toolRegistry, services.devicePosture); // the `phone` tool is the only path that reaches the device capability service, so without this every `device.*` posture key governs nothing a session can observe; the housekeeping timer starts here (not at composition) so building a runtime in a test sweeps nothing
|
|
385
385
|
// Note: installWrfcAgentToolGuard is called after routeOrBuffer is defined (further below) so the onTrace callback routes guard decisions through the pre-router buffer.
|
|
386
386
|
services.agentOrchestrator.setDependencies({
|
|
387
387
|
surfaceRoot: services.surface.surfaceRoot,
|
package/src/runtime/bootstrap.ts
CHANGED
|
@@ -264,7 +264,7 @@ export async function bootstrapRuntime(
|
|
|
264
264
|
}, ACP_TASK_SYNC_INTERVAL_MS);
|
|
265
265
|
bootstrapUnsubs.push(() => clearInterval(acpTaskSyncInterval));
|
|
266
266
|
orchestrator.registerDelegateTool(acpManager);
|
|
267
|
-
const opsTaskManager = createTaskManager(store, runtimeBus, userSessionId);
|
|
267
|
+
const opsTaskManager = createTaskManager(store, runtimeBus, userSessionId, services.featureFlags); // featureFlags required: without it runtime.unifiedTasks configured nothing (permissive fallback); SDK default matches shipped behaviour, so this is a no-op for existing installs.
|
|
268
268
|
// Operator interventions ride the control-plane gateway capability
|
|
269
269
|
// (controlPlane.gateway, on by default; the old never-registered gate id shipped this dead).
|
|
270
270
|
const opsControlPlane = services.featureFlags.isEnabled('control-plane-gateway')
|
|
@@ -482,9 +482,9 @@ export async function bootstrapRuntime(
|
|
|
482
482
|
};
|
|
483
483
|
|
|
484
484
|
// Embedded DaemonServer/HttpListener factories (incl. the facade auto-update
|
|
485
|
-
// suppression) — see embedded-service-factories.ts.
|
|
485
|
+
// suppression), plus the daemon STATE directory — see embedded-service-factories.ts.
|
|
486
486
|
const createExternalServiceFactories = (token: string): ExternalServiceFactories =>
|
|
487
|
-
createEmbeddedServiceFactories(token, services.clusterCoordinator, services.clusterGroup.verbs);
|
|
487
|
+
createEmbeddedServiceFactories(token, services.clusterCoordinator, services.clusterGroup.verbs, join(services.homeDirectory, '.goodvibes', 'daemon'));
|
|
488
488
|
|
|
489
489
|
let externalServices: ExternalServicesHandle = {
|
|
490
490
|
daemonServer: null, httpListener: null,
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* device-posture-composition.ts — the paired-phone feature inside THIS daemon.
|
|
3
|
+
*
|
|
4
|
+
* A phone pairs with whichever daemon the person runs, and for this owner that
|
|
5
|
+
* daemon is hosted by the terminal app. Until this module existed the app had no
|
|
6
|
+
* device posture at all: `device.nodes.maxPaired` was enforced at the pairing
|
|
7
|
+
* path (SDK-side) and `device.capabilities.mode` was an onboarding toggle, while
|
|
8
|
+
* the other eleven `device.*` keys were recorded, read back, and governed
|
|
9
|
+
* nothing — the capability service they describe was never built here, so there
|
|
10
|
+
* was nothing for them to govern.
|
|
11
|
+
*
|
|
12
|
+
* The feature itself is platform-owned (`platform/devices`): the settings→policy
|
|
13
|
+
* mapping, the grants ledger, the capture store, the housekeeping sweeps, the
|
|
14
|
+
* confirmation flow, and the `phone` tool. This module supplies the three seams
|
|
15
|
+
* that are actually ours and nothing else:
|
|
16
|
+
*
|
|
17
|
+
* - the peer transport — the same DistributedRuntimeManager the remote surface
|
|
18
|
+
* pairs devices onto, so a phone paired here is a node here,
|
|
19
|
+
* - the approval path — the shared approval broker, so the confirmation
|
|
20
|
+
* appears wherever the person is looking (terminal, web app, companion),
|
|
21
|
+
* - the storage root and the live config manager.
|
|
22
|
+
*
|
|
23
|
+
* Constructing this starts nothing. `startHousekeeping()` is called from the
|
|
24
|
+
* bootstrap tail: grants and captures both outlive a restart, so a grant whose
|
|
25
|
+
* phone is gone, or a capture torn by a crash, is reaped BEFORE the first
|
|
26
|
+
* request of this run is served — and the periodic sweep after it is what keeps
|
|
27
|
+
* a long-running daemon from going days without one. A failed sweep is logged
|
|
28
|
+
* and the app still runs: housekeeping failing is a reason to say so, not a
|
|
29
|
+
* reason to refuse to start.
|
|
30
|
+
*/
|
|
31
|
+
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
32
|
+
import { createDevicePostureRuntime, registerDevicePhoneTool } from '@pellux/goodvibes-sdk/platform/devices';
|
|
33
|
+
import type {
|
|
34
|
+
DeviceApprovalBridge,
|
|
35
|
+
DevicePeerTransport,
|
|
36
|
+
DevicePhoneToolRegistry,
|
|
37
|
+
DevicePostureRuntime,
|
|
38
|
+
} from '@pellux/goodvibes-sdk/platform/devices';
|
|
39
|
+
import { registerDevicesGatewayMethods } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
40
|
+
import type { GatewayMethodCatalog } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
41
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
42
|
+
|
|
43
|
+
/** Who this surface records in the device audit trail. */
|
|
44
|
+
export const TUI_DEVICE_ACTOR = 'tui:phone-tool';
|
|
45
|
+
|
|
46
|
+
export interface DevicePostureCompositionOptions {
|
|
47
|
+
readonly configManager: ConfigManager;
|
|
48
|
+
/** The runtime devices pair onto; its listPeers/invokePeer are the transport. */
|
|
49
|
+
readonly distributedRuntime: DevicePeerTransport;
|
|
50
|
+
readonly approvals: DeviceApprovalBridge;
|
|
51
|
+
/** Surface-scoped directory the grants ledger, captures and disclosure live in. */
|
|
52
|
+
readonly stateDirectory: string;
|
|
53
|
+
/**
|
|
54
|
+
* Binding the catalog turns devices.nodes.list / devices.grants.* /
|
|
55
|
+
* devices.housekeeping.run from cataloged-but-unhandled into real handlers,
|
|
56
|
+
* which is what makes the grants surface work in the web app and the companion
|
|
57
|
+
* rather than only through this app's own tool.
|
|
58
|
+
*/
|
|
59
|
+
readonly gatewayMethods?: GatewayMethodCatalog | undefined;
|
|
60
|
+
readonly getSessionId?: (() => string | undefined) | undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface DevicePostureServices {
|
|
64
|
+
readonly devicePosture: DevicePostureRuntime;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Build the device posture runtime and bind the device verbs to it. */
|
|
68
|
+
export function createDevicePostureServices(options: DevicePostureCompositionOptions): DevicePostureServices {
|
|
69
|
+
const devicePosture = createDevicePostureRuntime({
|
|
70
|
+
transport: options.distributedRuntime,
|
|
71
|
+
approvals: options.approvals,
|
|
72
|
+
config: options.configManager,
|
|
73
|
+
stateDirectory: options.stateDirectory,
|
|
74
|
+
actor: TUI_DEVICE_ACTOR,
|
|
75
|
+
...(options.getSessionId ? { getSessionId: options.getSessionId } : {}),
|
|
76
|
+
});
|
|
77
|
+
if (options.gatewayMethods) registerDevicesGatewayMethods(options.gatewayMethods, devicePosture);
|
|
78
|
+
return { devicePosture };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Everything a host with a tool registry has to do once it is up: register the
|
|
83
|
+
* `phone` tool — the only path that reaches the capability service, so without it
|
|
84
|
+
* the posture keys govern nothing a session can observe — and start housekeeping.
|
|
85
|
+
*
|
|
86
|
+
* One call, because these two belong to the same feature and a host that did the
|
|
87
|
+
* first and forgot the second would serve requests while never reaping a grant
|
|
88
|
+
* whose phone is gone.
|
|
89
|
+
*/
|
|
90
|
+
export function installDevicePosture(
|
|
91
|
+
toolRegistry: DevicePhoneToolRegistry,
|
|
92
|
+
devicePosture: DevicePostureRuntime,
|
|
93
|
+
): void {
|
|
94
|
+
registerDevicePhoneTool(toolRegistry, devicePosture);
|
|
95
|
+
startDeviceHousekeeping(devicePosture);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The recovery sweep plus the periodic timer. Separate from construction so
|
|
100
|
+
* composing a runtime in a test starts no timer and touches no disk, and
|
|
101
|
+
* separate from the call above so the standalone daemon — which registers no
|
|
102
|
+
* tools — still sweeps.
|
|
103
|
+
*/
|
|
104
|
+
export function startDeviceHousekeeping(devicePosture: DevicePostureRuntime): void {
|
|
105
|
+
void devicePosture.startHousekeeping().catch((error: unknown) => {
|
|
106
|
+
logger.warn('Device housekeeping sweep failed at startup', {
|
|
107
|
+
error: error instanceof Error ? error.message : String(error),
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -38,6 +38,14 @@ export { createDisposalScope };
|
|
|
38
38
|
export interface SurfaceRuntimePollerOwners extends Omit<RuntimePollerOwners, 'stopConfigWatch'> {
|
|
39
39
|
/** Fork-only: the repeating crash-residue sweep (durability-services.ts). */
|
|
40
40
|
readonly stopDurabilityHousekeeping: () => void;
|
|
41
|
+
/**
|
|
42
|
+
* Fork-only: the wake-word recovery sweep and a pending boot provision
|
|
43
|
+
* (voice-setup-services.ts). Started only when an entrypoint opted into boot
|
|
44
|
+
* provisioning, and a no-op otherwise — but it is on this list unconditionally,
|
|
45
|
+
* because "the graph did not start it this time" is not a reason for the
|
|
46
|
+
* teardown path to have no way to stop it.
|
|
47
|
+
*/
|
|
48
|
+
readonly stopWakeHousekeeping: () => void;
|
|
41
49
|
/**
|
|
42
50
|
* Fork-only: the daemon handler surfaces (daemon-handler-composition.ts).
|
|
43
51
|
*
|
|
@@ -47,6 +55,12 @@ export interface SurfaceRuntimePollerOwners extends Omit<RuntimePollerOwners, 's
|
|
|
47
55
|
* exists, so if this surface does not stop them nothing does.
|
|
48
56
|
*/
|
|
49
57
|
readonly daemonHandlers: { readonly unregister: () => void };
|
|
58
|
+
/**
|
|
59
|
+
* Fork-only: the paired-phone feature's housekeeping timer
|
|
60
|
+
* (device-posture-composition.ts). Started by whichever entry point boots the
|
|
61
|
+
* host, so the stop belongs on this list rather than in one shutdown path.
|
|
62
|
+
*/
|
|
63
|
+
readonly devicePosture: { readonly stopHousekeeping: () => void };
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
/**
|
|
@@ -72,6 +86,8 @@ export function registerSurfaceRuntimePollers(
|
|
|
72
86
|
): void {
|
|
73
87
|
registerRuntimePollers(registry, { ...services, stopConfigWatch: extras.stopConfigWatch });
|
|
74
88
|
registry.add('durability housekeeping', services.stopDurabilityHousekeeping);
|
|
89
|
+
registry.add('device housekeeping', () => services.devicePosture.stopHousekeeping());
|
|
90
|
+
registry.add('wake-word housekeeping', services.stopWakeHousekeeping);
|
|
75
91
|
// Registered LAST so it tears down FIRST (the scope unwinds in reverse), which
|
|
76
92
|
// is the order daemon/cli.ts already used by hand: release the handler surfaces
|
|
77
93
|
// — closing the inbox store and stopping its poll timers — before the pollers
|
|
@@ -27,9 +27,25 @@ export function createEmbeddedServiceFactories(
|
|
|
27
27
|
sharedDaemonToken: string,
|
|
28
28
|
clusterCoordinator: ClusterCoordinator,
|
|
29
29
|
clusterGroupVerbs: ClusterGroupVerbSurface,
|
|
30
|
+
daemonStateDirectory: string,
|
|
30
31
|
): ExternalServiceFactories {
|
|
31
32
|
return {
|
|
32
33
|
sharedDaemonToken,
|
|
34
|
+
// `daemonRuntimeDir` names the daemon's own STATE directory — the one
|
|
35
|
+
// holding operator-tokens.json, auth-users.json and daemon-settings.json —
|
|
36
|
+
// which is `<home>/.goodvibes/daemon`, not the user home above it. Every
|
|
37
|
+
// reader in this repository resolves it that way (config/goodvibes-home.ts,
|
|
38
|
+
// cli/service-posture.ts, runtime/bootstrap.ts). Passing it explicitly
|
|
39
|
+
// through this typed field (rather than appending a duplicate
|
|
40
|
+
// `--daemon-home` CLI arg via `daemonLaunchArgs` and relying on
|
|
41
|
+
// last-wins flag parsing) also keeps a detached spawn correct when
|
|
42
|
+
// `GOODVIBES_HOME` overrides the tree root away from the OS home
|
|
43
|
+
// directory, which the SDK's own `daemonHomeDir` default does not account
|
|
44
|
+
// for. The SDK's
|
|
45
|
+
// fix (sdk commit d1336a2b) that put the state directory behind the FIRST
|
|
46
|
+
// `--daemon-home` argument shipped in 1.21.0 and made the old
|
|
47
|
+
// flag-duplication workaround redundant; this is its replacement.
|
|
48
|
+
daemonRuntimeDir: daemonStateDirectory,
|
|
33
49
|
createDaemonServer: (bus, userAuth, runtimeServices) => {
|
|
34
50
|
// No updateArtifact: the embedded daemon keeps updates host-managed (the
|
|
35
51
|
// TUI client updates itself), so the facade runs no self-update loop.
|
|
@@ -135,7 +135,18 @@ export function buildManagedDaemonServiceManager(params: BuildManagedDaemonServi
|
|
|
135
135
|
description: MANAGED_SERVICE_DESCRIPTION,
|
|
136
136
|
workingDirectory,
|
|
137
137
|
command: params.binaryPath,
|
|
138
|
-
|
|
138
|
+
// `--daemon-home` names the daemon's own STATE directory — the one holding
|
|
139
|
+
// operator-tokens.json, auth-users.json and daemon-settings.json — which is
|
|
140
|
+
// `<home>/.goodvibes/daemon`. This baked the USER HOME, so a serviced
|
|
141
|
+
// daemon filed its identity a level above where every reader in this
|
|
142
|
+
// repository looks: config/goodvibes-home.ts resolves the flag AS the state
|
|
143
|
+
// directory, cli/service-posture.ts already writes the state directory into
|
|
144
|
+
// GOODVIBES_DAEMON_HOME for the unit it installs, and runtime/bootstrap.ts
|
|
145
|
+
// reads the companion token from the state directory. On a normal machine
|
|
146
|
+
// the mismatch is invisible from the outside — the daemon simply mints a
|
|
147
|
+
// second operator-tokens.json in the home directory and the client keeps
|
|
148
|
+
// reading the empty one under .goodvibes/daemon.
|
|
149
|
+
args: ['--daemon-home', join(params.homeDir, '.goodvibes', 'daemon')],
|
|
139
150
|
env: {},
|
|
140
151
|
restartOnFailure: true,
|
|
141
152
|
};
|
|
@@ -38,6 +38,7 @@ import type { createDurabilityServices } from './durability-services.ts';
|
|
|
38
38
|
import type { MemorySpineClient } from '@pellux/goodvibes-sdk/platform/runtime/memory-spine';
|
|
39
39
|
import type { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
|
|
40
40
|
import type { DomainDispatch, RuntimeStore } from './store/index.ts';
|
|
41
|
+
import type { DevicePostureRuntime } from '@pellux/goodvibes-sdk/platform/devices';
|
|
41
42
|
import type { RuntimeEventBus, DistributedRuntimeManager, RemoteRunnerRegistry, RemoteSupervisor, IntegrationHelperService, IdempotencyStore, ComponentHealthMonitor, WorktreeRegistry, SandboxSessionRegistry, ShellPathService, FeatureFlagManager, PolicyRuntimeState, SessionSurface } from '@/runtime/index.ts';
|
|
42
43
|
import type { VoiceProviderRegistry, VoiceService } from '@pellux/goodvibes-sdk/platform/voice';
|
|
43
44
|
import type { CacheRegistry, PauseController, MemoryGovernor } from '@pellux/goodvibes-sdk/platform/runtime/memory';
|
|
@@ -91,6 +92,13 @@ export interface RuntimeServicesOptions {
|
|
|
91
92
|
readonly powerSeam?: Parameters<typeof wireIdlePowerAndLiveTurn>[0]['powerSeam'];
|
|
92
93
|
/** Live session id, read per crash-residue sweep so the running session is exempt — see durability-services.ts. */
|
|
93
94
|
readonly currentSessionId?: (() => string | null) | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* Wake-word boot provisioning opt-in. Same shape as `powerSeam`: the real
|
|
97
|
+
* entrypoints (daemon/cli.ts, bootstrap-core.ts) ask for it, the one-shot CLI
|
|
98
|
+
* commands do not, and a test composing this graph gets neither a network fetch
|
|
99
|
+
* nor an hourly sweep it did not ask for. See voice-setup-services.ts.
|
|
100
|
+
*/
|
|
101
|
+
readonly provisionWakeModelsAtBoot?: boolean | undefined;
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
export interface RuntimeServices {
|
|
@@ -176,6 +184,13 @@ export interface RuntimeServices {
|
|
|
176
184
|
readonly providerRegistry: ProviderRegistry;
|
|
177
185
|
readonly toolLLM: ToolLLM;
|
|
178
186
|
readonly distributedRuntime: DistributedRuntimeManager;
|
|
187
|
+
/**
|
|
188
|
+
* The paired-phone feature for this host: the grants ledger, the capture
|
|
189
|
+
* store, the housekeeping sweeps, and the capability service every `device.*`
|
|
190
|
+
* setting governs. Bound to the `devices.*` verbs at composition; the `phone`
|
|
191
|
+
* tool is registered on it in the bootstrap tail.
|
|
192
|
+
*/
|
|
193
|
+
readonly devicePosture: DevicePostureRuntime;
|
|
179
194
|
readonly daemonHandlers: DaemonHandlerSurfaces;
|
|
180
195
|
/** Elects the one node on this network that consumes inbound messages; hand it to the DaemonServer so its consumers share this leadership instead of holding a second election. */
|
|
181
196
|
readonly clusterCoordinator: ClusterCoordinator;
|
|
@@ -211,6 +226,8 @@ export interface RuntimeServices {
|
|
|
211
226
|
readonly appendOnlyRetentionScheduler: ReturnType<typeof createDurabilityServices>['appendOnlyRetentionScheduler']; // periodic append-only sweep; unref'd timers, stop() on teardown
|
|
212
227
|
/** Stops the recurring crash-residue sweep; idempotent, unref'd timer (hosts that tear a runtime down call it). */
|
|
213
228
|
readonly stopDurabilityHousekeeping: () => void;
|
|
229
|
+
/** Stops the wake-word recovery sweep and a pending boot provision; a no-op unless `provisionWakeModelsAtBoot` was set. */
|
|
230
|
+
readonly stopWakeHousekeeping: () => void;
|
|
214
231
|
readonly memoryConsolidationScheduler: MemoryConsolidationScheduler;
|
|
215
232
|
readonly powerManager: PowerManager;
|
|
216
233
|
/** The daemon's memory governor (default ON). Backs ops.memory.get and defends the daemon's footprint by tier. */
|
package/src/runtime/services.ts
CHANGED
|
@@ -55,6 +55,10 @@ import { createWorkstreamServices } from './workstream-services.ts';
|
|
|
55
55
|
import { wireFleetNeedsInputPush } from './fleet-needs-input-push.ts';
|
|
56
56
|
import { codeIndexDbPath, createCodeIndexServices, createStoreRerooter, isCodeInjectionSettingEnabled } from './code-index-services.ts';
|
|
57
57
|
import { createDaemonHandlerComposition } from './daemon-handler-composition.ts';
|
|
58
|
+
import { createDevicePostureServices } from './device-posture-composition.ts';
|
|
59
|
+
// Re-exported so the shell's bootstrap reaches the install through the same
|
|
60
|
+
// module it already imports the runtime graph from.
|
|
61
|
+
export { installDevicePosture } from './device-posture-composition.ts';
|
|
58
62
|
import { createClusterServices, startClusterServices } from './cluster-group-composition.ts';
|
|
59
63
|
import { WorkspaceTrustManager } from './trust/workspace-trust.ts';
|
|
60
64
|
import { ensureConfiguredModelIsRoutable } from './provider-fallback.ts';
|
|
@@ -159,8 +163,24 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
159
163
|
join(surface.sessionsDir, 'task-graph.json'),
|
|
160
164
|
);
|
|
161
165
|
const hookActivityTracker = new HookActivityTracker();
|
|
166
|
+
// featureFlags is REQUIRED here in practice, even though the SDK types it
|
|
167
|
+
// optional. isFeatureGateEnabled(null, ...) is permissive by design — a
|
|
168
|
+
// narrow embed with no manager wired gets the capability rather than a
|
|
169
|
+
// silent off — so omitting it did not disable the watcher framework when
|
|
170
|
+
// watchers.enabled is turned off; it made the setting configure nothing.
|
|
171
|
+
// Threading it preserves current effective behaviour rather than changing
|
|
172
|
+
// it: watchers.enabled defaults true, the watcher-framework flag's own
|
|
173
|
+
// defaultState is 'enabled', and the flag declares no notOperable record —
|
|
174
|
+
// so with nothing configured the gate reads exactly as before, and the
|
|
175
|
+
// difference is only that turning it OFF now turns it off. This composes
|
|
176
|
+
// with bootstrap-core.ts's own `if (configManager.get('watchers.enabled'))`
|
|
177
|
+
// check around the built-in polling watcher registration: that check reads
|
|
178
|
+
// the same key, so the two never disagree, and this fix only reaches the
|
|
179
|
+
// watcher registry's OTHER entry points (registerWatcher/startWatcher/etc.)
|
|
180
|
+
// that bootstrap-core.ts's narrower check does not cover.
|
|
162
181
|
const watcherRegistry = new WatcherRegistry({
|
|
163
182
|
storePath: shellPaths.resolveProjectPath('tui', 'watchers.json'),
|
|
183
|
+
featureFlags,
|
|
164
184
|
});
|
|
165
185
|
watcherRegistry.attachRuntime({
|
|
166
186
|
runtimeStore: options.runtimeStore,
|
|
@@ -223,6 +243,15 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
223
243
|
// modal): bootstrap.ts activates the wire transport when a compatible
|
|
224
244
|
// external daemon is adopted, same signal as the session spine.
|
|
225
245
|
const memorySpine = new MemorySpineClient({ local: createLocalMemoryAccess(memoryRegistry) });
|
|
246
|
+
// featureFlags is REQUIRED here in practice, even though the SDK types it
|
|
247
|
+
// optional. isFeatureGateEnabled(null, ...) is permissive by design, so
|
|
248
|
+
// omitting it did not disable delivery tracking when
|
|
249
|
+
// integrations.deliveryTracking is turned off — it made the setting
|
|
250
|
+
// configure nothing: deliverText/deliverJobRun kept running either way.
|
|
251
|
+
// Threading it preserves current effective behaviour rather than changing
|
|
252
|
+
// it: the config default is true, the delivery-engine flag's own
|
|
253
|
+
// defaultState is 'enabled', and the flag declares no notOperable record —
|
|
254
|
+
// so with nothing configured the gate reads exactly as before.
|
|
226
255
|
const deliveryManager = new AutomationDeliveryManager({
|
|
227
256
|
configManager,
|
|
228
257
|
// This manager builds the delivery router the daemon actually replies
|
|
@@ -236,7 +265,20 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
236
265
|
runtimeStore: options.runtimeStore,
|
|
237
266
|
routeBindings,
|
|
238
267
|
artifactStore,
|
|
268
|
+
featureFlags,
|
|
239
269
|
});
|
|
270
|
+
// Same shape as deliveryManager above: automation.enabled defaults true,
|
|
271
|
+
// the automation-domain flag's own defaultState is 'enabled', so threading
|
|
272
|
+
// featureFlags here does not change what a default install does — it only
|
|
273
|
+
// makes turning automation.enabled off actually turn AutomationManager's own
|
|
274
|
+
// create/update/run/list surface off. This composes with bootstrap.ts's own
|
|
275
|
+
// `if (configManager.get('automation.enabled'))` gate around scheduling
|
|
276
|
+
// automationManager.start(): that check reads the same key, and
|
|
277
|
+
// AutomationManager.start() re-checks the same gate internally (it no-ops
|
|
278
|
+
// and stops rather than starting when disabled), so the two can never
|
|
279
|
+
// disagree — this fix only reaches the manager's OTHER entry points
|
|
280
|
+
// (createJob/updateJob/runNow/etc.) that the bootstrap.ts gate does not
|
|
281
|
+
// cover, the same way watchers.enabled and the watcher registry above do.
|
|
240
282
|
const automationManager = new AutomationManager({
|
|
241
283
|
configManager,
|
|
242
284
|
defaultSurfaceKind: 'tui',
|
|
@@ -248,6 +290,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
248
290
|
// Same live registry: a bare model id on an automation job resolves through
|
|
249
291
|
// the shared resolver instead of a format-only rejection.
|
|
250
292
|
providerRegistry,
|
|
293
|
+
featureFlags,
|
|
251
294
|
spawnTask: (input) => {
|
|
252
295
|
const record = agentManager.spawn({
|
|
253
296
|
mode: 'spawn',
|
|
@@ -309,6 +352,16 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
309
352
|
approvalBridge: approvalBroker,
|
|
310
353
|
automationBridge: automationManager,
|
|
311
354
|
});
|
|
355
|
+
// The paired-phone feature for this host, on the SAME runtime phones pair onto
|
|
356
|
+
// and the SAME approval broker every other confirmation rides. Every `device.*`
|
|
357
|
+
// setting is read live through this; see device-posture-composition.ts.
|
|
358
|
+
const { devicePosture } = createDevicePostureServices({
|
|
359
|
+
configManager,
|
|
360
|
+
distributedRuntime,
|
|
361
|
+
approvals: approvalBroker,
|
|
362
|
+
stateDirectory: shellPaths.resolveProjectPath('tui', 'devices'),
|
|
363
|
+
gatewayMethods,
|
|
364
|
+
});
|
|
312
365
|
|
|
313
366
|
// Which machines on this network are "us", and which of them reads the
|
|
314
367
|
// shared inbox. Both inert until startCluster() — no socket, no key material
|
|
@@ -334,7 +387,18 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
334
387
|
= createRemoteExecutionServices({
|
|
335
388
|
agentManager, workingDirectory, hookDispatcher, configManager, runtimeBus: options.runtimeBus,
|
|
336
389
|
});
|
|
337
|
-
|
|
390
|
+
// featureFlags is REQUIRED here in practice, even though the SDK types it
|
|
391
|
+
// optional. isFeatureGateEnabled(null, ...) is permissive by design, so
|
|
392
|
+
// omitting it did not disable managed blocking when
|
|
393
|
+
// security.tokenAudit.enabled is turned off. Threading it preserves current
|
|
394
|
+
// effective behaviour rather than changing it: managed is hardcoded false
|
|
395
|
+
// here (advisory reporting only; excess-scope/overdue tokens are reported,
|
|
396
|
+
// never blocked, regardless of this flag — see isBlocked()'s
|
|
397
|
+
// `this._config.managed && this._managedBlockingEnabled()` guard), the
|
|
398
|
+
// config default for security.tokenAudit.enabled is true, and the
|
|
399
|
+
// token-scope-rotation-audit flag's own defaultState is 'enabled' — so with
|
|
400
|
+
// nothing configured the gate reads exactly as before either way.
|
|
401
|
+
const tokenAuditor = new ApiTokenAuditor({ managed: false, featureFlags });
|
|
338
402
|
const componentHealthMonitor = new ComponentHealthMonitor();
|
|
339
403
|
const worktreeRegistry = new WorktreeRegistry(workingDirectory);
|
|
340
404
|
const webhookNotifier = new WebhookNotifier();
|
|
@@ -386,7 +450,15 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
386
450
|
runtimeBus: options.runtimeBus,
|
|
387
451
|
observeExternalAgents: options.observeExternalAgents, providerRegistry, // observeExternalAgents is daemon-side only
|
|
388
452
|
});
|
|
389
|
-
|
|
453
|
+
// featureFlags is REQUIRED here in practice, even though the SDK types it
|
|
454
|
+
// optional. isFeatureGateEnabled(null, ...) is permissive by design, so
|
|
455
|
+
// omitting it did not disable the HITL UX mode system when
|
|
456
|
+
// behavior.hitlMode is set to 'off' — setHITLMode/setDomainVerbosity kept
|
|
457
|
+
// accepting writes either way. Threading it preserves current effective
|
|
458
|
+
// behaviour rather than changing it: behavior.hitlMode defaults to
|
|
459
|
+
// 'balanced' (not 'off'), and the hitl-ux-modes flag's own defaultState is
|
|
460
|
+
// 'enabled' — so with nothing configured the gate reads exactly as before.
|
|
461
|
+
const modeManager = new ModeManager({ featureFlags }); const fileUndoManager = new FileUndoManager();
|
|
390
462
|
const workspaceCheckpointManager = createWorkspaceCheckpointing({ workspaceRoot: workingDirectory, surface, runtimeBus: options.runtimeBus, configManager });
|
|
391
463
|
// memory-consolidation honors governor backpressure: it ticks only when idle
|
|
392
464
|
// AND the 'memory-consolidation' job is not paused AND expensive work is
|
|
@@ -414,7 +486,11 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
414
486
|
|
|
415
487
|
// Managed local-voice provisioning (voice.local.status/install) — single-flight
|
|
416
488
|
// one-act install + no-network status; see voice-setup-services.ts.
|
|
417
|
-
const { voiceSetup } = wireVoiceSetup({ configManager, shellPaths, voiceProviders, admitExpensiveWork
|
|
489
|
+
const { voiceSetup, stopWakeHousekeeping } = wireVoiceSetup({ configManager, shellPaths, voiceProviders, admitExpensiveWork,
|
|
490
|
+
// Boot provisioning of the wake-word model + its recovery sweep, opted into
|
|
491
|
+
// by the real entrypoints only (same treatment as powerSeam) so a one-shot
|
|
492
|
+
// CLI command and a test composing this graph fetch nothing and start no timer.
|
|
493
|
+
provisionWakeModelsAtBoot: options.provisionWakeModelsAtBoot === true });
|
|
418
494
|
|
|
419
495
|
// Terminal-shell wrapper over the SDK registerGatewayVerbGroups (gateway-verbs.ts); checkin.*/fleet-needs-input/pairing.* register only when their deps are present. memoryGovernor lights up ops.memory.get; voiceSetup lights up voice.local.status/install.
|
|
420
496
|
// calendar.*/email.* are platform-served; these two let it register (mail-composition.ts).
|
|
@@ -552,6 +628,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
552
628
|
providerRegistry,
|
|
553
629
|
toolLLM,
|
|
554
630
|
distributedRuntime,
|
|
631
|
+
devicePosture,
|
|
555
632
|
daemonHandlers,
|
|
556
633
|
clusterCoordinator,
|
|
557
634
|
clusterGroup,
|
|
@@ -577,7 +654,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
577
654
|
workstreamCommands,
|
|
578
655
|
codeIndexStore,
|
|
579
656
|
codeIndexReindexScheduler,
|
|
580
|
-
storeSnapshotScheduler, appendOnlyRetentionScheduler, stopDurabilityHousekeeping,
|
|
657
|
+
storeSnapshotScheduler, appendOnlyRetentionScheduler, stopDurabilityHousekeeping, stopWakeHousekeeping,
|
|
581
658
|
memoryConsolidationScheduler,
|
|
582
659
|
powerManager,
|
|
583
660
|
memoryGovernor,
|
|
@@ -23,30 +23,76 @@ import {
|
|
|
23
23
|
createVoiceSetupService,
|
|
24
24
|
type VoiceSetupService,
|
|
25
25
|
} from '@pellux/goodvibes-sdk/platform/runtime/voice-setup';
|
|
26
|
-
import
|
|
26
|
+
import {
|
|
27
|
+
startWakeBootProvisioning,
|
|
28
|
+
type VoiceProviderRegistry,
|
|
29
|
+
} from '@pellux/goodvibes-sdk/platform/voice';
|
|
27
30
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
31
|
+
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
28
32
|
import type { ShellPathService } from '@/runtime/index.ts';
|
|
29
33
|
|
|
30
34
|
export type { VoiceSetupService };
|
|
31
35
|
|
|
36
|
+
/**
|
|
37
|
+
* The recovery act this surface names when a boot provision degrades. The SDK
|
|
38
|
+
* would otherwise name the control-plane verb, which is correct everywhere and
|
|
39
|
+
* useless to someone sitting in a terminal.
|
|
40
|
+
*/
|
|
41
|
+
export const WAKE_RECOVERY_COMMAND = '/voice wake setup';
|
|
42
|
+
|
|
32
43
|
export interface VoiceSetupServicesDeps {
|
|
33
44
|
readonly configManager: ConfigManager;
|
|
34
45
|
readonly shellPaths: Pick<ShellPathService, 'resolveUserPath'>;
|
|
35
46
|
readonly voiceProviders: VoiceProviderRegistry;
|
|
36
47
|
/** Critical-tier admission gate — a provision run allocates archive + model buffers. */
|
|
37
48
|
readonly admitExpensiveWork: (label: string) => { allowed: boolean; reason?: string | undefined };
|
|
49
|
+
/**
|
|
50
|
+
* Opt into boot-time wake-model provisioning and the recovery sweeper.
|
|
51
|
+
*
|
|
52
|
+
* Off unless a real entrypoint asks for it, matching how this graph treats the
|
|
53
|
+
* host power seam: `daemon/cli.ts` and `runtime/bootstrap-core.ts` opt in, the
|
|
54
|
+
* one-shot CLI commands do not, and a test composing the graph gets neither a
|
|
55
|
+
* network fetch nor a repeating timer it did not ask for.
|
|
56
|
+
*/
|
|
57
|
+
readonly provisionWakeModelsAtBoot?: boolean | undefined;
|
|
58
|
+
/** Injected in tests: the boot starter, so no sweeper and no fetch are real. */
|
|
59
|
+
readonly startBootProvisioning?: typeof startWakeBootProvisioning | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface VoiceSetupServices {
|
|
63
|
+
readonly voiceSetup: VoiceSetupService;
|
|
64
|
+
/**
|
|
65
|
+
* Stop the wake-word recovery sweeper and cancel a pending boot provision.
|
|
66
|
+
* A no-op when boot provisioning was not opted into. Registered on the
|
|
67
|
+
* disposal scope (disposal-wiring.ts) — an hourly timer nothing stops is a
|
|
68
|
+
* poller this surface leaked, which is exactly what that scope exists for.
|
|
69
|
+
*/
|
|
70
|
+
readonly stopWakeHousekeeping: () => void;
|
|
38
71
|
}
|
|
39
72
|
|
|
40
73
|
/** Compose the managed-voice single-flight install + no-network status read. */
|
|
41
|
-
export function wireVoiceSetup(deps: VoiceSetupServicesDeps):
|
|
74
|
+
export function wireVoiceSetup(deps: VoiceSetupServicesDeps): VoiceSetupServices {
|
|
42
75
|
const { configManager, voiceProviders } = deps;
|
|
76
|
+
const managedVoiceRoot = deps.shellPaths.resolveUserPath('voice');
|
|
43
77
|
const voiceSetup = createVoiceSetupService({
|
|
44
|
-
managedVoiceRoot
|
|
78
|
+
managedVoiceRoot,
|
|
45
79
|
getConfig: (key) => String(configManager.get(key as Parameters<typeof configManager.get>[0]) ?? ''),
|
|
46
80
|
setConfig: (key, value) => configManager.setDynamic(key as Parameters<typeof configManager.setDynamic>[0], value),
|
|
47
81
|
// A successful (re-)install clears any tripped local-engine breaker.
|
|
48
82
|
resetLocalEngineFailureState: () => voiceProviders.get('local')?.resetEngineFailureState?.(),
|
|
49
83
|
admitExpensiveWork: deps.admitExpensiveWork,
|
|
50
84
|
});
|
|
51
|
-
|
|
85
|
+
if (deps.provisionWakeModelsAtBoot !== true) {
|
|
86
|
+
return { voiceSetup, stopWakeHousekeeping: () => {} };
|
|
87
|
+
}
|
|
88
|
+
// Sweep the wake tree, then fetch whatever the install could not. The SDK owns
|
|
89
|
+
// the policy — never throws, one plain message, reaps before it retries — and
|
|
90
|
+
// routes the attempt through the setup service's single flight so a boot
|
|
91
|
+
// attempt and a user typing /voice wake setup are one download.
|
|
92
|
+
const boot = (deps.startBootProvisioning ?? startWakeBootProvisioning)({
|
|
93
|
+
managedRoot: managedVoiceRoot,
|
|
94
|
+
ensureProvisioned: () => voiceSetup.wakeEnsureProvisioned({ recoveryHint: WAKE_RECOVERY_COMMAND }),
|
|
95
|
+
announce: (message) => { logger.info(message); },
|
|
96
|
+
});
|
|
97
|
+
return { voiceSetup, stopWakeHousekeeping: () => boot.stop() };
|
|
52
98
|
}
|