aiden-runtime 4.1.1 → 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.
Files changed (55) hide show
  1. package/README.md +78 -26
  2. package/dist/cli/v4/aidenCLI.js +159 -9
  3. package/dist/cli/v4/callbacks.js +5 -2
  4. package/dist/cli/v4/chatSession.js +525 -15
  5. package/dist/cli/v4/commands/auth.js +6 -3
  6. package/dist/cli/v4/commands/help.js +4 -0
  7. package/dist/cli/v4/commands/index.js +10 -1
  8. package/dist/cli/v4/commands/reloadSoul.js +37 -0
  9. package/dist/cli/v4/commands/update.js +102 -0
  10. package/dist/cli/v4/defaultSoul.js +68 -2
  11. package/dist/cli/v4/display.js +28 -10
  12. package/dist/cli/v4/doctor.js +112 -0
  13. package/dist/cli/v4/doctorLiveness.js +65 -10
  14. package/dist/cli/v4/promotionPrompt.js +202 -0
  15. package/dist/cli/v4/providerBootSelector.js +144 -0
  16. package/dist/cli/v4/sessionSummaryGate.js +66 -0
  17. package/dist/cli/v4/toolPreview.js +139 -0
  18. package/dist/core/v4/aidenAgent.js +91 -29
  19. package/dist/core/v4/capabilities.js +89 -0
  20. package/dist/core/v4/contextCompressor.js +25 -8
  21. package/dist/core/v4/distillationIndex.js +167 -0
  22. package/dist/core/v4/distillationStore.js +98 -0
  23. package/dist/core/v4/logger/logger.js +40 -9
  24. package/dist/core/v4/promotionCandidates.js +234 -0
  25. package/dist/core/v4/promptBuilder.js +145 -1
  26. package/dist/core/v4/sessionDistiller.js +405 -0
  27. package/dist/core/v4/skillMining/skillMiner.js +43 -6
  28. package/dist/core/v4/skillOutcomeTracker.js +323 -0
  29. package/dist/core/v4/subsystemHealth.js +143 -0
  30. package/dist/core/v4/update/executeInstall.js +233 -0
  31. package/dist/core/version.js +1 -1
  32. package/dist/moat/memoryGuard.js +111 -0
  33. package/dist/moat/skillTeacher.js +14 -5
  34. package/dist/providers/v4/chatCompletionsAdapter.js +9 -0
  35. package/dist/providers/v4/errors.js +20 -4
  36. package/dist/providers/v4/modelDefaults.js +65 -0
  37. package/dist/providers/v4/registry.js +9 -2
  38. package/dist/providers/v4/runtimeResolver.js +6 -0
  39. package/dist/tools/v4/index.js +57 -1
  40. package/dist/tools/v4/memory/memoryRemove.js +57 -2
  41. package/dist/tools/v4/memory/sessionSummary.js +151 -0
  42. package/dist/tools/v4/sessions/recallSession.js +163 -0
  43. package/dist/tools/v4/sessions/sessionSearch.js +5 -1
  44. package/dist/tools/v4/system/_psHelpers.js +55 -0
  45. package/dist/tools/v4/system/aidenSelfUpdate.js +162 -0
  46. package/dist/tools/v4/system/appClose.js +79 -0
  47. package/dist/tools/v4/system/appLaunch.js +92 -0
  48. package/dist/tools/v4/system/clipboardRead.js +54 -0
  49. package/dist/tools/v4/system/clipboardWrite.js +84 -0
  50. package/dist/tools/v4/system/mediaKey.js +78 -0
  51. package/dist/tools/v4/system/osProcessList.js +99 -0
  52. package/dist/tools/v4/system/screenshot.js +106 -0
  53. package/dist/tools/v4/system/volumeSet.js +157 -0
  54. package/package.json +4 -1
  55. 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 handler.
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
- sigintHandler = () => {
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 = AIDEN_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
- if (tokens.models?.length) {
90
- ctx.display.write(` models: ${tokens.models.join(', ')}\n`);
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 ──