@pellux/goodvibes-agent 1.5.5 → 1.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +1 -1
- package/dist/package/main.js +94130 -79335
- package/docs/tools-and-commands.md +2 -1
- package/package.json +6 -3
- package/src/agent/autonomy-schedule-format.ts +1 -1
- package/src/agent/autonomy-schedule.ts +3 -3
- package/src/agent/calendar/calendar-oauth-service.ts +324 -0
- package/src/agent/calendar-subscription-registry.ts +497 -0
- package/src/agent/email/email-service.ts +57 -0
- package/src/agent/email/smtp-client.ts +30 -10
- package/src/agent/memory-prompt.ts +143 -12
- package/src/agent/operator-actions.ts +26 -22
- package/src/agent/prompt-context-receipts.ts +45 -18
- package/src/agent/reminder-schedule-format.ts +1 -1
- package/src/agent/reminder-schedule.ts +3 -3
- package/src/agent/routine-schedule-format.ts +2 -2
- package/src/agent/routine-schedule-promotion.ts +5 -5
- package/src/agent/routine-schedule-receipts.ts +2 -2
- package/src/agent/schedule-edit-format.ts +1 -1
- package/src/agent/schedule-edit.ts +5 -5
- package/src/agent/session-registration.ts +281 -0
- package/src/agent/vibe-file.ts +50 -0
- package/src/cli/local-library-command-shared.ts +146 -0
- package/src/cli/management.ts +2 -1
- package/src/cli/personas-command.ts +252 -0
- package/src/cli/resume-relaunch-notice.ts +151 -0
- package/src/cli/routines-command.ts +25 -2
- package/src/cli/service-posture.ts +3 -2
- package/src/cli/skill-bundle-command.ts +175 -0
- package/src/cli/skills-command.ts +309 -0
- package/src/cli/status.ts +43 -7
- package/src/cli/tui-startup.ts +24 -1
- package/src/config/agent-settings-policy.ts +0 -1
- package/src/config/secret-config.ts +4 -0
- package/src/core/conversation-rendering.ts +20 -11
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/system-message-noise.ts +108 -0
- package/src/core/system-message-router.ts +54 -1
- package/src/core/thinking-overlay.ts +83 -0
- package/src/input/agent-workspace-access-command-editor-submission.ts +75 -108
- package/src/input/agent-workspace-access-command-editors.ts +108 -128
- package/src/input/agent-workspace-activation.ts +15 -0
- package/src/input/agent-workspace-basic-command-editor-submission.ts +292 -513
- package/src/input/agent-workspace-basic-command-editors.ts +433 -564
- package/src/input/agent-workspace-calendar-connect-editor.ts +116 -0
- package/src/input/agent-workspace-calendar-oauth-editor.ts +152 -0
- package/src/input/agent-workspace-calendar-subscribe-editor.ts +128 -0
- package/src/input/agent-workspace-categories.ts +7 -2
- package/src/input/agent-workspace-channel-command-editor-submission.ts +38 -52
- package/src/input/agent-workspace-channel-command-editors.ts +42 -46
- package/src/input/agent-workspace-command-editor-engine.ts +133 -0
- package/src/input/agent-workspace-command-editor.ts +4 -0
- package/src/input/agent-workspace-direct-editor-submission.ts +59 -0
- package/src/input/agent-workspace-email-connect-editor.ts +162 -0
- package/src/input/agent-workspace-knowledge-command-editor-submission.ts +52 -81
- package/src/input/agent-workspace-knowledge-command-editors.ts +71 -71
- package/src/input/agent-workspace-library-command-editor-submission.ts +10 -28
- package/src/input/agent-workspace-library-command-editors.ts +16 -2
- package/src/input/agent-workspace-live-counters.ts +55 -0
- package/src/input/agent-workspace-mcp-command-editor-submission.ts +60 -82
- package/src/input/agent-workspace-mcp-command-editors.ts +52 -0
- package/src/input/agent-workspace-media-command-editor-submission.ts +12 -39
- package/src/input/agent-workspace-media-command-editors.ts +10 -8
- package/src/input/agent-workspace-memory-command-editor-submission.ts +76 -151
- package/src/input/agent-workspace-memory-command-editors.ts +116 -141
- package/src/input/agent-workspace-operations-command-editor-submission.ts +131 -184
- package/src/input/agent-workspace-operations-command-editors.ts +150 -162
- package/src/input/agent-workspace-provider-command-editor-submission.ts +60 -106
- package/src/input/agent-workspace-provider-command-editors.ts +58 -68
- package/src/input/agent-workspace-search.ts +8 -1
- package/src/input/agent-workspace-session-command-editor-submission.ts +104 -132
- package/src/input/agent-workspace-session-command-editors.ts +160 -195
- package/src/input/agent-workspace-settings.ts +40 -1
- package/src/input/agent-workspace-skill-bundle-command-editor-submission.ts +55 -71
- package/src/input/agent-workspace-skill-bundle-command-editors.ts +65 -69
- package/src/input/agent-workspace-snapshot-builders.ts +431 -0
- package/src/input/agent-workspace-snapshot-config.ts +43 -0
- package/src/input/agent-workspace-snapshot.ts +198 -432
- package/src/input/agent-workspace-task-command-editor-submission.ts +34 -44
- package/src/input/agent-workspace-task-command-editors.ts +27 -28
- package/src/input/agent-workspace-types.ts +35 -1
- package/src/input/agent-workspace.ts +5 -14
- package/src/input/command-registry.ts +23 -1
- package/src/input/commands/calendar-connect-runtime.ts +226 -0
- package/src/input/commands/calendar-runtime.ts +110 -7
- package/src/input/commands/calendar-subscription-runtime.ts +225 -0
- package/src/input/commands/email-runtime.ts +100 -40
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/network-scan-runtime.ts +75 -0
- package/src/input/commands/operator-actions-runtime.ts +9 -6
- package/src/input/commands/personas-runtime.ts +1 -1
- package/src/input/commands/schedule-runtime.ts +4 -4
- package/src/input/commands/session-content.ts +13 -1
- package/src/input/commands/session-workflow.ts +15 -20
- package/src/input/commands/session.ts +3 -1
- package/src/input/commands/shell-core.ts +20 -6
- package/src/input/commands.ts +2 -2
- package/src/input/delete-key-policy.ts +46 -0
- package/src/input/feed-context-factory.ts +10 -0
- package/src/input/handler-feed.ts +87 -0
- package/src/input/handler-modal-routes.ts +6 -1
- package/src/input/handler.ts +5 -0
- package/src/input/panel-paste-flood-guard.ts +94 -0
- package/src/input/settings-modal-types.ts +5 -3
- package/src/input/settings-modal.ts +21 -0
- package/src/main.ts +36 -36
- package/src/permissions/approval-posture.ts +141 -0
- package/src/renderer/agent-workspace-context-lines.ts +10 -2
- package/src/renderer/compositor.ts +27 -4
- package/src/renderer/diff.ts +61 -18
- package/src/renderer/fullscreen-primitives.ts +37 -18
- package/src/renderer/markdown.ts +20 -10
- package/src/renderer/modal-factory.ts +25 -15
- package/src/renderer/overlay-box.ts +23 -12
- package/src/renderer/process-indicator.ts +8 -3
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +2 -0
- package/src/renderer/startup-theme-probe.ts +35 -0
- package/src/renderer/status-glyphs.ts +11 -15
- package/src/renderer/system-message.ts +17 -2
- package/src/renderer/term-caps.ts +318 -0
- package/src/renderer/terminal-bg-probe.ts +373 -0
- package/src/renderer/terminal-escapes.ts +24 -0
- package/src/renderer/theme-mode-config.ts +87 -0
- package/src/renderer/theme.ts +241 -0
- package/src/renderer/thinking.ts +12 -3
- package/src/renderer/tool-call.ts +7 -3
- package/src/renderer/ui-factory.ts +92 -36
- package/src/renderer/ui-primitives.ts +33 -93
- package/src/runtime/bootstrap-core.ts +15 -0
- package/src/runtime/bootstrap-hook-bridge.ts +6 -0
- package/src/runtime/bootstrap-shell.ts +2 -0
- package/src/runtime/bootstrap.ts +68 -5
- package/src/runtime/calendar-boot-refresh.ts +105 -0
- package/src/runtime/lan-scan-consent.ts +253 -0
- package/src/runtime/services.ts +127 -2
- package/src/runtime/session-spine-rest-transport.ts +160 -0
- package/src/runtime/terminal-output-guard.ts +6 -1
- package/src/runtime/ui-services.ts +3 -0
- package/src/shell/agent-workspace-fullscreen.ts +5 -0
- package/src/shell/terminal-focus-mode.ts +120 -0
- package/src/shell/ui-openers.ts +13 -4
- package/src/tools/agent-harness-autonomy-intake.ts +5 -5
- package/src/tools/agent-harness-autonomy-queue.ts +1 -1
- package/src/tools/agent-harness-background-processes.ts +2 -1
- package/src/tools/agent-harness-metadata.ts +16 -8
- package/src/tools/agent-harness-operator-methods.ts +44 -8
- package/src/tools/agent-harness-personal-ops-discovery.ts +57 -10
- package/src/tools/agent-harness-personal-ops-lanes.ts +17 -5
- package/src/tools/agent-harness-personal-ops-operations.ts +7 -7
- package/src/tools/agent-harness-personal-ops-types.ts +6 -0
- package/src/tools/agent-harness-prompt-context.ts +26 -12
- package/src/tools/agent-harness-workspace-actions.ts +4 -0
- package/src/tools/agent-local-registry-args.ts +117 -0
- package/src/tools/agent-local-registry-memory.ts +227 -0
- package/src/tools/agent-local-registry-tool.ts +19 -237
- package/src/tools/agent-operator-briefing-tool.ts +2 -2
- package/src/tools/agent-operator-method-tool.ts +13 -0
- package/src/tools/agent-policy-explanation.ts +39 -4
- package/src/tools/agent-schedule-tool.ts +5 -5
- package/src/utils/terminal-width.ts +98 -1
- package/src/version.ts +1 -1
- package/src/cli/local-library-command.ts +0 -825
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAN-scan consent gate (Wave-4 A7).
|
|
3
|
+
*
|
|
4
|
+
* The SDK's background provider-discovery pass (`startBackgroundProviderDiscovery`
|
|
5
|
+
* in `@pellux/goodvibes-sdk/platform/runtime`) probes every host on this
|
|
6
|
+
* computer's local subnet (254 addresses per non-internal network interface)
|
|
7
|
+
* looking for local model servers such as Ollama or LM Studio, then dumps one
|
|
8
|
+
* raw "[Scan] Found <name> at <host>:<port>" line per server straight into the
|
|
9
|
+
* activity feed. It ran unconditionally, with no consent step, and no way to
|
|
10
|
+
* turn it off.
|
|
11
|
+
*
|
|
12
|
+
* This module is the only place in the Agent that is allowed to call the SDK
|
|
13
|
+
* discovery entry point. It:
|
|
14
|
+
* 1. Gates the call behind a persisted, explicit consent decision. The very
|
|
15
|
+
* first time the Agent starts, nothing is scanned; the user is told what
|
|
16
|
+
* the feature does, what it touches (the local subnet — nothing beyond
|
|
17
|
+
* it), and what it stores, then the decision defaults to "off" until the
|
|
18
|
+
* user turns it on.
|
|
19
|
+
* 2. Reframes whatever the SDK emits when consent has been granted: instead
|
|
20
|
+
* of one raw host:port line per server, the Agent surfaces a single
|
|
21
|
+
* framed summary ("Discovered N local model servers ..."), with full
|
|
22
|
+
* detail available on demand via /provider.
|
|
23
|
+
*
|
|
24
|
+
* The SDK scanner itself is not modified — see
|
|
25
|
+
* node_modules/@pellux/goodvibes-sdk/dist/platform/discovery/scanner.js and
|
|
26
|
+
* node_modules/@pellux/goodvibes-sdk/dist/platform/runtime/bootstrap-background.js.
|
|
27
|
+
*/
|
|
28
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
29
|
+
import { dirname } from 'node:path';
|
|
30
|
+
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
31
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
32
|
+
import {
|
|
33
|
+
startBackgroundProviderRegistration,
|
|
34
|
+
type BackgroundProviderDiscoveryOptions,
|
|
35
|
+
type BackgroundRuntimeTaskHandle,
|
|
36
|
+
type HostSystemMessageSink,
|
|
37
|
+
type ShellPathService,
|
|
38
|
+
} from '@/runtime/index.ts';
|
|
39
|
+
|
|
40
|
+
export type LanScanDecision = 'granted' | 'declined';
|
|
41
|
+
|
|
42
|
+
export interface LanScanConsentState {
|
|
43
|
+
readonly decision: LanScanDecision;
|
|
44
|
+
readonly decidedAt: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type ConsentRoots = Pick<ShellPathService, 'resolveUserPath'>;
|
|
48
|
+
|
|
49
|
+
const CONSENT_FILE_NAME = 'network-discovery-consent.json';
|
|
50
|
+
const DISCOVERED_PROVIDERS_FILE = 'discovered-providers.json';
|
|
51
|
+
|
|
52
|
+
function consentFilePath(roots: ConsentRoots, surfaceRoot: string): string {
|
|
53
|
+
return roots.resolveUserPath(surfaceRoot, CONSENT_FILE_NAME);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The path the SDK persists discovered servers to, for display in user-facing
|
|
58
|
+
* copy (this module does not read or write that file — the SDK owns it).
|
|
59
|
+
*/
|
|
60
|
+
export function discoveredProvidersFilePath(roots: ConsentRoots, surfaceRoot: string): string {
|
|
61
|
+
return roots.resolveUserPath(surfaceRoot, DISCOVERED_PROVIDERS_FILE);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Load the persisted LAN-scan consent decision.
|
|
66
|
+
* Returns undefined when no decision has ever been recorded (true first run).
|
|
67
|
+
*/
|
|
68
|
+
export function loadLanScanConsent(roots: ConsentRoots, surfaceRoot: string): LanScanConsentState | undefined {
|
|
69
|
+
const path = consentFilePath(roots, surfaceRoot);
|
|
70
|
+
try {
|
|
71
|
+
if (!existsSync(path)) return undefined;
|
|
72
|
+
const raw = readFileSync(path, 'utf-8');
|
|
73
|
+
const parsed = JSON.parse(raw) as Partial<LanScanConsentState> | null;
|
|
74
|
+
if (!parsed || (parsed.decision !== 'granted' && parsed.decision !== 'declined')) {
|
|
75
|
+
logger.warn('[LanScanConsent] ignoring invalid consent file', { path });
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
decision: parsed.decision,
|
|
80
|
+
decidedAt: typeof parsed.decidedAt === 'string' ? parsed.decidedAt : new Date(0).toISOString(),
|
|
81
|
+
};
|
|
82
|
+
} catch (err) {
|
|
83
|
+
logger.warn('[LanScanConsent] load failed; treating as undecided', { path, error: summarizeError(err) });
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Persist the user's LAN-scan consent decision (explicit grant, decline, or the first-run default). */
|
|
89
|
+
export function saveLanScanConsent(roots: ConsentRoots, surfaceRoot: string, decision: LanScanDecision): void {
|
|
90
|
+
const path = consentFilePath(roots, surfaceRoot);
|
|
91
|
+
try {
|
|
92
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
93
|
+
const state: LanScanConsentState = { decision, decidedAt: new Date().toISOString() };
|
|
94
|
+
writeFileSync(path, `${JSON.stringify(state, null, 2)}\n`, 'utf-8');
|
|
95
|
+
} catch (err) {
|
|
96
|
+
logger.warn('[LanScanConsent] save failed; decision will not persist', { path, error: summarizeError(err) });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Wording ──────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
export const NETWORK_SCAN_ENABLE_COMMAND = '/network-scan on';
|
|
103
|
+
export const NETWORK_SCAN_DISABLE_COMMAND = '/network-scan off';
|
|
104
|
+
export const NETWORK_SCAN_STATUS_COMMAND = '/network-scan';
|
|
105
|
+
|
|
106
|
+
/** Shown exactly once — the very first time the Agent starts and no decision has been recorded yet. */
|
|
107
|
+
export function firstRunConsentMessage(roots: ConsentRoots, surfaceRoot: string): string {
|
|
108
|
+
const storePath = discoveredProvidersFilePath(roots, surfaceRoot);
|
|
109
|
+
return (
|
|
110
|
+
'[Scan] Local network scanning for model servers is off by default. ' +
|
|
111
|
+
'If turned on, it checks other devices on this computer\'s local network (your subnet only — nothing beyond it) ' +
|
|
112
|
+
'for model servers such as Ollama or LM Studio, so they can be added as chat providers. ' +
|
|
113
|
+
`Servers it finds are saved to ${storePath}. ` +
|
|
114
|
+
`Turn it on with ${NETWORK_SCAN_ENABLE_COMMAND}; nothing is scanned until you do.`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* F3 fix: the first-run explanation (firstRunConsentMessage) already told the
|
|
120
|
+
* user this is off and how to turn it on. Printing that reminder again on
|
|
121
|
+
* EVERY subsequent boot forever — with no way to quiet it short of turning
|
|
122
|
+
* the feature on — was the friction: an indefinitely repeating notice for a
|
|
123
|
+
* state the user already chose. The off state is still fully discoverable on
|
|
124
|
+
* demand via `/network-scan status`; boot itself stays silent about it from
|
|
125
|
+
* the second run onward. The granted-path behavior (framed scan summary) is
|
|
126
|
+
* unchanged.
|
|
127
|
+
*/
|
|
128
|
+
|
|
129
|
+
// ── Output framing ───────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
const FOUND_OR_RESTORED_RE = /\bat\s+\S+:\d+\s+\(\d+\s+models?\)/;
|
|
132
|
+
const REMOVED_RE = /\bat\s+\S+:\d+\s+is no longer reachable\b/;
|
|
133
|
+
const RAW_IP_PAREN_RE = /\s*\(\d{1,3}(?:\.\d{1,3}){3}\)/g;
|
|
134
|
+
|
|
135
|
+
/** Strips a bare "(1.2.3.4)" parenthetical from an otherwise-fine message, defense in depth. */
|
|
136
|
+
function stripRawIpMention(message: string): string {
|
|
137
|
+
return message.replace(RAW_IP_PAREN_RE, '');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Wraps the real system-message sink + requestRender callback so that the raw
|
|
142
|
+
* per-server "[Scan] Found X at host:port" / "[Local] X at host:port ... from
|
|
143
|
+
* last session" / "[Scan] X at host:port is no longer reachable" lines the SDK
|
|
144
|
+
* emits are never forwarded verbatim. Instead they are counted, and a single
|
|
145
|
+
* framed summary line is emitted the next time requestRender fires — which the
|
|
146
|
+
* SDK reliably calls exactly once per batch of emitted messages (see
|
|
147
|
+
* bootstrap-background.js), so this stays in lockstep with the underlying
|
|
148
|
+
* scan/restore passes without needing to touch the SDK.
|
|
149
|
+
*/
|
|
150
|
+
function createFramedDiscoverySink(
|
|
151
|
+
realRouter: HostSystemMessageSink,
|
|
152
|
+
realRequestRender: () => void,
|
|
153
|
+
): { readonly sink: HostSystemMessageSink; readonly requestRender: () => void } {
|
|
154
|
+
let foundCount = 0;
|
|
155
|
+
let removedCount = 0;
|
|
156
|
+
|
|
157
|
+
const sink: HostSystemMessageSink = {
|
|
158
|
+
low(message: string) {
|
|
159
|
+
if (FOUND_OR_RESTORED_RE.test(message)) {
|
|
160
|
+
foundCount += 1;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (REMOVED_RE.test(message)) {
|
|
164
|
+
removedCount += 1;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
realRouter.low(stripRawIpMention(message));
|
|
168
|
+
},
|
|
169
|
+
high(message: string) {
|
|
170
|
+
realRouter.high(stripRawIpMention(message));
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const requestRender = () => {
|
|
175
|
+
if (foundCount > 0) {
|
|
176
|
+
realRouter.low(
|
|
177
|
+
`[Scan] Discovered ${foundCount} local model server${foundCount === 1 ? '' : 's'} on this network — run /provider to see them.`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (removedCount > 0) {
|
|
181
|
+
realRouter.low(
|
|
182
|
+
`[Scan] ${removedCount} previously found server${removedCount === 1 ? '' : 's'} no longer reachable — removed.`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
foundCount = 0;
|
|
186
|
+
removedCount = 0;
|
|
187
|
+
realRequestRender();
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return { sink, requestRender };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function noopHandle(): BackgroundRuntimeTaskHandle {
|
|
194
|
+
let stopped = false;
|
|
195
|
+
return {
|
|
196
|
+
get stopped() {
|
|
197
|
+
return stopped;
|
|
198
|
+
},
|
|
199
|
+
stop() {
|
|
200
|
+
stopped = true;
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface LanScanGateOptions extends BackgroundProviderDiscoveryOptions {
|
|
206
|
+
readonly shellPaths: BackgroundProviderDiscoveryOptions['shellPaths'] & ConsentRoots;
|
|
207
|
+
/**
|
|
208
|
+
* Injectable seam for tests. Defaults to the real SDK discovery pass
|
|
209
|
+
* (`startBackgroundProviderRegistration`, which performs a real subnet
|
|
210
|
+
* scan). Tests must supply a fake here instead of exercising the real
|
|
211
|
+
* network — never let a test fall through to the default.
|
|
212
|
+
*/
|
|
213
|
+
readonly startDiscovery?: (opts: BackgroundProviderDiscoveryOptions) => BackgroundRuntimeTaskHandle;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The single gated entry point to LAN provider discovery. Bootstrap's Phase 8
|
|
218
|
+
* and the /network-scan command are the only two callers; neither calls the
|
|
219
|
+
* SDK's startBackgroundProviderRegistration/startBackgroundProviderDiscovery
|
|
220
|
+
* directly.
|
|
221
|
+
*
|
|
222
|
+
* - No decision recorded (first run): shows the full consent explanation,
|
|
223
|
+
* persists "declined" as the honest first-run default, and returns without
|
|
224
|
+
* scanning.
|
|
225
|
+
* - Decision is "declined" (F3): stays silent and returns without scanning —
|
|
226
|
+
* the first-run boot already explained the off state and how to turn it
|
|
227
|
+
* on; the ongoing state is discoverable on demand via `/network-scan
|
|
228
|
+
* status`, not repeated on every boot.
|
|
229
|
+
* - Decision is "granted": runs the SDK discovery pass through a framing
|
|
230
|
+
* wrapper that replaces the raw per-server feed dump with a single summary
|
|
231
|
+
* line.
|
|
232
|
+
*/
|
|
233
|
+
export function runGatedLanScan(options: LanScanGateOptions): BackgroundRuntimeTaskHandle {
|
|
234
|
+
const { shellPaths, surfaceRoot, systemMessageRouter, requestRender, startDiscovery = startBackgroundProviderRegistration } = options;
|
|
235
|
+
const existing = loadLanScanConsent(shellPaths, surfaceRoot);
|
|
236
|
+
|
|
237
|
+
if (!existing) {
|
|
238
|
+
saveLanScanConsent(shellPaths, surfaceRoot, 'declined');
|
|
239
|
+
systemMessageRouter.low(firstRunConsentMessage(shellPaths, surfaceRoot));
|
|
240
|
+
return noopHandle();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (existing.decision === 'declined') {
|
|
244
|
+
return noopHandle();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const framed = createFramedDiscoverySink(systemMessageRouter, requestRender);
|
|
248
|
+
return startDiscovery({
|
|
249
|
+
...options,
|
|
250
|
+
systemMessageRouter: framed.sink,
|
|
251
|
+
requestRender: framed.requestRender,
|
|
252
|
+
});
|
|
253
|
+
}
|
package/src/runtime/services.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
|
3
3
|
import { shell as runtimeShell } from '@pellux/goodvibes-sdk/platform/runtime';
|
|
4
4
|
import type { shell as RuntimeShell } from '@pellux/goodvibes-sdk/platform/runtime';
|
|
5
5
|
import { SecretsManager } from '../config/secrets.ts';
|
|
6
|
+
import { FocusTracker } from '../core/focus-tracker.ts';
|
|
6
7
|
import { ServiceRegistry } from '@pellux/goodvibes-sdk/platform/config';
|
|
7
8
|
import { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
8
9
|
import { AutomationDeliveryManager, AutomationManager, AutomationRouteStore } from '@pellux/goodvibes-sdk/platform/automation';
|
|
@@ -10,6 +11,13 @@ import { ChannelPluginRegistry, ChannelPolicyManager, RouteBindingManager, Surfa
|
|
|
10
11
|
import { ChannelDeliveryRouter } from '@pellux/goodvibes-sdk/platform/channels';
|
|
11
12
|
import { ApprovalBroker, GatewayMethodCatalog, SharedSessionBroker } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
12
13
|
import type { SharedSessionRoutingIntent } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
14
|
+
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
15
|
+
import { AGENT_SPINE_PARTICIPANT, SessionSpineClient } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
|
|
16
|
+
import {
|
|
17
|
+
createSpineConnectionResolver,
|
|
18
|
+
createSpineRestProbe,
|
|
19
|
+
createSpineRestTransport,
|
|
20
|
+
} from './session-spine-rest-transport.ts';
|
|
13
21
|
import { WatcherRegistry } from '@pellux/goodvibes-sdk/platform/watchers';
|
|
14
22
|
import { ArtifactStore } from '@pellux/goodvibes-sdk/platform/artifacts';
|
|
15
23
|
import {
|
|
@@ -32,11 +40,19 @@ import { AgentMessageBus } from '@pellux/goodvibes-sdk/platform/agents';
|
|
|
32
40
|
import { WrfcController } from '@pellux/goodvibes-sdk/platform/agents';
|
|
33
41
|
import { AgentOrchestrator } from '@pellux/goodvibes-sdk/platform/agents';
|
|
34
42
|
import { ArchetypeLoader } from '@pellux/goodvibes-sdk/platform/agents';
|
|
43
|
+
import { CodeIndexStore } from '@pellux/goodvibes-sdk/platform/state';
|
|
44
|
+
import { CodeIndexReindexScheduler } from '@pellux/goodvibes-sdk/platform/state';
|
|
45
|
+
import { createProcessRegistry } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
|
|
46
|
+
import { createOrchestrationEngine } from '@pellux/goodvibes-sdk/platform/orchestration';
|
|
47
|
+
import { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
|
|
35
48
|
import { ProcessManager } from '@pellux/goodvibes-sdk/platform/tools';
|
|
36
49
|
import { ModeManager } from '@pellux/goodvibes-sdk/platform/state';
|
|
37
50
|
import { FileUndoManager } from '@pellux/goodvibes-sdk/platform/state';
|
|
38
51
|
import { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
|
|
39
52
|
import { MemoryStore } from '@pellux/goodvibes-sdk/platform/state';
|
|
53
|
+
// W6-C2 (E6): one canonical cross-surface memory store + no-loss legacy fold.
|
|
54
|
+
import { resolveCanonicalMemoryDbPath, foldMemoryStores } from '@pellux/goodvibes-sdk/platform/state';
|
|
55
|
+
import type { LegacyMemorySource, MemoryFoldReport } from '@pellux/goodvibes-sdk/platform/state';
|
|
40
56
|
import type { RuntimeEventBus } from '@/runtime/index.ts';
|
|
41
57
|
import { createDomainDispatch } from './store/index.ts';
|
|
42
58
|
import type { DomainDispatch, RuntimeStore } from './store/index.ts';
|
|
@@ -228,7 +244,11 @@ function createDisabledAgentWrfcWorktreeOps(): AgentWrfcWorktreeOps {
|
|
|
228
244
|
async cleanup(_agentId: string): Promise<void> {
|
|
229
245
|
throw agentWrfcWorktreeError('cleanup');
|
|
230
246
|
},
|
|
231
|
-
|
|
247
|
+
// No explicit return-type annotation: this always throws before returning,
|
|
248
|
+
// so it structurally satisfies whatever CommitWorkingTreeResult shape the
|
|
249
|
+
// SDK's WrfcController#createWorktree option currently declares (derived
|
|
250
|
+
// via AgentWrfcWorktreeOps above) without importing that SDK-internal type.
|
|
251
|
+
async commitWorkingTree(_message: string) {
|
|
232
252
|
throw agentWrfcWorktreeError('commit');
|
|
233
253
|
},
|
|
234
254
|
async currentHead(): Promise<string | null> {
|
|
@@ -395,6 +415,7 @@ export interface RuntimeServices extends SdkRuntimeServices {
|
|
|
395
415
|
readonly watcherRegistry: WatcherRegistry;
|
|
396
416
|
readonly approvalBroker: ApprovalBroker;
|
|
397
417
|
readonly sessionBroker: SharedSessionBroker;
|
|
418
|
+
readonly sessionSpineClient: SessionSpineClient;
|
|
398
419
|
readonly deliveryManager: AutomationDeliveryManager;
|
|
399
420
|
readonly automationManager: AutomationManager;
|
|
400
421
|
readonly gatewayMethods: GatewayMethodCatalog;
|
|
@@ -435,6 +456,8 @@ export interface RuntimeServices extends SdkRuntimeServices {
|
|
|
435
456
|
readonly worktreeRegistry: WorktreeRegistry;
|
|
436
457
|
readonly sandboxSessionRegistry: SandboxSessionRegistry;
|
|
437
458
|
readonly webhookNotifier: WebhookNotifier;
|
|
459
|
+
/** W4-R3 — OS-level terminal focus tracker, ported from goodvibes-tui's W2.3 (core/focus-tracker.ts). */
|
|
460
|
+
readonly focusTracker: FocusTracker;
|
|
438
461
|
readonly replayEngine: DeterministicReplayEngine;
|
|
439
462
|
readonly providerOptimizer: ProviderOptimizer;
|
|
440
463
|
readonly providerCapabilityRegistry: ProviderCapabilityRegistry;
|
|
@@ -590,6 +613,21 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
590
613
|
agentStatusProvider: agentManager,
|
|
591
614
|
messageSender: agentMessageBus,
|
|
592
615
|
});
|
|
616
|
+
// W3-A1: the SDK's extracted session-spine core, consumed via this surface's
|
|
617
|
+
// own REST transport adapter (session-spine-rest-transport.ts) — version-
|
|
618
|
+
// tolerant, since the agent may compile against a pinned SDK predating the
|
|
619
|
+
// typed sessions.register client. Live-immediately mode: passing `transport`
|
|
620
|
+
// at construction starts the keepalive now, for the whole process lifetime
|
|
621
|
+
// (no separate activate() step — the agent has none). Client-only; never
|
|
622
|
+
// starts the daemon. All calls are fire-and-forget; a down daemon degrades to
|
|
623
|
+
// an honest offline queue while the local broker keeps rendering.
|
|
624
|
+
const spineResolveConnection = createSpineConnectionResolver(configManager, homeDirectory);
|
|
625
|
+
const sessionSpineClient = new SessionSpineClient({
|
|
626
|
+
participant: AGENT_SPINE_PARTICIPANT,
|
|
627
|
+
transport: createSpineRestTransport({ resolveConnection: spineResolveConnection }),
|
|
628
|
+
probe: createSpineRestProbe({ resolveConnection: spineResolveConnection }),
|
|
629
|
+
log: logger,
|
|
630
|
+
});
|
|
593
631
|
sessionBroker.setContinuationRunner(async ({ task, input }) => {
|
|
594
632
|
const record = agentManager.spawn({
|
|
595
633
|
mode: 'spawn',
|
|
@@ -601,7 +639,11 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
601
639
|
});
|
|
602
640
|
const artifactStore = new ArtifactStore({ configManager });
|
|
603
641
|
const memoryEmbeddingRegistry = new MemoryEmbeddingProviderRegistry({ configManager });
|
|
604
|
-
|
|
642
|
+
// W6-C2 (E6): the agent no longer owns a private per-surface memory.sqlite. It opens
|
|
643
|
+
// the ONE canonical cross-surface store so a fact learned here recalls in the TUI (and
|
|
644
|
+
// vice-versa). The old agent-global path is folded in at boot with no loss (see
|
|
645
|
+
// foldAgentLegacyMemory), then left in place — migration never deletes.
|
|
646
|
+
const memoryDbPath = resolveCanonicalMemoryDbPath(shellPaths.homeDirectory);
|
|
605
647
|
const memoryStore = new MemoryStore(memoryDbPath, {
|
|
606
648
|
embeddingRegistry: memoryEmbeddingRegistry,
|
|
607
649
|
});
|
|
@@ -737,6 +779,10 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
737
779
|
const worktreeRegistry = createDisabledAgentWorktreeRegistry(workingDirectory);
|
|
738
780
|
// Configured and attached to the runtime bus during bootstrap when webhook URLs are present.
|
|
739
781
|
const webhookNotifier = new WebhookNotifier();
|
|
782
|
+
// W4-R3 — one shared instance for the process lifetime (mirrors goodvibes-tui's
|
|
783
|
+
// runtime/services.ts); fed from 'focus' tokens in handler-feed.ts and read by
|
|
784
|
+
// the approval-alert wiring in main.ts.
|
|
785
|
+
const focusTracker = new FocusTracker();
|
|
740
786
|
const replayEngine = new DeterministicReplayEngine(workingDirectory);
|
|
741
787
|
const providerOptimizer = new ProviderOptimizer(providerRegistry, providerCapabilityRegistry, false);
|
|
742
788
|
const sessionMemoryStore = new SessionMemoryStore();
|
|
@@ -820,12 +866,70 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
820
866
|
workflowServices: workflow,
|
|
821
867
|
});
|
|
822
868
|
|
|
869
|
+
// ── One-Platform Wave 0: RuntimeServices members required by SDK 0.38 ──────
|
|
870
|
+
// The Agent is delegation-only: it does not own local build/worktree work.
|
|
871
|
+
// These are constructed as real-but-inert SDK services purely to satisfy the
|
|
872
|
+
// RuntimeServices contract — none auto-start:
|
|
873
|
+
// • orchestrationEngine — never .start()ed here (delegation goes through
|
|
874
|
+
// /delegate to the TUI); worktree isolation is
|
|
875
|
+
// omitted so any accidental run degrades to shared.
|
|
876
|
+
// • codeIndexStore — constructed but neither schema-initialized nor
|
|
877
|
+
// auto-built; the Agent runs no repo source-tree
|
|
878
|
+
// code index (inert unless explicitly invoked).
|
|
879
|
+
// • codeIndexReindexScheduler — real-but-inert twin of the above: isEnabled
|
|
880
|
+
// is a permanent `() => false`, so tool-driven
|
|
881
|
+
// reindex scheduling (Stage B, TUI-only feature)
|
|
882
|
+
// never fires for the Agent.
|
|
883
|
+
// • processRegistry — fleet observability over the managers the Agent
|
|
884
|
+
// already owns (agents, wrfc, processes, watchers).
|
|
885
|
+
// • workspaceCheckpointManager — constructed without a runtimeBus so it
|
|
886
|
+
// never auto-subscribes to turn events / snapshots
|
|
887
|
+
// the workspace; inert unless explicitly invoked.
|
|
888
|
+
const orchestrationEngine = createOrchestrationEngine({
|
|
889
|
+
agentManager,
|
|
890
|
+
configManager,
|
|
891
|
+
runtimeBus: options.runtimeBus,
|
|
892
|
+
projectRoot: workingDirectory,
|
|
893
|
+
});
|
|
894
|
+
const codeIndexStore = new CodeIndexStore(
|
|
895
|
+
workingDirectory,
|
|
896
|
+
join(workingDirectory, '.goodvibes', 'agent', 'code-index.sqlite'),
|
|
897
|
+
memoryEmbeddingRegistry,
|
|
898
|
+
);
|
|
899
|
+
const codeIndexReindexScheduler = new CodeIndexReindexScheduler({
|
|
900
|
+
target: codeIndexStore,
|
|
901
|
+
workingDirectory,
|
|
902
|
+
isEnabled: () => false,
|
|
903
|
+
});
|
|
904
|
+
const processRegistry = createProcessRegistry({
|
|
905
|
+
agentManager,
|
|
906
|
+
wrfcController,
|
|
907
|
+
orchestrationEngine,
|
|
908
|
+
codeIndexService: codeIndexStore,
|
|
909
|
+
processManager,
|
|
910
|
+
watcherRegistry,
|
|
911
|
+
workflow,
|
|
912
|
+
approvalBroker,
|
|
913
|
+
sessionBroker,
|
|
914
|
+
messageBus: agentMessageBus,
|
|
915
|
+
automationManager,
|
|
916
|
+
runtimeBus: options.runtimeBus,
|
|
917
|
+
});
|
|
918
|
+
const workspaceCheckpointManager = new WorkspaceCheckpointManager({
|
|
919
|
+
workspaceRoot: workingDirectory,
|
|
920
|
+
});
|
|
921
|
+
|
|
823
922
|
return {
|
|
824
923
|
workingDirectory,
|
|
825
924
|
homeDirectory,
|
|
826
925
|
shellPaths,
|
|
827
926
|
configManager,
|
|
828
927
|
featureFlags,
|
|
928
|
+
orchestrationEngine,
|
|
929
|
+
codeIndexStore,
|
|
930
|
+
codeIndexReindexScheduler,
|
|
931
|
+
processRegistry,
|
|
932
|
+
workspaceCheckpointManager,
|
|
829
933
|
runtimeBus: options.runtimeBus,
|
|
830
934
|
runtimeStore: options.runtimeStore,
|
|
831
935
|
runtimeDispatch,
|
|
@@ -838,6 +942,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
838
942
|
watcherRegistry,
|
|
839
943
|
approvalBroker,
|
|
840
944
|
sessionBroker,
|
|
945
|
+
sessionSpineClient,
|
|
841
946
|
deliveryManager,
|
|
842
947
|
automationManager,
|
|
843
948
|
gatewayMethods,
|
|
@@ -877,6 +982,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
877
982
|
worktreeRegistry,
|
|
878
983
|
sandboxSessionRegistry,
|
|
879
984
|
webhookNotifier,
|
|
985
|
+
focusTracker,
|
|
880
986
|
replayEngine,
|
|
881
987
|
providerOptimizer,
|
|
882
988
|
providerCapabilityRegistry,
|
|
@@ -912,3 +1018,22 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
912
1018
|
},
|
|
913
1019
|
};
|
|
914
1020
|
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* W6-C2 (E6): fold the agent's legacy per-surface memory store into the canonical
|
|
1024
|
+
* cross-surface store. Called once at boot AFTER `memoryStore.init()` so any records
|
|
1025
|
+
* written before unification survive. Id-keyed and idempotent — a re-run imports
|
|
1026
|
+
* nothing new and never deletes the legacy file. Returns the report so boot can log
|
|
1027
|
+
* exactly what moved (nothing is silently dropped).
|
|
1028
|
+
*/
|
|
1029
|
+
export async function foldAgentLegacyMemory(
|
|
1030
|
+
memoryStore: MemoryStore,
|
|
1031
|
+
memoryEmbeddingRegistry: MemoryEmbeddingProviderRegistry,
|
|
1032
|
+
shellPaths: ShellPathService,
|
|
1033
|
+
): Promise<MemoryFoldReport> {
|
|
1034
|
+
const legacyAgentGlobal = shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'memory.sqlite');
|
|
1035
|
+
const sources: LegacyMemorySource[] = [
|
|
1036
|
+
{ label: 'agent-global (pre-E6)', dbPath: legacyAgentGlobal },
|
|
1037
|
+
];
|
|
1038
|
+
return foldMemoryStores(memoryStore, sources, { embeddingRegistry: memoryEmbeddingRegistry });
|
|
1039
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-spine-rest-transport.ts
|
|
3
|
+
*
|
|
4
|
+
* The agent's REST transport adapter for the SDK's extracted session-spine core
|
|
5
|
+
* (`@pellux/goodvibes-sdk/platform/runtime/session-spine`). The core is
|
|
6
|
+
* transport-agnostic and never assumes a typed sessions client exists — that is
|
|
7
|
+
* exactly why the agent, which compiles against a pinned/version-tolerant npm
|
|
8
|
+
* SDK that may predate `sessions.register`, supplies this hand-rolled REST
|
|
9
|
+
* mirror instead of a typed client (see
|
|
10
|
+
* docs/decisions/2026-07-05-session-spine-sdk-extraction.md, divergence ruling
|
|
11
|
+
* #1). Do not switch this adapter to a typed client.
|
|
12
|
+
*
|
|
13
|
+
* Also agent-local (per the same decision record, ruling #6 and the SDK-side
|
|
14
|
+
* rejection of "pulling token-reading into the SDK"):
|
|
15
|
+
* - `createSpineConnectionResolver`: builds the daemon base URL from
|
|
16
|
+
* ConfigManager and reads the connected-host operator token via
|
|
17
|
+
* ../runtime/connected-host-auth.ts (home-dir-specific, trust-boundary-bound
|
|
18
|
+
* — the SDK core never reads token files; it only ever receives a
|
|
19
|
+
* pre-built resolver through this adapter).
|
|
20
|
+
* - register/close/probe TIMEOUTS: the SDK core no longer makes the wire
|
|
21
|
+
* call (transport does), so it has nothing to time out. Timeouts are a REST
|
|
22
|
+
* implementation concern and live here, in the adapter that owns fetch.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
26
|
+
import type { RegisterSharedSessionInput } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
27
|
+
import type { SpineResult, SpineTransport } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
|
|
28
|
+
import {
|
|
29
|
+
postSessionClose,
|
|
30
|
+
postSessionRegister,
|
|
31
|
+
type SessionCloseResult,
|
|
32
|
+
type SessionRegisterResult,
|
|
33
|
+
type SessionRegistrationConnection,
|
|
34
|
+
type SessionRegistrationInput,
|
|
35
|
+
} from '../agent/session-registration.ts';
|
|
36
|
+
import { readConnectedHostOperatorToken } from './connected-host-auth.ts';
|
|
37
|
+
|
|
38
|
+
const DEFAULT_REGISTER_TIMEOUT_MS = 1_500;
|
|
39
|
+
const DEFAULT_CLOSE_TIMEOUT_MS = 500;
|
|
40
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 1_500;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Builds a per-op connection resolver from the config manager (host/port,
|
|
44
|
+
* default 127.0.0.1:3421) and the connected-host operator token (env
|
|
45
|
+
* overrides + the canonical operator-tokens.json). Re-read every op so a
|
|
46
|
+
* token that appears after boot is picked up.
|
|
47
|
+
*/
|
|
48
|
+
export function createSpineConnectionResolver(
|
|
49
|
+
configManager: Pick<ConfigManager, 'get'>,
|
|
50
|
+
homeDirectory: string,
|
|
51
|
+
): () => SessionRegistrationConnection {
|
|
52
|
+
return () => {
|
|
53
|
+
const hostValue = configManager.get('controlPlane.host');
|
|
54
|
+
const portValue = configManager.get('controlPlane.port');
|
|
55
|
+
const host = typeof hostValue === 'string' && hostValue.trim().length > 0 ? hostValue.trim() : '127.0.0.1';
|
|
56
|
+
const port = typeof portValue === 'number' && Number.isFinite(portValue) ? portValue : 3421;
|
|
57
|
+
const token = readConnectedHostOperatorToken(homeDirectory);
|
|
58
|
+
return { baseUrl: `http://${host}:${port}`, token: token.token, tokenPath: token.path };
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Maps the SDK's canonical register input onto the REST mirror's wire shape. No `kind` field: the REST mirror always stamps SESSION_REGISTER_KIND server-side (see session-registration.ts), matching the SDK's `recordKind` left unset for this participant. */
|
|
63
|
+
function toRegistrationInput(input: RegisterSharedSessionInput): SessionRegistrationInput {
|
|
64
|
+
return {
|
|
65
|
+
sessionId: input.sessionId,
|
|
66
|
+
...(input.project ? { project: input.project } : {}),
|
|
67
|
+
...(input.title ? { title: input.title } : {}),
|
|
68
|
+
participant: {
|
|
69
|
+
surfaceKind: input.participant.surfaceKind,
|
|
70
|
+
surfaceId: input.participant.surfaceId,
|
|
71
|
+
...(input.participant.externalId ? { externalId: input.participant.externalId } : {}),
|
|
72
|
+
...(input.participant.userId ? { userId: input.participant.userId } : {}),
|
|
73
|
+
...(input.participant.displayName ? { displayName: input.participant.displayName } : {}),
|
|
74
|
+
...(input.participant.routeId ? { routeId: input.participant.routeId } : {}),
|
|
75
|
+
lastSeenAt: input.participant.lastSeenAt,
|
|
76
|
+
},
|
|
77
|
+
...(input.reopen ? { reopen: true } : {}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Folds the REST mirror's richer failure vocabulary onto the SDK core's
|
|
83
|
+
* ok/offline/rejected outcome (decision-record ruling #5): `ok` stays `ok`;
|
|
84
|
+
* `connected_host_unavailable` (a transient connectivity fault) becomes
|
|
85
|
+
* `offline` so the core enqueues for reconnect replay; every DURABLE refusal
|
|
86
|
+
* (`auth_required`, `connected_host_route_unavailable`, `connected_host_error`)
|
|
87
|
+
* becomes `rejected` so the core logs it and never enqueues it (no
|
|
88
|
+
* retry-forever).
|
|
89
|
+
*/
|
|
90
|
+
function foldResult(result: SessionRegisterResult | SessionCloseResult): SpineResult {
|
|
91
|
+
if (result.ok) return { outcome: 'ok' };
|
|
92
|
+
if (result.kind === 'connected_host_unavailable') return { outcome: 'offline', error: result.error };
|
|
93
|
+
return { outcome: 'rejected', error: result.error };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface SpineRestTransportOptions {
|
|
97
|
+
readonly resolveConnection: () => SessionRegistrationConnection;
|
|
98
|
+
readonly registerTimeoutMs?: number;
|
|
99
|
+
readonly closeTimeoutMs?: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The agent's version-tolerant REST transport injected into the SDK
|
|
104
|
+
* `SessionSpineClient` at construction (live-immediately mode). Wraps
|
|
105
|
+
* postSessionRegister/postSessionClose and folds their result kinds onto
|
|
106
|
+
* {@link SpineResult}.
|
|
107
|
+
*/
|
|
108
|
+
export function createSpineRestTransport(options: SpineRestTransportOptions): SpineTransport {
|
|
109
|
+
const registerTimeoutMs = options.registerTimeoutMs ?? DEFAULT_REGISTER_TIMEOUT_MS;
|
|
110
|
+
const closeTimeoutMs = options.closeTimeoutMs ?? DEFAULT_CLOSE_TIMEOUT_MS;
|
|
111
|
+
return {
|
|
112
|
+
async register(input: RegisterSharedSessionInput): Promise<SpineResult> {
|
|
113
|
+
const connection = options.resolveConnection();
|
|
114
|
+
const result = await postSessionRegister(connection, toRegistrationInput(input), { timeoutMs: registerTimeoutMs });
|
|
115
|
+
return foldResult(result);
|
|
116
|
+
},
|
|
117
|
+
async close(sessionId: string): Promise<SpineResult> {
|
|
118
|
+
const connection = options.resolveConnection();
|
|
119
|
+
const result = await postSessionClose(connection, sessionId, { timeoutMs: closeTimeoutMs });
|
|
120
|
+
return foldResult(result);
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function defaultProbe(connection: SessionRegistrationConnection, timeoutMs: number): Promise<boolean> {
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
128
|
+
try {
|
|
129
|
+
// Any HTTP response (even 401) means the host answered -> reachable. Auth is
|
|
130
|
+
// a separate concern surfaced by register/close results, not the probe.
|
|
131
|
+
await fetch(`${connection.baseUrl}/status`, {
|
|
132
|
+
headers: connection.token ? { authorization: `Bearer ${connection.token}` } : undefined,
|
|
133
|
+
signal: controller.signal,
|
|
134
|
+
});
|
|
135
|
+
return true;
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
} finally {
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface SpineRestProbeOptions {
|
|
144
|
+
readonly resolveConnection: () => SessionRegistrationConnection;
|
|
145
|
+
readonly probeTimeoutMs?: number;
|
|
146
|
+
/** Override for tests; default does a short GET {baseUrl}/status. */
|
|
147
|
+
readonly probeImpl?: (connection: SessionRegistrationConnection, timeoutMs: number) => Promise<boolean>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Builds the zero-argument `probe` the SDK core calls directly from
|
|
152
|
+
* `probeReachability()` (its injected-probe shape takes no parameters — the
|
|
153
|
+
* core has no connection to hand it). This closure captures the resolver and
|
|
154
|
+
* timeout so the core stays connection-agnostic.
|
|
155
|
+
*/
|
|
156
|
+
export function createSpineRestProbe(options: SpineRestProbeOptions): () => Promise<boolean> {
|
|
157
|
+
const probeTimeoutMs = options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
158
|
+
const probeImpl = options.probeImpl ?? defaultProbe;
|
|
159
|
+
return () => probeImpl(options.resolveConnection(), probeTimeoutMs);
|
|
160
|
+
}
|
|
@@ -229,7 +229,12 @@ export function installTuiTerminalOutputGuard(options: TuiTerminalOutputGuardOpt
|
|
|
229
229
|
capturedWriteCount = 0;
|
|
230
230
|
lastNoticeAt = now;
|
|
231
231
|
const plural = count === 1 ? '' : 's';
|
|
232
|
-
|
|
232
|
+
const notice = `[Terminal] Captured ${count} direct ${event.source} write${plural} that would have corrupted the TUI: ${event.preview}`;
|
|
233
|
+
// Keep the aggregate count reachable in the activity log even though the
|
|
234
|
+
// noise gate (system-message-noise.ts) drops the notice from the Recent
|
|
235
|
+
// feed — drop-from-the-feed, not delete. (W4-R2 honest degraded state.)
|
|
236
|
+
logger.info(notice, { source: event.source, count });
|
|
237
|
+
options.notify(notice);
|
|
233
238
|
},
|
|
234
239
|
});
|
|
235
240
|
}
|
|
@@ -54,6 +54,8 @@ export interface UiPlatformServices {
|
|
|
54
54
|
readonly tokenAuditor: RuntimeServices['tokenAuditor'];
|
|
55
55
|
readonly replayEngine: RuntimeServices['replayEngine'];
|
|
56
56
|
readonly webhookNotifier: RuntimeServices['webhookNotifier'];
|
|
57
|
+
/** W4-R3 — OS-level terminal focus tracker, ported from goodvibes-tui's W2.3. */
|
|
58
|
+
readonly focusTracker: RuntimeServices['focusTracker'];
|
|
57
59
|
readonly policyRuntimeState: RuntimeServices['policyRuntimeState'];
|
|
58
60
|
readonly externalServices?: {
|
|
59
61
|
inspect(): {
|
|
@@ -165,6 +167,7 @@ export function createUiRuntimeServices(
|
|
|
165
167
|
tokenAuditor: runtimeServices.tokenAuditor,
|
|
166
168
|
replayEngine: runtimeServices.replayEngine,
|
|
167
169
|
webhookNotifier: runtimeServices.webhookNotifier,
|
|
170
|
+
focusTracker: runtimeServices.focusTracker,
|
|
168
171
|
policyRuntimeState: runtimeServices.policyRuntimeState,
|
|
169
172
|
},
|
|
170
173
|
planning: {
|