aiden-runtime 4.1.0 → 4.1.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/README.md +89 -33
- package/dist/cli/v4/aidenCLI.js +162 -11
- package/dist/cli/v4/callbacks.js +5 -2
- package/dist/cli/v4/chatSession.js +525 -15
- package/dist/cli/v4/commands/auth.js +6 -3
- package/dist/cli/v4/commands/help.js +4 -0
- package/dist/cli/v4/commands/index.js +10 -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.js +28 -10
- package/dist/cli/v4/doctor.js +173 -1
- package/dist/cli/v4/doctorLiveness.js +384 -0
- package/dist/cli/v4/promotionPrompt.js +202 -0
- package/dist/cli/v4/providerBootSelector.js +144 -0
- package/dist/cli/v4/sessionSummaryGate.js +66 -0
- package/dist/cli/v4/toolPreview.js +139 -0
- 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 +405 -0
- package/dist/core/v4/skillMining/extractorPrompt.js +28 -21
- package/dist/core/v4/skillMining/proposalBuilder.js +3 -2
- 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/update/executeInstall.js +233 -0
- package/dist/core/version.js +1 -1
- package/dist/moat/dangerousPatterns.js +1 -1
- package/dist/moat/memoryGuard.js +111 -0
- package/dist/moat/skillTeacher.js +14 -5
- package/dist/providers/v4/chatCompletionsAdapter.js +9 -0
- package/dist/providers/v4/codexResponsesAdapter.js +7 -2
- package/dist/providers/v4/errors.js +67 -1
- package/dist/providers/v4/modelDefaults.js +65 -0
- package/dist/providers/v4/ollamaPromptToolsAdapter.js +9 -2
- package/dist/providers/v4/registry.js +9 -2
- package/dist/providers/v4/runtimeResolver.js +6 -0
- package/dist/tools/v4/index.js +57 -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 +163 -0
- package/dist/tools/v4/sessions/sessionSearch.js +5 -1
- package/dist/tools/v4/subagent/subagentFanout.js +24 -0
- package/dist/tools/v4/system/_psHelpers.js +55 -0
- package/dist/tools/v4/system/aidenSelfUpdate.js +162 -0
- package/dist/tools/v4/system/appClose.js +79 -0
- package/dist/tools/v4/system/appLaunch.js +92 -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 +78 -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 +135 -69
|
@@ -19,11 +19,45 @@
|
|
|
19
19
|
* 5. Re-renders the status line after every turn.
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
25
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
26
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
27
|
+
}
|
|
28
|
+
Object.defineProperty(o, k2, desc);
|
|
29
|
+
}) : (function(o, m, k, k2) {
|
|
30
|
+
if (k2 === undefined) k2 = k;
|
|
31
|
+
o[k2] = m[k];
|
|
32
|
+
}));
|
|
33
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
34
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
35
|
+
}) : function(o, v) {
|
|
36
|
+
o["default"] = v;
|
|
37
|
+
});
|
|
38
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
39
|
+
var ownKeys = function(o) {
|
|
40
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
41
|
+
var ar = [];
|
|
42
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
43
|
+
return ar;
|
|
44
|
+
};
|
|
45
|
+
return ownKeys(o);
|
|
46
|
+
};
|
|
47
|
+
return function (mod) {
|
|
48
|
+
if (mod && mod.__esModule) return mod;
|
|
49
|
+
var result = {};
|
|
50
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
51
|
+
__setModuleDefault(result, mod);
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
})();
|
|
22
55
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
23
56
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
24
57
|
};
|
|
25
58
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
59
|
exports.BOOT_TRY_HINT = exports.ChatSession = void 0;
|
|
60
|
+
exports.parseSessionBulletsResponse = parseSessionBulletsResponse;
|
|
27
61
|
exports.renderCommandLabel = renderCommandLabel;
|
|
28
62
|
exports.detectOS = detectOS;
|
|
29
63
|
exports.detectShell = detectShell;
|
|
@@ -36,14 +70,70 @@ exports.formatDuration = formatDuration;
|
|
|
36
70
|
exports.renderMemoryConfirmations = renderMemoryConfirmations;
|
|
37
71
|
const display_1 = require("./display");
|
|
38
72
|
const uiBuild_1 = require("./uiBuild");
|
|
73
|
+
const sessionSummaryGate_1 = require("./sessionSummaryGate");
|
|
39
74
|
const aidenPrompt_1 = __importDefault(require("./aidenPrompt"));
|
|
40
75
|
const historyStore_1 = require("./historyStore");
|
|
41
76
|
const modelMetadata_1 = require("../../core/v4/modelMetadata");
|
|
77
|
+
const sessionDistiller_1 = require("../../core/v4/sessionDistiller");
|
|
78
|
+
const version_1 = require("../../core/version");
|
|
79
|
+
const distillationStore_1 = require("../../core/v4/distillationStore");
|
|
80
|
+
const promotionCandidates_1 = require("../../core/v4/promotionCandidates");
|
|
81
|
+
const promotionPrompt_1 = require("./promotionPrompt");
|
|
82
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
42
83
|
const bracketedPaste_1 = require("./bracketedPaste");
|
|
43
84
|
const pasteCompression_1 = require("./pasteCompression");
|
|
44
85
|
const pasteIntercept_1 = require("./pasteIntercept");
|
|
45
86
|
const shellInterpolation_1 = require("./shellInterpolation");
|
|
46
87
|
const resizeGuard_1 = require("./resizeGuard");
|
|
88
|
+
/**
|
|
89
|
+
* Phase v4.1.2 session-summary-followup: parse the auxiliary client's
|
|
90
|
+
* JSON-array response into a clean `string[]` of bullets. Defensive —
|
|
91
|
+
* tries direct JSON.parse first, then a fenced-code-block strip, then
|
|
92
|
+
* a "first [...] block" extraction. Returns null when nothing usable
|
|
93
|
+
* comes out so the caller can retry once with a stricter prompt.
|
|
94
|
+
*
|
|
95
|
+
* Exported for unit tests.
|
|
96
|
+
*/
|
|
97
|
+
function parseSessionBulletsResponse(raw) {
|
|
98
|
+
if (typeof raw !== 'string' || raw.trim().length === 0)
|
|
99
|
+
return null;
|
|
100
|
+
const tryParseArray = (s) => {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(s);
|
|
103
|
+
if (!Array.isArray(parsed))
|
|
104
|
+
return null;
|
|
105
|
+
const strings = parsed
|
|
106
|
+
.filter((x) => typeof x === 'string')
|
|
107
|
+
.map((x) => x.trim())
|
|
108
|
+
.filter((x) => x.length > 0);
|
|
109
|
+
return strings.length > 0 ? strings : null;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
// 1. Try the response as-is.
|
|
116
|
+
const direct = tryParseArray(raw.trim());
|
|
117
|
+
if (direct)
|
|
118
|
+
return direct;
|
|
119
|
+
// 2. Strip Markdown code fences if present (```json ... ``` or ``` ... ```).
|
|
120
|
+
const fenceMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
121
|
+
if (fenceMatch && fenceMatch[1]) {
|
|
122
|
+
const inFence = tryParseArray(fenceMatch[1].trim());
|
|
123
|
+
if (inFence)
|
|
124
|
+
return inFence;
|
|
125
|
+
}
|
|
126
|
+
// 3. Extract the first balanced [...] block from anywhere in the text.
|
|
127
|
+
const bracketStart = raw.indexOf('[');
|
|
128
|
+
const bracketEnd = raw.lastIndexOf(']');
|
|
129
|
+
if (bracketStart >= 0 && bracketEnd > bracketStart) {
|
|
130
|
+
const slice = raw.slice(bracketStart, bracketEnd + 1);
|
|
131
|
+
const extracted = tryParseArray(slice);
|
|
132
|
+
if (extracted)
|
|
133
|
+
return extracted;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
47
137
|
/**
|
|
48
138
|
* Tier-3.1 helper: render a slash-command label honouring the
|
|
49
139
|
* `AIDEN_UI_ICONS` opt-in. Default OFF — emoji icons are gated to
|
|
@@ -55,18 +145,23 @@ function renderCommandLabel(cmd) {
|
|
|
55
145
|
? `${cmd.icon} /${cmd.name}`
|
|
56
146
|
: `/${cmd.name}`;
|
|
57
147
|
}
|
|
58
|
-
/** Aiden version pulled from package.json at require-time; falls back
|
|
59
|
-
* to a static literal so TS compiles without a JSON resolution wobble. */
|
|
60
|
-
const AIDEN_VERSION = (() => {
|
|
61
|
-
try {
|
|
62
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
63
|
-
return require('../../package.json').version ?? '4.0.0';
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return '4.0.0';
|
|
67
|
-
}
|
|
68
|
-
})();
|
|
69
148
|
const STATUS_BAR_WIDTH = 10;
|
|
149
|
+
/**
|
|
150
|
+
* Phase v4.1.2-memory-AB: hard cap on the session distillation
|
|
151
|
+
* auxiliary call. Default 4000 ms — comfortable headroom for
|
|
152
|
+
* chatgpt-plus (typical ~1-2s), generous for groq (typical <1s).
|
|
153
|
+
* Override via `AIDEN_SUMMARY_TIMEOUT_MS` env var for power users.
|
|
154
|
+
* Above this we abandon the LLM half (still write a deterministic-
|
|
155
|
+
* only distillation so the session isn't lost) and exit honestly.
|
|
156
|
+
*/
|
|
157
|
+
const SUMMARY_TIMEOUT_MS_DEFAULT = 4000;
|
|
158
|
+
function resolveSummaryTimeoutMs() {
|
|
159
|
+
const raw = process.env.AIDEN_SUMMARY_TIMEOUT_MS;
|
|
160
|
+
if (!raw)
|
|
161
|
+
return SUMMARY_TIMEOUT_MS_DEFAULT;
|
|
162
|
+
const parsed = Number.parseInt(raw, 10);
|
|
163
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : SUMMARY_TIMEOUT_MS_DEFAULT;
|
|
164
|
+
}
|
|
70
165
|
class ChatSession {
|
|
71
166
|
constructor(opts) {
|
|
72
167
|
this.opts = opts;
|
|
@@ -88,6 +183,31 @@ class ChatSession {
|
|
|
88
183
|
// provider used last turn (so a switch surfaces as `groq ──→ together`).
|
|
89
184
|
this.lastTurnElapsedMs = 0;
|
|
90
185
|
this.lastFooterProvider = null;
|
|
186
|
+
/**
|
|
187
|
+
* Phase v4.1.2-memory-AB:
|
|
188
|
+
* Accumulated tool-call trace across every `runConversation` call
|
|
189
|
+
* in this ChatSession instance. Fed to the session distiller at
|
|
190
|
+
* exit to derive deterministic fields (files_touched, tools_used).
|
|
191
|
+
* Reset only when ChatSession itself is re-instantiated.
|
|
192
|
+
*/
|
|
193
|
+
this.sessionToolTrace = [];
|
|
194
|
+
/**
|
|
195
|
+
* Phase v4.1.2-memory-AB:
|
|
196
|
+
* Idempotency flag. Set ONLY after a successful summary write
|
|
197
|
+
* (verified-on-disk via MemoryGuard). A failed or timed-out attempt
|
|
198
|
+
* leaves this `false` so the next exit path retries — matches the
|
|
199
|
+
* "honest by design / best-effort, log clearly" stance.
|
|
200
|
+
* Scoped to ChatSession instance lifetime (no DB persistence).
|
|
201
|
+
*/
|
|
202
|
+
this.summarized = false;
|
|
203
|
+
/**
|
|
204
|
+
* Phase v4.1.2-memory-D:
|
|
205
|
+
* Last successful distillation, cached so the promotion-prompt flow
|
|
206
|
+
* (`/quit` path only — SIGINT/SIGTERM skip) can extract candidates
|
|
207
|
+
* without re-driving the auxiliary LLM. Mirrors `summarized` —
|
|
208
|
+
* populated alongside it after a verified write.
|
|
209
|
+
*/
|
|
210
|
+
this.lastDistillation = null;
|
|
91
211
|
this.currentProviderId = opts.initialProviderId;
|
|
92
212
|
this.currentModelId = opts.initialModelId;
|
|
93
213
|
this.modelMetadata = opts.modelMetadata ?? new modelMetadata_1.ModelMetadata();
|
|
@@ -129,6 +249,11 @@ class ChatSession {
|
|
|
129
249
|
paths: this.opts.paths,
|
|
130
250
|
});
|
|
131
251
|
this.opts.agent.setProvider(adapter);
|
|
252
|
+
// Phase v4.1.2-bug2: keep the prompt's Runtime slot in lockstep
|
|
253
|
+
// with the routed provider. Without this, the agent's adapter
|
|
254
|
+
// swaps correctly but its system prompt keeps self-describing as
|
|
255
|
+
// the boot-time provider/model for the rest of the session.
|
|
256
|
+
this.opts.agent.setActiveModel(providerId, modelId);
|
|
132
257
|
this.currentProviderId = providerId;
|
|
133
258
|
this.currentModelId = modelId;
|
|
134
259
|
}
|
|
@@ -152,15 +277,49 @@ class ChatSession {
|
|
|
152
277
|
}
|
|
153
278
|
// 2. Boxed startup card.
|
|
154
279
|
await this.renderStartupCard();
|
|
155
|
-
// 3. Optional SIGINT
|
|
280
|
+
// 3. Optional SIGINT / SIGTERM handlers.
|
|
281
|
+
//
|
|
282
|
+
// Phase v4.1.2-memory-AB: SIGINT used to do `process.exit(0)` directly,
|
|
283
|
+
// bypassing session_summary + the new distillation file. The Ctrl-C
|
|
284
|
+
// path is the most common premature exit, so it's now hooked too.
|
|
285
|
+
// Both signals route to the same async-with-timeout helper; on
|
|
286
|
+
// timeout (default 4s, override AIDEN_SUMMARY_TIMEOUT_MS) the exit
|
|
287
|
+
// proceeds anyway with a dim log line — honest about the skip.
|
|
156
288
|
let sigintHandler = null;
|
|
289
|
+
let sigtermHandler = null;
|
|
290
|
+
let exitHandler = null;
|
|
157
291
|
if (this.opts.installSignalHandler !== false) {
|
|
158
|
-
|
|
292
|
+
const makeHandler = (sig) => async () => {
|
|
159
293
|
this.opts.display.write('\n');
|
|
294
|
+
this.opts.display.dim(`Got ${sig.toUpperCase()} — saving session before exit…`);
|
|
295
|
+
try {
|
|
296
|
+
await this.maybeAutoSummarizeWithTimeout(sig);
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
this.opts.display.warn(`Session summary skipped on ${sig}: ${err.message}`);
|
|
300
|
+
}
|
|
160
301
|
this.opts.display.dim('Goodbye.');
|
|
161
302
|
process.exit(0);
|
|
162
303
|
};
|
|
304
|
+
sigintHandler = makeHandler('sigint');
|
|
305
|
+
sigtermHandler = makeHandler('sigterm');
|
|
163
306
|
process.on('SIGINT', sigintHandler);
|
|
307
|
+
process.on('SIGTERM', sigtermHandler);
|
|
308
|
+
// Last-resort safety net: synchronous-only hook, so we can't run
|
|
309
|
+
// the auxiliary call here. Just log when we exited without
|
|
310
|
+
// summarizing so the user knows where to look for missing data.
|
|
311
|
+
exitHandler = () => {
|
|
312
|
+
if (!this.summarized) {
|
|
313
|
+
// Best-effort one-liner — stderr because stdout may be torn
|
|
314
|
+
// down already.
|
|
315
|
+
try {
|
|
316
|
+
process.stderr.write('[aiden] process exiting without session summary — ' +
|
|
317
|
+
'distillation file not written for this session.\n');
|
|
318
|
+
}
|
|
319
|
+
catch { /* nothing to do */ }
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
process.on('exit', exitHandler);
|
|
164
323
|
}
|
|
165
324
|
// 4. Main loop.
|
|
166
325
|
// Tier-3.1.1: feed the new aidenPrompt with live slash commands +
|
|
@@ -254,8 +413,25 @@ class ChatSession {
|
|
|
254
413
|
// Phase 18: raw text prompt for /auth login OAuth code paste.
|
|
255
414
|
prompt: (msg) => promptApi.readLine(msg),
|
|
256
415
|
});
|
|
257
|
-
if (result.exit)
|
|
416
|
+
if (result.exit) {
|
|
417
|
+
// Phase v4.1.2 alive-core / Phase v4.1.2-memory-AB:
|
|
418
|
+
// auto-trigger session distillation on /quit when the
|
|
419
|
+
// session was substantive (≥3 user turns). SIGINT and
|
|
420
|
+
// SIGTERM now also hit this path via their own handlers
|
|
421
|
+
// above; the in-memory `summarized` flag prevents double-
|
|
422
|
+
// writes. The /quit path tags exit_path='quit' so the
|
|
423
|
+
// distillation file records which exit class fired.
|
|
424
|
+
await this.maybeAutoSummarizeWithTimeout('quit');
|
|
425
|
+
// Phase v4.1.2-memory-D: promotion prompt — only on /quit,
|
|
426
|
+
// NEVER from signal handlers (async stdin in a signal
|
|
427
|
+
// handler context is unsafe). Distillation files from
|
|
428
|
+
// SIGINT-exited sessions stay on disk; their candidates
|
|
429
|
+
// surface on the next `/quit` only if the conversation
|
|
430
|
+
// is resumed in the same process (not today's behavior),
|
|
431
|
+
// otherwise they're skipped — documented in commit.
|
|
432
|
+
await this.maybeRunPromotion(promptApi);
|
|
258
433
|
break;
|
|
434
|
+
}
|
|
259
435
|
if (result.clearHistory)
|
|
260
436
|
this.history = [];
|
|
261
437
|
// Phase 23.6 — v3 doesn't print a status footer after slash
|
|
@@ -268,6 +444,10 @@ class ChatSession {
|
|
|
268
444
|
finally {
|
|
269
445
|
if (sigintHandler)
|
|
270
446
|
process.off('SIGINT', sigintHandler);
|
|
447
|
+
if (sigtermHandler)
|
|
448
|
+
process.off('SIGTERM', sigtermHandler);
|
|
449
|
+
if (exitHandler)
|
|
450
|
+
process.off('exit', exitHandler);
|
|
271
451
|
if (pasteEnabled)
|
|
272
452
|
(0, bracketedPaste_1.disableBracketedPaste)(stdout);
|
|
273
453
|
restorePasteInterceptor();
|
|
@@ -275,6 +455,326 @@ class ChatSession {
|
|
|
275
455
|
}
|
|
276
456
|
}
|
|
277
457
|
// ── Inner: a single agent turn ─────────────────────────────────────
|
|
458
|
+
/**
|
|
459
|
+
* Phase v4.1.2 alive-core (refined v4.1.2-followup-2): auto-trigger
|
|
460
|
+
* `session_summary` on /quit when the session was substantive
|
|
461
|
+
* (≥3 user turns). The synthetic prompt forces the model to call
|
|
462
|
+
* the tool — prose-only responses are not acceptable.
|
|
463
|
+
*
|
|
464
|
+
* Every non-success path is logged explicitly so users always know
|
|
465
|
+
* what happened:
|
|
466
|
+
* - threshold-skip → log: "session too short, skipping summary"
|
|
467
|
+
* - unconfigured-skip → log: "no provider, skipping summary"
|
|
468
|
+
* - tool-not-called (model returned prose) → log a clear warning
|
|
469
|
+
* - tool-errored (throw) → log the error verbatim
|
|
470
|
+
* - tool-succeeded → log the absolute MEMORY.md path
|
|
471
|
+
*
|
|
472
|
+
* Post-run verification: compare MEMORY.md size+mtime before vs
|
|
473
|
+
* after the synthetic turn. If unchanged, the model didn't actually
|
|
474
|
+
* fire the tool and the user gets a "run /session-summary manually
|
|
475
|
+
* next time" hint.
|
|
476
|
+
*
|
|
477
|
+
* SIGINT and crash paths skip this method entirely because the
|
|
478
|
+
* signal handler does process.exit(0) before this slash-command
|
|
479
|
+
* branch runs.
|
|
480
|
+
*/
|
|
481
|
+
/**
|
|
482
|
+
* Phase v4.1.2-memory-AB: combined Phase A (reliable session-end
|
|
483
|
+
* firing) + Phase B (structured distillation) entry point.
|
|
484
|
+
*
|
|
485
|
+
* Drives one auxiliary-LLM call, produces a SessionDistillation,
|
|
486
|
+
* writes the distillation JSON to <paths.root>/distillations/, AND
|
|
487
|
+
* writes the bullets-only summary to MEMORY.md via the existing
|
|
488
|
+
* sessionSummaryTool — both artifacts populated from the single
|
|
489
|
+
* LLM call (no extra cost over the previous Path D).
|
|
490
|
+
*
|
|
491
|
+
* Idempotency: `this.summarized` is set to true ONLY on full
|
|
492
|
+
* success (MEMORY.md write verified). Failed or timed-out attempts
|
|
493
|
+
* leave the flag false so the next exit path retries. Lightweight
|
|
494
|
+
* in-memory flag pattern — clears on normal completion, only set
|
|
495
|
+
* after a fully verified write.
|
|
496
|
+
*
|
|
497
|
+
* Timeout: SUMMARY_TIMEOUT_MS_DEFAULT (4s) override via env var.
|
|
498
|
+
* On timeout the LLM result is treated as empty → distillation
|
|
499
|
+
* file written with `partial: true` + deterministic fields only;
|
|
500
|
+
* MEMORY.md not updated (no bullets to write).
|
|
501
|
+
*
|
|
502
|
+
* Honest logging: every skip / timeout / partial path produces a
|
|
503
|
+
* user-visible dim or warn line. No silent drops.
|
|
504
|
+
*/
|
|
505
|
+
async maybeAutoSummarizeWithTimeout(exitPath) {
|
|
506
|
+
// Idempotency check first — cheapest possible bail.
|
|
507
|
+
if (this.summarized) {
|
|
508
|
+
this.opts.display.dim(`Session already summarized; skipping ${exitPath} re-fire.`);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const userTurns = this.history.filter((m) => m.role === 'user').length;
|
|
512
|
+
const memoryPath = this.opts.paths?.memoryMd;
|
|
513
|
+
const gate = (0, sessionSummaryGate_1.shouldAutoSummarize)({
|
|
514
|
+
userTurns,
|
|
515
|
+
unconfigured: !!this.opts.unconfigured,
|
|
516
|
+
memoryPath,
|
|
517
|
+
});
|
|
518
|
+
if (gate.fire === false) {
|
|
519
|
+
switch (gate.reason) {
|
|
520
|
+
case 'short':
|
|
521
|
+
this.opts.display.dim(`Skipping session summary — only ${userTurns} user turn(s), need ${sessionSummaryGate_1.SESSION_SUMMARY_MIN_TURNS}+.`);
|
|
522
|
+
return;
|
|
523
|
+
case 'unconfigured':
|
|
524
|
+
this.opts.display.dim('Skipping session summary — no provider configured.');
|
|
525
|
+
return;
|
|
526
|
+
case 'no-paths':
|
|
527
|
+
this.opts.display.dim('Skipping session summary — no aiden paths wired (test mode?).');
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (!this.opts.auxiliaryClient || !this.opts.memoryGuard || !this.opts.memoryManager) {
|
|
532
|
+
this.opts.display.warn('Skipping session summary — auxiliary client / memory plumbing not wired ' +
|
|
533
|
+
'(this is normal in test mode; real CLI sessions get all three).');
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const timeoutMs = resolveSummaryTimeoutMs();
|
|
537
|
+
const memoryPathSafe = memoryPath;
|
|
538
|
+
this.opts.display.dim(`Generating session distillation via auxiliary client (timeout ${timeoutMs}ms)…`);
|
|
539
|
+
// Snapshot MEMORY.md state to detect post-write whether the write
|
|
540
|
+
// actually advanced the file — preserves the verify-on-disk check
|
|
541
|
+
// from the pre-AB path.
|
|
542
|
+
const before = await this.snapshotMemoryStat(memoryPathSafe);
|
|
543
|
+
// Single auxiliary call → SessionDistillation. distillSession
|
|
544
|
+
// owns its own internal timeout, so we don't need an outer race
|
|
545
|
+
// here; the deterministic fields populate regardless of LLM
|
|
546
|
+
// outcome (so even a full timeout produces a useful artifact).
|
|
547
|
+
let dist;
|
|
548
|
+
try {
|
|
549
|
+
dist = await (0, sessionDistiller_1.distillSession)({
|
|
550
|
+
sessionId: this.sessionId ?? `unbound-${Date.now()}`,
|
|
551
|
+
startedAt: new Date(this.startedAt).toISOString(),
|
|
552
|
+
exitPath,
|
|
553
|
+
userTurns,
|
|
554
|
+
messages: this.history,
|
|
555
|
+
toolTrace: this.sessionToolTrace,
|
|
556
|
+
auxiliaryClient: this.opts.auxiliaryClient,
|
|
557
|
+
timeoutMs,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
catch (err) {
|
|
561
|
+
this.opts.display.warn(`Session distillation failed: ${err.message}. ` +
|
|
562
|
+
`MEMORY.md unchanged at: ${memoryPathSafe}`);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
// Persist the distillation JSON. Failures are recorded into the
|
|
566
|
+
// slice3 subsystem health surface (when the agent wires one) and
|
|
567
|
+
// logged here; they don't block the MEMORY.md write.
|
|
568
|
+
if (this.opts.paths?.root) {
|
|
569
|
+
const dir = node_path_1.default.join(this.opts.paths.root, 'distillations');
|
|
570
|
+
try {
|
|
571
|
+
const file = await (0, distillationStore_1.writeDistillation)(dir, dist);
|
|
572
|
+
this.opts.display.dim(`Session distillation${dist.partial ? ' (partial)' : ''} saved to ${file}`);
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
this.opts.display.warn(`Distillation write failed: ${err.message}. ` +
|
|
576
|
+
`(Continuing to MEMORY.md update.)`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// Update MEMORY.md `## Recent sessions` via the existing tool — no
|
|
580
|
+
// change to its on-disk shape (back-compat per slice's hard
|
|
581
|
+
// constraint). Skip when bullets are empty (full LLM timeout) —
|
|
582
|
+
// a zero-bullet entry would just be noise in MEMORY.md.
|
|
583
|
+
if (dist.bullets.length === 0) {
|
|
584
|
+
this.opts.display.warn(`Session summary skipped MEMORY.md update — auxiliary returned no bullets ` +
|
|
585
|
+
`(distillation file may still have deterministic fields).`);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
const { sessionSummaryTool } = await Promise.resolve().then(() => __importStar(require('../../tools/v4/memory/sessionSummary')));
|
|
590
|
+
const result = await sessionSummaryTool.execute({ bullets: dist.bullets, trigger: 'auto-quit' }, {
|
|
591
|
+
cwd: process.cwd(),
|
|
592
|
+
paths: this.opts.paths,
|
|
593
|
+
memory: this.opts.memoryManager,
|
|
594
|
+
memoryGuard: this.opts.memoryGuard,
|
|
595
|
+
});
|
|
596
|
+
if (!result.success) {
|
|
597
|
+
this.opts.display.warn(`Session summary failed: ${result.error ?? 'unknown error'}. ` +
|
|
598
|
+
`MEMORY.md may be unchanged at: ${memoryPathSafe}`);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
catch (err) {
|
|
603
|
+
this.opts.display.warn(`Session summary failed during write: ${err.message}. ` +
|
|
604
|
+
`MEMORY.md unchanged at: ${memoryPathSafe}`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const after = await this.snapshotMemoryStat(memoryPathSafe);
|
|
608
|
+
if ((0, sessionSummaryGate_1.memoryGrewBetween)(before, after)) {
|
|
609
|
+
this.opts.display.dim(`Session summary saved to ${memoryPathSafe}`);
|
|
610
|
+
// Mark summarized ONLY after both writes verified — partial
|
|
611
|
+
// states leave the flag false so the next exit path retries.
|
|
612
|
+
this.summarized = true;
|
|
613
|
+
// Phase v4.1.2-memory-D: cache the distillation for the promotion
|
|
614
|
+
// flow. The /quit handler (and only /quit) consults this to build
|
|
615
|
+
// candidates without re-driving the auxiliary LLM.
|
|
616
|
+
this.lastDistillation = dist;
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
this.opts.display.warn(`Session summary write completed but MEMORY.md size+mtime did not advance. ` +
|
|
620
|
+
`Check ${memoryPathSafe} manually.`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Phase v4.1.2-memory-D: promotion-prompt flow.
|
|
625
|
+
*
|
|
626
|
+
* Called from the `/quit` path ONLY (NOT from SIGINT/SIGTERM
|
|
627
|
+
* handlers — async stdin can't be safely driven from a signal
|
|
628
|
+
* handler context). Builds candidates from `this.history` +
|
|
629
|
+
* `this.lastDistillation`, dedups against the existing
|
|
630
|
+
* `## Durable facts` section in MEMORY.md, prompts the user,
|
|
631
|
+
* persists approved selections.
|
|
632
|
+
*
|
|
633
|
+
* Gates (any false → silent no-op):
|
|
634
|
+
* - this.summarized (need a fresh distillation)
|
|
635
|
+
* - this.lastDistillation (set alongside summarized)
|
|
636
|
+
* - this.opts.memoryManager (real CLI sessions only)
|
|
637
|
+
* - this.opts.memoryGuard (real CLI sessions only)
|
|
638
|
+
*
|
|
639
|
+
* UX rules per Phase D's Q5 first-run experience:
|
|
640
|
+
* - 0 candidates AND 0 totalBeforeDedup → completely silent
|
|
641
|
+
* - 0 candidates AFTER dedup, but some were dropped → dim line
|
|
642
|
+
* "N candidates already in durable facts — nothing new to promote"
|
|
643
|
+
* - >0 candidates → prompt for approval, write approved
|
|
644
|
+
*/
|
|
645
|
+
async maybeRunPromotion(api) {
|
|
646
|
+
if (!this.summarized || !this.lastDistillation)
|
|
647
|
+
return;
|
|
648
|
+
if (!this.opts.memoryManager || !this.opts.memoryGuard)
|
|
649
|
+
return;
|
|
650
|
+
let existingBody;
|
|
651
|
+
try {
|
|
652
|
+
existingBody = await (0, promotionPrompt_1.readExistingDurableFactsBody)(this.opts.memoryManager);
|
|
653
|
+
}
|
|
654
|
+
catch (err) {
|
|
655
|
+
this.opts.display.warn(`Could not read existing durable facts: ${err.message}. ` +
|
|
656
|
+
`Promotion skipped.`);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
const built = (0, promotionCandidates_1.extractCandidates)(this.history, this.lastDistillation, existingBody);
|
|
660
|
+
// Silent on truly empty sessions; reward the user on "all already saved".
|
|
661
|
+
if (built.candidates.length === 0) {
|
|
662
|
+
if (built.totalBeforeDedup === 0) {
|
|
663
|
+
return; // no signals + no distillation gold to promote — silent
|
|
664
|
+
}
|
|
665
|
+
if (built.dedupedAgainstExisting > 0) {
|
|
666
|
+
this.opts.display.dim(`${built.dedupedAgainstExisting} candidate${built.dedupedAgainstExisting === 1 ? '' : 's'} ` +
|
|
667
|
+
`already in durable facts — nothing new to promote.`);
|
|
668
|
+
}
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
let approved;
|
|
672
|
+
try {
|
|
673
|
+
approved = await (0, promotionPrompt_1.promptForApproval)(api, this.opts.display, built.candidates);
|
|
674
|
+
}
|
|
675
|
+
catch (err) {
|
|
676
|
+
// The prompt API throwing is rare (broken stdin, etc.) — log
|
|
677
|
+
// and skip; no auto-write on error per "opt-in by design".
|
|
678
|
+
this.opts.display.warn(`Promotion prompt failed: ${err.message}. ` +
|
|
679
|
+
`Nothing was written to durable facts.`);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
if (approved.length === 0)
|
|
683
|
+
return; // user replied skip / none / unparseable
|
|
684
|
+
try {
|
|
685
|
+
const result = await (0, promotionPrompt_1.writeApprovedDurableFacts)(this.opts.memoryManager, this.opts.memoryGuard, approved);
|
|
686
|
+
if (result.ok && result.verified) {
|
|
687
|
+
this.opts.display.dim(`Promoted ${approved.length} fact${approved.length === 1 ? '' : 's'} ` +
|
|
688
|
+
`to MEMORY.md \`## Durable facts\`.`);
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
this.opts.display.warn(`Durable-facts write completed but did not verify: ` +
|
|
692
|
+
`${result.reason ?? 'unknown'}. Inspect MEMORY.md manually.`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
catch (err) {
|
|
696
|
+
this.opts.display.warn(`Durable-facts write failed: ${err.message}. ` +
|
|
697
|
+
`MEMORY.md may be unchanged.`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Phase v4.1.2 session-summary-followup: ask the auxiliary client
|
|
702
|
+
* for a JSON array of 5 session-summary bullets. One retry on
|
|
703
|
+
* malformed output with a stricter "JSON only" reminder, then we
|
|
704
|
+
* surface the failure honestly via the caller's warn() log.
|
|
705
|
+
*
|
|
706
|
+
* Returns `null` when both attempts fail to yield a valid array.
|
|
707
|
+
*/
|
|
708
|
+
async requestSessionBulletsFromAuxiliary() {
|
|
709
|
+
const aux = this.opts.auxiliaryClient;
|
|
710
|
+
const transcript = this.buildSessionTranscriptForSummary();
|
|
711
|
+
const promptStrict = (extraNote) => [
|
|
712
|
+
'Summarize this session in EXACTLY 5 short bullets. Focus on:',
|
|
713
|
+
'- what we worked on',
|
|
714
|
+
'- decisions made',
|
|
715
|
+
'- files / commits changed',
|
|
716
|
+
'- problems solved',
|
|
717
|
+
'- open items',
|
|
718
|
+
'',
|
|
719
|
+
'Respond with ONLY a JSON array of 5 strings. No prose. No explanation. ' +
|
|
720
|
+
'No code fences. No leading or trailing text.',
|
|
721
|
+
'',
|
|
722
|
+
'Example: ["Shipped v4.1.1 to npm", "Diagnosed OAuth bug", "Patched tool schema", "Added doctor --providers", "Queued auxiliary fallback"]',
|
|
723
|
+
'',
|
|
724
|
+
extraNote,
|
|
725
|
+
'',
|
|
726
|
+
'Session transcript:',
|
|
727
|
+
transcript,
|
|
728
|
+
].filter((s) => s.length > 0).join('\n');
|
|
729
|
+
const attempt = async (note) => {
|
|
730
|
+
const res = await aux.call({
|
|
731
|
+
purpose: 'session_summary',
|
|
732
|
+
prompt: promptStrict(note),
|
|
733
|
+
maxTokens: 800,
|
|
734
|
+
timeoutMs: 30000,
|
|
735
|
+
});
|
|
736
|
+
return parseSessionBulletsResponse(res.content);
|
|
737
|
+
};
|
|
738
|
+
const first = await attempt('');
|
|
739
|
+
if (first)
|
|
740
|
+
return first;
|
|
741
|
+
const second = await attempt('STRICT: Your previous response was not parseable. Return ONLY the JSON array, nothing else.');
|
|
742
|
+
return second;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Compress recent history into a transcript blob the auxiliary
|
|
746
|
+
* client can summarise. Caps to the last 30 messages so the
|
|
747
|
+
* auxiliary prompt stays under typical small-model context limits;
|
|
748
|
+
* the auxiliary's `maxTokens: 800` output budget bounds the cost.
|
|
749
|
+
*/
|
|
750
|
+
buildSessionTranscriptForSummary() {
|
|
751
|
+
const recent = this.history.slice(-30);
|
|
752
|
+
const lines = [];
|
|
753
|
+
for (const m of recent) {
|
|
754
|
+
const role = m.role === 'user' ? 'USER' : m.role === 'assistant' ? 'AIDEN' : m.role.toUpperCase();
|
|
755
|
+
const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
|
|
756
|
+
// Truncate any single message to 800 chars so a giant paste
|
|
757
|
+
// doesn't blow the prompt budget.
|
|
758
|
+
const trimmed = content.length > 800 ? `${content.slice(0, 800)}…` : content;
|
|
759
|
+
lines.push(`${role}: ${trimmed}`);
|
|
760
|
+
}
|
|
761
|
+
return lines.join('\n\n');
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Read MEMORY.md size + mtime for the pre/post-write comparison in
|
|
765
|
+
* `maybeAutoSummarize`. Missing file is normalised to zeros so the
|
|
766
|
+
* "did MEMORY.md grow" comparison is well-defined even on fresh installs.
|
|
767
|
+
*/
|
|
768
|
+
async snapshotMemoryStat(p) {
|
|
769
|
+
try {
|
|
770
|
+
const { promises: fsPromises } = await Promise.resolve().then(() => __importStar(require('node:fs')));
|
|
771
|
+
const stat = await fsPromises.stat(p);
|
|
772
|
+
return { size: stat.size, mtime: stat.mtimeMs };
|
|
773
|
+
}
|
|
774
|
+
catch {
|
|
775
|
+
return { size: 0, mtime: 0 };
|
|
776
|
+
}
|
|
777
|
+
}
|
|
278
778
|
async runAgentTurn(userInput) {
|
|
279
779
|
// Phase 30.2.1 — explore mode: short-circuit BEFORE building the
|
|
280
780
|
// turn-status spinner / agent call. The wizard skipped, so there's
|
|
@@ -368,6 +868,12 @@ class ChatSession {
|
|
|
368
868
|
// Unverified writes get a quieter line so the user knows the model
|
|
369
869
|
// tried but the round-trip didn't confirm.
|
|
370
870
|
renderMemoryConfirmations(result.toolCallTrace, this.opts.display);
|
|
871
|
+
// Phase v4.1.2-memory-AB: accumulate the turn's tool-call trace
|
|
872
|
+
// so the session distiller can derive deterministic fields
|
|
873
|
+
// (files_touched / tools_used) at exit.
|
|
874
|
+
if (result.toolCallTrace && result.toolCallTrace.length > 0) {
|
|
875
|
+
this.sessionToolTrace.push(...result.toolCallTrace);
|
|
876
|
+
}
|
|
371
877
|
// When streaming was active and emitted the final content already,
|
|
372
878
|
// skip the markdown re-render — we'd otherwise duplicate text.
|
|
373
879
|
if (result.finalContent && !streamingActive) {
|
|
@@ -444,7 +950,7 @@ class ChatSession {
|
|
|
444
950
|
const cols = display.cols();
|
|
445
951
|
const isNarrow = cols < 60;
|
|
446
952
|
const showEnvCapBlock = cols >= 70;
|
|
447
|
-
const version =
|
|
953
|
+
const version = version_1.VERSION;
|
|
448
954
|
display.write('\n');
|
|
449
955
|
if (isNarrow) {
|
|
450
956
|
// Compact — single-line text logo + one-line capability summary.
|
|
@@ -462,12 +968,16 @@ class ChatSession {
|
|
|
462
968
|
display.write('\n');
|
|
463
969
|
}
|
|
464
970
|
// Status pills.
|
|
971
|
+
// Phase v4.1.2-version-display: append the running version as the
|
|
972
|
+
// fifth pill so users see what they're on without invoking
|
|
973
|
+
// `aiden --version`. Sourced from the build-injected core/version.ts.
|
|
465
974
|
display.write(display.statusPillsRow({
|
|
466
975
|
coreOnline: true,
|
|
467
976
|
mode: 'auto',
|
|
468
977
|
model: this.currentModelId,
|
|
469
978
|
memoryActive: true,
|
|
470
979
|
providerOk: !this.opts.unconfigured,
|
|
980
|
+
version: version_1.VERSION,
|
|
471
981
|
}) + '\n');
|
|
472
982
|
// Tier-3.1b: rule + environment/capabilities block + rule + scroll
|
|
473
983
|
// + bottom prompt hint. Skipped at <70 cols to keep the narrow
|
|
@@ -86,9 +86,12 @@ function renderStatus(ctx, providerId, tokens) {
|
|
|
86
86
|
if (tokens.account)
|
|
87
87
|
ctx.display.write(` account: ${tokens.account}\n`);
|
|
88
88
|
ctx.display.write(` ${formatRelativeExpiry(tokens.expiresAtMs)}\n`);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
// Post-v4.1.1 cleanup: don't render `models:` — the stored list is
|
|
90
|
+
// captured at OAuth mint time and never refreshed, so it goes stale
|
|
91
|
+
// when the provider rotates its model catalog (e.g. OpenAI retired
|
|
92
|
+
// gpt-5 / gpt-5-mini in Feb 2026). The live model list lives in
|
|
93
|
+
// /model picker → providerCatalog.ts. /auth status is for AUTH
|
|
94
|
+
// state, not catalog state.
|
|
92
95
|
if (ctx.paths) {
|
|
93
96
|
ctx.display.dim(` file: ${node_path_1.default.join(ctx.paths.root, 'auth', `${providerId}.json`)}`);
|
|
94
97
|
}
|
|
@@ -36,6 +36,8 @@ exports.SUBSECTION_MAP = {
|
|
|
36
36
|
'debug-prompt': 'Configuration',
|
|
37
37
|
// ── Identity ── SOUL.md introspection
|
|
38
38
|
identity: 'Identity',
|
|
39
|
+
// Phase v4.1.2 alive-core: manual SOUL.md cache invalidation.
|
|
40
|
+
'reload-soul': 'Identity',
|
|
39
41
|
// ── System ── housekeeping & process control (default fallback)
|
|
40
42
|
doctor: 'System',
|
|
41
43
|
license: 'System',
|
|
@@ -54,6 +56,8 @@ exports.SUBSECTION_MAP = {
|
|
|
54
56
|
status: 'System',
|
|
55
57
|
show: 'System',
|
|
56
58
|
history: 'System',
|
|
59
|
+
// Phase v4.1.2-update — npm self-update for the running install.
|
|
60
|
+
update: 'System',
|
|
57
61
|
// ── Authentication ──
|
|
58
62
|
auth: 'Authentication',
|
|
59
63
|
// ── Help ──
|