aiden-runtime 4.1.1 → 4.1.3
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/README.md +78 -26
- package/dist/cli/v4/aidenCLI.js +169 -9
- package/dist/cli/v4/callbacks.js +20 -2
- package/dist/cli/v4/chatSession.js +644 -16
- package/dist/cli/v4/commands/auth.js +6 -3
- package/dist/cli/v4/commands/doctor.js +23 -27
- package/dist/cli/v4/commands/help.js +4 -0
- package/dist/cli/v4/commands/index.js +10 -1
- package/dist/cli/v4/commands/model.js +30 -1
- package/dist/cli/v4/commands/reloadSoul.js +37 -0
- package/dist/cli/v4/commands/update.js +102 -0
- package/dist/cli/v4/defaultSoul.js +68 -2
- package/dist/cli/v4/display/capabilityCard.js +135 -0
- package/dist/cli/v4/display/sessionEndCard.js +127 -0
- package/dist/cli/v4/display/toolTrail.js +172 -0
- package/dist/cli/v4/display.js +492 -142
- package/dist/cli/v4/doctor.js +472 -58
- package/dist/cli/v4/doctorLiveness.js +65 -10
- package/dist/cli/v4/promotionPrompt.js +332 -0
- package/dist/cli/v4/providerBootSelector.js +144 -0
- package/dist/cli/v4/replyRenderer.js +311 -20
- package/dist/cli/v4/sessionSummaryGate.js +66 -0
- package/dist/cli/v4/skinEngine.js +14 -3
- package/dist/cli/v4/toolPreview.js +153 -0
- package/dist/core/tools/nowPlaying.js +7 -15
- package/dist/core/v4/aidenAgent.js +91 -29
- package/dist/core/v4/capabilities.js +89 -0
- package/dist/core/v4/contextCompressor.js +25 -8
- package/dist/core/v4/distillationIndex.js +167 -0
- package/dist/core/v4/distillationStore.js +98 -0
- package/dist/core/v4/logger/logger.js +40 -9
- package/dist/core/v4/promotionCandidates.js +234 -0
- package/dist/core/v4/promptBuilder.js +145 -1
- package/dist/core/v4/sessionDistiller.js +452 -0
- package/dist/core/v4/skillMining/skillMiner.js +43 -6
- package/dist/core/v4/skillOutcomeTracker.js +323 -0
- package/dist/core/v4/subsystemHealth.js +143 -0
- package/dist/core/v4/toolRegistry.js +16 -1
- package/dist/core/v4/update/executeInstall.js +233 -0
- package/dist/core/version.js +1 -1
- package/dist/moat/memoryGuard.js +111 -0
- package/dist/moat/plannerGuard.js +19 -0
- package/dist/moat/skillTeacher.js +14 -5
- package/dist/providers/v4/chatCompletionsAdapter.js +9 -0
- package/dist/providers/v4/errors.js +112 -4
- package/dist/providers/v4/modelDefaults.js +65 -0
- package/dist/providers/v4/registry.js +9 -2
- package/dist/providers/v4/runtimeResolver.js +6 -0
- package/dist/tools/v4/index.js +80 -1
- package/dist/tools/v4/memory/memoryRemove.js +57 -2
- package/dist/tools/v4/memory/sessionSummary.js +151 -0
- package/dist/tools/v4/sessions/recallSession.js +177 -0
- package/dist/tools/v4/sessions/sessionSearch.js +5 -1
- package/dist/tools/v4/system/_psHelpers.js +123 -0
- package/dist/tools/v4/system/aidenSelfUpdate.js +162 -0
- package/dist/tools/v4/system/appClose.js +79 -0
- package/dist/tools/v4/system/appInput.js +154 -0
- package/dist/tools/v4/system/appLaunch.js +218 -0
- package/dist/tools/v4/system/clipboardRead.js +54 -0
- package/dist/tools/v4/system/clipboardWrite.js +84 -0
- package/dist/tools/v4/system/mediaKey.js +109 -0
- package/dist/tools/v4/system/mediaSessions.js +163 -0
- package/dist/tools/v4/system/mediaTransport.js +211 -0
- package/dist/tools/v4/system/osProcessList.js +99 -0
- package/dist/tools/v4/system/screenshot.js +106 -0
- package/dist/tools/v4/system/volumeSet.js +157 -0
- package/package.json +4 -1
- package/skills/system_control.md +185 -69
|
@@ -2,27 +2,19 @@
|
|
|
2
2
|
// core/tools/nowPlaying.ts — Live media session query via Windows WinRT
|
|
3
3
|
// Uses GlobalSystemMediaTransportControlsSessionManager (works for Spotify,
|
|
4
4
|
// YouTube in browser, Windows Media Player, and any SMTC-registered app).
|
|
5
|
+
//
|
|
6
|
+
// v4.1.4-media: the WinRT `Await` PS5.1 reflection bridge moved into the
|
|
7
|
+
// shared helper `tools/v4/system/_psHelpers.ts::winRtAwaitPreamble()` so
|
|
8
|
+
// the three GSMTC callers (this file + mediaSessions + mediaTransport)
|
|
9
|
+
// share one canonical implementation.
|
|
5
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
11
|
exports.getNowPlaying = getNowPlaying;
|
|
7
12
|
const child_process_1 = require("child_process");
|
|
8
13
|
const util_1 = require("util");
|
|
14
|
+
const _psHelpers_1 = require("../../tools/v4/system/_psHelpers");
|
|
9
15
|
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
10
|
-
// PowerShell uses System.WindowsRuntimeSystemExtensions.AsTask to bridge
|
|
11
|
-
// WinRT IAsyncOperation<T> into a .NET Task — required because PS5.1 cannot
|
|
12
|
-
// await WinRT async operations natively via .GetAwaiter().
|
|
13
16
|
const PS_SCRIPT = `
|
|
14
|
-
|
|
15
|
-
function Await($WinRtTask, $ResultType) {
|
|
16
|
-
$m = ([System.WindowsRuntimeSystemExtensions].GetMethods() | Where-Object {
|
|
17
|
-
$_.Name -eq 'AsTask' -and
|
|
18
|
-
$_.GetParameters().Count -eq 1 -and
|
|
19
|
-
$_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation\`1'
|
|
20
|
-
})[0]
|
|
21
|
-
$m = $m.MakeGenericMethod($ResultType)
|
|
22
|
-
$t = $m.Invoke($null, @($WinRtTask))
|
|
23
|
-
$t.Wait(-1) | Out-Null
|
|
24
|
-
$t.Result
|
|
25
|
-
}
|
|
17
|
+
${(0, _psHelpers_1.winRtAwaitPreamble)()}
|
|
26
18
|
$mgType = [Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager,Windows.Media.Control,ContentType=WindowsRuntime]
|
|
27
19
|
$mgr = Await ($mgType::RequestAsync()) $mgType
|
|
28
20
|
$s = $mgr.GetCurrentSession()
|
|
@@ -61,7 +61,11 @@ class AidenAgent {
|
|
|
61
61
|
/** Cached system prompt — invalidated by setPersonalityOverlay/markMemoryDirty/explicit. */
|
|
62
62
|
this.cachedSystemPrompt = null;
|
|
63
63
|
this.compressionEvents = 0;
|
|
64
|
-
|
|
64
|
+
// Phase v4.1.2: tracks which identity / memory files need a system-
|
|
65
|
+
// prompt rebuild on the next turn. Empty set = clean. Plain Set keeps
|
|
66
|
+
// the membership-test path O(1) and avoids the combinatorial union
|
|
67
|
+
// type the previous representation grew when SOUL.md joined the list.
|
|
68
|
+
this.memoryDirty = new Set();
|
|
65
69
|
/** Process-scoped tracker metrics for `/doctor`. */
|
|
66
70
|
this.skillEnforcementMetrics = {
|
|
67
71
|
recovered: 0, failed: 0, armed: 0, preArmed: 0,
|
|
@@ -100,6 +104,17 @@ class AidenAgent {
|
|
|
100
104
|
this.refreshMemorySnapshot = opts.refreshMemorySnapshot;
|
|
101
105
|
this.onMemoryRefresh = opts.onMemoryRefresh;
|
|
102
106
|
this.lookupSkillRequiredTools = opts.lookupSkillRequiredTools;
|
|
107
|
+
// Phase v4.1.2-slice3: optional health registry (constructor-
|
|
108
|
+
// injected per the slice3 decision tree — no singleton). When
|
|
109
|
+
// wired, the caller already plumbed trackers into each subsystem
|
|
110
|
+
// via their own constructors; we just hold the read handle.
|
|
111
|
+
this
|
|
112
|
+
.subsystemHealthRegistry = opts.subsystemHealthRegistry;
|
|
113
|
+
// Phase v4.1.2-slice4: same pattern for the outcome tracker. The
|
|
114
|
+
// caller composes the tracker into `onToolCall`; we just keep a
|
|
115
|
+
// read handle for doctor.
|
|
116
|
+
this
|
|
117
|
+
.skillOutcomeTracker = opts.skillOutcomeTracker;
|
|
103
118
|
}
|
|
104
119
|
// ── Public method surface ────────────────────────────────────────────
|
|
105
120
|
setProvider(adapter) {
|
|
@@ -124,6 +139,38 @@ class AidenAgent {
|
|
|
124
139
|
this.cachedSystemPrompt = null;
|
|
125
140
|
return true;
|
|
126
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Phase v4.1.2-bug2: replace the active provider/model fed into the
|
|
144
|
+
* `## Runtime` slot of the system prompt. Mirrors
|
|
145
|
+
* `setPersonalityOverlay` shape — mutate the cached PromptBuilder
|
|
146
|
+
* options + null the system-prompt cache so the next runConversation
|
|
147
|
+
* rebuilds with fresh values. Returns `true` when at least one of
|
|
148
|
+
* `providerId`/`modelId` actually changed; `false` is a no-op
|
|
149
|
+
* (caller may skip downstream signalling).
|
|
150
|
+
*
|
|
151
|
+
* This is NOT a dirty-bit invalidation — provider/model are
|
|
152
|
+
* in-memory field updates, not disk-backed reloads. The existing
|
|
153
|
+
* MemoryFile dirty-bit (`memory|user|soul`) governs file reload
|
|
154
|
+
* semantics and is intentionally not extended here.
|
|
155
|
+
*
|
|
156
|
+
* Called by chatSession.setProvider() after the adapter swap so the
|
|
157
|
+
* prompt's self-description stays in lockstep with the routed
|
|
158
|
+
* provider. Without this, `/model groq → chatgpt-plus` swaps the
|
|
159
|
+
* adapter (real requests route correctly) but the prompt keeps
|
|
160
|
+
* claiming "Provider: groq" for the rest of the session.
|
|
161
|
+
*/
|
|
162
|
+
setActiveModel(providerId, modelId) {
|
|
163
|
+
const cur = this.promptBuilderOptions;
|
|
164
|
+
if (cur?.providerId === providerId && cur?.modelId === modelId)
|
|
165
|
+
return false;
|
|
166
|
+
this.promptBuilderOptions = {
|
|
167
|
+
...(cur ?? {}),
|
|
168
|
+
providerId,
|
|
169
|
+
modelId,
|
|
170
|
+
};
|
|
171
|
+
this.cachedSystemPrompt = null;
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
127
174
|
/**
|
|
128
175
|
* Build (or return the cached) system prompt without driving the
|
|
129
176
|
* provider. Powers the `/debug-prompt` command. Returns `null` when no
|
|
@@ -138,23 +185,31 @@ class AidenAgent {
|
|
|
138
185
|
return this.cachedSystemPrompt;
|
|
139
186
|
}
|
|
140
187
|
/**
|
|
141
|
-
* Mark MEMORY.md / USER.md as dirty. The next
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
188
|
+
* Mark MEMORY.md / USER.md / SOUL.md as dirty. The next
|
|
189
|
+
* `runConversation` will rebuild the prompt, fire `onMemoryRefresh`,
|
|
190
|
+
* and clear the dirty set.
|
|
191
|
+
*
|
|
192
|
+
* - 'memory' / 'user' refresh through `refreshMemorySnapshot` (the
|
|
193
|
+
* in-memory MEMORY.md / USER.md blobs need a re-read). No-op when
|
|
194
|
+
* no refresh callback is wired (frozen-snapshot semantics).
|
|
195
|
+
* - 'soul' just invalidates the prompt cache; SOUL.md is re-read
|
|
196
|
+
* from disk by `PromptBuilder.build()` on the next rebuild. No
|
|
197
|
+
* snapshot callback required, so this kind always takes effect.
|
|
145
198
|
*/
|
|
146
199
|
markMemoryDirty(file) {
|
|
147
|
-
if (!this.refreshMemorySnapshot)
|
|
200
|
+
if ((file === 'memory' || file === 'user') && !this.refreshMemorySnapshot) {
|
|
148
201
|
return;
|
|
149
|
-
if (this.memoryDirty === null) {
|
|
150
|
-
this.memoryDirty = file;
|
|
151
|
-
}
|
|
152
|
-
else if (this.memoryDirty !== file) {
|
|
153
|
-
this.memoryDirty = 'both';
|
|
154
202
|
}
|
|
203
|
+
this.memoryDirty.add(file);
|
|
155
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Returns the set of dirty files as a stable-sorted readonly array.
|
|
207
|
+
* Empty array = clean. (Phase v4.1.2: replaces the older
|
|
208
|
+
* `'memory' | 'user' | 'both' | null` return type now that SOUL.md
|
|
209
|
+
* joins the rotation — a Set scales without union-type explosion.)
|
|
210
|
+
*/
|
|
156
211
|
getMemoryDirtyState() {
|
|
157
|
-
return this.memoryDirty;
|
|
212
|
+
return [...this.memoryDirty].sort();
|
|
158
213
|
}
|
|
159
214
|
/** /doctor accessor for cumulative skill-enforcement counters. */
|
|
160
215
|
getSkillEnforcementMetrics() {
|
|
@@ -319,28 +374,35 @@ class AidenAgent {
|
|
|
319
374
|
}
|
|
320
375
|
// ── Private helpers ──────────────────────────────────────────────────
|
|
321
376
|
async refreshSystemPromptIfDirty() {
|
|
322
|
-
if (this.memoryDirty ===
|
|
377
|
+
if (this.memoryDirty.size === 0)
|
|
323
378
|
return;
|
|
324
|
-
if (!this.
|
|
325
|
-
this.memoryDirty
|
|
379
|
+
if (!this.promptBuilder || !this.promptBuilderOptions) {
|
|
380
|
+
this.memoryDirty.clear();
|
|
326
381
|
return;
|
|
327
382
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
383
|
+
const dirtyFiles = [...this.memoryDirty].sort();
|
|
384
|
+
// 'soul' is satisfied by a cache invalidation alone — SOUL.md is
|
|
385
|
+
// re-read by PromptBuilder.build() on the next rebuild. 'memory'
|
|
386
|
+
// / 'user' need a snapshot refresh first.
|
|
387
|
+
const needsSnapshot = this.memoryDirty.has('memory') || this.memoryDirty.has('user');
|
|
388
|
+
if (needsSnapshot && this.refreshMemorySnapshot) {
|
|
389
|
+
let snapshot;
|
|
390
|
+
try {
|
|
391
|
+
snapshot = await this.refreshMemorySnapshot();
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
// Leave the dirty set as-is so the next turn retries. We don't
|
|
395
|
+
// break this turn over a transient memory-read failure.
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
this.promptBuilderOptions = {
|
|
399
|
+
...this.promptBuilderOptions,
|
|
400
|
+
memorySnapshot: snapshot,
|
|
401
|
+
};
|
|
336
402
|
}
|
|
337
|
-
this.promptBuilderOptions = {
|
|
338
|
-
...this.promptBuilderOptions,
|
|
339
|
-
memorySnapshot: snapshot,
|
|
340
|
-
};
|
|
341
403
|
this.cachedSystemPrompt = null;
|
|
342
|
-
this.onMemoryRefresh?.(
|
|
343
|
-
this.memoryDirty
|
|
404
|
+
this.onMemoryRefresh?.(dirtyFiles);
|
|
405
|
+
this.memoryDirty.clear();
|
|
344
406
|
}
|
|
345
407
|
async ensureSystemPrompt() {
|
|
346
408
|
if (!this.promptBuilder || !this.promptBuilderOptions)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/capabilities.ts — Phase v4.1.2-followup self-awareness.
|
|
10
|
+
*
|
|
11
|
+
* Runtime-computed manifest of what Aiden actually has loaded. Fed
|
|
12
|
+
* into the `## Runtime` slot of the system prompt so the model can
|
|
13
|
+
* answer questions like "what version are you" and "what tools do
|
|
14
|
+
* you have" from facts in its context, instead of hallucinating from
|
|
15
|
+
* whatever stale text used to live in SOUL.md.
|
|
16
|
+
*
|
|
17
|
+
* The manifest is computed at prompt-build time, never cached
|
|
18
|
+
* separately — it piggybacks on the existing system-prompt cache.
|
|
19
|
+
* On dirty-bit invalidation (memory / user / soul write, or
|
|
20
|
+
* personality overlay change) the prompt rebuilds and so do these
|
|
21
|
+
* numbers.
|
|
22
|
+
*
|
|
23
|
+
* No hardcoded "shipped vs deferred" framing here. The slot describes
|
|
24
|
+
* what IS loaded; absence is absence, not declared deferral.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.buildRuntimeManifest = buildRuntimeManifest;
|
|
28
|
+
exports.renderRuntimeSlot = renderRuntimeSlot;
|
|
29
|
+
const version_1 = require("../version");
|
|
30
|
+
/**
|
|
31
|
+
* TODO(v4.2): replace with a proper channel registry enumeration once
|
|
32
|
+
* channels expose a registration API. Today the gateway adapters are
|
|
33
|
+
* wired directly in `api/server.ts` (DiscordAdapter, SlackAdapter,
|
|
34
|
+
* TelegramAdapter, WhatsAppAdapter, EmailAdapter, WebhookAdapter,
|
|
35
|
+
* TwilioAdapter, IMessageAdapter, SignalAdapter — nine in total) and
|
|
36
|
+
* interaction surfaces (cli REPL, MCP server, OpenAI-compat HTTP, voice
|
|
37
|
+
* mode, headless --no-ui, web dashboard) are scattered across cli/v4
|
|
38
|
+
* and api/. This list conflates the two for the user-visible "channels
|
|
39
|
+
* Aiden is available on" count; a follow-up should pick a single
|
|
40
|
+
* definition and back this with a real registry.
|
|
41
|
+
*/
|
|
42
|
+
const KNOWN_CHANNELS = Object.freeze([
|
|
43
|
+
'cli',
|
|
44
|
+
'telegram',
|
|
45
|
+
'discord',
|
|
46
|
+
'slack',
|
|
47
|
+
'mcp',
|
|
48
|
+
'voice',
|
|
49
|
+
'headless',
|
|
50
|
+
'web',
|
|
51
|
+
'api',
|
|
52
|
+
]);
|
|
53
|
+
/**
|
|
54
|
+
* Build the manifest from caller-supplied counts + persistent imports.
|
|
55
|
+
* Pure function — no side effects, no async, no I/O — so PromptBuilder
|
|
56
|
+
* can call it inline and keep the same determinism contract for its
|
|
57
|
+
* prefix-cache friendliness.
|
|
58
|
+
*/
|
|
59
|
+
function buildRuntimeManifest(opts) {
|
|
60
|
+
return {
|
|
61
|
+
version: version_1.VERSION,
|
|
62
|
+
toolCount: opts.toolCount,
|
|
63
|
+
skillCount: opts.skillCount,
|
|
64
|
+
channels: KNOWN_CHANNELS,
|
|
65
|
+
providerId: opts.providerId,
|
|
66
|
+
modelId: opts.modelId,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Render the manifest as the `## Runtime` prompt slot. Visual style
|
|
71
|
+
* mirrors the other slots in PromptBuilder — h2 header, simple
|
|
72
|
+
* `key: value` lines, no marketing speak.
|
|
73
|
+
*
|
|
74
|
+
* Always emits a complete block even when provider/model are unknown;
|
|
75
|
+
* the contract is "always present, even if some values are unknown"
|
|
76
|
+
* so the model doesn't second-guess whether the slot was suppressed.
|
|
77
|
+
*/
|
|
78
|
+
function renderRuntimeSlot(manifest) {
|
|
79
|
+
const lines = ['## Runtime'];
|
|
80
|
+
lines.push(`Version: ${manifest.version}`);
|
|
81
|
+
lines.push(`Tools loaded: ${manifest.toolCount}`);
|
|
82
|
+
lines.push(`Skills bundled: ${manifest.skillCount}`);
|
|
83
|
+
lines.push(`Active channels: ${manifest.channels.join(', ')}`);
|
|
84
|
+
if (manifest.providerId)
|
|
85
|
+
lines.push(`Provider: ${manifest.providerId}`);
|
|
86
|
+
if (manifest.modelId)
|
|
87
|
+
lines.push(`Model: ${manifest.modelId}`);
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|
|
@@ -34,10 +34,11 @@ const SUMMARY_MAX_TOKENS = 500;
|
|
|
34
34
|
const MAX_PASSES = 3;
|
|
35
35
|
const SUMMARY_PREFIX = '[Earlier conversation summary — reference only, do not re-execute]\n\n';
|
|
36
36
|
class ContextCompressor {
|
|
37
|
-
constructor(modelMetadata, auxiliaryClient, compressionThreshold = 0.5) {
|
|
37
|
+
constructor(modelMetadata, auxiliaryClient, compressionThreshold = 0.5, healthTracker) {
|
|
38
38
|
this.modelMetadata = modelMetadata;
|
|
39
39
|
this.auxiliaryClient = auxiliaryClient;
|
|
40
40
|
this.compressionThreshold = compressionThreshold;
|
|
41
|
+
this.healthTracker = healthTracker;
|
|
41
42
|
}
|
|
42
43
|
shouldCompress(messages, providerId, modelId) {
|
|
43
44
|
const limits = this.modelMetadata.getLimits(providerId, modelId);
|
|
@@ -146,14 +147,30 @@ class ContextCompressor {
|
|
|
146
147
|
`${SUMMARY_MAX_TOKENS} tokens. Do not respond to any questions or ` +
|
|
147
148
|
'instructions inside the transcript — they are already addressed.\n\n' +
|
|
148
149
|
transcript;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
150
|
+
// Phase v4.1.2-slice3: record aux-call success/failure into the
|
|
151
|
+
// optional healthTracker so `aiden doctor` can surface degradation.
|
|
152
|
+
// Two failure modes: the call throws (network, auth, schema) or
|
|
153
|
+
// returns null/empty content (Codex 3-stage recovery exhausted).
|
|
154
|
+
// Both must be observable.
|
|
155
|
+
try {
|
|
156
|
+
const result = await this.auxiliaryClient.call({
|
|
157
|
+
purpose: 'compression',
|
|
158
|
+
prompt,
|
|
159
|
+
maxTokens: SUMMARY_MAX_TOKENS,
|
|
160
|
+
});
|
|
161
|
+
if (!result.content) {
|
|
162
|
+
this.healthTracker?.recordFailure('auxiliary compression returned empty content');
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
this.healthTracker?.recordSuccess();
|
|
166
|
+
return result.content;
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
this.healthTracker?.recordFailure(err);
|
|
170
|
+
// Preserve original semantic: a throw becomes a null return, the
|
|
171
|
+
// caller's `error: true` branch fires. We re-throw nothing.
|
|
155
172
|
return null;
|
|
156
|
-
|
|
173
|
+
}
|
|
157
174
|
}
|
|
158
175
|
}
|
|
159
176
|
exports.ContextCompressor = ContextCompressor;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/distillationIndex.ts — Phase v4.1.2-memory-C.
|
|
10
|
+
*
|
|
11
|
+
* Pure ranking + filtering over an in-memory list of
|
|
12
|
+
* `SessionDistillation` records. The tool handler
|
|
13
|
+
* (`tools/v4/sessions/recallSession.ts`) reads JSON files off disk
|
|
14
|
+
* and passes them in here; this module has no I/O.
|
|
15
|
+
*
|
|
16
|
+
* Ranking rules (per slice's Q3):
|
|
17
|
+
* - No query → recency-only: sort by `ended_at` desc, take top N.
|
|
18
|
+
* - Query present → score by total keyword-substring matches across:
|
|
19
|
+
* keywords[], bullets[], decisions[], open_items[],
|
|
20
|
+
* tools_used[].name
|
|
21
|
+
* Recency breaks ties.
|
|
22
|
+
* - `days` window filters out anything with
|
|
23
|
+
* `now - ended_at > days * 86_400_000` BEFORE scoring.
|
|
24
|
+
*
|
|
25
|
+
* No hybrid weighting, no LLM call, no embeddings — those are Phase E
|
|
26
|
+
* concerns. Today's ranking stays debuggable: the user can read why a
|
|
27
|
+
* result ranked where it did from `relevance` + the match field
|
|
28
|
+
* listed in each candidate.
|
|
29
|
+
*
|
|
30
|
+
* Index strategy: scan-all. Expected file count is <1000 per user;
|
|
31
|
+
* the tool handler reads every file from disk per query (sub-100ms at
|
|
32
|
+
* that scale). When real usage shows query latency >500ms, migrate
|
|
33
|
+
* directly to SQLite FTS5 — skip a JSON-index intermediate step.
|
|
34
|
+
*/
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.rankDistillations = rankDistillations;
|
|
37
|
+
exports.scoreMatch = scoreMatch;
|
|
38
|
+
// ── Constants ─────────────────────────────────────────────────────────────
|
|
39
|
+
const DEFAULT_LIMIT = 5;
|
|
40
|
+
const MAX_LIMIT = 25;
|
|
41
|
+
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
42
|
+
// ── Pure ranking ──────────────────────────────────────────────────────────
|
|
43
|
+
/**
|
|
44
|
+
* Rank + filter distillations against a query. Pure: no I/O, no
|
|
45
|
+
* side effects, no clock-reads other than `nowMs` for the recency
|
|
46
|
+
* window (injectable for deterministic tests).
|
|
47
|
+
*
|
|
48
|
+
* @param dists Every distillation read from disk. Scan-all per slice's
|
|
49
|
+
* index strategy — caller owns the I/O.
|
|
50
|
+
* @param query User-supplied recall query.
|
|
51
|
+
* @param nowMs Reference time for the days window. Defaults to
|
|
52
|
+
* `Date.now()`; tests inject a fixed value.
|
|
53
|
+
*/
|
|
54
|
+
function rankDistillations(dists, query = {}, nowMs = Date.now()) {
|
|
55
|
+
const scanned = dists.length;
|
|
56
|
+
const limit = clampLimit(query.limit);
|
|
57
|
+
const keyword = (query.query ?? '').trim().toLowerCase();
|
|
58
|
+
// 1. Days window filter.
|
|
59
|
+
let pool;
|
|
60
|
+
if (typeof query.days === 'number' && query.days > 0) {
|
|
61
|
+
const cutoff = nowMs - query.days * ONE_DAY_MS;
|
|
62
|
+
pool = dists.filter((d) => {
|
|
63
|
+
const t = Date.parse(d.ended_at);
|
|
64
|
+
return Number.isFinite(t) && t >= cutoff;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
pool = [...dists];
|
|
69
|
+
}
|
|
70
|
+
// 2. Score + filter.
|
|
71
|
+
let scored;
|
|
72
|
+
let relevance;
|
|
73
|
+
if (keyword.length === 0) {
|
|
74
|
+
// Recency-only: every survivor passes; score by inverse end-time
|
|
75
|
+
// so the sort below is identical to "newest first".
|
|
76
|
+
scored = pool.map((d) => ({
|
|
77
|
+
d,
|
|
78
|
+
score: 0,
|
|
79
|
+
ended: safeEndedMs(d),
|
|
80
|
+
}));
|
|
81
|
+
relevance = 'recency';
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
scored = [];
|
|
85
|
+
for (const d of pool) {
|
|
86
|
+
const score = scoreMatch(d, keyword);
|
|
87
|
+
if (score > 0)
|
|
88
|
+
scored.push({ d, score, ended: safeEndedMs(d) });
|
|
89
|
+
}
|
|
90
|
+
relevance = 'keyword';
|
|
91
|
+
}
|
|
92
|
+
// 3. Sort. Keyword path: score desc, recency tiebreak. Recency
|
|
93
|
+
// path: ended desc.
|
|
94
|
+
scored.sort((a, b) => {
|
|
95
|
+
if (keyword.length > 0 && a.score !== b.score)
|
|
96
|
+
return b.score - a.score;
|
|
97
|
+
return b.ended - a.ended;
|
|
98
|
+
});
|
|
99
|
+
const total_found = scored.length;
|
|
100
|
+
const matches = scored
|
|
101
|
+
.slice(0, limit)
|
|
102
|
+
.map(({ d }) => toRecallMatch(d, relevance, query.include_full === true));
|
|
103
|
+
return { matches, total_found, scanned };
|
|
104
|
+
}
|
|
105
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
106
|
+
function clampLimit(raw) {
|
|
107
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw))
|
|
108
|
+
return DEFAULT_LIMIT;
|
|
109
|
+
return Math.max(1, Math.min(MAX_LIMIT, Math.floor(raw)));
|
|
110
|
+
}
|
|
111
|
+
function safeEndedMs(d) {
|
|
112
|
+
const t = Date.parse(d.ended_at);
|
|
113
|
+
return Number.isFinite(t) ? t : 0;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Compute the keyword-match score for one distillation. Counts every
|
|
117
|
+
* field-string that contains the keyword as a substring (case-fold).
|
|
118
|
+
* Each hit = 1 point — simple and debuggable. Multi-occurrence inside
|
|
119
|
+
* one string still counts as 1 hit (we score field presence, not
|
|
120
|
+
* frequency).
|
|
121
|
+
*
|
|
122
|
+
* Fields scanned (per slice's explicit list):
|
|
123
|
+
* - keywords[]
|
|
124
|
+
* - bullets[]
|
|
125
|
+
* - decisions[]
|
|
126
|
+
* - open_items[]
|
|
127
|
+
* - tools_used[].name
|
|
128
|
+
*/
|
|
129
|
+
function scoreMatch(d, keyword) {
|
|
130
|
+
let score = 0;
|
|
131
|
+
for (const k of d.keywords)
|
|
132
|
+
if (k.toLowerCase().includes(keyword))
|
|
133
|
+
score += 1;
|
|
134
|
+
for (const b of d.bullets)
|
|
135
|
+
if (b.toLowerCase().includes(keyword))
|
|
136
|
+
score += 1;
|
|
137
|
+
for (const dc of d.decisions)
|
|
138
|
+
if (dc.toLowerCase().includes(keyword))
|
|
139
|
+
score += 1;
|
|
140
|
+
for (const o of d.open_items)
|
|
141
|
+
if (o.toLowerCase().includes(keyword))
|
|
142
|
+
score += 1;
|
|
143
|
+
for (const t of d.tools_used)
|
|
144
|
+
if (t.name.toLowerCase().includes(keyword))
|
|
145
|
+
score += 1;
|
|
146
|
+
return score;
|
|
147
|
+
}
|
|
148
|
+
function toRecallMatch(d, relevance, includeFull) {
|
|
149
|
+
const out = {
|
|
150
|
+
session_id: d.session_id,
|
|
151
|
+
started_at: d.started_at,
|
|
152
|
+
ended_at: d.ended_at,
|
|
153
|
+
exit_path: d.exit_path,
|
|
154
|
+
relevance,
|
|
155
|
+
bullets: d.bullets,
|
|
156
|
+
decisions: d.decisions,
|
|
157
|
+
open_items: d.open_items,
|
|
158
|
+
files_touched: d.files_touched,
|
|
159
|
+
};
|
|
160
|
+
if (includeFull) {
|
|
161
|
+
out.tools_used = d.tools_used;
|
|
162
|
+
out.keywords = d.keywords;
|
|
163
|
+
}
|
|
164
|
+
if (d.partial)
|
|
165
|
+
out.partial = true;
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/distillationStore.ts — Phase v4.1.2-memory-AB.
|
|
10
|
+
*
|
|
11
|
+
* On-disk persistence for `SessionDistillation` objects. One JSON file
|
|
12
|
+
* per session at `<dir>/<session_id>.json`. Atomic writes via tempfile
|
|
13
|
+
* + rename (same pattern as slice4's SkillOutcomeTracker).
|
|
14
|
+
*
|
|
15
|
+
* Disk layout intentionally flat — Phase C's retrieval surface will
|
|
16
|
+
* scan this directory and index the results. No subdirectories,
|
|
17
|
+
* no sharding; sessions are bounded enough that a single dir works
|
|
18
|
+
* (the typical user produces tens to low-hundreds of sessions/year).
|
|
19
|
+
*
|
|
20
|
+
* Failures are caught + surfaced via a slice3 SubsystemHealthTracker
|
|
21
|
+
* when one is wired; the write resolves anyway so the caller (chat
|
|
22
|
+
* session exit path) is never stuck on a disk failure.
|
|
23
|
+
*/
|
|
24
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
25
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
26
|
+
};
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.writeDistillation = writeDistillation;
|
|
29
|
+
exports.readDistillation = readDistillation;
|
|
30
|
+
exports.listDistillationIds = listDistillationIds;
|
|
31
|
+
const node_fs_1 = require("node:fs");
|
|
32
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
33
|
+
/**
|
|
34
|
+
* Write one distillation file under `dir/<session_id>.json`. Atomic:
|
|
35
|
+
* writes to `<file>.tmp` then renames. Returns the final path on
|
|
36
|
+
* success, throws when the rename can't complete.
|
|
37
|
+
*
|
|
38
|
+
* @param healthTracker Optional — if provided, success/failure is
|
|
39
|
+
* recorded for `aiden doctor` surfacing.
|
|
40
|
+
*/
|
|
41
|
+
async function writeDistillation(dir, dist, healthTracker) {
|
|
42
|
+
const file = node_path_1.default.join(dir, `${dist.session_id}.json`);
|
|
43
|
+
const tmp = `${file}.tmp`;
|
|
44
|
+
try {
|
|
45
|
+
await node_fs_1.promises.mkdir(dir, { recursive: true });
|
|
46
|
+
await node_fs_1.promises.writeFile(tmp, JSON.stringify(dist, null, 2) + '\n', 'utf-8');
|
|
47
|
+
await node_fs_1.promises.rename(tmp, file);
|
|
48
|
+
healthTracker?.recordSuccess();
|
|
49
|
+
return file;
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
healthTracker?.recordFailure(err);
|
|
53
|
+
// Clean up any orphaned tempfile — best-effort.
|
|
54
|
+
try {
|
|
55
|
+
await node_fs_1.promises.unlink(tmp);
|
|
56
|
+
}
|
|
57
|
+
catch { /* ignore */ }
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Read one distillation by session id. Returns `null` when the file
|
|
63
|
+
* doesn't exist; throws on parse / permission errors.
|
|
64
|
+
*
|
|
65
|
+
* Caller is responsible for validating `schema_version` if it cares
|
|
66
|
+
* about future migrations. No version coercion in v1.
|
|
67
|
+
*/
|
|
68
|
+
async function readDistillation(dir, sessionId) {
|
|
69
|
+
const file = node_path_1.default.join(dir, `${sessionId}.json`);
|
|
70
|
+
try {
|
|
71
|
+
const raw = await node_fs_1.promises.readFile(file, 'utf-8');
|
|
72
|
+
return JSON.parse(raw);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (err.code === 'ENOENT')
|
|
76
|
+
return null;
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* List session ids that have a distillation on disk. Returns the
|
|
82
|
+
* basenames (without `.json` extension), sorted lexicographically.
|
|
83
|
+
* Used by Phase C's retrieval index.
|
|
84
|
+
*/
|
|
85
|
+
async function listDistillationIds(dir) {
|
|
86
|
+
try {
|
|
87
|
+
const entries = await node_fs_1.promises.readdir(dir);
|
|
88
|
+
return entries
|
|
89
|
+
.filter((e) => e.endsWith('.json') && !e.endsWith('.tmp.json'))
|
|
90
|
+
.map((e) => e.slice(0, -'.json'.length))
|
|
91
|
+
.sort();
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
if (err.code === 'ENOENT')
|
|
95
|
+
return [];
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
}
|