@pellux/goodvibes-agent 1.8.1 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -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 +135337 -118733
- package/dist/package/{web-tree-sitter-vmqc5d26.wasm → web-tree-sitter-jbz042ba.wasm} +0 -0
- package/docs/README.md +1 -1
- package/docs/tools-and-commands.md +9 -0
- package/package.json +5 -3
- 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 +250 -14
- 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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-accounting-source.ts
|
|
3
|
+
*
|
|
4
|
+
* Binds the SDK's `context_accounting` tool (SDK 1.6.1,
|
|
5
|
+
* platform/tools/context-accounting) to THIS fork's own interactive
|
|
6
|
+
* Orchestrator so the tool reports real, live session data instead of the
|
|
7
|
+
* unbound-holder honesty message ("no live session context bound"). GoodVibes
|
|
8
|
+
* Agent is an interactive consumer with exactly one Orchestrator driving the
|
|
9
|
+
* main conversation — see bootstrap.ts, where `bindOrchestratorContextAccounting`
|
|
10
|
+
* is called once, right after the Orchestrator is constructed and
|
|
11
|
+
* `setCoreServices` has run.
|
|
12
|
+
*
|
|
13
|
+
* The three ContextAccountingSource facets map onto data this fork already
|
|
14
|
+
* tracks, nothing new is measured:
|
|
15
|
+
* - Token state: Orchestrator.usage (input/output/cacheRead/cacheWrite,
|
|
16
|
+
* public fields) + Orchestrator.lastInputTokens, plus the active model's
|
|
17
|
+
* context window from providerRegistry.getContextWindowForModel — the
|
|
18
|
+
* exact call bootstrap.ts already makes for the system prompt.
|
|
19
|
+
* - Turn injections: Orchestrator.getTurnInjections() (public accessor).
|
|
20
|
+
* - Compaction state: Orchestrator does not expose `isCompacting` publicly
|
|
21
|
+
* (private field), so this reads runtimeStore.getState().session.compactionState
|
|
22
|
+
* instead — the SAME state the SDK's own updateSessionState reducer
|
|
23
|
+
* already derives from the 'compaction' domain's runtime-bus events (see
|
|
24
|
+
* agent-runtime-events.ts's `runtimeBus.onDomain('compaction', ...)`
|
|
25
|
+
* subscription, wired before this bind call runs). compactionCount is a
|
|
26
|
+
* local counter incremented on each applied COMPACTION_RECEIPT for THIS
|
|
27
|
+
* session — the SDK does not track a running count itself.
|
|
28
|
+
*/
|
|
29
|
+
import type { ContextAccountingHolder, ContextAccountingSource } from '@pellux/goodvibes-sdk/platform/tools';
|
|
30
|
+
import type { Orchestrator } from '../core/orchestrator.ts';
|
|
31
|
+
import type { CompactionEvent, RuntimeEventBus } from '@/runtime/index.ts';
|
|
32
|
+
import type { RuntimeStore } from './store/index.ts';
|
|
33
|
+
|
|
34
|
+
/** compactionState values that mean "a compaction pass is currently running" — every non-idle, non-terminal state. */
|
|
35
|
+
const COMPACTING_STATES: ReadonlySet<string> = new Set([
|
|
36
|
+
'checking_threshold',
|
|
37
|
+
'microcompact',
|
|
38
|
+
'collapse',
|
|
39
|
+
'autocompact',
|
|
40
|
+
'reactive_compact',
|
|
41
|
+
'boundary_commit',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
export interface BindOrchestratorContextAccountingOptions {
|
|
45
|
+
/** The interactive session's Orchestrator. Only the public surface this source reads is required. */
|
|
46
|
+
readonly orchestrator: Pick<Orchestrator, 'usage' | 'lastInputTokens' | 'getTurnInjections'>;
|
|
47
|
+
readonly holder: ContextAccountingHolder;
|
|
48
|
+
readonly runtimeBus: RuntimeEventBus;
|
|
49
|
+
readonly runtimeStore: RuntimeStore;
|
|
50
|
+
/** Same sessionId passed to the Orchestrator's own constructor. */
|
|
51
|
+
readonly sessionId: string;
|
|
52
|
+
/** Resolves the active model's context window; omitted/null when unknown (matches providerRegistry.getContextWindowForModel). */
|
|
53
|
+
readonly getContextWindow: () => number | null;
|
|
54
|
+
/** Human-readable scope label so the tool's caller knows whose context this is. Defaults to 'main session'. */
|
|
55
|
+
readonly scope?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Binds an Orchestrator-backed ContextAccountingSource onto `holder` and
|
|
60
|
+
* returns an unbind function (removes the compaction-receipt subscription and
|
|
61
|
+
* clears the holder back to unbound). Safe to call once per Orchestrator
|
|
62
|
+
* lifetime; bootstrap.ts pushes the returned function into `runtimeUnsubs` so
|
|
63
|
+
* shutdown leaves no dangling bus subscription.
|
|
64
|
+
*/
|
|
65
|
+
export function bindOrchestratorContextAccounting(
|
|
66
|
+
options: BindOrchestratorContextAccountingOptions,
|
|
67
|
+
): () => void {
|
|
68
|
+
const { orchestrator, holder, runtimeBus, runtimeStore, sessionId, getContextWindow } = options;
|
|
69
|
+
const scope = options.scope ?? 'main session';
|
|
70
|
+
|
|
71
|
+
// Local running count of applied compactions for THIS session — the SDK
|
|
72
|
+
// emits the mandatory COMPACTION_RECEIPT after every compaction path
|
|
73
|
+
// (agent-runtime-events.ts routes the same event into the visible system
|
|
74
|
+
// message; this tracks only the count, filtered to this session so a
|
|
75
|
+
// background agent's own compaction does not inflate the main session's
|
|
76
|
+
// number).
|
|
77
|
+
let compactionCount = 0;
|
|
78
|
+
const unsubReceipt = runtimeBus.on<Extract<CompactionEvent, { type: 'COMPACTION_RECEIPT' }>>(
|
|
79
|
+
'COMPACTION_RECEIPT',
|
|
80
|
+
({ payload }) => {
|
|
81
|
+
if (payload.sessionId === sessionId && payload.outcome === 'applied') compactionCount += 1;
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const source: ContextAccountingSource = {
|
|
86
|
+
scope,
|
|
87
|
+
sessionId,
|
|
88
|
+
getTurnInjections: () => orchestrator.getTurnInjections(),
|
|
89
|
+
getTokenState: () => ({
|
|
90
|
+
measured: {
|
|
91
|
+
input: orchestrator.usage.input,
|
|
92
|
+
output: orchestrator.usage.output,
|
|
93
|
+
cacheRead: orchestrator.usage.cacheRead,
|
|
94
|
+
cacheWrite: orchestrator.usage.cacheWrite,
|
|
95
|
+
},
|
|
96
|
+
lastInputTokens: orchestrator.lastInputTokens,
|
|
97
|
+
contextWindow: getContextWindow(),
|
|
98
|
+
}),
|
|
99
|
+
getCompactionState: () => {
|
|
100
|
+
const compactionState = runtimeStore.getState().session.compactionState;
|
|
101
|
+
return {
|
|
102
|
+
isCompacting: COMPACTING_STATES.has(compactionState),
|
|
103
|
+
compactionCount,
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
holder.setSource(source);
|
|
109
|
+
|
|
110
|
+
return () => {
|
|
111
|
+
unsubReceipt();
|
|
112
|
+
holder.setSource(null);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// conversation-rewind-port.ts — this Agent's ConversationManager as the SDK
|
|
3
|
+
// unified rewind service's RewindConversationPort.
|
|
4
|
+
//
|
|
5
|
+
// The SDK's UnifiedRewindService (platform/rewind) joins files rewind (workspace
|
|
6
|
+
// checkpoints) with conversation rewind through two ports. The conversation port
|
|
7
|
+
// is "a daemon-hosted mutable conversation store": preview() reports how many
|
|
8
|
+
// messages would truncate to a recorded turn boundary, and rewind() performs the
|
|
9
|
+
// truncation and captures the pre-/post-truncation snapshots so the reversal can
|
|
10
|
+
// be undone and re-applied. The truncation boundary is the message count recorded
|
|
11
|
+
// for the anchor's turnId at TURN_COMPLETED (rewind-turn-anchors.ts) — the same
|
|
12
|
+
// join key files rewind uses against the workspace checkpoint.
|
|
13
|
+
//
|
|
14
|
+
// Ported from goodvibes-tui's src/runtime/conversation-rewind-port.ts (same
|
|
15
|
+
// port shape, same registry pattern) so the two front-ends' rewind semantics
|
|
16
|
+
// cannot drift. This Agent has no in-process /rewind command today (see
|
|
17
|
+
// src/test/daemon/gateway-rewind-conversation-scope.test.ts for the honesty
|
|
18
|
+
// note on that), so the only consumer of this port here is the composed
|
|
19
|
+
// daemon's rewind.plan/apply verbs, resolving the live conversation per
|
|
20
|
+
// anchor.sessionId from the registry below.
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
RewindAnchor,
|
|
25
|
+
RewindConversationOutcome,
|
|
26
|
+
RewindConversationPort,
|
|
27
|
+
RewindConversationPreview,
|
|
28
|
+
} from '@pellux/goodvibes-sdk/platform/rewind';
|
|
29
|
+
import type { ConversationManager } from '../core/conversation.ts';
|
|
30
|
+
import { resolveTurnAnchor } from '../core/rewind-turn-anchors.ts';
|
|
31
|
+
|
|
32
|
+
type ConversationJson = Parameters<ConversationManager['fromJSON']>[0];
|
|
33
|
+
|
|
34
|
+
/** The port plus the reversal accessors an in-process undo/redo surface would need. */
|
|
35
|
+
export interface ConversationRewindPort extends RewindConversationPort {
|
|
36
|
+
/** Restore the pre-truncation conversation (the undo direction). */
|
|
37
|
+
restoreBefore(undoSnapshotId: string): boolean;
|
|
38
|
+
/** Restore the post-truncation conversation (the redo direction). */
|
|
39
|
+
restoreAfter(undoSnapshotId: string): boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** One truncation's captured state — the target conversation and its snapshots. */
|
|
43
|
+
interface SnapshotPair {
|
|
44
|
+
readonly conv: ConversationManager;
|
|
45
|
+
readonly before: ConversationJson;
|
|
46
|
+
readonly after: ConversationJson;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build a conversation rewind port. `resolveConversation` maps an anchor's
|
|
51
|
+
* sessionId to the live ConversationManager — for this Agent (a single-session
|
|
52
|
+
* process) that is the one bound conversation when the sessionId matches, and
|
|
53
|
+
* null otherwise. A null resolution means no live conversation for that
|
|
54
|
+
* session, reported as "nothing to drop" rather than a fabricated count.
|
|
55
|
+
*/
|
|
56
|
+
export function createConversationRewindPort(
|
|
57
|
+
resolveConversation: (sessionId: string) => ConversationManager | null,
|
|
58
|
+
): ConversationRewindPort {
|
|
59
|
+
const snapshots = new Map<string, SnapshotPair>();
|
|
60
|
+
|
|
61
|
+
function keepFor(anchor: RewindAnchor): { conv: ConversationManager | null; keep: number; total: number } {
|
|
62
|
+
const conv = resolveConversation(anchor.sessionId);
|
|
63
|
+
if (!conv) return { conv: null, keep: 0, total: 0 };
|
|
64
|
+
const total = conv.getMessageCount();
|
|
65
|
+
const rec = anchor.turnId ? resolveTurnAnchor(anchor.sessionId, anchor.turnId) : null;
|
|
66
|
+
const keep = rec ? Math.min(rec.messageCount, total) : total;
|
|
67
|
+
return { conv, keep, total };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function restore(snapshot: SnapshotPair | undefined, which: 'before' | 'after'): boolean {
|
|
71
|
+
if (!snapshot) return false;
|
|
72
|
+
snapshot.conv.fromJSON(snapshot[which]);
|
|
73
|
+
snapshot.conv.rebuildHistory();
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
async preview(anchor: RewindAnchor): Promise<RewindConversationPreview> {
|
|
79
|
+
const { keep, total } = keepFor(anchor);
|
|
80
|
+
return { messagesToDrop: Math.max(0, total - keep), messagesRemaining: keep };
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
async rewind(anchor: RewindAnchor): Promise<RewindConversationOutcome> {
|
|
84
|
+
const { conv, keep, total } = keepFor(anchor);
|
|
85
|
+
const undoSnapshotId = `rwc_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
86
|
+
if (!conv) return { droppedMessages: 0, undoSnapshotId };
|
|
87
|
+
const before = conv.toJSON() as ConversationJson;
|
|
88
|
+
conv.removeMessagesAfter(keep);
|
|
89
|
+
conv.rebuildHistory();
|
|
90
|
+
const after = conv.toJSON() as ConversationJson;
|
|
91
|
+
snapshots.set(undoSnapshotId, { conv, before, after });
|
|
92
|
+
return { droppedMessages: Math.max(0, total - keep), undoSnapshotId };
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
restoreBefore(undoSnapshotId: string): boolean {
|
|
96
|
+
return restore(snapshots.get(undoSnapshotId), 'before');
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
restoreAfter(undoSnapshotId: string): boolean {
|
|
100
|
+
return restore(snapshots.get(undoSnapshotId), 'after');
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Live per-session conversation registry — the daemon-hosted mutable store the
|
|
107
|
+
// composed daemon's rewind.plan/apply verbs resolve conversations from. This
|
|
108
|
+
// Agent registers its active session's ConversationManager at bootstrap; a
|
|
109
|
+
// session with no registration reports conversation rewind as "nothing to
|
|
110
|
+
// drop".
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
const liveConversations = new Map<string, ConversationManager>();
|
|
114
|
+
|
|
115
|
+
/** Register a session's live conversation so the daemon rewind verbs can serve it. */
|
|
116
|
+
export function registerSessionConversation(sessionId: string, conversation: ConversationManager): void {
|
|
117
|
+
if (sessionId) liveConversations.set(sessionId, conversation);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Drop a session's conversation registration. */
|
|
121
|
+
export function unregisterSessionConversation(sessionId: string): void {
|
|
122
|
+
liveConversations.delete(sessionId);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The conversation rewind port the composed daemon threads into
|
|
127
|
+
* registerGatewayVerbGroups — it resolves each anchor's live conversation from
|
|
128
|
+
* the registry above, so the daemon's own rewind verbs serve conversation scope
|
|
129
|
+
* live in this process.
|
|
130
|
+
*/
|
|
131
|
+
export function createSessionConversationRewindPort(): ConversationRewindPort {
|
|
132
|
+
return createConversationRewindPort((sessionId) => liveConversations.get(sessionId) ?? null);
|
|
133
|
+
}
|
package/src/runtime/index.ts
CHANGED
|
@@ -98,34 +98,14 @@ export const restoreSavedModel = bootstrap.restoreRuntimeModel;
|
|
|
98
98
|
export const synchronizeConfiguredServices = bootstrap.synchronizeConfiguredServices;
|
|
99
99
|
export const syncConfiguredServices = bootstrap.synchronizeConfiguredServices;
|
|
100
100
|
export const registerHostRuntimeEvents = bootstrap.registerHostRuntimeEvents;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
mode: service === 'daemon' ? 'external' : 'disabled',
|
|
111
|
-
host,
|
|
112
|
-
port,
|
|
113
|
-
baseUrl: `http://${host}:${port}`,
|
|
114
|
-
reason: service === 'daemon'
|
|
115
|
-
? 'GoodVibes Agent connects to a GoodVibes host owned outside this product and does not start or restart it.'
|
|
116
|
-
: 'GoodVibes Agent does not own listener lifecycle.',
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export const startHostServices: typeof bootstrap.startHostServices = async (config) => ({
|
|
121
|
-
daemonServer: null,
|
|
122
|
-
httpListener: null,
|
|
123
|
-
daemonStatus: agentExternalHostStatus(config, 'daemon'),
|
|
124
|
-
httpListenerStatus: agentExternalHostStatus(config, 'httpListener'),
|
|
125
|
-
listRecentControlPlaneEvents: () => [],
|
|
126
|
-
async stop(): Promise<void> {},
|
|
127
|
-
});
|
|
128
|
-
export const startExternalServices = startHostServices;
|
|
101
|
+
// Shared adopt-or-spawn policy (daemon-adoption-policy.ts): probes the
|
|
102
|
+
// configured host/port, band-checks any GoodVibes daemon found there, and
|
|
103
|
+
// only ever ADOPTS a compatible one — Agent passes `adoptOnly: true` at the
|
|
104
|
+
// call site (bootstrap.ts) so it never spawns or embeds a daemon itself. This
|
|
105
|
+
// replaces a local stub that hard-declared every daemon 'external' without
|
|
106
|
+
// probing or version-checking it (an unsafe divergence, resolved SDK-side).
|
|
107
|
+
export const startHostServices = bootstrap.startHostServices;
|
|
108
|
+
export const startExternalServices = bootstrap.startHostServices;
|
|
129
109
|
export const registerBootstrapHookBridge = bootstrap.registerBootstrapHookBridge;
|
|
130
110
|
export const createDeferredStartupCoordinator = bootstrap.createDeferredStartupCoordinator;
|
|
131
111
|
export const shutdownRuntime = bootstrap.shutdownRuntime;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { MemoryConsolidationRunReceipt, MemoryConsolidationTrigger } from '@pellux/goodvibes-sdk/platform/state';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Decides WHEN idle-time consolidation runs, keeping the real-state idleness
|
|
5
|
+
* check and the schedule cadence out of bootstrap wiring.
|
|
6
|
+
*
|
|
7
|
+
* Idleness is defined from real runtime state: `isIdle()` returns true only when
|
|
8
|
+
* there is no active turn in flight (bootstrap wires it to
|
|
9
|
+
* `activePromptTurnId === null`). The scheduler is poked on every turn-settled
|
|
10
|
+
* event; a run fires only when the job is enabled, the agent is idle, no run is
|
|
11
|
+
* already in flight, and at least `minIntervalMs` has elapsed since the last run.
|
|
12
|
+
* The same `maybeRun` path also serves an explicit trigger.
|
|
13
|
+
*/
|
|
14
|
+
export interface MemoryConsolidationSchedulerDeps {
|
|
15
|
+
/** Master switch — reads the resolved config's `enabled` (default false). */
|
|
16
|
+
readonly isEnabled: () => boolean;
|
|
17
|
+
/** Real-state idleness: true only when no turn is in flight. */
|
|
18
|
+
readonly isIdle: () => boolean;
|
|
19
|
+
/** Minimum ms between runs (the schedule cadence). */
|
|
20
|
+
readonly minIntervalMs: () => number;
|
|
21
|
+
/** Monotonic-enough clock, injected for tests. */
|
|
22
|
+
readonly now: () => number;
|
|
23
|
+
/** Performs the work + persists the receipt; returns it, or null when it did nothing. */
|
|
24
|
+
readonly run: (trigger: MemoryConsolidationTrigger) => MemoryConsolidationRunReceipt | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type MemoryConsolidationSkipReason =
|
|
28
|
+
| 'disabled'
|
|
29
|
+
| 'busy'
|
|
30
|
+
| 'not-idle'
|
|
31
|
+
| 'interval-not-elapsed';
|
|
32
|
+
|
|
33
|
+
export interface MemoryConsolidationSchedulerOutcome {
|
|
34
|
+
readonly ran: boolean;
|
|
35
|
+
readonly skipped?: MemoryConsolidationSkipReason;
|
|
36
|
+
readonly receipt?: MemoryConsolidationRunReceipt;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class MemoryConsolidationScheduler {
|
|
40
|
+
private lastRunAt = 0;
|
|
41
|
+
private running = false;
|
|
42
|
+
|
|
43
|
+
public constructor(private readonly deps: MemoryConsolidationSchedulerDeps) {}
|
|
44
|
+
|
|
45
|
+
/** Poke from a turn-settled event — the natural moment the agent becomes idle. */
|
|
46
|
+
public onTurnSettled(): MemoryConsolidationSchedulerOutcome {
|
|
47
|
+
return this.maybeRun('idle');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public maybeRun(trigger: MemoryConsolidationTrigger): MemoryConsolidationSchedulerOutcome {
|
|
51
|
+
if (!this.deps.isEnabled()) return { ran: false, skipped: 'disabled' };
|
|
52
|
+
if (this.running) return { ran: false, skipped: 'busy' };
|
|
53
|
+
if (!this.deps.isIdle()) return { ran: false, skipped: 'not-idle' };
|
|
54
|
+
const now = this.deps.now();
|
|
55
|
+
if (this.lastRunAt !== 0 && now - this.lastRunAt < this.deps.minIntervalMs()) {
|
|
56
|
+
return { ran: false, skipped: 'interval-not-elapsed' };
|
|
57
|
+
}
|
|
58
|
+
this.running = true;
|
|
59
|
+
try {
|
|
60
|
+
const receipt = this.deps.run(trigger);
|
|
61
|
+
this.lastRunAt = now;
|
|
62
|
+
return receipt ? { ran: true, receipt } : { ran: true };
|
|
63
|
+
} finally {
|
|
64
|
+
this.running = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
2
|
+
import {
|
|
3
|
+
resolveMemoryConsolidationConfig,
|
|
4
|
+
runMemoryConsolidation,
|
|
5
|
+
type MemoryRegistry,
|
|
6
|
+
type MemoryConsolidationUsageLookup,
|
|
7
|
+
} from '@pellux/goodvibes-sdk/platform/state';
|
|
8
|
+
import { logger, summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
9
|
+
import type { ShellPathService } from '@/runtime/index.ts';
|
|
10
|
+
import { MemoryConsolidationReceiptStore } from '../agent/memory-consolidation-receipts.ts';
|
|
11
|
+
import { MemoryConsolidationScheduler } from './memory-consolidation-scheduler.ts';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Assemble the idle-time memory consolidation scheduler from its runtime deps.
|
|
15
|
+
* Kept out of bootstrap so the run callback (config read → run → persist receipt)
|
|
16
|
+
* lives next to the rest of the consolidation module rather than inline in the
|
|
17
|
+
* boot sequence.
|
|
18
|
+
*/
|
|
19
|
+
export interface MemoryConsolidationWiringInput {
|
|
20
|
+
readonly configManager: Pick<ConfigManager, 'getRaw'>;
|
|
21
|
+
readonly memoryRegistry: MemoryRegistry;
|
|
22
|
+
readonly shellPaths: ShellPathService;
|
|
23
|
+
/** Real-state idleness: no turn in flight. */
|
|
24
|
+
readonly isIdle: () => boolean;
|
|
25
|
+
/** Optional usage instrumentation so never-referenced records decay first. */
|
|
26
|
+
readonly usageLookup?: MemoryConsolidationUsageLookup;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createMemoryConsolidationScheduler(input: MemoryConsolidationWiringInput): MemoryConsolidationScheduler {
|
|
30
|
+
const receipts = new MemoryConsolidationReceiptStore(input.shellPaths);
|
|
31
|
+
return new MemoryConsolidationScheduler({
|
|
32
|
+
isEnabled: () => resolveMemoryConsolidationConfig(input.configManager).enabled,
|
|
33
|
+
isIdle: input.isIdle,
|
|
34
|
+
minIntervalMs: () => resolveMemoryConsolidationConfig(input.configManager).intervalMs,
|
|
35
|
+
now: () => Date.now(),
|
|
36
|
+
run: (trigger) => {
|
|
37
|
+
try {
|
|
38
|
+
const config = resolveMemoryConsolidationConfig(input.configManager);
|
|
39
|
+
if (!config.enabled) return null;
|
|
40
|
+
const receipt = runMemoryConsolidation({
|
|
41
|
+
memoryRegistry: input.memoryRegistry,
|
|
42
|
+
config,
|
|
43
|
+
now: Date.now(),
|
|
44
|
+
trigger,
|
|
45
|
+
idle: input.isIdle(),
|
|
46
|
+
...(input.usageLookup ? { usageLookup: input.usageLookup } : {}),
|
|
47
|
+
});
|
|
48
|
+
receipts.record(receipt);
|
|
49
|
+
return receipt;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
logger.debug('Idle memory consolidation run failed', { error: summarizeError(error) });
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MemoryUsageStatsStore,
|
|
3
|
+
detectReferencedMemoryIds,
|
|
4
|
+
type MemoryRegistry,
|
|
5
|
+
type MemoryConsolidationUsageSignal,
|
|
6
|
+
type MemoryReferenceInput,
|
|
7
|
+
} from '@pellux/goodvibes-sdk/platform/state';
|
|
8
|
+
import type { ShellPathService } from '@/runtime/index.ts';
|
|
9
|
+
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
10
|
+
import type { PromptContextReceiptDraft } from '../agent/prompt-context-receipts.ts';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Ties the two ends of a turn together for usage-outcome instrumentation:
|
|
14
|
+
* records which memory ids were injected when a prompt is composed, then when the
|
|
15
|
+
* turn completes runs heuristic reference detection over the model's output to
|
|
16
|
+
* mark which of those injections were plausibly used. Aborted turns (error or
|
|
17
|
+
* cancel) simply forget the injection — no reference credit is invented.
|
|
18
|
+
*/
|
|
19
|
+
export function extractInjectedMemoryIds(receipt: PromptContextReceiptDraft): string[] {
|
|
20
|
+
const segment = receipt.segments.find((entry) => entry.id === 'memory');
|
|
21
|
+
const selected = segment?.selected ?? [];
|
|
22
|
+
const ids: string[] = [];
|
|
23
|
+
for (const entry of selected) {
|
|
24
|
+
const id = (entry as Record<string, unknown>).id;
|
|
25
|
+
if (typeof id === 'string' && id.trim()) ids.push(id);
|
|
26
|
+
}
|
|
27
|
+
return ids;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class MemoryUsageTracker {
|
|
31
|
+
private readonly injectedByTurn = new Map<string, readonly string[]>();
|
|
32
|
+
|
|
33
|
+
public constructor(
|
|
34
|
+
private readonly store: MemoryUsageStatsStore,
|
|
35
|
+
private readonly memoryRegistry: MemoryRegistry,
|
|
36
|
+
) {}
|
|
37
|
+
|
|
38
|
+
/** At prompt composition: count the injection and remember it for this turn. */
|
|
39
|
+
public onComposed(turnId: string | null, receipt: PromptContextReceiptDraft): void {
|
|
40
|
+
const ids = extractInjectedMemoryIds(receipt);
|
|
41
|
+
if (ids.length === 0) return;
|
|
42
|
+
this.store.recordInjected(ids);
|
|
43
|
+
if (turnId) this.injectedByTurn.set(turnId, ids);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** At turn completion: detect which injected memories the output plausibly used. */
|
|
47
|
+
public onTurnCompleted(turnId: string, response: string): void {
|
|
48
|
+
const ids = this.injectedByTurn.get(turnId);
|
|
49
|
+
this.injectedByTurn.delete(turnId);
|
|
50
|
+
if (!ids || ids.length === 0) return;
|
|
51
|
+
const records: MemoryReferenceInput[] = [];
|
|
52
|
+
for (const id of ids) {
|
|
53
|
+
const record = this.memoryRegistry.get(id);
|
|
54
|
+
if (record) records.push({ id: record.id, summary: record.summary, detail: record.detail });
|
|
55
|
+
}
|
|
56
|
+
if (records.length === 0) return;
|
|
57
|
+
const result = detectReferencedMemoryIds(response, records);
|
|
58
|
+
if (result.referenced.length > 0) this.store.recordReferenced(result.referenced);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** At turn error/cancel: forget the injection without crediting a reference. */
|
|
62
|
+
public onTurnAborted(turnId: string): void {
|
|
63
|
+
this.injectedByTurn.delete(turnId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Consolidation decay-ordering seam. */
|
|
67
|
+
public lookup(id: string): MemoryConsolidationUsageSignal | undefined {
|
|
68
|
+
return this.store.lookup(id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createMemoryUsageTracker(shellPaths: ShellPathService, memoryRegistry: MemoryRegistry): MemoryUsageTracker {
|
|
73
|
+
const store = new MemoryUsageStatsStore(shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'learning', 'memory-usage.json'));
|
|
74
|
+
return new MemoryUsageTracker(store, memoryRegistry);
|
|
75
|
+
}
|