@pellux/goodvibes-tui 0.21.0 → 0.23.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 +45 -0
- package/README.md +1 -1
- package/package.json +2 -1
- package/src/cli/completions/generate.ts +4 -8
- package/src/cli/entrypoint.ts +6 -0
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management-utils.ts +352 -0
- package/src/cli/management.ts +36 -334
- package/src/cli/parser.ts +17 -0
- package/src/cli/surface-command.ts +1 -1
- package/src/cli/types.ts +2 -0
- package/src/config/goodvibes-home-audit.ts +2 -0
- package/src/core/context-auto-compact.ts +110 -0
- package/src/core/conversation-rendering.ts +5 -2
- package/src/core/conversation-types.ts +24 -0
- package/src/core/conversation.ts +7 -12
- package/src/core/stream-event-wiring.ts +125 -7
- package/src/core/turn-event-wiring.ts +124 -0
- package/src/daemon/cli.ts +5 -0
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/channel-runtime.ts +139 -0
- package/src/input/commands/control-room-runtime.ts +5 -5
- package/src/input/commands/provider.ts +57 -3
- package/src/input/commands/runtime-services.ts +30 -1
- package/src/input/commands/session-workflow.ts +8 -16
- package/src/input/commands/session.ts +70 -20
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +54 -4
- package/src/input/commands.ts +2 -2
- package/src/input/handler-modal-routes.ts +37 -0
- package/src/input/handler-modal-token-routes.ts +19 -5
- package/src/input/handler-onboarding.ts +18 -0
- package/src/input/handler.ts +1 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +10 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +14 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +6 -0
- package/src/input/onboarding/onboarding-wizard-validation.ts +77 -0
- package/src/input/settings-modal-behavior.ts +5 -0
- package/src/input/settings-modal-data.ts +77 -3
- package/src/input/settings-modal-mutations.ts +3 -0
- package/src/input/settings-modal-reset.ts +154 -0
- package/src/input/settings-modal.ts +55 -13
- package/src/main.ts +58 -50
- package/src/panels/agent-inspector-panel.ts +120 -18
- package/src/panels/agent-inspector-shared.ts +29 -0
- package/src/panels/builtin/development.ts +1 -0
- package/src/panels/builtin/knowledge.ts +14 -13
- package/src/panels/builtin/operations.ts +22 -1
- package/src/panels/builtin/shared.ts +7 -0
- package/src/panels/cockpit-panel.ts +123 -3
- package/src/panels/cockpit-read-model.ts +232 -0
- package/src/panels/index.ts +1 -1
- package/src/panels/knowledge-graph-panel.ts +84 -0
- package/src/panels/memory-panel.ts +370 -40
- package/src/panels/session-maintenance.ts +66 -15
- package/src/renderer/agent-detail-modal.ts +107 -3
- package/src/renderer/compaction-history-modal.ts +55 -0
- package/src/renderer/compaction-preview.ts +146 -0
- package/src/renderer/context-status-hint.ts +54 -0
- package/src/renderer/settings-modal-helpers.ts +2 -2
- package/src/renderer/settings-modal.ts +14 -3
- package/src/renderer/shell-surface.ts +10 -0
- package/src/runtime/bootstrap-command-parts.ts +4 -0
- package/src/runtime/bootstrap-core.ts +116 -0
- package/src/runtime/bootstrap-shell.ts +11 -0
- package/src/runtime/bootstrap.ts +7 -0
- package/src/runtime/services.ts +6 -1
- package/src/utils/browser.ts +29 -0
- package/src/version.ts +1 -1
- package/src/panels/knowledge-panel.ts +0 -343
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { UiRuntimeEvents } from '@/runtime/index.ts';
|
|
2
2
|
import { createStreamStallWatchdog } from './stream-stall-watchdog.ts';
|
|
3
3
|
import { formatUserFacingErrorLine } from './format-user-error.ts';
|
|
4
|
+
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Live stream and tool-execution metrics maintained by wireStreamEventMetrics.
|
|
@@ -29,9 +30,28 @@ interface StreamOrchestrator {
|
|
|
29
30
|
readonly streamingOutputTokens: number;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
/** Minimal provider surface required for the stream stall watchdog. */
|
|
33
|
+
/** Minimal provider surface required for the stream stall watchdog and failover switching. */
|
|
33
34
|
interface StreamProviderRegistry {
|
|
34
|
-
getCurrentModel(): { readonly provider: string };
|
|
35
|
+
getCurrentModel(): { readonly provider: string; readonly registryKey?: string };
|
|
36
|
+
setCurrentModel(registryKey: string): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Minimal fallback-chain node shape returned by ProviderOptimizer.testFallback().
|
|
41
|
+
* Only the fields consumed by the failover path are declared here.
|
|
42
|
+
*/
|
|
43
|
+
interface FailoverChainNode {
|
|
44
|
+
readonly position: number;
|
|
45
|
+
readonly providerId: string;
|
|
46
|
+
readonly modelId: string;
|
|
47
|
+
readonly capable: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Minimal ProviderOptimizer surface required by the failover path. */
|
|
51
|
+
interface FailoverOptimizer {
|
|
52
|
+
readonly enabled: boolean;
|
|
53
|
+
testFallback(profile?: Record<string, unknown>): { readonly chain: readonly FailoverChainNode[] };
|
|
54
|
+
recordFallbackTransition(from: string, to: string, reason: string): void;
|
|
35
55
|
}
|
|
36
56
|
|
|
37
57
|
/** Minimal system-message surface required for user-visible notifications. */
|
|
@@ -56,6 +76,32 @@ export interface WireStreamEventMetricsOptions {
|
|
|
56
76
|
* so the render closure can read it without a forward-reference issue.
|
|
57
77
|
*/
|
|
58
78
|
readonly metrics: StreamMetrics;
|
|
79
|
+
/**
|
|
80
|
+
* When provided and enabled, the optimizer is consulted on TURN_ERROR to
|
|
81
|
+
* attempt the next viable provider before surfacing the error to the user.
|
|
82
|
+
* When absent or optimizer.enabled is false, behaviour is identical to the
|
|
83
|
+
* pre-failover baseline: error surfaces immediately via systemMessageRouter.
|
|
84
|
+
*/
|
|
85
|
+
readonly providerOptimizer?: FailoverOptimizer;
|
|
86
|
+
/**
|
|
87
|
+
* Callback the caller provides to re-submit the last user turn on a
|
|
88
|
+
* different provider after a successful failover switch. Called only when
|
|
89
|
+
* the optimizer is enabled and a viable next provider exists in the chain.
|
|
90
|
+
*/
|
|
91
|
+
readonly retryTurn?: () => void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Result of wireStreamEventMetrics. */
|
|
95
|
+
export interface WireStreamEventMetricsResult {
|
|
96
|
+
/** Unsubscribe functions; push into the parent unsubs array for cleanup on exit. */
|
|
97
|
+
readonly unsubs: ReadonlyArray<() => void>;
|
|
98
|
+
/**
|
|
99
|
+
* Clear the per-turn failover visited-provider set.
|
|
100
|
+
* Call this on every new user submission so the visited set does not bleed
|
|
101
|
+
* across independent turns (the set is also cleared automatically on
|
|
102
|
+
* TURN_COMPLETED, but a new submission may arrive before TURN_COMPLETED fires).
|
|
103
|
+
*/
|
|
104
|
+
readonly clearFailoverVisited: () => void;
|
|
59
105
|
}
|
|
60
106
|
|
|
61
107
|
/**
|
|
@@ -64,8 +110,7 @@ export interface WireStreamEventMetricsOptions {
|
|
|
64
110
|
* and declares it before render() so both the render closure and the returned
|
|
65
111
|
* event handlers share the same reference.
|
|
66
112
|
*
|
|
67
|
-
* Returns an
|
|
68
|
-
* array so they are cleaned up on exit.
|
|
113
|
+
* Returns an object with unsubscribe functions and a clearFailoverVisited helper.
|
|
69
114
|
*
|
|
70
115
|
* Responsibilities:
|
|
71
116
|
* - Track stream start time, delta count, token speed, and TTFT
|
|
@@ -75,8 +120,11 @@ export interface WireStreamEventMetricsOptions {
|
|
|
75
120
|
*/
|
|
76
121
|
export function wireStreamEventMetrics(
|
|
77
122
|
options: WireStreamEventMetricsOptions,
|
|
78
|
-
):
|
|
79
|
-
const {
|
|
123
|
+
): WireStreamEventMetricsResult {
|
|
124
|
+
const {
|
|
125
|
+
events, metrics, orchestrator, providerRegistry,
|
|
126
|
+
systemMessageRouter, render, providerOptimizer, retryTurn,
|
|
127
|
+
} = options;
|
|
80
128
|
|
|
81
129
|
const unsubs: Array<() => void> = [];
|
|
82
130
|
|
|
@@ -103,8 +151,78 @@ export function wireStreamEventMetrics(
|
|
|
103
151
|
metrics.tokenSpeed = elapsed > 0 ? tokenCount / elapsed : 0;
|
|
104
152
|
}));
|
|
105
153
|
|
|
154
|
+
// Per-turn visited-provider set: tracks providers already attempted this turn
|
|
155
|
+
// so failover cannot ping-pong between two mutually-failing providers.
|
|
156
|
+
// True invariant: at most one retry per provider per turn; exhaustion fires
|
|
157
|
+
// after the chain is consumed.
|
|
158
|
+
// Cleared on TURN_COMPLETED (see handler below) and on new user submission
|
|
159
|
+
// (caller clears via clearFailoverVisited(), wired in main.ts).
|
|
160
|
+
const failoverVisited = new Set<string>();
|
|
161
|
+
|
|
162
|
+
unsubs.push(events.turns.on('TURN_COMPLETED', () => {
|
|
163
|
+
failoverVisited.clear();
|
|
164
|
+
}));
|
|
165
|
+
|
|
106
166
|
unsubs.push(events.turns.on('TURN_ERROR', (event) => {
|
|
107
167
|
const errVal: string = event.error;
|
|
168
|
+
|
|
169
|
+
// --- Optimizer-gated failover path ---
|
|
170
|
+
// When the optimizer is present and enabled, attempt to advance to the next
|
|
171
|
+
// viable provider in the fallback chain before surfacing the error. When
|
|
172
|
+
// the optimizer is absent or disabled, behaviour is identical to baseline:
|
|
173
|
+
// error surfaces immediately.
|
|
174
|
+
if (providerOptimizer?.enabled && retryTurn) {
|
|
175
|
+
const fromProvider = providerRegistry.getCurrentModel().provider;
|
|
176
|
+
// Mark the failing provider as visited so it will never be selected again
|
|
177
|
+
// in this turn, even if a second TURN_ERROR arrives (e.g. ping-pong).
|
|
178
|
+
failoverVisited.add(fromProvider);
|
|
179
|
+
const result = providerOptimizer.testFallback({});
|
|
180
|
+
// Find the first capable node that is NOT already visited this turn and
|
|
181
|
+
// is NOT synthetic. Synthetic nodes are skipped permanently by design:
|
|
182
|
+
// a synthetic model is itself a fallback ladder over real backends, so
|
|
183
|
+
// failing over INTO one after a real backend already failed is unsound
|
|
184
|
+
// double-indirection (it can route straight back to the failed provider).
|
|
185
|
+
const next = result.chain.find(
|
|
186
|
+
(node) =>
|
|
187
|
+
node.capable &&
|
|
188
|
+
!failoverVisited.has(node.providerId) &&
|
|
189
|
+
node.providerId !== 'synthetic',
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (next) {
|
|
193
|
+
const toRegistryKey = `${next.providerId}:${next.modelId}`;
|
|
194
|
+
const errorClass = formatUserFacingErrorLine(errVal);
|
|
195
|
+
try {
|
|
196
|
+
providerRegistry.setCurrentModel(toRegistryKey);
|
|
197
|
+
} catch (switchErr) {
|
|
198
|
+
// Switch failed — fall through to honest error display.
|
|
199
|
+
logger.debug('failover setCurrentModel failed', { toRegistryKey, error: String(switchErr) });
|
|
200
|
+
systemMessageRouter.high(`[Error] ${errorClass}`);
|
|
201
|
+
render();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Record the selected provider as visited before the retry fires so
|
|
205
|
+
// a subsequent TURN_ERROR from that provider also skips it.
|
|
206
|
+
failoverVisited.add(next.providerId);
|
|
207
|
+
providerOptimizer.recordFallbackTransition(fromProvider, next.providerId, errorClass);
|
|
208
|
+
systemMessageRouter.high(
|
|
209
|
+
`[Failover] ${fromProvider} -> ${next.providerId} (${errorClass})`,
|
|
210
|
+
);
|
|
211
|
+
render();
|
|
212
|
+
// Re-submit the last user turn on the new provider.
|
|
213
|
+
retryTurn();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Chain exhausted — all capable candidates have been visited or none exist.
|
|
218
|
+
systemMessageRouter.high(
|
|
219
|
+
`[Failover] Chain exhausted — no alternative provider available. Original error: ${formatUserFacingErrorLine(errVal)}`,
|
|
220
|
+
);
|
|
221
|
+
render();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Baseline: optimizer disabled or not wired — surface error immediately.
|
|
108
226
|
const formatted = formatUserFacingErrorLine(errVal);
|
|
109
227
|
systemMessageRouter.high(`[Error] ${formatted}`);
|
|
110
228
|
render();
|
|
@@ -140,5 +258,5 @@ export function wireStreamEventMetrics(
|
|
|
140
258
|
metrics.activeToolName = undefined;
|
|
141
259
|
}));
|
|
142
260
|
|
|
143
|
-
return unsubs;
|
|
261
|
+
return { unsubs, clearFailoverVisited: () => failoverVisited.clear() };
|
|
144
262
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { UiRuntimeEvents } from '@/runtime/index.ts';
|
|
2
|
+
import { buildPersistedSessionContext, persistConversation } from '@/runtime/index.ts';
|
|
3
|
+
import { maybeAutoCompact } from './context-auto-compact.ts';
|
|
4
|
+
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
5
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
6
|
+
import type { HookDispatcher, HookPhase, HookCategory, HookEventPath } from '@pellux/goodvibes-sdk/platform/hooks';
|
|
7
|
+
import type { ConversationManager } from './conversation.ts';
|
|
8
|
+
|
|
9
|
+
/** Infer the options param of persistConversation to pick up SessionManager correctly. */
|
|
10
|
+
type PersistOptions = NonNullable<Parameters<typeof persistConversation>[5]>;
|
|
11
|
+
|
|
12
|
+
/** Minimal orchestrator surface required by turn-event wiring. */
|
|
13
|
+
interface TurnOrchestrator {
|
|
14
|
+
readonly lastInputTokens: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Minimal provider registry surface required by turn-event wiring. */
|
|
18
|
+
interface TurnProviderRegistry {
|
|
19
|
+
getCurrentModel(): { readonly contextWindow: number };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Minimal config manager surface required by turn-event wiring. */
|
|
23
|
+
interface TurnConfigManager {
|
|
24
|
+
get(key: string): unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Minimal system message router surface required by turn-event wiring. */
|
|
28
|
+
interface TurnSystemMessageRouter {
|
|
29
|
+
high(message: string): void;
|
|
30
|
+
low(message: string): void;
|
|
31
|
+
routeSystemMessage(message: string, level: string): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface WireTurnEventHandlersOptions {
|
|
35
|
+
readonly events: UiRuntimeEvents;
|
|
36
|
+
readonly conversation: ConversationManager;
|
|
37
|
+
readonly runtime: { sessionId: string; model: string; provider: string };
|
|
38
|
+
readonly orchestrator: TurnOrchestrator;
|
|
39
|
+
readonly configManager: TurnConfigManager;
|
|
40
|
+
readonly providerRegistry: TurnProviderRegistry;
|
|
41
|
+
readonly systemMessageRouter: TurnSystemMessageRouter;
|
|
42
|
+
readonly hookDispatcher: HookDispatcher;
|
|
43
|
+
readonly workingDir: string;
|
|
44
|
+
readonly homeDirectory: string;
|
|
45
|
+
readonly sessionManager: PersistOptions['sessionManager'];
|
|
46
|
+
readonly gitStatusProvider: { refresh(): Promise<unknown> };
|
|
47
|
+
readonly lastGitInfoRef: { value: unknown };
|
|
48
|
+
readonly buildSessionContinuityHints: () => Record<string, unknown>;
|
|
49
|
+
readonly render: () => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WireTurnEventHandlersResult {
|
|
53
|
+
/** Trigger a git status refresh; may be called from external code after tool execution. */
|
|
54
|
+
readonly refreshGit: () => void;
|
|
55
|
+
/** Unsubscribe functions to push into the parent unsubs array. */
|
|
56
|
+
readonly unsubs: ReadonlyArray<() => void>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Wire TURN_COMPLETED, TOOL_SUCCEEDED, and TOOL_FAILED runtime events.
|
|
61
|
+
*
|
|
62
|
+
* Responsibilities:
|
|
63
|
+
* - Auto-save conversation to persistent store after each LLM turn
|
|
64
|
+
* - Fire the Lifecycle:session:save hook
|
|
65
|
+
* - Trigger auto-compact when context usage exceeds the configured threshold
|
|
66
|
+
* - Refresh git status after turns and tool results
|
|
67
|
+
*
|
|
68
|
+
* Returns refreshGit (callable externally) and unsubs (push into parent unsubs).
|
|
69
|
+
*/
|
|
70
|
+
export function wireTurnEventHandlers(
|
|
71
|
+
options: WireTurnEventHandlersOptions,
|
|
72
|
+
): WireTurnEventHandlersResult {
|
|
73
|
+
const {
|
|
74
|
+
events, conversation, runtime, orchestrator, configManager,
|
|
75
|
+
providerRegistry, systemMessageRouter, hookDispatcher,
|
|
76
|
+
workingDir, homeDirectory, sessionManager, gitStatusProvider,
|
|
77
|
+
lastGitInfoRef, buildSessionContinuityHints, render,
|
|
78
|
+
} = options;
|
|
79
|
+
|
|
80
|
+
const unsubs: Array<() => void> = [];
|
|
81
|
+
|
|
82
|
+
const refreshGit = (): void => {
|
|
83
|
+
gitStatusProvider.refresh().then((info) => { lastGitInfoRef.value = info; render(); }).catch(() => { /* non-fatal */ });
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
unsubs.push(events.turns.on('TURN_COMPLETED', () => {
|
|
87
|
+
// Auto-save after every LLM turn so kills don't lose the session
|
|
88
|
+
try {
|
|
89
|
+
const snapshot = conversation.toJSON() as { messages: Array<import('./conversation.ts').ConversationMessageSnapshot>; timestamp?: number };
|
|
90
|
+
const persisted = buildPersistedSessionContext(snapshot.messages, conversation.getTitleSource(), buildSessionContinuityHints());
|
|
91
|
+
persistConversation(
|
|
92
|
+
runtime.sessionId,
|
|
93
|
+
{ ...snapshot, ...persisted },
|
|
94
|
+
runtime.model,
|
|
95
|
+
runtime.provider,
|
|
96
|
+
conversation.title || '',
|
|
97
|
+
{ workingDirectory: workingDir, homeDirectory, sessionManager },
|
|
98
|
+
);
|
|
99
|
+
hookDispatcher.fire({ path: 'Lifecycle:session:save' as HookEventPath, phase: 'Lifecycle' as HookPhase, category: 'session' as HookCategory, specific: 'save', sessionId: runtime.sessionId, timestamp: Date.now(), payload: { sessionId: runtime.sessionId } }).catch((err: unknown) => logger.debug('hook fire error', { error: summarizeError(err) }));
|
|
100
|
+
} catch (e) { logger.debug('auto-save on turn:complete failed', { error: summarizeError(e) }); }
|
|
101
|
+
// Auto-compact: check context usage and compact if threshold exceeded
|
|
102
|
+
const currentModelForCompact = providerRegistry.getCurrentModel();
|
|
103
|
+
maybeAutoCompact({
|
|
104
|
+
configManager: configManager as Parameters<typeof maybeAutoCompact>[0]['configManager'],
|
|
105
|
+
conversation,
|
|
106
|
+
providerRegistry: providerRegistry as Parameters<typeof maybeAutoCompact>[0]['providerRegistry'],
|
|
107
|
+
systemMessageRouter: systemMessageRouter as Parameters<typeof maybeAutoCompact>[0]['systemMessageRouter'],
|
|
108
|
+
model: runtime.model,
|
|
109
|
+
provider: runtime.provider,
|
|
110
|
+
lastInputTokens: orchestrator.lastInputTokens,
|
|
111
|
+
contextWindow: currentModelForCompact.contextWindow,
|
|
112
|
+
}).catch((err: unknown) => logger.debug('maybeAutoCompact error', { error: summarizeError(err) }));
|
|
113
|
+
refreshGit();
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
unsubs.push(events.tools.on('TOOL_SUCCEEDED', () => {
|
|
117
|
+
refreshGit();
|
|
118
|
+
}));
|
|
119
|
+
unsubs.push(events.tools.on('TOOL_FAILED', () => {
|
|
120
|
+
refreshGit();
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
return { refreshGit, unsubs };
|
|
124
|
+
}
|
package/src/daemon/cli.ts
CHANGED
|
@@ -104,6 +104,11 @@ async function main(): Promise<void> {
|
|
|
104
104
|
console.error(renderGoodVibesDaemonHelp('goodvibes-daemon'));
|
|
105
105
|
process.exit(2);
|
|
106
106
|
}
|
|
107
|
+
if (cli.warnings.length > 0) {
|
|
108
|
+
for (const warning of cli.warnings) {
|
|
109
|
+
console.warn(`[goodvibes-daemon] warning: ${warning}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
107
112
|
if (cli.flags.help || cli.command === 'help') {
|
|
108
113
|
console.log(renderGoodVibesDaemonHelp('goodvibes-daemon'));
|
|
109
114
|
process.exit(0);
|
|
@@ -118,6 +118,7 @@ export interface CommandShellUiOpeners {
|
|
|
118
118
|
openMcpWorkspace?: () => void;
|
|
119
119
|
openSecurityPanel?: () => void;
|
|
120
120
|
openKnowledgePanel?: () => void;
|
|
121
|
+
openMemoryPanel?: () => void;
|
|
121
122
|
openRemotePanel?: () => void;
|
|
122
123
|
openSubscriptionPanel?: () => void;
|
|
123
124
|
/**
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { CommandRegistry } from '../command-registry.ts';
|
|
2
|
+
import { requireIntegrationHelpers } from './runtime-services.ts';
|
|
3
|
+
|
|
4
|
+
export function registerChannelRuntimeCommands(registry: CommandRegistry): void {
|
|
5
|
+
registry.register({
|
|
6
|
+
name: 'channel',
|
|
7
|
+
aliases: [],
|
|
8
|
+
description: 'Inspect channel routes, delivery strategies, and ingress policies',
|
|
9
|
+
usage: '[status|routes|delivery|policy] [--json]',
|
|
10
|
+
argsHint: 'status | routes | delivery | policy',
|
|
11
|
+
handler(args, ctx) {
|
|
12
|
+
const sub = args[0];
|
|
13
|
+
const asJson = args.includes('--json');
|
|
14
|
+
|
|
15
|
+
if (!sub || sub === 'open' || sub === 'panel') {
|
|
16
|
+
if (ctx.showPanel) ctx.showPanel('routes');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const helpers = requireIntegrationHelpers(ctx);
|
|
21
|
+
|
|
22
|
+
if (sub === 'status') {
|
|
23
|
+
const review = helpers.buildReview();
|
|
24
|
+
if (asJson) {
|
|
25
|
+
ctx.print(JSON.stringify(review, null, 2));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const lines: string[] = [
|
|
29
|
+
'Channel Status',
|
|
30
|
+
` routes: ${review.routes.length}`,
|
|
31
|
+
` api families: ${review.apiFamilies.join(', ') || '(none)'}`,
|
|
32
|
+
` sessions: ${review.sessions}`,
|
|
33
|
+
` tasks: ${review.tasks}`,
|
|
34
|
+
` pending approvals: ${review.pendingApprovals}`,
|
|
35
|
+
` remote contracts: ${review.remoteContracts}`,
|
|
36
|
+
'',
|
|
37
|
+
`Active route families: ${review.routes.join(', ') || '(none)'}`,
|
|
38
|
+
'',
|
|
39
|
+
'Use /channel routes for delivery binding details.',
|
|
40
|
+
'Use /channel delivery for outbound delivery snapshot.',
|
|
41
|
+
'Use /channel policy for ingress policy snapshot.',
|
|
42
|
+
];
|
|
43
|
+
ctx.print(lines.join('\n'));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (sub === 'routes') {
|
|
48
|
+
const snapshot = helpers.getRouteSnapshot();
|
|
49
|
+
if (asJson) {
|
|
50
|
+
ctx.print(JSON.stringify(snapshot, null, 2));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const entries = Object.entries(snapshot);
|
|
54
|
+
if (entries.length === 0) {
|
|
55
|
+
ctx.print('No route bindings active.\n\nRoutes become active when channel surfaces (slack, discord, ntfy, webhook, etc.) are configured and the daemon is running.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const lines: string[] = ['Channel Routes'];
|
|
59
|
+
for (const [key, value] of entries) {
|
|
60
|
+
lines.push(` ${String(key).padEnd(28)} ${JSON.stringify(value)}`);
|
|
61
|
+
}
|
|
62
|
+
lines.push('');
|
|
63
|
+
lines.push('Route bindings reflect active daemon surface registrations.');
|
|
64
|
+
ctx.print(lines.join('\n'));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (sub === 'delivery') {
|
|
69
|
+
const snapshot = helpers.getDeliverySnapshot();
|
|
70
|
+
if (asJson) {
|
|
71
|
+
ctx.print(JSON.stringify(snapshot, null, 2));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const entries = Object.entries(snapshot);
|
|
75
|
+
if (entries.length === 0) {
|
|
76
|
+
ctx.print('No delivery snapshot available.\n\nDelivery state is populated when the daemon handles outbound channel messages.');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const lines: string[] = ['Channel Delivery Snapshot'];
|
|
80
|
+
for (const [key, value] of entries) {
|
|
81
|
+
lines.push(` ${String(key).padEnd(28)} ${JSON.stringify(value)}`);
|
|
82
|
+
}
|
|
83
|
+
ctx.print(lines.join('\n'));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (sub === 'policy') {
|
|
88
|
+
const configManager = ctx.platform.configManager;
|
|
89
|
+
// Channel policy is persisted by ChannelPolicyManager in
|
|
90
|
+
// .goodvibes/tui/channels/policies.json — surface via configManager
|
|
91
|
+
// category (runtime-accessible without a daemon round-trip).
|
|
92
|
+
const surfaces = [
|
|
93
|
+
'slack', 'discord', 'ntfy', 'webhook', 'homeassistant',
|
|
94
|
+
'telegram', 'google-chat', 'signal', 'whatsapp',
|
|
95
|
+
'imessage', 'msteams', 'bluebubbles', 'mattermost', 'matrix',
|
|
96
|
+
];
|
|
97
|
+
const lines: string[] = ['Channel Ingress Policies'];
|
|
98
|
+
let found = false;
|
|
99
|
+
for (const surface of surfaces) {
|
|
100
|
+
const key = `surfaces.${surface}.enabled` as Parameters<typeof configManager.get>[0];
|
|
101
|
+
const enabled = configManager.get(key);
|
|
102
|
+
if (enabled !== undefined && enabled !== null) {
|
|
103
|
+
found = true;
|
|
104
|
+
lines.push(` ${surface.padEnd(20)} enabled=${String(enabled)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!found) {
|
|
108
|
+
lines.push(' No channel surfaces configured.');
|
|
109
|
+
lines.push('');
|
|
110
|
+
lines.push(' Configure surfaces via /onboarding or Settings > Surfaces.');
|
|
111
|
+
lines.push(' Fine-grained ingress policies (allowedCommands, requireMention, groupPolicies)');
|
|
112
|
+
lines.push(' are managed by ChannelPolicyManager in .goodvibes/tui/channels/policies.json.');
|
|
113
|
+
} else {
|
|
114
|
+
lines.push('');
|
|
115
|
+
lines.push(' Fine-grained ingress policies (allowedCommands, requireMention, groupPolicies)');
|
|
116
|
+
lines.push(' are managed by ChannelPolicyManager in .goodvibes/tui/channels/policies.json.');
|
|
117
|
+
}
|
|
118
|
+
if (asJson) {
|
|
119
|
+
ctx.print(JSON.stringify({ surfaces: Object.fromEntries(surfaces.map((s) => [s, configManager.get(`surfaces.${s}.enabled` as Parameters<typeof configManager.get>[0])])) }, null, 2));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
ctx.print(lines.join('\n'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
ctx.print(
|
|
127
|
+
'Usage: /channel <subcommand>\n'
|
|
128
|
+
+ ' (no args) — open the Routes panel\n'
|
|
129
|
+
+ ' status — channel overview: routes, sessions, tasks, pending approvals\n'
|
|
130
|
+
+ ' routes — active route binding snapshot\n'
|
|
131
|
+
+ ' delivery — outbound delivery snapshot\n'
|
|
132
|
+
+ ' policy — configured channel surfaces and ingress policy location\n'
|
|
133
|
+
+ '\n'
|
|
134
|
+
+ 'Options:\n'
|
|
135
|
+
+ ' --json Output raw JSON for scripting'
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
@@ -203,11 +203,11 @@ export function registerControlRoomRuntimeCommands(registry: CommandRegistry): v
|
|
|
203
203
|
handler(args, ctx) {
|
|
204
204
|
const subcommand = (args[0] ?? 'open').toLowerCase();
|
|
205
205
|
if (subcommand === 'open') {
|
|
206
|
-
if (ctx.
|
|
207
|
-
ctx.
|
|
206
|
+
if (ctx.openMemoryPanel) {
|
|
207
|
+
ctx.openMemoryPanel();
|
|
208
208
|
return;
|
|
209
209
|
}
|
|
210
|
-
ctx.print('
|
|
210
|
+
ctx.print('Memory panel is not available in this runtime.');
|
|
211
211
|
return;
|
|
212
212
|
}
|
|
213
213
|
const memory = getMemoryApi(ctx);
|
|
@@ -245,8 +245,8 @@ export function registerControlRoomRuntimeCommands(registry: CommandRegistry): v
|
|
|
245
245
|
ctx.print(prompt ?? 'No reviewed project knowledge matched that task.');
|
|
246
246
|
return;
|
|
247
247
|
}
|
|
248
|
-
if (ctx.
|
|
249
|
-
ctx.
|
|
248
|
+
if (ctx.openMemoryPanel) {
|
|
249
|
+
ctx.openMemoryPanel();
|
|
250
250
|
return;
|
|
251
251
|
}
|
|
252
252
|
ctx.print(`Unknown project-memory subcommand: ${subcommand}`);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Implements the Provider Optimizer panel commands:
|
|
5
5
|
*
|
|
6
|
+
* /provider optimizer on|off — Enable or disable the provider optimizer
|
|
6
7
|
* /provider route auto|manual — Set optimizer routing mode
|
|
7
8
|
* /provider explain-route — Print current route explanation
|
|
8
9
|
* /provider pin <provider:model> — Pin routing to a specific provider/model
|
|
@@ -10,14 +11,19 @@
|
|
|
10
11
|
*
|
|
11
12
|
* When the optimizer is disabled, commands report its status and
|
|
12
13
|
* explain-route still works (reads current model capabilities).
|
|
14
|
+
* Enabling the optimizer persists the change to config so it survives restart.
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
import type { SlashCommand, CommandContext } from '../command-registry.ts';
|
|
18
|
+
import type { ConfigKey } from '../../config/index.ts';
|
|
16
19
|
import type { RouteExplanation } from '@pellux/goodvibes-sdk/platform/providers';
|
|
17
20
|
import type { FallbackTestResult, FallbackTransition } from '@pellux/goodvibes-sdk/platform/providers';
|
|
18
21
|
import type { ProviderApiModelRecord } from '@pellux/goodvibes-sdk/platform/providers';
|
|
19
22
|
import { requireProviderApi } from './runtime-services.ts';
|
|
20
23
|
|
|
24
|
+
const PROVIDER_OPTIMIZER_FLAG = 'provider-optimizer';
|
|
25
|
+
const PROVIDER_OPTIMIZER_CONFIG_KEY = `featureFlags.${PROVIDER_OPTIMIZER_FLAG}` as ConfigKey;
|
|
26
|
+
|
|
21
27
|
// ---------------------------------------------------------------------------
|
|
22
28
|
// Formatting helpers
|
|
23
29
|
// ---------------------------------------------------------------------------
|
|
@@ -65,6 +71,49 @@ function fmtExplanation(expl: RouteExplanation, context: CommandContext): void {
|
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
73
|
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// /provider optimizer on|off
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
function handleOptimizerToggle(
|
|
79
|
+
args: string[],
|
|
80
|
+
context: CommandContext,
|
|
81
|
+
): void {
|
|
82
|
+
const optimizer = requireProviderOptimizer(context);
|
|
83
|
+
if (!optimizer) return;
|
|
84
|
+
const sub = args[0];
|
|
85
|
+
|
|
86
|
+
if (sub !== 'on' && sub !== 'off') {
|
|
87
|
+
context.print('[provider] Usage: /provider optimizer on|off');
|
|
88
|
+
context.print(` Current state: optimizer is ${optimizer.enabled ? 'enabled' : 'disabled'}`);
|
|
89
|
+
context.print(' "on" — activates intelligent failover and auto-routing');
|
|
90
|
+
context.print(' "off" — disables optimizer; provider selection is manual only');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const enable = sub === 'on';
|
|
95
|
+
const wasEnabled = optimizer.enabled;
|
|
96
|
+
optimizer.setEnabled(enable);
|
|
97
|
+
|
|
98
|
+
// Persist to config so the setting survives restart.
|
|
99
|
+
const flagValue = enable ? 'enabled' : 'disabled';
|
|
100
|
+
context.platform.configManager.setDynamic(PROVIDER_OPTIMIZER_CONFIG_KEY, flagValue);
|
|
101
|
+
|
|
102
|
+
if (enable && !wasEnabled) {
|
|
103
|
+
context.print('[provider] Optimizer enabled.');
|
|
104
|
+
context.print(' Intelligent failover is now active: on a request error the optimizer');
|
|
105
|
+
context.print(' will attempt the next viable provider and surface a transcript notice');
|
|
106
|
+
context.print(' naming the from→to transition and reason before retrying.');
|
|
107
|
+
context.print(' Use "/provider route auto" to enable fully automatic routing.');
|
|
108
|
+
} else if (!enable && wasEnabled) {
|
|
109
|
+
context.print('[provider] Optimizer disabled.');
|
|
110
|
+
context.print(' Provider selection returns to manual-only mode. No automatic failover.');
|
|
111
|
+
context.print(' Pinned targets and fallback log are preserved; re-enable to resume.');
|
|
112
|
+
} else {
|
|
113
|
+
context.print(`[provider] Optimizer already ${enable ? 'enabled' : 'disabled'} — no change.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
68
117
|
// ---------------------------------------------------------------------------
|
|
69
118
|
// /provider route auto|manual
|
|
70
119
|
// ---------------------------------------------------------------------------
|
|
@@ -85,9 +134,9 @@ function handleRoute(
|
|
|
85
134
|
|
|
86
135
|
if (!optimizer.enabled) {
|
|
87
136
|
context.print(
|
|
88
|
-
'[provider] Optimizer is
|
|
137
|
+
'[provider] Optimizer is off — routing mode recorded but failover will not fire until optimizer is enabled.',
|
|
89
138
|
);
|
|
90
|
-
context.print(
|
|
139
|
+
context.print(' Enable with: /provider optimizer on');
|
|
91
140
|
}
|
|
92
141
|
|
|
93
142
|
optimizer.setMode(sub);
|
|
@@ -318,11 +367,15 @@ export const providerCommand: SlashCommand = {
|
|
|
318
367
|
aliases: ['prov-opt'],
|
|
319
368
|
description: 'Manage provider routing optimizer (route, pin, explain, fallback).',
|
|
320
369
|
usage: '<subcommand> [args]',
|
|
321
|
-
argsHint: 'route|explain-route|pin|fallback',
|
|
370
|
+
argsHint: 'optimizer|route|explain-route|pin|fallback',
|
|
322
371
|
handler: async (args: string[], context: CommandContext): Promise<void> => {
|
|
323
372
|
const [sub, ...rest] = args;
|
|
324
373
|
|
|
325
374
|
switch (sub) {
|
|
375
|
+
case 'optimizer':
|
|
376
|
+
handleOptimizerToggle(rest, context);
|
|
377
|
+
break;
|
|
378
|
+
|
|
326
379
|
case 'route':
|
|
327
380
|
handleRoute(rest, context);
|
|
328
381
|
break;
|
|
@@ -345,6 +398,7 @@ export const providerCommand: SlashCommand = {
|
|
|
345
398
|
if (!optimizer) return;
|
|
346
399
|
const lines = [
|
|
347
400
|
'Usage: /provider <subcommand>',
|
|
401
|
+
' optimizer on|off — Enable or disable the provider optimizer',
|
|
348
402
|
' route auto|manual — Set optimizer routing mode',
|
|
349
403
|
' explain-route — Show current route explanation',
|
|
350
404
|
' pin <provider:model> — Pin routing to specific provider/model',
|
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
CommandSessionServices,
|
|
8
8
|
CommandWorkspaceServices,
|
|
9
9
|
} from '../command-registry.ts';
|
|
10
|
+
import { getLastCompactionEvent } from '@pellux/goodvibes-sdk/platform/core';
|
|
11
|
+
import type { CompactionContext, CompactionEvent } from '@pellux/goodvibes-sdk/platform/core';
|
|
10
12
|
import type { UiReadModels } from '../../runtime/ui-read-models.ts';
|
|
11
13
|
import type { ShellPathService } from '@/runtime/index.ts';
|
|
12
14
|
import type { EcosystemCatalogPathOptions } from '@/runtime/index.ts';
|
|
@@ -236,13 +238,40 @@ export function requireProviderApi(context: CommandContext): ProviderApi {
|
|
|
236
238
|
return requireContextValue(context.clients?.providerApi, 'clients.providerApi');
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Compact the conversation and return the CompactionEvent recorded by the SDK,
|
|
243
|
+
* or null if no event was recorded (e.g. compaction was skipped or produced no
|
|
244
|
+
* change).
|
|
245
|
+
*/
|
|
246
|
+
export async function compactConversation(context: CommandContext): Promise<CompactionEvent | null> {
|
|
247
|
+
const eventBefore = getLastCompactionEvent();
|
|
248
|
+
const sessionMemories = context.session.sessionMemoryStore?.list() ?? [];
|
|
249
|
+
const compactionCtx: CompactionContext = {
|
|
250
|
+
messages: context.session.conversationManager.getMessagesForLLM(),
|
|
251
|
+
sessionMemories,
|
|
252
|
+
agents: [],
|
|
253
|
+
wrfcChains: [],
|
|
254
|
+
activePlan: null,
|
|
255
|
+
lineageEntries: [],
|
|
256
|
+
compactionCount: 0,
|
|
257
|
+
contextWindow: 0,
|
|
258
|
+
trigger: 'manual',
|
|
259
|
+
extractionModelId: context.session.runtime.model,
|
|
260
|
+
extractionProvider: context.session.runtime.provider,
|
|
261
|
+
};
|
|
240
262
|
await context.session.conversationManager.compact(
|
|
241
263
|
context.provider.providerRegistry,
|
|
242
264
|
context.session.runtime.model,
|
|
243
265
|
'manual',
|
|
244
266
|
context.session.runtime.provider,
|
|
267
|
+
compactionCtx,
|
|
245
268
|
);
|
|
269
|
+
const eventAfter = getLastCompactionEvent();
|
|
270
|
+
// Return the new event only if it differs from the one recorded before the call.
|
|
271
|
+
if (eventAfter !== null && eventAfter !== eventBefore) {
|
|
272
|
+
return eventAfter;
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
246
275
|
}
|
|
247
276
|
|
|
248
277
|
export function requireKnowledgeApi(context: CommandContext): KnowledgeApi {
|