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