@pellux/goodvibes-tui 1.7.0 → 1.9.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/README.md +10 -9
- package/docs/foundation-artifacts/operator-contract.json +4045 -1763
- package/package.json +2 -2
- package/src/audio/spoken-turn-controller.ts +12 -2
- package/src/audio/spoken-turn-wiring.ts +2 -1
- package/src/cli/help.ts +8 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/cli/status.ts +2 -1
- package/src/cli/surface-command.ts +2 -2
- package/src/core/composer-state.ts +11 -3
- package/src/core/conversation-line-cache.ts +27 -3
- package/src/core/conversation-rendering.ts +71 -14
- package/src/core/stream-event-wiring.ts +20 -1
- package/src/core/system-message-noise.ts +87 -0
- package/src/core/system-message-router.ts +68 -1
- package/src/core/turn-cancellation.ts +7 -2
- package/src/core/turn-event-wiring.ts +10 -2
- package/src/daemon/cli.ts +29 -2
- package/src/daemon/handlers/register.ts +8 -1
- package/src/daemon/service-commands.ts +329 -0
- package/src/input/autocomplete.ts +27 -1
- package/src/input/command-registry.ts +46 -4
- package/src/input/commands/codebase-runtime.ts +46 -6
- package/src/input/commands/config.ts +43 -3
- package/src/input/commands/health-runtime.ts +9 -1
- package/src/input/commands/memory.ts +68 -35
- package/src/input/commands/operator-panel-runtime.ts +31 -7
- package/src/input/commands/planning-runtime.ts +95 -6
- package/src/input/commands/qrcode-runtime.ts +25 -5
- package/src/input/commands/remote-runtime-setup.ts +5 -3
- package/src/input/commands/session-content.ts +20 -9
- package/src/input/commands/settings-sync-runtime.ts +15 -3
- package/src/input/commands/shell-core.ts +10 -1
- package/src/input/commands/workstream-runtime.ts +168 -18
- package/src/input/config-modal-types.ts +15 -1
- package/src/input/config-modal.ts +227 -12
- package/src/input/feed-context-factory.ts +3 -0
- package/src/input/handler-command-route.ts +10 -3
- package/src/input/handler-content-actions.ts +17 -2
- package/src/input/handler-feed-routes.ts +43 -121
- package/src/input/handler-feed.ts +32 -4
- package/src/input/handler-modal-routes.ts +78 -13
- package/src/input/handler-modal-stack.ts +11 -2
- package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
- package/src/input/handler-onboarding.ts +71 -59
- package/src/input/handler-picker-routes.ts +15 -8
- package/src/input/handler-shortcuts.ts +59 -9
- package/src/input/handler.ts +6 -1
- package/src/input/keybindings.ts +6 -5
- package/src/input/model-picker.ts +19 -2
- package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
- package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
- package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
- package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
- package/src/input/panel-mouse-geometry.ts +97 -0
- package/src/input/panel-paste-flood-guard.ts +86 -0
- package/src/input/selection-modal.ts +11 -0
- package/src/input/session-picker-modal.ts +44 -4
- package/src/input/settings-modal-data.ts +51 -2
- package/src/input/settings-modal-types.ts +4 -2
- package/src/main.ts +28 -27
- package/src/panels/builtin/shared.ts +9 -3
- package/src/panels/fleet-deep-link.ts +31 -0
- package/src/panels/fleet-panel-format.ts +62 -0
- package/src/panels/fleet-panel-worktree-detail.ts +48 -0
- package/src/panels/fleet-panel.ts +89 -131
- package/src/panels/fleet-read-model.ts +43 -0
- package/src/panels/fleet-steer.ts +35 -2
- package/src/panels/fleet-stop.ts +29 -1
- package/src/panels/modals/keybindings-modal.ts +16 -1
- package/src/panels/modals/modal-theme.ts +35 -29
- package/src/panels/modals/pairing-modal.ts +25 -6
- package/src/panels/modals/planning-modal.ts +43 -21
- package/src/panels/modals/work-plan-modal.ts +28 -0
- package/src/panels/panel-manager.ts +15 -4
- package/src/panels/polish-core.ts +38 -25
- package/src/panels/project-planning-answer-actions.ts +8 -13
- package/src/panels/types.ts +24 -0
- package/src/permissions/prompt.ts +160 -13
- package/src/renderer/autocomplete-overlay.ts +28 -2
- package/src/renderer/compositor.ts +2 -3
- package/src/renderer/config-modal.ts +19 -5
- package/src/renderer/footer-tips.ts +5 -1
- package/src/renderer/fullscreen-primitives.ts +32 -22
- package/src/renderer/git-status.ts +3 -1
- package/src/renderer/layout.ts +0 -4
- package/src/renderer/markdown.ts +7 -3
- package/src/renderer/modal-factory.ts +25 -20
- package/src/renderer/model-workspace.ts +18 -3
- package/src/renderer/overlay-box.ts +21 -17
- package/src/renderer/process-indicator.ts +14 -3
- package/src/renderer/selection-modal-overlay.ts +6 -1
- package/src/renderer/session-picker-modal.ts +196 -3
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +7 -0
- package/src/renderer/shell-surface.ts +8 -1
- package/src/renderer/status-glyphs.ts +14 -15
- package/src/renderer/system-message.ts +15 -3
- package/src/renderer/terminal-bg-probe.ts +339 -0
- package/src/renderer/terminal-escapes.ts +20 -0
- package/src/renderer/theme-mode-config.ts +67 -0
- package/src/renderer/theme.ts +91 -1
- package/src/renderer/thinking.ts +11 -3
- package/src/renderer/tool-call.ts +15 -9
- package/src/renderer/tool-result-summary.ts +148 -0
- package/src/renderer/turn-injection.ts +22 -3
- package/src/renderer/ui-factory.ts +154 -85
- package/src/renderer/ui-primitives.ts +30 -129
- package/src/runtime/bootstrap-command-context.ts +6 -0
- package/src/runtime/bootstrap-command-parts.ts +8 -4
- package/src/runtime/bootstrap-core.ts +33 -11
- package/src/runtime/bootstrap-hook-bridge.ts +7 -0
- package/src/runtime/bootstrap-shell.ts +19 -1
- package/src/runtime/bootstrap.ts +118 -5
- package/src/runtime/code-index-services.ts +25 -2
- package/src/runtime/legacy-daemon-migration.ts +516 -0
- package/src/runtime/memory-fold.ts +26 -0
- package/src/runtime/onboarding/derivation.ts +7 -2
- package/src/runtime/onboarding/snapshot.ts +27 -1
- package/src/runtime/onboarding/types.ts +31 -1
- package/src/runtime/operator-token-cleanup.ts +82 -1
- package/src/runtime/orchestrator-core-services.ts +10 -0
- package/src/runtime/resume-notice.ts +209 -0
- package/src/runtime/services.ts +12 -8
- package/src/runtime/session-inbound-inputs.ts +252 -0
- package/src/runtime/session-spine-transport.ts +64 -0
- package/src/runtime/terminal-output-guard.ts +15 -8
- package/src/runtime/ui-services.ts +19 -3
- package/src/runtime/workstream-services.ts +160 -28
- package/src/runtime/wrfc-persistence.ts +124 -17
- package/src/shell/blocking-input.ts +46 -3
- package/src/shell/recovery-input-helpers.ts +170 -1
- package/src/shell/ui-openers.ts +42 -9
- package/src/utils/terminal-width.ts +52 -0
- package/src/version.ts +1 -1
|
@@ -1,134 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
moreBelow: '▼',
|
|
29
|
-
next: '→',
|
|
30
|
-
back: '←',
|
|
31
|
-
pipeSeparator: '│',
|
|
32
|
-
},
|
|
33
|
-
status: {
|
|
34
|
-
success: '✓',
|
|
35
|
-
failure: '✕',
|
|
36
|
-
pending: '•',
|
|
37
|
-
active: '●',
|
|
38
|
-
idle: '◌',
|
|
39
|
-
info: '○',
|
|
40
|
-
warn: '⚠',
|
|
41
|
-
blocked: '⊘',
|
|
42
|
-
skipped: '◇',
|
|
43
|
-
review: '◈',
|
|
44
|
-
retry: '↻',
|
|
45
|
-
handoff: '⇢',
|
|
46
|
-
reference: '↗',
|
|
47
|
-
partial: '◐',
|
|
48
|
-
dualPane: '◆',
|
|
49
|
-
star: '★',
|
|
50
|
-
},
|
|
51
|
-
meter: {
|
|
52
|
-
filled: '█',
|
|
53
|
-
medium: '▓',
|
|
54
|
-
light: '▒',
|
|
55
|
-
empty: '░',
|
|
56
|
-
spark: ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'],
|
|
57
|
-
},
|
|
58
|
-
} as const;
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// ui-primitives.ts — glyph registry + tone-token table.
|
|
3
|
+
//
|
|
4
|
+
// W6-P1: these four tables (GLYPHS, UI_TONES, DIFF_TONES, SPINNER_FRAMES) are
|
|
5
|
+
// no longer minted locally. They are the SDK presentation contract
|
|
6
|
+
// (@pellux/goodvibes-sdk/platform/presentation, landed by W4-S1 and already
|
|
7
|
+
// adopted by the agent in W4-R4), consumed here so the TUI and the agent
|
|
8
|
+
// share ONE source (Mike's move-to-SDK ruling — machinery needed by 2+
|
|
9
|
+
// surfaces => SDK). The TUI was the reference these values were lifted from
|
|
10
|
+
// verbatim, so this swap is byte-identical. See
|
|
11
|
+
// docs/decisions/2026-07-05-presentation-contract-sdk-extraction.md in the SDK.
|
|
12
|
+
//
|
|
13
|
+
// Re-exported under the historical names (GLYPHS, UI_TONES) so every existing
|
|
14
|
+
// importer keeps working with no call-site churn. UI_TONES is the dark-mode
|
|
15
|
+
// tone table (== resolveTones('dark')); light is resolved via theme.ts's
|
|
16
|
+
// activeUiTones() / resolveUiTones(), which composes the mode dimension over
|
|
17
|
+
// this dark constant unchanged.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
GLYPHS,
|
|
22
|
+
TONE_TOKENS,
|
|
23
|
+
DIFF_TONES,
|
|
24
|
+
SPINNER_FRAMES,
|
|
25
|
+
} from '@pellux/goodvibes-sdk/platform/presentation';
|
|
26
|
+
|
|
27
|
+
export { GLYPHS, DIFF_TONES, SPINNER_FRAMES };
|
|
59
28
|
|
|
60
29
|
/**
|
|
61
30
|
* UI_TONES — the single chrome/color-token source for src/renderer and
|
|
62
31
|
* src/panels. Concrete (dark-mode) values; resolveUiTones(mode) in theme.ts
|
|
63
|
-
* is the single mode-resolved read path — this object is the 'dark' entry
|
|
64
|
-
|
|
65
|
-
export const UI_TONES = {
|
|
66
|
-
fg: {
|
|
67
|
-
primary: '#e2e8f0',
|
|
68
|
-
secondary: '#cbd5e1',
|
|
69
|
-
muted: '#94a3b8',
|
|
70
|
-
dim: '#475569',
|
|
71
|
-
inverse: '#0f172a',
|
|
72
|
-
/** Empty-state / placeholder foreground (formerly DEFAULT_PANEL_PALETTE.empty literal). */
|
|
73
|
-
empty: '#334155',
|
|
74
|
-
},
|
|
75
|
-
bg: {
|
|
76
|
-
base: '#11131a',
|
|
77
|
-
surface: '#161a22',
|
|
78
|
-
title: '#0f172a',
|
|
79
|
-
section: '#18202b',
|
|
80
|
-
summary: '#1b2430',
|
|
81
|
-
selected: '#223049',
|
|
82
|
-
input: '#1e293b',
|
|
83
|
-
warning: '#2b2116',
|
|
84
|
-
error: '#2a161b',
|
|
85
|
-
success: '#14241b',
|
|
86
|
-
/** Fullscreen/shell footer background. */
|
|
87
|
-
footer: '#111827',
|
|
88
|
-
},
|
|
89
|
-
state: {
|
|
90
|
-
info: '#38bdf8',
|
|
91
|
-
good: '#22c55e',
|
|
92
|
-
warn: '#f59e0b',
|
|
93
|
-
bad: '#ef4444',
|
|
94
|
-
blocked: '#f97316',
|
|
95
|
-
active: '#60a5fa',
|
|
96
|
-
/** Single canonical reasoning/thinking purple (replaces #9945FF and ad-hoc purples). */
|
|
97
|
-
reasoning: '#a855f7',
|
|
98
|
-
},
|
|
99
|
-
accent: {
|
|
100
|
-
browser: '#7dd3fc',
|
|
101
|
-
control: '#22d3ee',
|
|
102
|
-
inspector: '#c4b5fd',
|
|
103
|
-
workflow: '#fbbf24',
|
|
104
|
-
conversation: '#93c5fd',
|
|
105
|
-
/** Neon brand accent — header/splash/thinking gradient only. */
|
|
106
|
-
brand: '#00ffff',
|
|
107
|
-
gradientStart: '#00ffff',
|
|
108
|
-
gradientEnd: '#d000ff',
|
|
109
|
-
},
|
|
110
|
-
/** Canonical border stroke color for fullscreen/panel chrome. */
|
|
111
|
-
border: '#64748b',
|
|
112
|
-
} as const;
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Diff surface accent color shared across the three diff-rendering surfaces —
|
|
116
|
-
* the conversation diff view (diff-view.ts), the file diff panel
|
|
117
|
-
* (diff-panel.ts), and the git panel's inline diff (git-panel.ts). Hunk-header
|
|
118
|
-
* blue has no matching UI_TONES.state/accent role, so WO-204 gives it one
|
|
119
|
-
* named token here (value converged on diff-panel.ts's pre-existing hunk
|
|
120
|
-
* color, the only one of the three surfaces that predates this token; add/del
|
|
121
|
-
* on all three surfaces already converge on UI_TONES.state.good/bad).
|
|
32
|
+
* is the single mode-resolved read path — this object is the 'dark' entry
|
|
33
|
+
* (== resolveTones('dark') from the SDK presentation contract).
|
|
122
34
|
*/
|
|
123
|
-
export const
|
|
124
|
-
// add/del carry diff-panel.ts's shipped colors: the diff panel is the only
|
|
125
|
-
// diff surface users have ever seen (diff-view.ts was unwired from v0.9.6
|
|
126
|
-
// until WO-204), so its look is the reference — the conversation surface
|
|
127
|
-
// adopts it, never the other way around.
|
|
128
|
-
add: '#00ff88',
|
|
129
|
-
del: '#ff4444',
|
|
130
|
-
hunk: '#88aaff',
|
|
131
|
-
} as const;
|
|
132
|
-
|
|
133
|
-
/** Single spinner-frame source — layout.ts and progress.ts both re-export this. */
|
|
134
|
-
export const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] as const;
|
|
35
|
+
export const UI_TONES = TONE_TOKENS;
|
|
@@ -123,6 +123,8 @@ export type CreateBootstrapCommandContextOptions = {
|
|
|
123
123
|
hydrateSessionUsage?: () => void;
|
|
124
124
|
workstreamEngine?: import('./workstream-services.ts').WorkstreamCommandService;
|
|
125
125
|
codeIndexStore?: import('@pellux/goodvibes-sdk/platform/state').CodeIndexStore;
|
|
126
|
+
codeIndexReindexScheduler?: import('@pellux/goodvibes-sdk/platform/state').CodeIndexReindexScheduler;
|
|
127
|
+
isPassiveCodeInjectionFlagEnabled?: () => boolean;
|
|
126
128
|
getMainSessionTurnInjections?: () => readonly import('../renderer/turn-injection.ts').TurnInjectionEntry[];
|
|
127
129
|
};
|
|
128
130
|
|
|
@@ -184,6 +186,8 @@ export function createBootstrapCommandContext(
|
|
|
184
186
|
hydrateSessionUsage,
|
|
185
187
|
workstreamEngine,
|
|
186
188
|
codeIndexStore,
|
|
189
|
+
codeIndexReindexScheduler,
|
|
190
|
+
isPassiveCodeInjectionFlagEnabled,
|
|
187
191
|
getMainSessionTurnInjections,
|
|
188
192
|
planManager,
|
|
189
193
|
adaptivePlanner,
|
|
@@ -244,6 +248,8 @@ export function createBootstrapCommandContext(
|
|
|
244
248
|
hydrateSessionUsage,
|
|
245
249
|
workstreamEngine,
|
|
246
250
|
codeIndexStore,
|
|
251
|
+
codeIndexReindexScheduler,
|
|
252
|
+
isPassiveCodeInjectionFlagEnabled,
|
|
247
253
|
getMainSessionTurnInjections,
|
|
248
254
|
});
|
|
249
255
|
const provider = createBootstrapCommandProviderSection({
|
|
@@ -5,7 +5,7 @@ import type { ConversationManager } from '../core/conversation';
|
|
|
5
5
|
import type { KnowledgeApi } from '@pellux/goodvibes-sdk/platform/knowledge';
|
|
6
6
|
import type { HookApi } from '@pellux/goodvibes-sdk/platform/hooks';
|
|
7
7
|
import type { McpApi } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
8
|
-
import type { PanelManager } from '../panels/panel-manager.ts';
|
|
8
|
+
import type { PanelManager, PanelDeepLinkTarget } from '../panels/panel-manager.ts';
|
|
9
9
|
import type { ProviderApi } from '@pellux/goodvibes-sdk/platform/providers';
|
|
10
10
|
import type { OpsApi } from '@/runtime/index.ts';
|
|
11
11
|
import type { MutableRuntimeState } from '@/runtime/index.ts';
|
|
@@ -118,6 +118,8 @@ export interface BootstrapCommandSectionOptions {
|
|
|
118
118
|
readonly hydrateSessionUsage?: () => void;
|
|
119
119
|
readonly workstreamEngine?: import('./workstream-services.ts').WorkstreamCommandService;
|
|
120
120
|
readonly codeIndexStore?: CodeIndexStore;
|
|
121
|
+
readonly codeIndexReindexScheduler?: import('@pellux/goodvibes-sdk/platform/state').CodeIndexReindexScheduler;
|
|
122
|
+
readonly isPassiveCodeInjectionFlagEnabled?: () => boolean;
|
|
121
123
|
readonly getMainSessionTurnInjections?: () => readonly import('../renderer/turn-injection.ts').TurnInjectionEntry[];
|
|
122
124
|
readonly agentManager?: ShellAgentManagerService;
|
|
123
125
|
readonly modeManager?: ShellModeManagerService;
|
|
@@ -190,7 +192,7 @@ export function createBootstrapCommandActions(
|
|
|
190
192
|
localUserAuthManager,
|
|
191
193
|
} = options;
|
|
192
194
|
|
|
193
|
-
const showPanel = (panelId: string, pane?: 'top' | 'bottom') => {
|
|
195
|
+
const showPanel = (panelId: string, pane?: 'top' | 'bottom', target?: PanelDeepLinkTarget) => {
|
|
194
196
|
// W6.1 (the purge): a MIGRATE-TO-MODAL id resolves to a modal, not a panel.
|
|
195
197
|
// panelManager.open() fires the injected openModal callback and returns a
|
|
196
198
|
// no-op sentinel — so skip panelManager.show() (which would reveal an empty
|
|
@@ -198,7 +200,7 @@ export function createBootstrapCommandActions(
|
|
|
198
200
|
// showPanel-based front-door (openHooksPanel/openSecurityPanel/… and the
|
|
199
201
|
// migrated command runtimes) opening the modal cleanly.
|
|
200
202
|
const redirected = panelManager.getModalRedirect(panelId) !== undefined;
|
|
201
|
-
panelManager.open(panelId, pane);
|
|
203
|
+
panelManager.open(panelId, pane, target);
|
|
202
204
|
if (!redirected) panelManager.show();
|
|
203
205
|
requestRender();
|
|
204
206
|
};
|
|
@@ -321,7 +323,7 @@ export function createBootstrapCommandActions(
|
|
|
321
323
|
export function createBootstrapCommandSessionSection(
|
|
322
324
|
options: Pick<
|
|
323
325
|
BootstrapCommandSectionOptions,
|
|
324
|
-
'conversation' | 'runtime' | 'sessionManager' | 'sessionMemoryStore' | 'sessionLineageTracker' | 'wrfcController' | 'changeTracker' | 'hydrateSessionUsage' | 'workstreamEngine' | 'codeIndexStore' | 'getMainSessionTurnInjections'
|
|
326
|
+
'conversation' | 'runtime' | 'sessionManager' | 'sessionMemoryStore' | 'sessionLineageTracker' | 'wrfcController' | 'changeTracker' | 'hydrateSessionUsage' | 'workstreamEngine' | 'codeIndexStore' | 'codeIndexReindexScheduler' | 'isPassiveCodeInjectionFlagEnabled' | 'getMainSessionTurnInjections'
|
|
325
327
|
>,
|
|
326
328
|
): BootstrapCommandSessionSection {
|
|
327
329
|
return {
|
|
@@ -335,6 +337,8 @@ export function createBootstrapCommandSessionSection(
|
|
|
335
337
|
hydrateSessionUsage: options.hydrateSessionUsage,
|
|
336
338
|
workstreamEngine: options.workstreamEngine,
|
|
337
339
|
codeIndexStore: options.codeIndexStore,
|
|
340
|
+
codeIndexReindexScheduler: options.codeIndexReindexScheduler,
|
|
341
|
+
isPassiveCodeInjectionFlagEnabled: options.isPassiveCodeInjectionFlagEnabled,
|
|
338
342
|
getMainSessionTurnInjections: options.getMainSessionTurnInjections,
|
|
339
343
|
};
|
|
340
344
|
}
|
|
@@ -33,6 +33,8 @@ import { join } from 'node:path';
|
|
|
33
33
|
import { installWrfcAgentToolGuard } from '../tools/wrfc-agent-guard.ts';
|
|
34
34
|
import { createWrfcPersistence, type WrfcPersistence } from './wrfc-persistence.ts';
|
|
35
35
|
import type { SystemMessagePriority } from '../core/system-message-router.ts';
|
|
36
|
+
import { SessionSpineClient, SessionUnionCache, TUI_SPINE_PARTICIPANT } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
|
|
37
|
+
import { SessionInboundInputPoller, createBootstrapInboundInputPoller } from './session-inbound-inputs.ts';
|
|
36
38
|
|
|
37
39
|
// ---------------------------------------------------------------------------
|
|
38
40
|
// Pre-router buffer
|
|
@@ -114,6 +116,12 @@ export interface BootstrapCoreState {
|
|
|
114
116
|
readonly requestRender: () => void;
|
|
115
117
|
readonly setRenderRequest: (fn: () => void) => void;
|
|
116
118
|
readonly runtimeSessionIdRef: { value: string };
|
|
119
|
+
/** Cross-surface identity mirror; permanently dormant for embedded/local-only (docs/decisions/2026-07-06-session-spine-mode-branch-is-permanent.md), activated by bootstrap.ts only for an adopted 'external' daemon. */
|
|
120
|
+
readonly sessionSpine: SessionSpineClient;
|
|
121
|
+
/** D3: inbound steer/follow-up delivery; dormant until bootstrap.ts activates it. */
|
|
122
|
+
readonly sessionInboundInputs: SessionInboundInputPoller;
|
|
123
|
+
/** Cache-backed read facade; bootstrap.ts drives its mode (embedded/external/local-only) from the same HostServiceMode as the spine above. */
|
|
124
|
+
readonly sessionUnionCache: SessionUnionCache;
|
|
117
125
|
/**
|
|
118
126
|
* WRFC chain persistence — call `rehydrate()` once after the SystemMessageRouter
|
|
119
127
|
* is wired so interrupted chains from a previous process are surfaced to the operator.
|
|
@@ -127,17 +135,8 @@ export type CompanionMessagePayload = Extract<SessionEvent, { type: 'COMPANION_M
|
|
|
127
135
|
// Operator narration of inbound channel events
|
|
128
136
|
// ---------------------------------------------------------------------------
|
|
129
137
|
|
|
130
|
-
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* When an external surface (GitHub, Slack, ntfy, etc.) triggers an agent turn,
|
|
134
|
-
* this function produces a human-readable system message so the operator can
|
|
135
|
-
* observe which event caused the turn. Returns null for internal/companion
|
|
136
|
-
* sources that do not need operator narration.
|
|
137
|
-
*
|
|
138
|
-
* @param event - The normalized inbound event descriptor.
|
|
139
|
-
* @returns A narration string, or null if no narration is appropriate.
|
|
140
|
-
*/
|
|
138
|
+
/** Narrate an inbound channel event (GitHub, Slack, ntfy, etc.) that triggered an
|
|
139
|
+
* agent turn, via the SystemMessageRouter — null for internal/companion sources. */
|
|
141
140
|
export function narrateInboundEvent(event: {
|
|
142
141
|
source: string;
|
|
143
142
|
metadata: Readonly<Record<string, unknown>> | undefined;
|
|
@@ -293,6 +292,12 @@ export async function initializeBootstrapCore(
|
|
|
293
292
|
surfaceRegistry,
|
|
294
293
|
watcherRegistry,
|
|
295
294
|
} = services;
|
|
295
|
+
// Permanently dormant for embedded/local-only (nothing to mirror to); bootstrap.ts activates it only for an adopted 'external' daemon.
|
|
296
|
+
const sessionSpine = new SessionSpineClient({ participant: TUI_SPINE_PARTICIPANT, recordKind: 'tui' });
|
|
297
|
+
// Cache-backed read facade over the local broker (passthrough until bootstrap.ts marks it
|
|
298
|
+
// embedded, or activates the adopted-daemon wire union for 'external').
|
|
299
|
+
// selfSessionIds keeps local authoritative for our own wire-mirrored session even when local/wire ids diverge (D-TUI-1).
|
|
300
|
+
const sessionUnionCache = new SessionUnionCache({ local: sharedSessionBroker, selfSessionIds: () => sessionSpine.mirroredSessionIds });
|
|
296
301
|
|
|
297
302
|
routeBindings.attachRuntime({ runtimeBus, runtimeStore: store });
|
|
298
303
|
surfaceRegistry.attachRuntime(store);
|
|
@@ -324,6 +329,7 @@ export async function initializeBootstrapCore(
|
|
|
324
329
|
const uiServices = createUiRuntimeServices(services, {
|
|
325
330
|
forensicsRegistry,
|
|
326
331
|
getControlPlaneRecentEvents,
|
|
332
|
+
sessionUnionCache,
|
|
327
333
|
});
|
|
328
334
|
|
|
329
335
|
const conversation = new ConversationManager(() => {
|
|
@@ -427,6 +433,10 @@ export async function initializeBootstrapCore(
|
|
|
427
433
|
} catch (err) {
|
|
428
434
|
logger.error('Render threw; next requestRender will reschedule', { error: String(err) });
|
|
429
435
|
}
|
|
436
|
+
// Debounced spine heartbeat on turn/render activity (no-op while dormant, i.e.
|
|
437
|
+
// embedded/local-only): a cheap synchronous no-op unless its own internal window has
|
|
438
|
+
// elapsed (at most one wire call per heartbeatMinIntervalMs) — safe on the hot path.
|
|
439
|
+
sessionSpine.heartbeat(runtimeSessionIdRef.value);
|
|
430
440
|
};
|
|
431
441
|
const requestRender = (): void => {
|
|
432
442
|
if (renderScheduled) return;
|
|
@@ -634,6 +644,11 @@ export async function initializeBootstrapCore(
|
|
|
634
644
|
},
|
|
635
645
|
));
|
|
636
646
|
|
|
647
|
+
// D3: inbound steer delivery — see createBootstrapInboundInputPoller's doc comment.
|
|
648
|
+
const sessionInboundInputs = createBootstrapInboundInputPoller({
|
|
649
|
+
runtimeSessionIdRef, routeOrBuffer, orchestratorHandleUserInputRef, conversation, requestRender,
|
|
650
|
+
});
|
|
651
|
+
|
|
637
652
|
providerRegistry.startWatching(runtimeBus);
|
|
638
653
|
|
|
639
654
|
// W2.3: attach the SAME WebhookNotifier instance that `/notify add|remove|clear`
|
|
@@ -724,6 +739,10 @@ export async function initializeBootstrapCore(
|
|
|
724
739
|
lastSeenAt: Date.now(),
|
|
725
740
|
},
|
|
726
741
|
}).catch((err) => { logger.debug('session broker create session failed at bootstrap', { err }); });
|
|
742
|
+
// Fire-and-forget spine mirror — a permanent parallel-write posture alongside (never
|
|
743
|
+
// instead of) the still-authoritative local SharedSessionBroker above. Dormant/queued
|
|
744
|
+
// until bootstrap.ts's deferred task activates it for an adopted 'external' daemon.
|
|
745
|
+
sessionSpine.register({ sessionId: runtime.sessionId, project: services.workingDirectory, title: 'Terminal UI session' });
|
|
727
746
|
|
|
728
747
|
domainDispatch.syncSessionState({
|
|
729
748
|
id: userSessionId,
|
|
@@ -774,5 +793,8 @@ export async function initializeBootstrapCore(
|
|
|
774
793
|
},
|
|
775
794
|
runtimeSessionIdRef,
|
|
776
795
|
wrfcPersistence,
|
|
796
|
+
sessionSpine,
|
|
797
|
+
sessionInboundInputs,
|
|
798
|
+
sessionUnionCache,
|
|
777
799
|
};
|
|
778
800
|
}
|
|
@@ -9,6 +9,7 @@ import { HelperModel } from '@pellux/goodvibes-sdk/platform/config';
|
|
|
9
9
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
10
10
|
import { formatReturnContextForDisplay, getReturnContextMode, maybeAssistReturnContextSummary } from '@/runtime/index.ts';
|
|
11
11
|
import type { SharedSessionBroker } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
12
|
+
import type { SessionSpineClient } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
|
|
12
13
|
import type { SessionManager } from '@pellux/goodvibes-sdk/platform/sessions';
|
|
13
14
|
import type { PanelManager } from '../panels/panel-manager.ts';
|
|
14
15
|
import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
|
|
@@ -22,6 +23,10 @@ export interface ResumeSessionOptions {
|
|
|
22
23
|
readonly requestRender: () => void;
|
|
23
24
|
readonly onSessionIdChanged?: (sessionId: string) => void;
|
|
24
25
|
readonly sharedSessionBroker: Pick<SharedSessionBroker, 'reopenSession'>;
|
|
26
|
+
/** S3c: fire-and-forget daemon-spine mirror. Reopen (not register) is the
|
|
27
|
+
* ONLY resume-time verb — see the SDK session-spine client.ts header doc. */
|
|
28
|
+
readonly sessionSpine: Pick<SessionSpineClient, 'reopen'>;
|
|
29
|
+
readonly project: string;
|
|
25
30
|
readonly writeLastSessionPointer: (sessionId: string) => void;
|
|
26
31
|
readonly hookDispatcher: HookDispatcher;
|
|
27
32
|
readonly sessionManager: SessionManager;
|
|
@@ -75,6 +80,8 @@ export function createResumeSessionHandler(options: ResumeSessionOptions): (sess
|
|
|
75
80
|
if (meta?.provider) options.runtime.provider = meta.provider;
|
|
76
81
|
options.writeLastSessionPointer(sessionId);
|
|
77
82
|
void options.sharedSessionBroker.reopenSession(sessionId).catch((err) => { logger.debug('session broker reopen session failed', { err }); });
|
|
83
|
+
// S3c: fire-and-forget spine mirror (reopen:true — the user resume verb).
|
|
84
|
+
options.sessionSpine.reopen({ sessionId, project: options.project, title: options.conversation.title || meta.title });
|
|
78
85
|
options.conversation.log(`Resumed session: ${sessionId}`, { fg: '135' });
|
|
79
86
|
const reopenedPanels: string[] = [];
|
|
80
87
|
if (meta.returnContext?.openPanels?.length) {
|
|
@@ -32,6 +32,7 @@ import type { PolicyRuntimeState } from '@/runtime/index.ts';
|
|
|
32
32
|
import type { TaskManager } from '@/runtime/index.ts';
|
|
33
33
|
import type { UiRuntimeServices } from './ui-services.ts';
|
|
34
34
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
35
|
+
import type { SessionSpineClient } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
|
|
35
36
|
|
|
36
37
|
export interface BootstrapShellState {
|
|
37
38
|
readonly commandRegistry: CommandRegistry;
|
|
@@ -47,6 +48,8 @@ export interface BootstrapShellOptions {
|
|
|
47
48
|
readonly runtimeBus: RuntimeEventBus;
|
|
48
49
|
readonly runtimeStore: RuntimeStore;
|
|
49
50
|
readonly services: RuntimeServices;
|
|
51
|
+
/** S3c: dormant until bootstrap.ts activates it for an adopted 'external' daemon. */
|
|
52
|
+
readonly sessionSpine: SessionSpineClient;
|
|
50
53
|
readonly conversation: ConversationManager;
|
|
51
54
|
readonly runtime: MutableRuntimeState;
|
|
52
55
|
readonly orchestrator: Orchestrator;
|
|
@@ -70,6 +73,7 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
70
73
|
runtimeBus,
|
|
71
74
|
runtimeStore,
|
|
72
75
|
services,
|
|
76
|
+
sessionSpine,
|
|
73
77
|
conversation,
|
|
74
78
|
runtime,
|
|
75
79
|
orchestrator,
|
|
@@ -103,6 +107,8 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
103
107
|
requestRender,
|
|
104
108
|
onSessionIdChanged,
|
|
105
109
|
sharedSessionBroker: services.sessionBroker,
|
|
110
|
+
sessionSpine,
|
|
111
|
+
project: services.workingDirectory,
|
|
106
112
|
writeLastSessionPointer,
|
|
107
113
|
hookDispatcher: services.hookDispatcher,
|
|
108
114
|
sessionManager: services.sessionManager,
|
|
@@ -175,7 +181,8 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
175
181
|
forensicsRegistry,
|
|
176
182
|
policyRuntimeState,
|
|
177
183
|
approvalBroker: services.approvalBroker,
|
|
178
|
-
|
|
184
|
+
// S3d: panels read the cross-surface union facade, not the raw local broker.
|
|
185
|
+
sessionBroker: uiServices.sessions.sessionBroker,
|
|
179
186
|
automationManager: services.automationManager,
|
|
180
187
|
getControlPlaneRecentEvents,
|
|
181
188
|
tokenAuditor: services.tokenAuditor,
|
|
@@ -207,6 +214,15 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
207
214
|
if (kind === 'operational') return ui.operationalMessages;
|
|
208
215
|
return ui.systemMessages;
|
|
209
216
|
},
|
|
217
|
+
{
|
|
218
|
+
// Suppress stale WRFC replay re-notifications for chains that can no
|
|
219
|
+
// longer act — gone (killed/removed → getChain null) or terminal
|
|
220
|
+
// (passed/failed). (UX-B item 1c.)
|
|
221
|
+
isChainTerminal: (chainId) => {
|
|
222
|
+
const chain = services.wrfcController.getChain(chainId);
|
|
223
|
+
return chain === null || chain.state === 'passed' || chain.state === 'failed';
|
|
224
|
+
},
|
|
225
|
+
},
|
|
210
226
|
);
|
|
211
227
|
orchestrator.setSystemMessageRouter(systemMessageRouter);
|
|
212
228
|
|
|
@@ -271,6 +287,8 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
271
287
|
wrfcController: services.wrfcController,
|
|
272
288
|
workstreamEngine: services.workstreamCommands,
|
|
273
289
|
codeIndexStore: services.codeIndexStore,
|
|
290
|
+
codeIndexReindexScheduler: services.codeIndexReindexScheduler,
|
|
291
|
+
isPassiveCodeInjectionFlagEnabled: () => services.featureFlags.isEnabled('agent-passive-code-injection'),
|
|
274
292
|
// Wave 5 (wo805): expose the MAIN session's per-turn passive-injection ring
|
|
275
293
|
// so `/recall injections` (no agent id) renders it — see recall-review.ts.
|
|
276
294
|
getMainSessionTurnInjections: () => orchestrator.getTurnInjections(),
|
package/src/runtime/bootstrap.ts
CHANGED
|
@@ -36,12 +36,16 @@ import {
|
|
|
36
36
|
import { scheduleBackgroundMcpDiscovery, startBackgroundProviderRegistration } from '@/runtime/index.ts';
|
|
37
37
|
import { restoreSavedModel } from '@/runtime/index.ts';
|
|
38
38
|
import { startExternalServices, type ExternalServicesHandle, type HostServiceStatus } from '@/runtime/index.ts';
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
39
|
+
import { createHttpTransport } from '@/runtime/index.ts';
|
|
40
|
+
import { foldLegacySpineStore, deriveSpineFooterStatus } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
|
|
41
|
+
import { createTuiSpineTransport, type SpineSessionsClient } from './session-spine-transport.ts';
|
|
42
|
+
import { pruneStaleOperatorTokens } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
43
|
+
import { resolveDaemonCompanionToken, workspaceOperatorTokenCandidates } from './operator-token-cleanup.ts';
|
|
41
44
|
import type { UiRuntimeServices } from './ui-services.ts';
|
|
42
45
|
import { createDeferredStartupCoordinator } from '@/runtime/index.ts';
|
|
43
46
|
import { initializeBootstrapCore } from './bootstrap-core.ts';
|
|
44
47
|
import { createBootstrapShell } from './bootstrap-shell.ts';
|
|
48
|
+
import { announceResumeState } from './resume-notice.ts';
|
|
45
49
|
import { buildSharedOrchestratorCoreServices } from './orchestrator-core-services.ts';
|
|
46
50
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
47
51
|
import { DaemonServer } from '@pellux/goodvibes-sdk/platform/daemon';
|
|
@@ -183,6 +187,9 @@ export async function bootstrapRuntime(
|
|
|
183
187
|
setRenderRequest,
|
|
184
188
|
runtimeSessionIdRef,
|
|
185
189
|
wrfcPersistence,
|
|
190
|
+
sessionSpine,
|
|
191
|
+
sessionInboundInputs,
|
|
192
|
+
sessionUnionCache,
|
|
186
193
|
} = await initializeBootstrapCore(stdout, options, (limit) => controlPlaneRecentEventsRef.value(limit));
|
|
187
194
|
const providerRegistry = services.providerRegistry;
|
|
188
195
|
const {
|
|
@@ -192,6 +199,11 @@ export async function bootstrapRuntime(
|
|
|
192
199
|
pluginManager,
|
|
193
200
|
} = services;
|
|
194
201
|
|
|
202
|
+
// D4b: a liveness flip is only PAINTED once something calls requestRender();
|
|
203
|
+
// without this, the footer's spine segment sat correct-but-undrawn until
|
|
204
|
+
// incidental activity redrew it (minutes, during an idle stretch).
|
|
205
|
+
sessionUnionCache.setOnTransition(() => requestRender());
|
|
206
|
+
|
|
195
207
|
// ── Phase 6: Orchestrator + AcpManager ───────────────────────────────────
|
|
196
208
|
|
|
197
209
|
// Mutable function refs so main.ts can patch these after constructing the scroll/viewport state.
|
|
@@ -262,6 +274,7 @@ export async function bootstrapRuntime(
|
|
|
262
274
|
runtimeBus,
|
|
263
275
|
runtimeStore: store,
|
|
264
276
|
services,
|
|
277
|
+
sessionSpine,
|
|
265
278
|
conversation,
|
|
266
279
|
runtime,
|
|
267
280
|
orchestrator,
|
|
@@ -287,6 +300,25 @@ export async function bootstrapRuntime(
|
|
|
287
300
|
wrfcPersistence.rehydrate();
|
|
288
301
|
const commandRegistry = shell.commandRegistry;
|
|
289
302
|
const commandContext = shell.commandContext;
|
|
303
|
+
// Boot resume notice (UX-D item 1): after rehydrate() so chain history is
|
|
304
|
+
// ready, before the operator can type anything. Fire-and-forget — the
|
|
305
|
+
// checkpoint-count lookup is async (WorkspaceCheckpointManager.list() awaits
|
|
306
|
+
// its own init()), and this must not block the rest of bootstrap the way
|
|
307
|
+
// main.ts's `void workspaceCheckpointManager.init().catch(() => {})`
|
|
308
|
+
// deliberately doesn't either. Local file I/O only; resolves well before a
|
|
309
|
+
// human can react to the first rendered frame.
|
|
310
|
+
void announceResumeState({
|
|
311
|
+
workingDirectory: services.workingDirectory,
|
|
312
|
+
homeDirectory: services.homeDirectory,
|
|
313
|
+
surfaceRoot: 'tui',
|
|
314
|
+
sessionManager: services.sessionManager,
|
|
315
|
+
checkpointManager: services.workspaceCheckpointManager,
|
|
316
|
+
chainHistory: wrfcPersistence.knownChains,
|
|
317
|
+
memoryAvailable: Boolean(commandContext.clients?.knowledgeApi?.memory),
|
|
318
|
+
router: systemMessageRouter,
|
|
319
|
+
}).catch(() => {
|
|
320
|
+
// Best-effort — never let the resume notice block or crash boot.
|
|
321
|
+
});
|
|
290
322
|
const gitStatusProvider = shell.gitStatusProvider;
|
|
291
323
|
const inputHistory = shell.inputHistory;
|
|
292
324
|
const lastGitInfoRef = shell.lastGitInfoRef;
|
|
@@ -343,7 +375,70 @@ export async function bootstrapRuntime(
|
|
|
343
375
|
|
|
344
376
|
const hostServiceIsActive = (status: HostServiceStatus): boolean => status.mode === 'embedded' || status.mode === 'external';
|
|
345
377
|
|
|
346
|
-
|
|
378
|
+
// 'blocked' (occupied by an unverified process) and 'incompatible' (occupied
|
|
379
|
+
// by a GoodVibes daemon we refused to adopt on a wire-version mismatch) both
|
|
380
|
+
// mean the configured port is held and unusable by this TUI instance.
|
|
381
|
+
const hostServiceIsBlocked = (status: HostServiceStatus): boolean => status.mode === 'blocked' || status.mode === 'incompatible';
|
|
382
|
+
|
|
383
|
+
// PERMANENT DESIGN (docs/decisions/2026-07-06-session-spine-mode-branch-is-permanent.md):
|
|
384
|
+
// 'embedded'/'external' are two distinct, both-supported daemon topologies, not
|
|
385
|
+
// migration stages — the ONE client-selection point for the session spine, driven by
|
|
386
|
+
// the SAME authoritative HostServiceMode adopt-or-start already computed above.
|
|
387
|
+
// 'embedded' (daemon.embedInProcess=true) means THIS process's own SharedSessionBroker
|
|
388
|
+
// already IS the daemon's broker — nothing to mirror TO, so the spine stays
|
|
389
|
+
// permanently dormant. Every other mode ('disabled'/'blocked'/'incompatible'/
|
|
390
|
+
// 'unavailable') also stays local-only and honest. Only 'external' (a separately-
|
|
391
|
+
// running daemon this TUI adopted) activates the wire mirror.
|
|
392
|
+
let spineActiveForBaseUrl: string | null = null;
|
|
393
|
+
const syncSessionSpineToHostStatus = (daemonStatus: HostServiceStatus, sharedDaemonToken: string): void => {
|
|
394
|
+
if (daemonStatus.mode !== 'external') {
|
|
395
|
+
if (spineActiveForBaseUrl !== null) {
|
|
396
|
+
sessionSpine.deactivate(`daemon mode changed to '${daemonStatus.mode}'`);
|
|
397
|
+
sessionInboundInputs.deactivate(`daemon mode changed to '${daemonStatus.mode}'`);
|
|
398
|
+
spineActiveForBaseUrl = null;
|
|
399
|
+
}
|
|
400
|
+
// Keep the read facade honest per topology (permanent, not staged): 'embedded'
|
|
401
|
+
// means this process's broker IS the daemon's broker (local reads are the whole
|
|
402
|
+
// truth); every other non-external mode is local-only/dormant.
|
|
403
|
+
if (daemonStatus.mode === 'embedded') sessionUnionCache.markEmbedded();
|
|
404
|
+
else sessionUnionCache.deactivate(`daemon mode '${daemonStatus.mode}'`);
|
|
405
|
+
logger.info(`[bootstrap] session spine: daemon mode '${daemonStatus.mode}' — local-only (no spine mirror)`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const baseUrl = daemonStatus.baseUrl;
|
|
409
|
+
if (spineActiveForBaseUrl === baseUrl) return; // already wired to this exact adopted daemon
|
|
410
|
+
const httpTransport = createHttpTransport({ baseUrl, authToken: sharedDaemonToken });
|
|
411
|
+
const sessionsClient: SpineSessionsClient = {
|
|
412
|
+
register: (input) => httpTransport.operator.sessions.register(input),
|
|
413
|
+
close: (sessionId) => httpTransport.operator.sessions.close(sessionId),
|
|
414
|
+
};
|
|
415
|
+
sessionSpine.activate(createTuiSpineTransport(sessionsClient));
|
|
416
|
+
// D3: adopt the same daemon's wire for the INBOUND steer path — collect
|
|
417
|
+
// steer/follow-up inputs another live surface queued for this session and
|
|
418
|
+
// inject them into the turn machinery (acking delivery on the wire).
|
|
419
|
+
sessionInboundInputs.activate({
|
|
420
|
+
listInputs: async (sessionId, opts) => ({
|
|
421
|
+
inputs: await httpTransport.operator.sessions.inputs(sessionId, opts.limit, { state: opts.state, since: opts.since }),
|
|
422
|
+
}),
|
|
423
|
+
deliverInput: (sessionId, inputId, opts) => httpTransport.operator.sessions.deliverInput(sessionId, inputId, opts),
|
|
424
|
+
});
|
|
425
|
+
// 'external' only: adopt the same daemon's wire as the read facade's cross-surface
|
|
426
|
+
// union source (interval-refreshed; served synchronously to panels).
|
|
427
|
+
sessionUnionCache.activate({ list: (limit) => httpTransport.operator.sessions.list(limit) });
|
|
428
|
+
spineActiveForBaseUrl = baseUrl;
|
|
429
|
+
logger.info(`[bootstrap] session spine: adopted external daemon at ${baseUrl} — mirroring session identity`);
|
|
430
|
+
// Legacy fold: one-time (marker-guarded) import of this project's own
|
|
431
|
+
// pre-spine control-plane sessions.json into the now-adopted daemon.
|
|
432
|
+
const legacyStorePath = services.shellPaths.resolveProjectPath('tui', 'control-plane', 'sessions.json');
|
|
433
|
+
const fold = foldLegacySpineStore(sessionSpine, {
|
|
434
|
+
storePath: legacyStorePath,
|
|
435
|
+
markerPath: `${legacyStorePath}.spine-migrated`,
|
|
436
|
+
project: services.workingDirectory,
|
|
437
|
+
});
|
|
438
|
+
if (fold.folded > 0) {
|
|
439
|
+
logger.info(`[bootstrap] session spine: folded ${fold.folded} legacy local session(s) into the adopted daemon`);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
347
442
|
|
|
348
443
|
const inspectExternalServices = () => {
|
|
349
444
|
const daemonStatus = externalServices.daemonStatus;
|
|
@@ -355,6 +450,16 @@ export async function bootstrapRuntime(
|
|
|
355
450
|
httpListenerPortInUse: hostServiceIsBlocked(httpListenerStatus),
|
|
356
451
|
daemonStatus,
|
|
357
452
|
httpListenerStatus,
|
|
453
|
+
// Honest session-spine posture, independent of daemonRunning —
|
|
454
|
+
// 'external'-adopted-but-currently-unreachable degrades to 'offline' here
|
|
455
|
+
// even though daemonRunning might still read true from a stale handle.
|
|
456
|
+
// D4: sessionSpine.status() alone is ACTIVITY-gated (only updates on a
|
|
457
|
+
// register/heartbeat/close), so after the daemon dies mid-idle it would
|
|
458
|
+
// keep reading 'online'. Derive the footer status from the union cache's
|
|
459
|
+
// 5s liveness probe too — one signal, no new timer — so offline surfaces
|
|
460
|
+
// within one refresh interval of the daemon dying.
|
|
461
|
+
sessionSpineActive: sessionSpine.active,
|
|
462
|
+
sessionSpineStatus: deriveSpineFooterStatus(sessionSpine.status(), sessionUnionCache.crossSurfaceView),
|
|
358
463
|
};
|
|
359
464
|
};
|
|
360
465
|
|
|
@@ -413,7 +518,7 @@ export async function bootstrapRuntime(
|
|
|
413
518
|
await waitForConfigDrivenRestarts(externalServices);
|
|
414
519
|
await externalServices.stop();
|
|
415
520
|
const daemonHomeDir = join(services.homeDirectory, '.goodvibes', 'daemon');
|
|
416
|
-
const companionTokenRecord =
|
|
521
|
+
const companionTokenRecord = resolveDaemonCompanionToken(daemonHomeDir);
|
|
417
522
|
externalServicesPromise = startExternalServices(
|
|
418
523
|
configManager,
|
|
419
524
|
runtimeBus,
|
|
@@ -423,6 +528,7 @@ export async function bootstrapRuntime(
|
|
|
423
528
|
);
|
|
424
529
|
externalServices = await externalServicesPromise;
|
|
425
530
|
controlPlaneRecentEventsRef.value = (limit) => externalServices.listRecentControlPlaneEvents(limit);
|
|
531
|
+
syncSessionSpineToHostStatus(externalServices.daemonStatus, companionTokenRecord.token);
|
|
426
532
|
requestRender();
|
|
427
533
|
return inspectExternalServices();
|
|
428
534
|
},
|
|
@@ -461,7 +567,7 @@ export async function bootstrapRuntime(
|
|
|
461
567
|
// bearer, so tokens scanned from the /qrcode panel's QR actually
|
|
462
568
|
// authenticate against the embedded daemon this surface starts.
|
|
463
569
|
const daemonHomeDir = join(services.homeDirectory, '.goodvibes', 'daemon');
|
|
464
|
-
const companionTokenRecord =
|
|
570
|
+
const companionTokenRecord = resolveDaemonCompanionToken(daemonHomeDir);
|
|
465
571
|
// F3 resolution (TUI 0.19.20): remove stale pre-0.21.28 workspace-scoped operator
|
|
466
572
|
// token files so only the canonical <daemonHomeDir>/operator-tokens.json survives.
|
|
467
573
|
// The prune is best-effort — it silently skips missing files, no-ops when tokens
|
|
@@ -486,6 +592,7 @@ export async function bootstrapRuntime(
|
|
|
486
592
|
);
|
|
487
593
|
externalServices = await externalServicesPromise;
|
|
488
594
|
controlPlaneRecentEventsRef.value = (limit) => externalServices.listRecentControlPlaneEvents(limit);
|
|
595
|
+
syncSessionSpineToHostStatus(externalServices.daemonStatus, companionTokenRecord.token);
|
|
489
596
|
requestRender();
|
|
490
597
|
},
|
|
491
598
|
onError: (error) => {
|
|
@@ -630,6 +737,12 @@ export async function bootstrapRuntime(
|
|
|
630
737
|
runtimeUnsubs.forEach((fn) => fn());
|
|
631
738
|
runtimeUnsubs.length = 0;
|
|
632
739
|
forensicsCollector.dispose();
|
|
740
|
+
// Honest close on exit — fire-and-forget (never blocks shutdown);
|
|
741
|
+
// a no-op when the spine was never activated (embedded/local-only topology).
|
|
742
|
+
sessionSpine.close(runtime.sessionId);
|
|
743
|
+
sessionSpine.dispose();
|
|
744
|
+
sessionInboundInputs.dispose(); // D3: stop the inbound steer poll interval on exit
|
|
745
|
+
sessionUnionCache.dispose(); // stop the wire-refresh interval on exit
|
|
633
746
|
await deferredStartup.drain(100);
|
|
634
747
|
if (externalServicesPromise) {
|
|
635
748
|
try {
|