@pellux/goodvibes-tui 1.16.1 → 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 +138 -9
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +5490 -1806
- package/package.json +7 -7
- package/src/cli/completions/generate.ts +5 -4
- package/src/cli/config-overrides.ts +51 -11
- package/src/cli/doctor.ts +4 -5
- package/src/cli/entrypoint.ts +13 -8
- package/src/cli/help.ts +7 -6
- package/src/cli/launch-auto-update.ts +214 -0
- package/src/cli/management-commands.ts +82 -15
- package/src/cli/management.ts +0 -2
- package/src/cli/parser.ts +5 -0
- package/src/cli/status.ts +13 -0
- package/src/cli/surface-command.ts +5 -5
- package/src/cli/tui-startup.ts +60 -97
- package/src/cli/types.ts +2 -0
- package/src/config/index.ts +1 -1
- package/src/config/tui-extension-settings.ts +33 -0
- package/src/core/composer-state.ts +1 -1
- package/src/core/consolidation-receipt.ts +41 -0
- package/src/core/conversation-line-cache.ts +3 -3
- package/src/core/conversation-rendering.ts +18 -9
- package/src/core/conversation.ts +2 -2
- package/src/core/long-task-notifier.ts +7 -7
- 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 +29 -6
- package/src/core/system-message-noise.ts +1 -1
- package/src/core/system-message-router.ts +6 -6
- package/src/core/turn-budget-outcome.ts +60 -0
- package/src/core/turn-cancellation.ts +40 -1
- package/src/core/turn-event-wiring.ts +14 -8
- package/src/daemon/cli.ts +64 -58
- package/src/daemon/lifecycle.ts +41 -0
- package/src/daemon/service-commands.ts +5 -5
- package/src/export/cost-utils.ts +128 -6
- package/src/input/autocomplete.ts +2 -2
- package/src/input/command-registry.ts +55 -9
- package/src/input/commands/acp-runtime.ts +72 -0
- package/src/input/commands/ci-runtime.ts +5 -1
- package/src/input/commands/codebase-runtime.ts +6 -6
- package/src/input/commands/cost-runtime.ts +1 -1
- 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/health-runtime.ts +2 -2
- package/src/input/commands/intelligence-runtime.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +1 -1
- package/src/input/commands/local-provider-runtime.ts +49 -10
- package/src/input/commands/local-runtime.ts +53 -5
- package/src/input/commands/memory.ts +2 -2
- package/src/input/commands/operator-panel-runtime.ts +5 -5
- package/src/input/commands/operator-rpc.ts +32 -11
- package/src/input/commands/permissions-provenance.ts +1 -1
- package/src/input/commands/permissions-runtime.ts +99 -8
- package/src/input/commands/planning-runtime.ts +3 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -1
- package/src/input/commands/power-runtime.ts +56 -0
- package/src/input/commands/provider-accounts-runtime.ts +71 -6
- package/src/input/commands/provider.ts +7 -5
- package/src/input/commands/qrcode-runtime.ts +14 -19
- package/src/input/commands/queue-runtime.ts +70 -0
- package/src/input/commands/recall-files-git.ts +13 -51
- package/src/input/commands/recall-files.ts +8 -11
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/relay-runtime.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/runtime-services.ts +4 -4
- package/src/input/commands/schedule-runtime.ts +14 -7
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +2 -2
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +46 -68
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/update-runtime.ts +153 -96
- package/src/input/commands/work-plan-runtime.ts +1 -1
- package/src/input/commands/workstream-attempts.ts +6 -1
- package/src/input/commands/workstream-runtime.ts +46 -27
- package/src/input/commands/worktree-runtime.ts +35 -8
- package/src/input/commands.ts +10 -2
- package/src/input/config-modal-types.ts +14 -3
- package/src/input/config-modal.ts +75 -13
- package/src/input/feature-unit-layout.ts +93 -82
- package/src/input/feed-context-factory.ts +2 -2
- package/src/input/handler-command-route.ts +2 -2
- package/src/input/handler-content-actions.ts +1 -1
- package/src/input/handler-feed-routes.ts +7 -7
- package/src/input/handler-feed.ts +4 -4
- package/src/input/handler-interactions.ts +3 -2
- package/src/input/handler-modal-routes.ts +12 -4
- package/src/input/handler-modal-token-routes.ts +1 -1
- package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
- package/src/input/handler-onboarding.ts +3 -2
- package/src/input/handler-picker-routes.ts +26 -13
- package/src/input/handler-shortcuts.ts +29 -6
- package/src/input/handler.ts +2 -2
- package/src/input/keybindings.ts +21 -3
- package/src/input/onboarding/onboarding-feature-units.ts +34 -27
- package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
- package/src/input/panel-integration-actions.ts +2 -2
- package/src/input/panel-mouse-geometry.ts +1 -1
- package/src/input/panel-paste-flood-guard.ts +1 -1
- package/src/input/provider-key-intake.ts +167 -0
- package/src/input/sandbox-exec-config.ts +2 -2
- package/src/input/selection-modal.ts +1 -1
- package/src/input/settings-modal-activation.ts +5 -0
- package/src/input/settings-modal-adjustment.ts +4 -31
- package/src/input/settings-modal-data.ts +52 -57
- package/src/input/settings-modal-mutations.ts +12 -68
- package/src/input/settings-modal-types.ts +27 -17
- package/src/input/settings-modal.ts +74 -55
- package/src/main.ts +90 -90
- package/src/panels/agent-inspector-shared.ts +4 -4
- package/src/panels/base-panel.ts +2 -2
- package/src/panels/builtin/agent.ts +4 -4
- package/src/panels/builtin/development.ts +4 -4
- package/src/panels/builtin/knowledge.ts +1 -1
- package/src/panels/builtin/operations.ts +83 -11
- package/src/panels/builtin/session.ts +5 -5
- package/src/panels/builtin/shared.ts +16 -4
- package/src/panels/builtin-modals.ts +59 -42
- package/src/panels/builtin-panels.ts +1 -1
- package/src/panels/cost-tracker-panel.ts +124 -7
- package/src/panels/diff-panel.ts +39 -6
- package/src/panels/eval-registry.ts +2 -2
- package/src/panels/fleet-acts.ts +446 -0
- package/src/panels/fleet-deep-link.ts +1 -1
- package/src/panels/fleet-gateway.ts +117 -0
- package/src/panels/fleet-observed-render.ts +128 -0
- package/src/panels/fleet-panel-format.ts +163 -11
- package/src/panels/fleet-panel-worktree-detail.ts +14 -0
- package/src/panels/fleet-panel.ts +73 -57
- package/src/panels/fleet-read-model.ts +84 -14
- package/src/panels/fleet-spawn.ts +266 -0
- package/src/panels/fleet-steer.ts +2 -2
- package/src/panels/fleet-stop.ts +41 -5
- package/src/panels/fleet-tabs.ts +15 -16
- package/src/panels/fleet-transcript.ts +5 -5
- package/src/panels/git-panel.ts +2 -2
- package/src/panels/index.ts +4 -1
- package/src/panels/local-auth-panel.ts +1 -1
- package/src/panels/memory-consolidation-gateway.ts +94 -0
- package/src/panels/modals/devices-modal.ts +161 -0
- package/src/panels/modals/hooks-modal.ts +1 -1
- package/src/panels/modals/keybindings-modal.ts +2 -2
- package/src/panels/modals/knowledge-modal.ts +1 -1
- package/src/panels/modals/marketplace-modal.ts +4 -4
- package/src/panels/modals/memory-modal.ts +153 -9
- package/src/panels/modals/modal-surface-helpers.ts +1 -1
- package/src/panels/modals/modal-theme.ts +1 -1
- package/src/panels/modals/pairing-modal.ts +198 -68
- package/src/panels/modals/planning-modal.ts +2 -2
- package/src/panels/modals/plugins-modal.ts +1 -1
- package/src/panels/modals/policy-modal.ts +1 -1
- package/src/panels/modals/security-modal.ts +7 -1
- package/src/panels/modals/skills-modal.ts +1 -1
- package/src/panels/modals/work-plan-modal.ts +3 -3
- package/src/panels/notifications-feed.ts +127 -0
- package/src/panels/notifications-panel.ts +160 -0
- package/src/panels/panel-manager.ts +9 -9
- package/src/panels/plugins-panel.ts +4 -4
- package/src/panels/project-planning-answer-actions.ts +1 -1
- package/src/panels/provider-health-routes.ts +2 -2
- package/src/panels/skills-panel.ts +4 -5
- package/src/panels/token-budget-panel.ts +11 -3
- package/src/panels/types.ts +5 -5
- package/src/panels/workstream-graph-render.ts +123 -0
- package/src/permissions/broker-approval-card.ts +201 -0
- package/src/permissions/hunk-selection.ts +28 -4
- package/src/permissions/prompt.ts +244 -29
- package/src/renderer/autocomplete-overlay.ts +107 -54
- package/src/renderer/code-block.ts +2 -2
- package/src/renderer/compaction-history-modal.ts +1 -1
- package/src/renderer/compaction-preview.ts +1 -1
- package/src/renderer/compaction-quality.ts +1 -1
- package/src/renderer/composer-fragments.ts +69 -0
- package/src/renderer/compositor.ts +2 -2
- package/src/renderer/config-modal.ts +1 -1
- package/src/renderer/conversation-overlays.ts +1 -1
- package/src/renderer/footer-tips.ts +3 -3
- package/src/renderer/hint-grammar.ts +1 -1
- package/src/renderer/modal-factory.ts +1 -1
- package/src/renderer/model-picker-overlay.ts +1 -1
- package/src/renderer/model-workspace.ts +7 -2
- package/src/renderer/panel-composite.ts +2 -2
- package/src/renderer/panel-workspace-bar.ts +1 -1
- package/src/renderer/process-indicator.ts +1 -1
- package/src/renderer/selection-modal-overlay.ts +1 -1
- package/src/renderer/session-picker-modal.ts +2 -2
- package/src/renderer/settings-modal-helpers.ts +23 -5
- package/src/renderer/settings-modal.ts +129 -39
- package/src/renderer/shell-surface.ts +6 -0
- package/src/renderer/terminal-bg-probe.ts +1 -1
- package/src/renderer/theme.ts +3 -3
- package/src/renderer/tool-call.ts +13 -7
- package/src/renderer/tool-result-summary.ts +1 -1
- package/src/renderer/turn-injection.ts +7 -6
- package/src/renderer/ui-factory.ts +38 -1
- package/src/runtime/bootstrap-command-context.ts +7 -2
- package/src/runtime/bootstrap-command-parts.ts +9 -5
- package/src/runtime/bootstrap-core.ts +41 -41
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap-shell.ts +13 -5
- package/src/runtime/bootstrap.ts +34 -32
- package/src/runtime/channel-composition.ts +46 -0
- package/src/runtime/code-index-services.ts +3 -4
- package/src/runtime/daemon-attach-notices.ts +120 -0
- package/src/runtime/durability-services.ts +90 -0
- package/src/runtime/embedded-service-factories.ts +40 -0
- package/src/runtime/feature-settings.ts +147 -0
- package/src/runtime/fleet-services.ts +41 -0
- package/src/runtime/idle-power-services.ts +85 -0
- package/src/runtime/index.ts +6 -21
- package/src/runtime/interaction-seams.ts +109 -0
- package/src/runtime/legacy-daemon-migration.ts +29 -19
- package/src/runtime/memory-fold.ts +1 -1
- package/src/runtime/notification-dispatch.ts +110 -0
- package/src/runtime/onboarding/apply.ts +3 -29
- package/src/runtime/onboarding/fast-path.ts +39 -6
- package/src/runtime/onboarding/provider-key-capture.ts +126 -0
- package/src/runtime/onboarding/snapshot.ts +0 -1
- package/src/runtime/onboarding/types.ts +9 -3
- package/src/runtime/power-keepawake-remote.ts +75 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/provider-fallback.ts +55 -0
- package/src/runtime/provider-optimizer-wiring.ts +55 -0
- package/src/runtime/release-artifacts.ts +19 -103
- package/src/runtime/resume-notice.ts +1 -1
- package/src/runtime/services.ts +113 -113
- package/src/runtime/session-continuity-hints.ts +38 -0
- package/src/runtime/surface-feature-flags.ts +40 -9
- package/src/runtime/terminal-output-guard.ts +2 -2
- package/src/runtime/trust/workspace-registration.ts +10 -2
- package/src/runtime/trust/workspace-trust.ts +58 -31
- package/src/runtime/ui-services.ts +6 -1
- package/src/runtime/update-check.ts +15 -69
- package/src/runtime/workstream-services.ts +2 -2
- package/src/shell/blocking-input.ts +108 -132
- package/src/shell/recovery-input-helpers.ts +53 -166
- package/src/shell/ui-openers.ts +38 -6
- package/src/utils/format-duration.ts +1 -1
- package/src/verification/verification-ledger.ts +21 -4
- package/src/version.ts +1 -1
- package/src/input/commands/flags-runtime.ts +0 -328
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stable-host.ts — a stable name for printed/QR pairing links.
|
|
3
|
+
*
|
|
4
|
+
* The legacy resolution used the first non-internal IPv4 in enumeration order
|
|
5
|
+
* (see getLocalNetworkIp in management-utils.ts). That address is bound to the
|
|
6
|
+
* current DHCP lease: when the lease changes, every printed link and every QR
|
|
7
|
+
* that encoded it stops resolving. This module resolves a name that survives a
|
|
8
|
+
* lease change where one exists, and only falls back to a routable address when
|
|
9
|
+
* no stable name is available.
|
|
10
|
+
*
|
|
11
|
+
* Resolution ladder (highest preference first):
|
|
12
|
+
* 1. Tailscale MagicDNS name — preferred when tailscale is up. It resolves on
|
|
13
|
+
* and off the LAN and is never bound to a DHCP lease.
|
|
14
|
+
* 2. mDNS `<hostname>.local` — a LAN name a DHCP change does not break.
|
|
15
|
+
* 3. Gateway-routed interface address — the IPv4 on the default-route
|
|
16
|
+
* interface (not merely the first non-internal one, which can land on a
|
|
17
|
+
* bridge/VPN). The last resort; DHCP-bound.
|
|
18
|
+
* 4. Loopback — nothing routable enumerated.
|
|
19
|
+
*
|
|
20
|
+
* The resolver (resolveStableHost) is a pure function of injected inputs so the
|
|
21
|
+
* ladder is testable under mocked interface / tailscale states; the probes that
|
|
22
|
+
* gather those inputs are impure and bounded, and degrade to undefined rather
|
|
23
|
+
* than throw when a tool is missing or slow.
|
|
24
|
+
*/
|
|
25
|
+
import { execFileSync } from 'node:child_process';
|
|
26
|
+
import { hostname as osHostname, networkInterfaces } from 'node:os';
|
|
27
|
+
|
|
28
|
+
export type StableHostKind = 'tailscale-magicdns' | 'mdns-local' | 'gateway-interface' | 'loopback';
|
|
29
|
+
|
|
30
|
+
export interface TailscaleState {
|
|
31
|
+
/** BackendState === 'Running'. */
|
|
32
|
+
readonly up: boolean;
|
|
33
|
+
/** Self.DNSName from `tailscale status --json`, trailing dot stripped. */
|
|
34
|
+
readonly magicDnsName?: string | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface StableHostInputs {
|
|
38
|
+
/** os.hostname(), possibly an FQDN — the `.local` name derives from its first label. */
|
|
39
|
+
readonly hostname: string;
|
|
40
|
+
/** The routable IPv4 on the default-route interface, when known. */
|
|
41
|
+
readonly gatewayInterfaceIp?: string | undefined;
|
|
42
|
+
/** First non-internal IPv4 in enumeration order — the legacy fallback address. */
|
|
43
|
+
readonly firstNonInternalIp?: string | undefined;
|
|
44
|
+
/** Tailscale state, when the CLI is present and answered in time. */
|
|
45
|
+
readonly tailscale?: TailscaleState | undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ResolvedStableHost {
|
|
49
|
+
readonly host: string;
|
|
50
|
+
readonly kind: StableHostKind;
|
|
51
|
+
/** True when `host` is a name that survives a DHCP lease change. */
|
|
52
|
+
readonly stable: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function stripTrailingDot(name: string): string {
|
|
56
|
+
return name.replace(/\.+$/, '');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `<hostname>.local` from the first label of a hostname, or null when there is no usable label. */
|
|
60
|
+
export function mdnsLocalName(host: string): string | null {
|
|
61
|
+
const label = (host ?? '').trim().split('.')[0]?.trim();
|
|
62
|
+
if (!label || label.toLowerCase() === 'localhost') return null;
|
|
63
|
+
// Already a `.local` name (hostname was set to the mDNS form): keep it as-is.
|
|
64
|
+
if (host.trim().toLowerCase().endsWith('.local')) return stripTrailingDot(host.trim());
|
|
65
|
+
return `${label}.local`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the stable host from injected inputs. Pure — no I/O — so the ladder is
|
|
70
|
+
* exercised directly under mocked interface / tailscale states.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveStableHost(inputs: StableHostInputs): ResolvedStableHost {
|
|
73
|
+
const magic = inputs.tailscale?.up ? inputs.tailscale.magicDnsName?.trim() : undefined;
|
|
74
|
+
if (magic) return { host: stripTrailingDot(magic), kind: 'tailscale-magicdns', stable: true };
|
|
75
|
+
|
|
76
|
+
const local = mdnsLocalName(inputs.hostname);
|
|
77
|
+
if (local) return { host: local, kind: 'mdns-local', stable: true };
|
|
78
|
+
|
|
79
|
+
const routed = inputs.gatewayInterfaceIp?.trim() || inputs.firstNonInternalIp?.trim();
|
|
80
|
+
if (routed) return { host: routed, kind: 'gateway-interface', stable: false };
|
|
81
|
+
|
|
82
|
+
return { host: '127.0.0.1', kind: 'loopback', stable: false };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** First non-internal IPv4 in enumeration order (the legacy fallback address). */
|
|
86
|
+
export function firstNonInternalIpv4(nets: ReturnType<typeof networkInterfaces>): string | undefined {
|
|
87
|
+
for (const name of Object.keys(nets)) {
|
|
88
|
+
for (const netInfo of nets[name] ?? []) {
|
|
89
|
+
if (netInfo.family === 'IPv4' && !netInfo.internal) return netInfo.address;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Parse the `src <ip>` field out of a Linux `ip route get` line. */
|
|
96
|
+
export function parseIpRouteSrc(output: string): string | undefined {
|
|
97
|
+
const match = /\bsrc\s+(\d+\.\d+\.\d+\.\d+)/.exec(output);
|
|
98
|
+
return match?.[1];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Parse `Self.DNSName` + running state out of `tailscale status --json` output. */
|
|
102
|
+
export function parseTailscaleStatus(json: string): TailscaleState | undefined {
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(json) as { BackendState?: unknown; Self?: { DNSName?: unknown } };
|
|
105
|
+
const up = parsed.BackendState === 'Running';
|
|
106
|
+
const dnsName = typeof parsed.Self?.DNSName === 'string' ? stripTrailingDot(parsed.Self.DNSName) : undefined;
|
|
107
|
+
return { up, ...(dnsName ? { magicDnsName: dnsName } : {}) };
|
|
108
|
+
} catch {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function probeGatewayInterfaceIp(): string | undefined {
|
|
114
|
+
try {
|
|
115
|
+
const out = execFileSync('ip', ['route', 'get', '1.1.1.1'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 1500 });
|
|
116
|
+
return parseIpRouteSrc(out);
|
|
117
|
+
} catch {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function probeTailscaleState(): TailscaleState | undefined {
|
|
123
|
+
try {
|
|
124
|
+
const out = execFileSync('tailscale', ['status', '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 1500 });
|
|
125
|
+
return parseTailscaleStatus(out);
|
|
126
|
+
} catch {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Gather the impure inputs the resolver needs, each bounded and failure-tolerant. */
|
|
132
|
+
export function probeStableHostInputs(): StableHostInputs {
|
|
133
|
+
let nets: ReturnType<typeof networkInterfaces> = {};
|
|
134
|
+
try {
|
|
135
|
+
nets = networkInterfaces();
|
|
136
|
+
} catch {
|
|
137
|
+
nets = {};
|
|
138
|
+
}
|
|
139
|
+
const first = firstNonInternalIpv4(nets);
|
|
140
|
+
const gateway = probeGatewayInterfaceIp();
|
|
141
|
+
const tailscale = probeTailscaleState();
|
|
142
|
+
return {
|
|
143
|
+
hostname: safeHostname(),
|
|
144
|
+
...(gateway ? { gatewayInterfaceIp: gateway } : {}),
|
|
145
|
+
...(first ? { firstNonInternalIp: first } : {}),
|
|
146
|
+
...(tailscale ? { tailscale } : {}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function safeHostname(): string {
|
|
151
|
+
try {
|
|
152
|
+
return osHostname();
|
|
153
|
+
} catch {
|
|
154
|
+
return '';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The stable host for a printed/QR link given the endpoint's bind host. A
|
|
160
|
+
* wildcard bind (0.0.0.0 / ::) resolves through the ladder; an explicit bind
|
|
161
|
+
* host is honored as-is (loopback stays loopback). `probe` is injectable for
|
|
162
|
+
* tests.
|
|
163
|
+
*/
|
|
164
|
+
export function stableUrlHostForBindHost(
|
|
165
|
+
host: string,
|
|
166
|
+
probe: () => StableHostInputs = probeStableHostInputs,
|
|
167
|
+
): ResolvedStableHost {
|
|
168
|
+
if (host === '0.0.0.0' || host === '::') return resolveStableHost(probe());
|
|
169
|
+
const bound = host || '127.0.0.1';
|
|
170
|
+
return { host: bound, kind: bound === '127.0.0.1' ? 'loopback' : 'gateway-interface', stable: false };
|
|
171
|
+
}
|
|
@@ -24,6 +24,13 @@ export interface StreamMetrics {
|
|
|
24
24
|
activeToolStartedAtMs: number | undefined;
|
|
25
25
|
/** Name of the currently executing tool; cleared when execution completes. */
|
|
26
26
|
activeToolName: string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* callId of the currently executing tool call; cleared when execution
|
|
29
|
+
* completes. This is the real orchestrator callId (from TOOL_EXECUTING),
|
|
30
|
+
* so a per-tool cancel affordance targets exactly the running call — never
|
|
31
|
+
* the synthetic 'live' render id.
|
|
32
|
+
*/
|
|
33
|
+
activeToolCallId: string | undefined;
|
|
27
34
|
/**
|
|
28
35
|
* Epoch ms of the most recent STREAM_START or STREAM_DELTA; undefined when
|
|
29
36
|
* idle (no turn in flight). Read every render frame — not just on the
|
|
@@ -49,6 +56,16 @@ export interface StreamMetrics {
|
|
|
49
56
|
reconnectMaxAttempts: number | undefined;
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
/** The idle initial StreamMetrics — mutated in place by wireStreamEventMetrics handlers. */
|
|
60
|
+
export function createStreamMetrics(): StreamMetrics {
|
|
61
|
+
return {
|
|
62
|
+
startTime: 0, deltaCount: 0, tokenSpeed: 0, ttftMs: undefined, ttftRecorded: false,
|
|
63
|
+
activeToolStartedAtMs: undefined, activeToolName: undefined, activeToolCallId: undefined,
|
|
64
|
+
lastDeltaAtMs: undefined, stallEpisode: 0,
|
|
65
|
+
reconnectAttempt: undefined, reconnectMaxAttempts: undefined,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
/** Minimal orchestrator surface required for stream token-speed calculation. */
|
|
53
70
|
interface StreamOrchestrator {
|
|
54
71
|
readonly streamingOutputTokens: number;
|
|
@@ -115,11 +132,11 @@ function isStreamRetryLikePayload(payload: unknown): payload is StreamRetryLikeP
|
|
|
115
132
|
|
|
116
133
|
/**
|
|
117
134
|
* Minimal cost lookup surface for attaching cost-delta information to failover notices.
|
|
118
|
-
* Returns USD-per-1M-token pricing for the given model ID
|
|
119
|
-
*
|
|
135
|
+
* Returns USD-per-1M-token pricing for the given model ID, or null when the
|
|
136
|
+
* catalog honestly does not know the model (never a fabricated zero).
|
|
120
137
|
*/
|
|
121
138
|
export interface FailoverCostLookup {
|
|
122
|
-
getCostFromCatalog(modelId: string): { readonly input: number; readonly output: number };
|
|
139
|
+
getCostFromCatalog(modelId: string): { readonly input: number; readonly output: number } | null;
|
|
123
140
|
}
|
|
124
141
|
|
|
125
142
|
export interface WireStreamEventMetricsOptions {
|
|
@@ -219,9 +236,11 @@ function buildCostDeltaSuffix(
|
|
|
219
236
|
// Registry key format: `provider:modelId` — modelId may itself contain `:`.
|
|
220
237
|
const fromModelId = fromRegistryKey ? fromRegistryKey.split(':').slice(1).join(':') : '';
|
|
221
238
|
const toModelId = toRegistryKey.split(':').slice(1).join(':');
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
239
|
+
// A null catalog answer means honestly unknown — treated exactly like the
|
|
240
|
+
// legacy zero-pricing sentinel below.
|
|
241
|
+
const fromCost = (fromModelId ? lookup.getCostFromCatalog(fromModelId) : null) ?? { input: 0, output: 0 };
|
|
242
|
+
const toCost = lookup.getCostFromCatalog(toModelId) ?? { input: 0, output: 0 };
|
|
243
|
+
// Report unavailable when either side has no pricing (unknown model).
|
|
225
244
|
if (fromCost.input === 0 && fromCost.output === 0 && !fromModelId) {
|
|
226
245
|
return ' [cost data unavailable]';
|
|
227
246
|
}
|
|
@@ -441,6 +460,7 @@ export function wireStreamEventMetrics(
|
|
|
441
460
|
unsubs.push(events.tools.on('TOOL_EXECUTING', (ev) => {
|
|
442
461
|
metrics.activeToolStartedAtMs = ev.startedAt;
|
|
443
462
|
metrics.activeToolName = ev.tool;
|
|
463
|
+
metrics.activeToolCallId = ev.callId;
|
|
444
464
|
render();
|
|
445
465
|
}));
|
|
446
466
|
// On every tool-completion path, reset lastDeltaAtMs to "now" so the
|
|
@@ -454,16 +474,19 @@ export function wireStreamEventMetrics(
|
|
|
454
474
|
unsubs.push(events.tools.on('TOOL_SUCCEEDED', () => {
|
|
455
475
|
metrics.activeToolStartedAtMs = undefined;
|
|
456
476
|
metrics.activeToolName = undefined;
|
|
477
|
+
metrics.activeToolCallId = undefined;
|
|
457
478
|
metrics.lastDeltaAtMs = Date.now();
|
|
458
479
|
}));
|
|
459
480
|
unsubs.push(events.tools.on('TOOL_FAILED', () => {
|
|
460
481
|
metrics.activeToolStartedAtMs = undefined;
|
|
461
482
|
metrics.activeToolName = undefined;
|
|
483
|
+
metrics.activeToolCallId = undefined;
|
|
462
484
|
metrics.lastDeltaAtMs = Date.now();
|
|
463
485
|
}));
|
|
464
486
|
unsubs.push(events.tools.on('TOOL_CANCELLED', () => {
|
|
465
487
|
metrics.activeToolStartedAtMs = undefined;
|
|
466
488
|
metrics.activeToolName = undefined;
|
|
489
|
+
metrics.activeToolCallId = undefined;
|
|
467
490
|
metrics.lastDeltaAtMs = Date.now();
|
|
468
491
|
}));
|
|
469
492
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* source these rules catch is emitted (from the SDK) through
|
|
5
5
|
* SystemMessageRouter, so this one funnel is where first-run plumbing is kept
|
|
6
6
|
* out of the user's stream while the information stays reachable elsewhere
|
|
7
|
-
* (fleet panel, /health, /model, activity log). (
|
|
7
|
+
* (fleet panel, /health, /model, activity log). (item 1.)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
export interface NoiseGateDeps {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* that target — plus whether a panel is attached — into a { toPanel,
|
|
10
10
|
* toConversation } decision.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* (the purge): the SystemMessagesPanel this router used to optionally
|
|
13
13
|
* push into was DELETE-disposition (no surviving human surface — a picker
|
|
14
14
|
* over the old panel registry, not something worth a dedicated console) and
|
|
15
15
|
* has been removed entirely, so this router now always resolves with
|
|
@@ -61,7 +61,7 @@ export type {
|
|
|
61
61
|
SystemMessageTarget,
|
|
62
62
|
} from '@/runtime/index.ts';
|
|
63
63
|
|
|
64
|
-
/** Panel emphasis level. Panel delivery was removed in
|
|
64
|
+
/** Panel emphasis level. Panel delivery was removed in (see file doc); kept as the priority vocabulary for callers and for the SDK's delivery-resolution signature. */
|
|
65
65
|
export type SystemMessagePriority = 'high' | 'low';
|
|
66
66
|
|
|
67
67
|
/**
|
|
@@ -88,7 +88,7 @@ function mustReachConversation(message: string): boolean {
|
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
90
|
* Routes system messages to the conversation based on priority level and
|
|
91
|
-
* configured target. See file doc for the
|
|
91
|
+
* configured target. See file doc for the panel removal.
|
|
92
92
|
*/
|
|
93
93
|
export class SystemMessageRouter {
|
|
94
94
|
/** Buffered provider "from last session" replay lines, folded on a microtask. */
|
|
@@ -123,7 +123,7 @@ export class SystemMessageRouter {
|
|
|
123
123
|
kind: SystemMessageKind,
|
|
124
124
|
): void {
|
|
125
125
|
// Noise gate — keep first-run plumbing out of the transcript while the
|
|
126
|
-
// information stays reachable via other live surfaces. (
|
|
126
|
+
// information stays reachable via other live surfaces. (item 1.)
|
|
127
127
|
const verdict = classifyNoise(message, this.noiseDeps);
|
|
128
128
|
if (verdict.action === 'drop') return;
|
|
129
129
|
if (verdict.action === 'foldProviderReplay') {
|
|
@@ -137,7 +137,7 @@ export class SystemMessageRouter {
|
|
|
137
137
|
/** Post-noise-gate delivery: resolve target and append to the conversation. */
|
|
138
138
|
private deliver(message: string, kind: SystemMessageKind): void {
|
|
139
139
|
const target = this.getTargetForKind(kind);
|
|
140
|
-
// hasPanel is always false
|
|
140
|
+
// hasPanel is always false now that panel delivery was removed — resolveSystemMessageDelivery's own
|
|
141
141
|
// contract means every target ('panel' | 'conversation' | 'both')
|
|
142
142
|
// resolves toConversation: true in that case (see file doc).
|
|
143
143
|
const delivery = resolveSystemMessageDelivery(target, false);
|
|
@@ -153,7 +153,7 @@ export class SystemMessageRouter {
|
|
|
153
153
|
* Buffer a provider "from last session" replay line and schedule a microtask
|
|
154
154
|
* flush. The SDK emits the whole persisted-provider burst synchronously, so a
|
|
155
155
|
* single microtask captures the full burst and folds it to one quiet line.
|
|
156
|
-
* (
|
|
156
|
+
* (item 1b.)
|
|
157
157
|
*/
|
|
158
158
|
private bufferProviderReplay(message: string): void {
|
|
159
159
|
this.providerReplayBuffer.push(message);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turn-budget-outcome.ts — render a turn-budget exhaustion as an honest budget
|
|
3
|
+
* line, distinct from an infrastructure failure.
|
|
4
|
+
*
|
|
5
|
+
* When a member agent spends its whole turn budget the SDK stamps a typed
|
|
6
|
+
* outcome — failureKind 'max_turns' with the ceiling that applied and which
|
|
7
|
+
* input set it — instead of leaving consumers to regex a prose message. This
|
|
8
|
+
* renders that typed outcome in plain language: a budget was reached (not a
|
|
9
|
+
* crash), with the number and where the number came from.
|
|
10
|
+
*/
|
|
11
|
+
export type TurnBudgetSource = 'default' | 'spawn-override' | 'policy-bound';
|
|
12
|
+
|
|
13
|
+
/** Plain-language provenance for a turn ceiling. */
|
|
14
|
+
export function describeTurnBudgetSource(source: TurnBudgetSource | undefined): string {
|
|
15
|
+
switch (source) {
|
|
16
|
+
case 'spawn-override':
|
|
17
|
+
return 'a per-spawn override';
|
|
18
|
+
case 'policy-bound':
|
|
19
|
+
return 'the policy cap (agents.maxTurnsCap)';
|
|
20
|
+
case 'default':
|
|
21
|
+
return 'the default limit (agents.maxTurns)';
|
|
22
|
+
default:
|
|
23
|
+
return 'the configured turn limit';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TurnBudgetOutcome {
|
|
28
|
+
readonly limit?: number | undefined;
|
|
29
|
+
readonly source?: TurnBudgetSource | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A short budget line: "reached its turn budget — 50 turns (the default limit
|
|
34
|
+
* (agents.maxTurns))". Honest about the fact that the run stopped because it hit
|
|
35
|
+
* a ceiling, not because something broke.
|
|
36
|
+
*/
|
|
37
|
+
export function formatTurnBudgetOutcome(outcome: TurnBudgetOutcome): string {
|
|
38
|
+
const source = describeTurnBudgetSource(outcome.source);
|
|
39
|
+
return outcome.limit !== undefined
|
|
40
|
+
? `reached its turn budget — ${outcome.limit} turn${outcome.limit === 1 ? '' : 's'} (${source})`
|
|
41
|
+
: `reached its turn budget (${source})`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The machine-readable failure reason the SDK stamps on a turn-budget exhaustion. */
|
|
45
|
+
export const TURN_BUDGET_FAILURE_REASON = 'max_turns';
|
|
46
|
+
|
|
47
|
+
export function isTurnBudgetReason(reason: string | null | undefined): boolean {
|
|
48
|
+
return reason === TURN_BUDGET_FAILURE_REASON;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Render a failure-reason string for a surface that only has the reason (not the
|
|
53
|
+
* typed limit/source event fields), mapping the machine-readable 'max_turns' to
|
|
54
|
+
* a plain budget phrase distinct from an infrastructure failure. Any other
|
|
55
|
+
* reason passes through unchanged.
|
|
56
|
+
*/
|
|
57
|
+
export function describeFailureReason(reason: string | null | undefined): string {
|
|
58
|
+
if (!reason) return '';
|
|
59
|
+
return isTurnBudgetReason(reason) ? 'reached its turn budget' : reason;
|
|
60
|
+
}
|
|
@@ -5,7 +5,7 @@ import type { SpokenTurnRuntime } from '../audio/spoken-turn-wiring.ts';
|
|
|
5
5
|
* Builds the shared "cancel the active turn" action: stops any in-flight
|
|
6
6
|
* spoken output, then aborts the orchestrator's turn if one is actively
|
|
7
7
|
* streaming. Extracted from main.ts so the file can stay under its 800-line
|
|
8
|
-
* cap while still wiring `commandContext.isGenerating` (see
|
|
8
|
+
* cap while still wiring `commandContext.isGenerating` (see FIX 1).
|
|
9
9
|
*
|
|
10
10
|
* Returns whether SPEECH was actively stopped — a Ctrl+C that silenced live
|
|
11
11
|
* TTS is consumed by that job (an earlier replay fix), same as a press that
|
|
@@ -23,3 +23,42 @@ export function createCancelGeneration(
|
|
|
23
23
|
return stoppedSpeech;
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
/** The narrow orchestrator surface the per-tool cancel needs. */
|
|
28
|
+
export interface ToolCancelOrchestrator {
|
|
29
|
+
/** Cancel ONE in-flight tool call; false when no such call is running. */
|
|
30
|
+
cancelToolCall(callId: string): boolean;
|
|
31
|
+
/** The callIds of tool calls currently in flight. */
|
|
32
|
+
listRunningToolCalls(): readonly string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Builds the "cancel the running tool call" action: stops JUST the currently-
|
|
37
|
+
* running tool call, leaving the turn to continue. The in-process orchestrator
|
|
38
|
+
* holds the per-call AbortController — this is the local-session equivalent of
|
|
39
|
+
* the sessions.toolCalls.cancel wire verb. The cancelled call settles as a
|
|
40
|
+
* structured "cancelled by user" result (partial output preserved) the model
|
|
41
|
+
* adapts to in the same turn.
|
|
42
|
+
*
|
|
43
|
+
* Target selection is honest: the tracked active callId (the live transcript
|
|
44
|
+
* row) when known, else the sole in-flight call when exactly one is running,
|
|
45
|
+
* else nothing (returns false — never guesses which of several to stop).
|
|
46
|
+
* `onCancelled` fires only on a real cancellation, with the callId stopped.
|
|
47
|
+
*/
|
|
48
|
+
export function createCancelToolCall(
|
|
49
|
+
orchestrator: ToolCancelOrchestrator,
|
|
50
|
+
getActiveToolCallId: () => string | undefined,
|
|
51
|
+
onCancelled: (callId: string) => void,
|
|
52
|
+
): () => boolean {
|
|
53
|
+
return () => {
|
|
54
|
+
let target = getActiveToolCallId();
|
|
55
|
+
if (target === undefined) {
|
|
56
|
+
const running = orchestrator.listRunningToolCalls();
|
|
57
|
+
if (running.length === 1) target = running[0];
|
|
58
|
+
}
|
|
59
|
+
if (target === undefined) return false;
|
|
60
|
+
const cancelled = orchestrator.cancelToolCall(target);
|
|
61
|
+
if (cancelled) onCancelled(target);
|
|
62
|
+
return cancelled;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { UiRuntimeEvents, RuntimeEventBus } from '@/runtime/index.ts';
|
|
2
2
|
import { buildPersistedSessionContext, persistConversation } from '@/runtime/index.ts';
|
|
3
3
|
import { buildCompactionReceiptBlock } from './compaction-receipt.ts';
|
|
4
|
+
import { formatTurnBudgetOutcome } from './turn-budget-outcome.ts';
|
|
4
5
|
import { persistTurnAnchors, recordTurnAnchor, summarizeTurnLabel } from './rewind-turn-anchors.ts';
|
|
5
6
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
6
7
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
@@ -78,12 +79,12 @@ export interface WireTurnEventHandlersOptions {
|
|
|
78
79
|
*/
|
|
79
80
|
readonly webhookNotifier?: WebhookNotifier | null;
|
|
80
81
|
/**
|
|
81
|
-
* Terminal focus tracker
|
|
82
|
+
* Terminal focus tracker. Gates the long-task, budget-breach, and
|
|
82
83
|
* agent/chain-failure desktop alerts wired in this module — see
|
|
83
|
-
* alert-gating.ts. Optional; when absent, none of the new
|
|
84
|
-
* behavior is gated by focus (long-task keeps its
|
|
85
|
-
* behavior, and budget-breach/failure alerts are
|
|
86
|
-
*
|
|
84
|
+
* alert-gating.ts. Optional; when absent, none of the new alert
|
|
85
|
+
* behavior is gated by focus (long-task keeps its always-fire
|
|
86
|
+
* behavior from before focus gating existed, and budget-breach/failure alerts are
|
|
87
|
+
* skipped entirely, since they have no pre-existing unconditional-fire path
|
|
87
88
|
* to fall back to).
|
|
88
89
|
*/
|
|
89
90
|
readonly focusTracker?: Pick<FocusTracker, 'shouldAlertWhenUnfocused'> | null;
|
|
@@ -152,7 +153,7 @@ export function wireTurnEventHandlers(
|
|
|
152
153
|
// Track turn start time for long-task notification threshold.
|
|
153
154
|
let turnStartTime: number | null = null;
|
|
154
155
|
|
|
155
|
-
// Budget-breach edge-trigger checker
|
|
156
|
+
// Budget-breach edge-trigger checker — one instance per session,
|
|
156
157
|
// piggybacking on the same TURN_COMPLETED handler as the long-task
|
|
157
158
|
// notification below rather than adding a second TURN_COMPLETED subscription.
|
|
158
159
|
const budgetBreachNotifier: BudgetBreachNotifier | null = focusTracker
|
|
@@ -193,7 +194,7 @@ export function wireTurnEventHandlers(
|
|
|
193
194
|
focusTracker,
|
|
194
195
|
configGet,
|
|
195
196
|
});
|
|
196
|
-
// Budget-breach alert
|
|
197
|
+
// Budget-breach alert: edge-triggered, piggybacks on this same
|
|
197
198
|
// TURN_COMPLETED handler rather than a second subscription.
|
|
198
199
|
if (budgetBreachNotifier) {
|
|
199
200
|
const sessionModel = providerRegistry.getCurrentModel().id ?? 'unknown';
|
|
@@ -264,7 +265,7 @@ export function wireTurnEventHandlers(
|
|
|
264
265
|
refreshGit();
|
|
265
266
|
}));
|
|
266
267
|
|
|
267
|
-
// Agent/chain-failure desktop alerts
|
|
268
|
+
// Agent/chain-failure desktop alerts. The SDK's WebhookNotifier and
|
|
268
269
|
// Notifier already fire webhook/Slack/Discord notifications for these two
|
|
269
270
|
// events unconditionally (attachToRuntimeBus in the SDK's
|
|
270
271
|
// platform/integrations — pre-existing, not focus-gated: an out-of-band
|
|
@@ -290,6 +291,11 @@ export function wireTurnEventHandlers(
|
|
|
290
291
|
// chain/owner/cohort surfaces.
|
|
291
292
|
if (payload.failureKind === 'cancelled') {
|
|
292
293
|
notifyCompletion('GoodVibes — WRFC chain cancelled', `chain ${payload.chainId.slice(0, 12)} cancelled: ${payload.reason}`, FORCE_NOTIFY_DURATION_MS);
|
|
294
|
+
} else if (payload.failureKind === 'max_turns') {
|
|
295
|
+
// A turn-budget exhaustion is a spent ceiling, not an infrastructure
|
|
296
|
+
// failure — narrate the limit and where it came from, from the typed
|
|
297
|
+
// event fields, never a regex of the prose reason.
|
|
298
|
+
notifyCompletion('GoodVibes — WRFC chain hit its turn budget', `chain ${payload.chainId.slice(0, 12)} ${formatTurnBudgetOutcome({ limit: payload.turnLimit, source: payload.turnLimitSource })}`, FORCE_NOTIFY_DURATION_MS);
|
|
293
299
|
} else {
|
|
294
300
|
const kindLabel = payload.failureKind === 'transport' ? 'transient transport error' : payload.reason;
|
|
295
301
|
notifyCompletion('GoodVibes — WRFC chain failed', `chain ${payload.chainId.slice(0, 12)} failed: ${kindLabel}`, FORCE_NOTIFY_DURATION_MS);
|