@pellux/goodvibes-agent 1.8.0 → 1.8.2
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/{ast-grep-napi.linux-x64-gnu-mkk8xwww.node → ast-grep-napi.linux-x64-gnu-44ar68xs.node} +0 -0
- package/dist/package/{ast-grep-napi.linux-x64-musl-ryqtgdv6.node → ast-grep-napi.linux-x64-musl-asvc2tv1.node} +0 -0
- package/dist/package/main.js +135434 -118764
- package/dist/package/{web-tree-sitter-vmqc5d26.wasm → web-tree-sitter-jbz042ba.wasm} +0 -0
- package/docs/tools-and-commands.md +9 -0
- package/package.json +3 -2
- package/src/agent/capability-registry.ts +300 -0
- package/src/agent/email/style-reply-lane.ts +13 -4
- package/src/agent/memory-consolidation-receipts.ts +79 -0
- package/src/agent/memory-prompt.ts +3 -1
- package/src/agent/operator-gateway-call.ts +134 -0
- package/src/agent/principal-attribution.ts +97 -0
- package/src/agent/prompt-context-receipts.ts +5 -1
- package/src/agent/unified-inbox.ts +17 -1
- package/src/cli/bundle-command.ts +2 -1
- package/src/cli/channel-profiles-command.ts +168 -0
- package/src/cli/ci-command.ts +214 -0
- package/src/cli/completion.ts +5 -0
- package/src/cli/connected-host-metrics.ts +165 -0
- package/src/cli/entrypoint.ts +25 -1
- package/src/cli/fleet-command.ts +178 -0
- package/src/cli/help.ts +115 -0
- package/src/cli/import-command.ts +208 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/management-commands.ts +9 -8
- package/src/cli/management.ts +42 -0
- package/src/cli/memory-command.ts +4 -3
- package/src/cli/openclaw-import.ts +382 -0
- package/src/cli/operator-command-args.ts +88 -0
- package/src/cli/parser.ts +12 -0
- package/src/cli/personas-command.ts +3 -2
- package/src/cli/principals-command.ts +230 -0
- package/src/cli/profiles-command.ts +2 -1
- package/src/cli/relay-command.ts +100 -0
- package/src/cli/routines-command.ts +4 -3
- package/src/cli/skill-bundle-command.ts +3 -2
- package/src/cli/skills-command.ts +3 -2
- package/src/cli/status.ts +114 -2
- package/src/cli/temporal-label.ts +89 -0
- package/src/cli/types.ts +7 -0
- package/src/cli/workspaces-command.ts +135 -0
- package/src/config/agent-settings-policy.ts +8 -0
- package/src/config/checkpoint-settings.ts +113 -0
- package/src/config/workspace-registration.ts +299 -0
- package/src/core/rewind-turn-anchors.ts +82 -0
- package/src/input/agent-workspace-categories.ts +1 -0
- package/src/input/agent-workspace-channel-triage.ts +27 -8
- package/src/input/agent-workspace-local-library-snapshot.ts +5 -1
- package/src/input/agent-workspace-onboarding-categories.ts +17 -1
- package/src/input/agent-workspace-snapshot.ts +25 -3
- package/src/input/agent-workspace-types.ts +5 -5
- package/src/input/commands/connected-host-admin-runtime.ts +75 -0
- package/src/input/commands.ts +2 -0
- package/src/input/settings-modal-types.ts +16 -5
- package/src/main.ts +17 -12
- package/src/permissions/approval-posture.ts +40 -5
- package/src/permissions/prompt.ts +35 -1
- package/src/renderer/settings-modal-helpers.ts +11 -0
- package/src/renderer/settings-modal.ts +21 -0
- package/src/renderer/terminal-escapes.ts +65 -12
- package/src/runtime/agent-runtime-events.ts +102 -3
- package/src/runtime/bootstrap-core.ts +111 -10
- package/src/runtime/bootstrap-external-services.ts +156 -0
- package/src/runtime/bootstrap.ts +73 -69
- package/src/runtime/context-accounting-source.ts +114 -0
- package/src/runtime/conversation-rewind-port.ts +133 -0
- package/src/runtime/index.ts +8 -28
- package/src/runtime/memory-consolidation-scheduler.ts +67 -0
- package/src/runtime/memory-consolidation-wiring.ts +56 -0
- package/src/runtime/memory-usage-wiring.ts +75 -0
- package/src/runtime/services.ts +257 -6
- package/src/runtime/tool-permission-safety.ts +18 -1
- package/src/shell/blocking-input.ts +38 -8
- package/src/shell/startup-wiring.ts +26 -1
- package/src/tools/agent-context-policy.ts +1 -1
- package/src/tools/agent-harness-cli-command-policy.ts +29 -3
- package/src/tools/agent-harness-learning-auto-promote.ts +7 -3
- package/src/tools/agent-harness-memory-posture.ts +57 -2
- package/src/tools/agent-harness-metadata.ts +12 -0
- package/src/tools/agent-harness-mode-catalog.ts +1 -1
- package/src/tools/agent-harness-personal-ops-lanes.ts +8 -6
- package/src/tools/agent-harness-prompt-context.ts +3 -1
- package/src/tools/agent-harness-tool.ts +3 -2
- package/src/tools/agent-policy-explanation.ts +40 -0
- package/src/tools/tool-definition-compaction.ts +2 -0
- package/src/version.ts +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ConversationManager } from '../core/conversation';
|
|
2
|
+
import { registerSessionConversation } from './conversation-rewind-port.ts';
|
|
2
3
|
import { SelectionManager } from '../input/selection.ts';
|
|
3
4
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
4
5
|
import { ConfigManager, getConfiguredSystemPrompt } from '../config/index.ts';
|
|
@@ -9,7 +10,7 @@ import { PermissionManager, createPermissionConfigReader } from '@pellux/goodvib
|
|
|
9
10
|
import { Notifier, WebhookNotifier } from '@pellux/goodvibes-sdk/platform/integrations';
|
|
10
11
|
|
|
11
12
|
import { Compositor } from '../renderer/compositor.ts';
|
|
12
|
-
import type { PermissionRequestHandler } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
13
|
+
import type { PermissionRequestHandler, PermissionPromptRequest } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
13
14
|
import type { SystemMessageRouter } from '../core/system-message-router.ts';
|
|
14
15
|
import type { ConversationFollowUpItem } from '@pellux/goodvibes-sdk/platform/core';
|
|
15
16
|
import type { OrchestratorUserInputOptions } from '../core/orchestrator.ts';
|
|
@@ -19,6 +20,7 @@ import type { BootstrapOptions } from './context.ts';
|
|
|
19
20
|
import { createFeatureFlagManager } from '@/runtime/index.ts';
|
|
20
21
|
import { RuntimeEventBus } from '@/runtime/index.ts';
|
|
21
22
|
import type { SessionEvent } from '@/runtime/index.ts';
|
|
23
|
+
import { emitPermissionModeChanged } from '@pellux/goodvibes-sdk/platform/runtime/emitters';
|
|
22
24
|
import { createRuntimeStore, createDomainDispatch, type RuntimeStore } from './store/index.ts';
|
|
23
25
|
import { ForensicsCollector, ForensicsRegistry } from '@/runtime/index.ts';
|
|
24
26
|
import {
|
|
@@ -135,6 +137,38 @@ export function companionMessageToOrchestratorInputOptions(
|
|
|
135
137
|
};
|
|
136
138
|
}
|
|
137
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Fleet-attention wiring: derive the shared-approval metadata for a
|
|
142
|
+
* permission ask. Background/subagent asks carry
|
|
143
|
+
* `request.attribution` (kind: 'background-agent', agentId, template — set by
|
|
144
|
+
* AgentOrchestrator's gateBackgroundToolCall once permissionManager is wired
|
|
145
|
+
* into it, see the `services.agentOrchestrator.setDependencies(...)` call
|
|
146
|
+
* below and orchestrator.test.ts's "background agent permission gating"
|
|
147
|
+
* suite). Forwarding `attribution.agentId` into the shared approval's
|
|
148
|
+
* `metadata.agentId` is what lets the SDK's fleet ProcessRegistry
|
|
149
|
+
* (runtime/fleet/registry.js, collectPendingApprovals) attribute the pending
|
|
150
|
+
* ask to that agent's ProcessNode — deriving
|
|
151
|
+
* `state: 'awaiting-approval'` / `needsAttention: { reason: 'approval' }` on
|
|
152
|
+
* the spawned agent itself, rather than leaving the ask an anonymous approval
|
|
153
|
+
* entry the fleet plane cannot attach to anything. Foreground asks (no
|
|
154
|
+
* attribution) return undefined, preserving today's un-attributed shape.
|
|
155
|
+
*
|
|
156
|
+
* PermissionAttribution is a discriminated union (SDK 1.6.1 added
|
|
157
|
+
* `mcp-server` and `sandbox-escalation` alongside the original
|
|
158
|
+
* `background-agent`); only `background-agent` carries an `agentId` the
|
|
159
|
+
* fleet ProcessRegistry can attach a pending approval to, so only that kind
|
|
160
|
+
* populates `metadata.agentId` here. The other two kinds are surfaced in the
|
|
161
|
+
* permission-prompt UI itself (see src/permissions/prompt.ts), not through
|
|
162
|
+
* fleet attribution metadata — an MCP server or a sandbox escalation is not
|
|
163
|
+
* a spawned agent's ProcessNode.
|
|
164
|
+
*/
|
|
165
|
+
export function approvalMetadataForRequest(
|
|
166
|
+
request: Pick<PermissionPromptRequest, 'attribution'>,
|
|
167
|
+
): Record<string, unknown> | undefined {
|
|
168
|
+
const attribution = request.attribution;
|
|
169
|
+
return attribution?.kind === 'background-agent' ? { agentId: attribution.agentId } : undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
138
172
|
export async function initializeBootstrapCore(
|
|
139
173
|
stdout: NodeJS.WriteStream,
|
|
140
174
|
options: BootstrapOptions,
|
|
@@ -150,6 +184,15 @@ export async function initializeBootstrapCore(
|
|
|
150
184
|
});
|
|
151
185
|
|
|
152
186
|
const userSessionId = `user-${generateUserSessionId()}`;
|
|
187
|
+
// Declared here, before createRuntimeServices, so the checkpoint manager can be
|
|
188
|
+
// handed a live session-id resolver at construction time. Its value starts as
|
|
189
|
+
// the bootstrap user session id and is advanced to the real runtime session id
|
|
190
|
+
// below (see runtimeSessionIdRef.value = runtime.sessionId). Because the
|
|
191
|
+
// resolver is consulted at the moment each automatic snapshot fires — not at
|
|
192
|
+
// subscription time — reading .value through this closure keeps same-launch
|
|
193
|
+
// checkpoints stamped with whatever session id is current when the turn ends,
|
|
194
|
+
// which is exactly the id the restore/rewind lookup filters on.
|
|
195
|
+
const runtimeSessionIdRef = { value: userSessionId };
|
|
153
196
|
const runtimeBus = new RuntimeEventBus();
|
|
154
197
|
const store = createRuntimeStore();
|
|
155
198
|
const domainDispatch = createDomainDispatch(store);
|
|
@@ -160,6 +203,7 @@ export async function initializeBootstrapCore(
|
|
|
160
203
|
runtimeBus,
|
|
161
204
|
runtimeStore: store,
|
|
162
205
|
getConversationTitle: () => getConversationTitle(),
|
|
206
|
+
resolveSessionId: () => runtimeSessionIdRef.value || undefined,
|
|
163
207
|
workingDir,
|
|
164
208
|
homeDirectory,
|
|
165
209
|
});
|
|
@@ -268,6 +312,13 @@ export async function initializeBootstrapCore(
|
|
|
268
312
|
serviceRegistry: services.serviceRegistry,
|
|
269
313
|
overflowHandler: services.overflowHandler,
|
|
270
314
|
changeTracker: services.sessionChangeTracker,
|
|
315
|
+
// Same holder instance `services.contextAccountingHolder` exposes — the
|
|
316
|
+
// context_accounting tool registered here and the bind call bootstrap.ts
|
|
317
|
+
// makes after constructing the Orchestrator (see
|
|
318
|
+
// context-accounting-source.ts) must share ONE holder, otherwise the tool
|
|
319
|
+
// would read an unbound holder of its own while the real source sits on a
|
|
320
|
+
// different instance nothing reads.
|
|
321
|
+
contextAccountingHolder: services.contextAccountingHolder,
|
|
271
322
|
});
|
|
272
323
|
registerAgentArtifactsTool(toolRegistry, services.artifactStore, { projectRoot: services.shellPaths.workingDirectory });
|
|
273
324
|
registerAgentDocumentsTool(toolRegistry, services.shellPaths, services.artifactStore);
|
|
@@ -309,7 +360,12 @@ export async function initializeBootstrapCore(
|
|
|
309
360
|
});
|
|
310
361
|
installToolExecutionSafetyGuard(toolRegistry);
|
|
311
362
|
compactRegisteredToolDefinitions(toolRegistry);
|
|
312
|
-
|
|
363
|
+
// Captured so the permissionManager-bearing follow-up call below (issued once
|
|
364
|
+
// permissionManager exists, further down this function) can replay every
|
|
365
|
+
// field — AgentOrchestrator.setDependencies() fully replaces its stored
|
|
366
|
+
// toolDeps rather than merging, so a partial second call would silently drop
|
|
367
|
+
// everything set here.
|
|
368
|
+
const agentOrchestratorToolDeps = {
|
|
313
369
|
surfaceRoot: GOODVIBES_AGENT_SURFACE_ROOT,
|
|
314
370
|
fileCache,
|
|
315
371
|
projectIndex,
|
|
@@ -334,7 +390,8 @@ export async function initializeBootstrapCore(
|
|
|
334
390
|
memoryRegistry: services.memoryRegistry,
|
|
335
391
|
sandboxSessionRegistry: services.sandboxSessionRegistry,
|
|
336
392
|
workflowServices: services.workflow,
|
|
337
|
-
}
|
|
393
|
+
};
|
|
394
|
+
services.agentOrchestrator.setDependencies(agentOrchestratorToolDeps);
|
|
338
395
|
|
|
339
396
|
const bootstrapUnsubs: Array<() => void> = [];
|
|
340
397
|
// D5 fix: stop the heartbeat watcher on shutdown so the setInterval is cleared.
|
|
@@ -421,7 +478,6 @@ export async function initializeBootstrapCore(
|
|
|
421
478
|
};
|
|
422
479
|
void approvalBroker.start();
|
|
423
480
|
void sharedSessionBroker.start();
|
|
424
|
-
const runtimeSessionIdRef = { value: userSessionId };
|
|
425
481
|
const systemMessageRouterRef: { value: SystemMessageRouter | null } = { value: null };
|
|
426
482
|
const conversationFollowUpRef: { value: ((item: ConversationFollowUpItem) => void) | null } = { value: null };
|
|
427
483
|
const { unsubs: runtimeUnsubs, agentStatusIntervalRef } = registerAgentRuntimeEvents({
|
|
@@ -432,6 +488,7 @@ export async function initializeBootstrapCore(
|
|
|
432
488
|
requestRender,
|
|
433
489
|
configManager,
|
|
434
490
|
agentManager: services.agentManager,
|
|
491
|
+
toolRegistry,
|
|
435
492
|
});
|
|
436
493
|
|
|
437
494
|
// Subscribe to companion main-chat messages received from the connected host's HTTP layer.
|
|
@@ -515,17 +572,30 @@ export async function initializeBootstrapCore(
|
|
|
515
572
|
await syncConfiguredServices(domainDispatch.syncIntegration, services.serviceRegistry);
|
|
516
573
|
|
|
517
574
|
const permissionManager = new PermissionManager(
|
|
518
|
-
(request) =>
|
|
519
|
-
request
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
575
|
+
(request) => {
|
|
576
|
+
const metadata = approvalMetadataForRequest(request);
|
|
577
|
+
return approvalBroker.requestApproval({
|
|
578
|
+
request,
|
|
579
|
+
sessionId: runtimeSessionIdRef.value,
|
|
580
|
+
localPrompt: permissionPromptRef.requestPermission,
|
|
581
|
+
...(metadata ? { metadata } : {}),
|
|
582
|
+
});
|
|
583
|
+
},
|
|
523
584
|
createPermissionConfigReader(configManager),
|
|
524
585
|
policyRuntimeState,
|
|
525
586
|
services.hookDispatcher,
|
|
526
587
|
featureFlags,
|
|
527
588
|
);
|
|
528
589
|
installPermissionManagerSafetyGuard(permissionManager);
|
|
590
|
+
// Wire permissionManager into the SAME AgentOrchestrator instance that runs
|
|
591
|
+
// spawned/background agent tool calls (services.agentOrchestrator, set up
|
|
592
|
+
// above with agentOrchestratorToolDeps before permissionManager existed).
|
|
593
|
+
// Without this, the SDK's gateBackgroundToolCall() sees no permissionManager
|
|
594
|
+
// on context and leaves every background tool call ungated — spawned agents
|
|
595
|
+
// would run with no permission check at all, regardless of permissions.mode
|
|
596
|
+
// or permissions.backgroundAgents. Replays the full toolDeps object because
|
|
597
|
+
// setDependencies() replaces rather than merges.
|
|
598
|
+
services.agentOrchestrator.setDependencies({ ...agentOrchestratorToolDeps, permissionManager });
|
|
529
599
|
await hookWorkbench.loadAndApplyManagedHooks();
|
|
530
600
|
|
|
531
601
|
const runtime: MutableRuntimeState = {
|
|
@@ -537,10 +607,21 @@ export async function initializeBootstrapCore(
|
|
|
537
607
|
sessionId: userSessionId,
|
|
538
608
|
};
|
|
539
609
|
runtimeSessionIdRef.value = runtime.sessionId;
|
|
610
|
+
// Register this process's live conversation so the composed daemon's
|
|
611
|
+
// rewind.plan/apply verbs can serve conversation scope for this session
|
|
612
|
+
// (see conversation-rewind-port.ts and services.ts's conversationRewindPort
|
|
613
|
+
// wiring). Ported from goodvibes-tui's identical bootstrap-time call.
|
|
614
|
+
registerSessionConversation(runtime.sessionId, conversation);
|
|
540
615
|
void sharedSessionBroker.createSession({
|
|
541
616
|
id: runtime.sessionId,
|
|
542
617
|
title: 'GoodVibes Agent session',
|
|
543
|
-
|
|
618
|
+
// Declares the session permission mode at creation time in the shared
|
|
619
|
+
// metadata bag (SDK 1.6.1 permissions.mode: prompt/allow-all/custom/
|
|
620
|
+
// plan/accept-edits). Read-only declaration — the permission layer
|
|
621
|
+
// itself always reads permissions.mode live off configManager, so this
|
|
622
|
+
// does not change enforcement; it lets cross-session tooling see which
|
|
623
|
+
// mode a session started under without re-deriving it.
|
|
624
|
+
metadata: { source: 'goodvibes-agent', permissionMode: configManager.get('permissions.mode') },
|
|
544
625
|
participant: {
|
|
545
626
|
surfaceKind: 'service',
|
|
546
627
|
surfaceId: 'surface:goodvibes-agent',
|
|
@@ -564,6 +645,26 @@ export async function initializeBootstrapCore(
|
|
|
564
645
|
uiServices.events.turns.on('TURN_COMPLETED', () => services.sessionSpineClient.heartbeat(runtimeSessionIdRef.value)),
|
|
565
646
|
);
|
|
566
647
|
|
|
648
|
+
// Producer half of the live mode-metadata refresh. permissions.mode can change
|
|
649
|
+
// mid-session (the Permission mode setting cycles it via configManager), and
|
|
650
|
+
// the SDK models that as a PERMISSION_MODE_CHANGED wire event — but nothing
|
|
651
|
+
// emitted it, so the runtime store's permission domain (and any surface built
|
|
652
|
+
// off it) stayed frozen at the boot-time value until restart. Bridging the
|
|
653
|
+
// config-key subscription onto the runtime bus makes every mode mutation, from
|
|
654
|
+
// any path that goes through configManager.set, publish the event; the
|
|
655
|
+
// consumer half in agent-runtime-events.ts folds it into the store and shows a
|
|
656
|
+
// system message. Guarded on an actual change so a no-op re-set stays silent.
|
|
657
|
+
runtimeUnsubs.push(
|
|
658
|
+
configManager.subscribe('permissions.mode', (newValue, oldValue) => {
|
|
659
|
+
if (newValue === oldValue) return;
|
|
660
|
+
emitPermissionModeChanged(
|
|
661
|
+
runtimeBus,
|
|
662
|
+
{ sessionId: runtimeSessionIdRef.value, source: 'goodvibes-agent', traceId: `perm-mode-${generateUserSessionId()}` },
|
|
663
|
+
{ mode: String(newValue), previousMode: String(oldValue) },
|
|
664
|
+
);
|
|
665
|
+
}),
|
|
666
|
+
);
|
|
667
|
+
|
|
567
668
|
domainDispatch.syncSessionState({
|
|
568
669
|
id: userSessionId,
|
|
569
670
|
projectRoot: workingDir,
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connected-host (daemon + HTTP listener) discovery for GoodVibes Agent.
|
|
3
|
+
*
|
|
4
|
+
* Split out of bootstrap.ts (which is near the 800-line architecture cap) to
|
|
5
|
+
* keep this concern — and its "GoodVibes Agent never owns the daemon
|
|
6
|
+
* lifecycle" framing — in one small, readable place.
|
|
7
|
+
*
|
|
8
|
+
* GoodVibes Agent never starts or restarts the connected daemon: every call
|
|
9
|
+
* routes through the SDK-shared adopt-or-spawn policy
|
|
10
|
+
* (decideDaemonAdoption/classifyDaemonProbe) with `adoptOnly: true`, so a
|
|
11
|
+
* reachable, version-compatible daemon is adopted and anything else (absent,
|
|
12
|
+
* blocked, or on an incompatible wire version) is reported honestly. This
|
|
13
|
+
* replaces a local stub that hard-declared every daemon 'external' without
|
|
14
|
+
* probing or version-checking it.
|
|
15
|
+
*/
|
|
16
|
+
import { startExternalServices } from '@/runtime/index.ts';
|
|
17
|
+
import type {
|
|
18
|
+
DeferredStartupCoordinator,
|
|
19
|
+
ExternalServicesHandle,
|
|
20
|
+
HostServiceStatus,
|
|
21
|
+
HostServicesConfig,
|
|
22
|
+
RuntimeEventBus,
|
|
23
|
+
} from '@/runtime/index.ts';
|
|
24
|
+
import type { HookDispatcher } from '@pellux/goodvibes-sdk/platform/hooks';
|
|
25
|
+
import { logger, summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
26
|
+
import type { SystemMessageRouter } from '../core/system-message-router.ts';
|
|
27
|
+
import type { RuntimeServices } from './services.ts';
|
|
28
|
+
import type { UiRuntimeServices } from './ui-services.ts';
|
|
29
|
+
|
|
30
|
+
function formatHostServiceBaseUrl(host: string, port: number): string {
|
|
31
|
+
const normalized = host.trim().toLowerCase();
|
|
32
|
+
const probeHost = normalized === '0.0.0.0'
|
|
33
|
+
? '127.0.0.1'
|
|
34
|
+
: normalized === '::' || normalized === '[::]'
|
|
35
|
+
? '::1'
|
|
36
|
+
: host;
|
|
37
|
+
const urlHost = probeHost.includes(':') && !probeHost.startsWith('[') ? `[${probeHost}]` : probeHost;
|
|
38
|
+
return `http://${urlHost}:${port}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Pending status shown before the deferred probe completes — honest
|
|
42
|
+
// 'unavailable', never a guessed 'external'.
|
|
43
|
+
function createPendingServiceStatus(
|
|
44
|
+
configManager: HostServicesConfig,
|
|
45
|
+
service: 'daemon' | 'httpListener',
|
|
46
|
+
): HostServiceStatus {
|
|
47
|
+
const host = String(configManager.get(service === 'daemon' ? 'controlPlane.host' : 'httpListener.host') ?? '127.0.0.1');
|
|
48
|
+
const port = Number(configManager.get(service === 'daemon' ? 'controlPlane.port' : 'httpListener.port') ?? (service === 'daemon' ? 3421 : 3422));
|
|
49
|
+
return {
|
|
50
|
+
mode: 'unavailable',
|
|
51
|
+
host,
|
|
52
|
+
port,
|
|
53
|
+
baseUrl: formatHostServiceBaseUrl(host, port),
|
|
54
|
+
reason: 'Connected-host discovery has not completed yet',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const hostServiceIsActive = (status: HostServiceStatus): boolean => status.mode === 'embedded' || status.mode === 'external';
|
|
59
|
+
|
|
60
|
+
// 'blocked' (occupied by an unverified process) and 'incompatible' (a
|
|
61
|
+
// GoodVibes daemon on a wire-version band this Agent build refuses to adopt)
|
|
62
|
+
// both mean the configured port is held and unusable by this instance.
|
|
63
|
+
const hostServiceIsBlocked = (status: HostServiceStatus): boolean => status.mode === 'blocked' || status.mode === 'incompatible';
|
|
64
|
+
|
|
65
|
+
export interface AgentExternalServicesController {
|
|
66
|
+
/** The most recently resolved (or pending) connected-host status. */
|
|
67
|
+
getStatus(): ExternalServicesHandle;
|
|
68
|
+
stop(): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function wireAgentExternalServices(options: {
|
|
72
|
+
readonly configManager: HostServicesConfig;
|
|
73
|
+
readonly runtimeBus: RuntimeEventBus;
|
|
74
|
+
readonly hookDispatcher: HookDispatcher;
|
|
75
|
+
readonly services: RuntimeServices;
|
|
76
|
+
readonly uiServices: UiRuntimeServices;
|
|
77
|
+
readonly deferredStartup: DeferredStartupCoordinator;
|
|
78
|
+
readonly systemMessageRouter: SystemMessageRouter;
|
|
79
|
+
readonly requestRender: () => void;
|
|
80
|
+
}): AgentExternalServicesController {
|
|
81
|
+
const { configManager, runtimeBus, hookDispatcher, services, uiServices, deferredStartup, systemMessageRouter, requestRender } = options;
|
|
82
|
+
|
|
83
|
+
const inspectAgentDependencies = () => {
|
|
84
|
+
const daemonStatus = externalServices.daemonStatus;
|
|
85
|
+
const httpListenerStatus = externalServices.httpListenerStatus;
|
|
86
|
+
return {
|
|
87
|
+
connectedHostRunning: hostServiceIsActive(daemonStatus),
|
|
88
|
+
connectedHostPortInUse: hostServiceIsBlocked(daemonStatus),
|
|
89
|
+
httpListenerRunning: hostServiceIsActive(httpListenerStatus),
|
|
90
|
+
httpListenerPortInUse: hostServiceIsBlocked(httpListenerStatus),
|
|
91
|
+
connectedHostStatus: daemonStatus,
|
|
92
|
+
httpListenerStatus,
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
let externalServices: ExternalServicesHandle = {
|
|
97
|
+
daemonServer: null,
|
|
98
|
+
httpListener: null,
|
|
99
|
+
daemonStatus: createPendingServiceStatus(configManager, 'daemon'),
|
|
100
|
+
httpListenerStatus: createPendingServiceStatus(configManager, 'httpListener'),
|
|
101
|
+
listRecentControlPlaneEvents: () => [],
|
|
102
|
+
async stop(): Promise<void> {},
|
|
103
|
+
};
|
|
104
|
+
let externalServicesPromise: Promise<ExternalServicesHandle> | null = null;
|
|
105
|
+
|
|
106
|
+
const startAgentExternalServices = (): Promise<ExternalServicesHandle> =>
|
|
107
|
+
startExternalServices(configManager, runtimeBus, hookDispatcher, services, { adoptOnly: true });
|
|
108
|
+
|
|
109
|
+
const platformExternalServices = uiServices.platform as typeof uiServices.platform & {
|
|
110
|
+
externalServices: NonNullable<typeof uiServices.platform.externalServices>;
|
|
111
|
+
};
|
|
112
|
+
platformExternalServices.externalServices = {
|
|
113
|
+
inspect: inspectAgentDependencies,
|
|
114
|
+
restart: async () => {
|
|
115
|
+
if (externalServicesPromise) {
|
|
116
|
+
try {
|
|
117
|
+
externalServices = await externalServicesPromise;
|
|
118
|
+
} catch {
|
|
119
|
+
// A failed previous startup should not prevent a re-probe attempt.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
await externalServices.stop();
|
|
123
|
+
externalServicesPromise = startAgentExternalServices();
|
|
124
|
+
externalServices = await externalServicesPromise;
|
|
125
|
+
systemMessageRouter.high('[Startup] GoodVibes Agent does not start or restart the connected GoodVibes host — it adopted the current status from a fresh probe.');
|
|
126
|
+
requestRender();
|
|
127
|
+
return inspectAgentDependencies();
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Connected-host discovery OFF the interactive path: probe the configured
|
|
132
|
+
// host/port through the shared adopt-or-spawn policy (adoptOnly — Agent
|
|
133
|
+
// never spawns or embeds) and replace the pending status with an honest
|
|
134
|
+
// one (adopted, incompatible, blocked, or unavailable).
|
|
135
|
+
deferredStartup.schedule({
|
|
136
|
+
label: 'external-services',
|
|
137
|
+
run: async () => {
|
|
138
|
+
externalServicesPromise = startAgentExternalServices();
|
|
139
|
+
externalServices = await externalServicesPromise;
|
|
140
|
+
requestRender();
|
|
141
|
+
},
|
|
142
|
+
onError: (error) => {
|
|
143
|
+
const message = summarizeError(error);
|
|
144
|
+
logger.error('Deferred connected-host discovery failed', { error: message });
|
|
145
|
+
systemMessageRouter.high(`[Startup] Connected-host discovery failed: ${message}`);
|
|
146
|
+
requestRender();
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
getStatus: () => externalServices,
|
|
152
|
+
stop: async () => {
|
|
153
|
+
await externalServices.stop();
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
package/src/runtime/bootstrap.ts
CHANGED
|
@@ -31,10 +31,12 @@ import { scheduleBackgroundMcpDiscovery } from '@/runtime/index.ts';
|
|
|
31
31
|
import { restoreSavedModel } from '@/runtime/index.ts';
|
|
32
32
|
import { runGatedLanScan } from './lan-scan-consent.ts';
|
|
33
33
|
import { scheduleCalendarSubscriptionBootRefresh } from './calendar-boot-refresh.ts';
|
|
34
|
-
import type {
|
|
34
|
+
import type { TurnEvent } from '@/runtime/index.ts';
|
|
35
35
|
import type { UiRuntimeServices } from './ui-services.ts';
|
|
36
36
|
import { createDeferredStartupCoordinator } from '@/runtime/index.ts';
|
|
37
|
+
import { wireAgentExternalServices } from './bootstrap-external-services.ts';
|
|
37
38
|
import { initializeBootstrapCore } from './bootstrap-core.ts';
|
|
39
|
+
import { bindOrchestratorContextAccounting } from './context-accounting-source.ts';
|
|
38
40
|
import { createBootstrapShell } from './bootstrap-shell.ts';
|
|
39
41
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
40
42
|
import { startMcpConfigAutoReload } from '../mcp/runtime-reload.ts';
|
|
@@ -42,6 +44,9 @@ import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
|
42
44
|
import { foldLegacySpineStore } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
|
|
43
45
|
import { reconcileMemorySpineAdoption } from './memory-spine-adoption.ts';
|
|
44
46
|
import { AgentPromptContextReceiptStore, composeRuntimePromptWithReceipt } from '../agent/prompt-context-receipts.ts';
|
|
47
|
+
import { createMemoryConsolidationScheduler } from './memory-consolidation-wiring.ts';
|
|
48
|
+
import { recordTurnAnchor, summarizeTurnLabel } from '../core/rewind-turn-anchors.ts';
|
|
49
|
+
import { createMemoryUsageTracker } from './memory-usage-wiring.ts';
|
|
45
50
|
import { registerAgentAuditTool } from '../tools/agent-audit-tool.ts';
|
|
46
51
|
import { registerAgentAutonomyTool } from '../tools/agent-autonomy-tool.ts';
|
|
47
52
|
import { registerAgentChannelsTool } from '../tools/agent-channels-tool.ts';
|
|
@@ -211,6 +216,20 @@ export async function bootstrapRuntime(
|
|
|
211
216
|
// was just never captured. Mirrors activePromptTurnId's lifecycle exactly: set on
|
|
212
217
|
// TURN_SUBMITTED, cleared on every terminal event for that turn.
|
|
213
218
|
let activePromptTurnText: string | null = null;
|
|
219
|
+
// Idle-time memory consolidation: reviews stored memory when the agent is
|
|
220
|
+
// genuinely idle (no active turn) and no sooner than the configured interval,
|
|
221
|
+
// off by default. Every run leaves a receipt of what it merged/archived/proposed.
|
|
222
|
+
// Usage-outcome instrumentation: records which memories were injected and, at
|
|
223
|
+
// turn completion, whether the model output plausibly referenced them (honest
|
|
224
|
+
// heuristic overlap). Feeds consolidation's never-referenced-first decay.
|
|
225
|
+
const memoryUsageTracker = createMemoryUsageTracker(services.shellPaths, services.memoryRegistry);
|
|
226
|
+
const memoryConsolidationScheduler = createMemoryConsolidationScheduler({
|
|
227
|
+
configManager,
|
|
228
|
+
memoryRegistry: services.memoryRegistry,
|
|
229
|
+
shellPaths: services.shellPaths,
|
|
230
|
+
isIdle: () => activePromptTurnId === null,
|
|
231
|
+
usageLookup: (id) => memoryUsageTracker.lookup(id),
|
|
232
|
+
});
|
|
214
233
|
runtimeUnsubs.push(
|
|
215
234
|
runtimeBus.on<Extract<TurnEvent, { type: 'TURN_SUBMITTED' }>>('TURN_SUBMITTED', (event) => {
|
|
216
235
|
activePromptTurnId = event.payload.turnId;
|
|
@@ -233,10 +252,30 @@ export async function bootstrapRuntime(
|
|
|
233
252
|
terminalEvent: 'TURN_COMPLETED',
|
|
234
253
|
stopReason: event.payload.stopReason,
|
|
235
254
|
});
|
|
255
|
+
// Record this turn's rewind anchor: pair the turnId (shared with the
|
|
256
|
+
// workspace checkpoint the turn engine snapshots for this same turn)
|
|
257
|
+
// with the live conversation message count, so a later message-anchored
|
|
258
|
+
// rewind.plan/apply (scope 'conversation' or 'both') can truncate the
|
|
259
|
+
// conversation to exactly this boundary — the join key between
|
|
260
|
+
// conversation and files rewind (see core/rewind-turn-anchors.ts and
|
|
261
|
+
// services.ts's conversationRewindPort wiring).
|
|
262
|
+
try {
|
|
263
|
+
recordTurnAnchor(runtimeSessionIdRef.value, {
|
|
264
|
+
turnId: event.payload.turnId,
|
|
265
|
+
label: summarizeTurnLabel(conversation.getLastUserMessage()),
|
|
266
|
+
messageCount: conversation.getMessageCount(),
|
|
267
|
+
at: Date.now(),
|
|
268
|
+
});
|
|
269
|
+
} catch (error) {
|
|
270
|
+
// Best-effort; a rewind-anchor miss must never break the turn.
|
|
271
|
+
logger.debug('rewind turn-anchor recording failed', { error: summarizeError(error) });
|
|
272
|
+
}
|
|
236
273
|
if (activePromptTurnId === event.payload.turnId) {
|
|
237
274
|
activePromptTurnId = null;
|
|
238
275
|
activePromptTurnText = null;
|
|
239
276
|
}
|
|
277
|
+
memoryUsageTracker.onTurnCompleted(event.payload.turnId, event.payload.response);
|
|
278
|
+
memoryConsolidationScheduler.onTurnSettled();
|
|
240
279
|
}),
|
|
241
280
|
runtimeBus.on<Extract<TurnEvent, { type: 'TURN_ERROR' }>>('TURN_ERROR', (event) => {
|
|
242
281
|
promptContextReceipts.recordTurnOutcome({
|
|
@@ -250,6 +289,8 @@ export async function bootstrapRuntime(
|
|
|
250
289
|
activePromptTurnId = null;
|
|
251
290
|
activePromptTurnText = null;
|
|
252
291
|
}
|
|
292
|
+
memoryUsageTracker.onTurnAborted(event.payload.turnId);
|
|
293
|
+
memoryConsolidationScheduler.onTurnSettled();
|
|
253
294
|
}),
|
|
254
295
|
runtimeBus.on<Extract<TurnEvent, { type: 'TURN_CANCEL' }>>('TURN_CANCEL', (event) => {
|
|
255
296
|
promptContextReceipts.recordTurnOutcome({
|
|
@@ -263,6 +304,8 @@ export async function bootstrapRuntime(
|
|
|
263
304
|
activePromptTurnId = null;
|
|
264
305
|
activePromptTurnText = null;
|
|
265
306
|
}
|
|
307
|
+
memoryUsageTracker.onTurnAborted(event.payload.turnId);
|
|
308
|
+
memoryConsolidationScheduler.onTurnSettled();
|
|
266
309
|
}),
|
|
267
310
|
);
|
|
268
311
|
const {
|
|
@@ -305,6 +348,7 @@ export async function bootstrapRuntime(
|
|
|
305
348
|
memoryRecallSnapshot: services.memorySpineClient.recallSnapshot(),
|
|
306
349
|
});
|
|
307
350
|
promptContextReceipts.record(composed.receipt);
|
|
351
|
+
memoryUsageTracker.onComposed(activePromptTurnId, composed.receipt);
|
|
308
352
|
return composed.prompt;
|
|
309
353
|
},
|
|
310
354
|
hookDispatcher,
|
|
@@ -336,6 +380,20 @@ export async function bootstrapRuntime(
|
|
|
336
380
|
});
|
|
337
381
|
conversation.setSessionLineageTracker(services.sessionLineageTracker);
|
|
338
382
|
|
|
383
|
+
// Bind the context_accounting tool (SDK 1.6.1) to THIS Orchestrator so it
|
|
384
|
+
// reports real session data instead of the unbound-holder honesty message.
|
|
385
|
+
// Must run after setCoreServices (nothing here depends on it, but this
|
|
386
|
+
// keeps every orchestrator.* wiring call grouped) and before the first
|
|
387
|
+
// turn — see context-accounting-source.ts for what each facet reads.
|
|
388
|
+
runtimeUnsubs.push(bindOrchestratorContextAccounting({
|
|
389
|
+
orchestrator,
|
|
390
|
+
holder: services.contextAccountingHolder,
|
|
391
|
+
runtimeBus,
|
|
392
|
+
runtimeStore: store,
|
|
393
|
+
sessionId: runtime.sessionId,
|
|
394
|
+
getContextWindow: () => providerRegistry.getContextWindowForModel(providerRegistry.getCurrentModel()),
|
|
395
|
+
}));
|
|
396
|
+
|
|
339
397
|
const opsTaskManager = createTaskManager(store, runtimeBus, userSessionId);
|
|
340
398
|
const opsControlPlane = services.featureFlags.isEnabled('operator-control-plane')
|
|
341
399
|
? new OpsControlPlane(opsTaskManager, runtimeBus, store, userSessionId)
|
|
@@ -420,74 +478,20 @@ export async function bootstrapRuntime(
|
|
|
420
478
|
|
|
421
479
|
const deferredStartup = createDeferredStartupCoordinator();
|
|
422
480
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
): HostServiceStatus => {
|
|
437
|
-
const host = String(configManager.get(service === 'daemon' ? 'controlPlane.host' : 'httpListener.host') ?? '127.0.0.1');
|
|
438
|
-
const port = Number(configManager.get(service === 'daemon' ? 'controlPlane.port' : 'httpListener.port') ?? (service === 'daemon' ? 3421 : 3422));
|
|
439
|
-
return {
|
|
440
|
-
mode: service === 'daemon' ? 'external' : 'disabled',
|
|
441
|
-
host,
|
|
442
|
-
port,
|
|
443
|
-
baseUrl: formatHostServiceBaseUrl(host, port),
|
|
444
|
-
reason: service === 'daemon'
|
|
445
|
-
? 'GoodVibes Agent connects to a GoodVibes host owned outside this product and does not start or restart it.'
|
|
446
|
-
: 'GoodVibes Agent does not own listener lifecycle.',
|
|
447
|
-
};
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
const hostServiceIsActive = (status: HostServiceStatus): boolean => status.mode === 'embedded' || status.mode === 'external';
|
|
451
|
-
|
|
452
|
-
const hostServiceIsBlocked = (status: HostServiceStatus): boolean => status.mode === 'blocked';
|
|
453
|
-
|
|
454
|
-
const inspectAgentDependencies = () => {
|
|
455
|
-
const daemonStatus = externalServices.daemonStatus;
|
|
456
|
-
const httpListenerStatus = externalServices.httpListenerStatus;
|
|
457
|
-
return {
|
|
458
|
-
connectedHostRunning: hostServiceIsActive(daemonStatus),
|
|
459
|
-
connectedHostPortInUse: hostServiceIsBlocked(daemonStatus),
|
|
460
|
-
httpListenerRunning: hostServiceIsActive(httpListenerStatus),
|
|
461
|
-
httpListenerPortInUse: hostServiceIsBlocked(httpListenerStatus),
|
|
462
|
-
connectedHostStatus: daemonStatus,
|
|
463
|
-
httpListenerStatus,
|
|
464
|
-
};
|
|
465
|
-
};
|
|
481
|
+
// GoodVibes Agent never owns the connected daemon's (or HTTP listener's)
|
|
482
|
+
// lifecycle — see bootstrap-external-services.ts for the adopt-only wiring
|
|
483
|
+
// and the deferred discovery probe it schedules.
|
|
484
|
+
const agentExternalServices = wireAgentExternalServices({
|
|
485
|
+
configManager,
|
|
486
|
+
runtimeBus,
|
|
487
|
+
hookDispatcher,
|
|
488
|
+
services,
|
|
489
|
+
uiServices,
|
|
490
|
+
deferredStartup,
|
|
491
|
+
systemMessageRouter,
|
|
492
|
+
requestRender,
|
|
493
|
+
});
|
|
466
494
|
|
|
467
|
-
let externalServices: ExternalServicesHandle = {
|
|
468
|
-
daemonServer: null,
|
|
469
|
-
httpListener: null,
|
|
470
|
-
daemonStatus: createAgentDependencyStatus('daemon'),
|
|
471
|
-
httpListenerStatus: createAgentDependencyStatus('httpListener'),
|
|
472
|
-
listRecentControlPlaneEvents: () => [],
|
|
473
|
-
async stop(): Promise<void> {},
|
|
474
|
-
};
|
|
475
|
-
const platformExternalServices = uiServices.platform as typeof uiServices.platform & {
|
|
476
|
-
externalServices: NonNullable<typeof uiServices.platform.externalServices>;
|
|
477
|
-
};
|
|
478
|
-
platformExternalServices.externalServices = {
|
|
479
|
-
inspect: inspectAgentDependencies,
|
|
480
|
-
restart: async () => {
|
|
481
|
-
externalServices = {
|
|
482
|
-
...externalServices,
|
|
483
|
-
daemonStatus: createAgentDependencyStatus('daemon'),
|
|
484
|
-
httpListenerStatus: createAgentDependencyStatus('httpListener'),
|
|
485
|
-
};
|
|
486
|
-
systemMessageRouter.high('[Startup] GoodVibes Agent does not start or restart the connected GoodVibes host. Start it from GoodVibes TUI or the owning host, then refresh status.');
|
|
487
|
-
requestRender();
|
|
488
|
-
return inspectAgentDependencies();
|
|
489
|
-
},
|
|
490
|
-
};
|
|
491
495
|
deferredStartup.schedule({
|
|
492
496
|
label: 'plugins',
|
|
493
497
|
run: async () => {
|
|
@@ -736,7 +740,7 @@ export async function bootstrapRuntime(
|
|
|
736
740
|
forensicsCollector.dispose();
|
|
737
741
|
services.executionLedger.dispose();
|
|
738
742
|
await deferredStartup.drain(100);
|
|
739
|
-
await
|
|
743
|
+
await agentExternalServices.stop();
|
|
740
744
|
// Clear agent status interval via ref (consistent with agentStatusIntervalRef usage)
|
|
741
745
|
if (agentStatusIntervalRef.value !== null) {
|
|
742
746
|
clearInterval(agentStatusIntervalRef.value);
|