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.
Files changed (68) hide show
  1. package/README.md +78 -26
  2. package/dist/cli/v4/aidenCLI.js +169 -9
  3. package/dist/cli/v4/callbacks.js +20 -2
  4. package/dist/cli/v4/chatSession.js +644 -16
  5. package/dist/cli/v4/commands/auth.js +6 -3
  6. package/dist/cli/v4/commands/doctor.js +23 -27
  7. package/dist/cli/v4/commands/help.js +4 -0
  8. package/dist/cli/v4/commands/index.js +10 -1
  9. package/dist/cli/v4/commands/model.js +30 -1
  10. package/dist/cli/v4/commands/reloadSoul.js +37 -0
  11. package/dist/cli/v4/commands/update.js +102 -0
  12. package/dist/cli/v4/defaultSoul.js +68 -2
  13. package/dist/cli/v4/display/capabilityCard.js +135 -0
  14. package/dist/cli/v4/display/sessionEndCard.js +127 -0
  15. package/dist/cli/v4/display/toolTrail.js +172 -0
  16. package/dist/cli/v4/display.js +492 -142
  17. package/dist/cli/v4/doctor.js +472 -58
  18. package/dist/cli/v4/doctorLiveness.js +65 -10
  19. package/dist/cli/v4/promotionPrompt.js +332 -0
  20. package/dist/cli/v4/providerBootSelector.js +144 -0
  21. package/dist/cli/v4/replyRenderer.js +311 -20
  22. package/dist/cli/v4/sessionSummaryGate.js +66 -0
  23. package/dist/cli/v4/skinEngine.js +14 -3
  24. package/dist/cli/v4/toolPreview.js +153 -0
  25. package/dist/core/tools/nowPlaying.js +7 -15
  26. package/dist/core/v4/aidenAgent.js +91 -29
  27. package/dist/core/v4/capabilities.js +89 -0
  28. package/dist/core/v4/contextCompressor.js +25 -8
  29. package/dist/core/v4/distillationIndex.js +167 -0
  30. package/dist/core/v4/distillationStore.js +98 -0
  31. package/dist/core/v4/logger/logger.js +40 -9
  32. package/dist/core/v4/promotionCandidates.js +234 -0
  33. package/dist/core/v4/promptBuilder.js +145 -1
  34. package/dist/core/v4/sessionDistiller.js +452 -0
  35. package/dist/core/v4/skillMining/skillMiner.js +43 -6
  36. package/dist/core/v4/skillOutcomeTracker.js +323 -0
  37. package/dist/core/v4/subsystemHealth.js +143 -0
  38. package/dist/core/v4/toolRegistry.js +16 -1
  39. package/dist/core/v4/update/executeInstall.js +233 -0
  40. package/dist/core/version.js +1 -1
  41. package/dist/moat/memoryGuard.js +111 -0
  42. package/dist/moat/plannerGuard.js +19 -0
  43. package/dist/moat/skillTeacher.js +14 -5
  44. package/dist/providers/v4/chatCompletionsAdapter.js +9 -0
  45. package/dist/providers/v4/errors.js +112 -4
  46. package/dist/providers/v4/modelDefaults.js +65 -0
  47. package/dist/providers/v4/registry.js +9 -2
  48. package/dist/providers/v4/runtimeResolver.js +6 -0
  49. package/dist/tools/v4/index.js +80 -1
  50. package/dist/tools/v4/memory/memoryRemove.js +57 -2
  51. package/dist/tools/v4/memory/sessionSummary.js +151 -0
  52. package/dist/tools/v4/sessions/recallSession.js +177 -0
  53. package/dist/tools/v4/sessions/sessionSearch.js +5 -1
  54. package/dist/tools/v4/system/_psHelpers.js +123 -0
  55. package/dist/tools/v4/system/aidenSelfUpdate.js +162 -0
  56. package/dist/tools/v4/system/appClose.js +79 -0
  57. package/dist/tools/v4/system/appInput.js +154 -0
  58. package/dist/tools/v4/system/appLaunch.js +218 -0
  59. package/dist/tools/v4/system/clipboardRead.js +54 -0
  60. package/dist/tools/v4/system/clipboardWrite.js +84 -0
  61. package/dist/tools/v4/system/mediaKey.js +109 -0
  62. package/dist/tools/v4/system/mediaSessions.js +163 -0
  63. package/dist/tools/v4/system/mediaTransport.js +211 -0
  64. package/dist/tools/v4/system/osProcessList.js +99 -0
  65. package/dist/tools/v4/system/screenshot.js +106 -0
  66. package/dist/tools/v4/system/volumeSet.js +157 -0
  67. package/package.json +4 -1
  68. package/skills/system_control.md +185 -69
@@ -19,12 +19,47 @@
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;
62
+ exports.bootSourceLabel = bootSourceLabel;
28
63
  exports.detectOS = detectOS;
29
64
  exports.detectShell = detectShell;
30
65
  exports.formatStatusState = formatStatusState;
@@ -36,14 +71,75 @@ exports.formatDuration = formatDuration;
36
71
  exports.renderMemoryConfirmations = renderMemoryConfirmations;
37
72
  const display_1 = require("./display");
38
73
  const uiBuild_1 = require("./uiBuild");
74
+ const sessionSummaryGate_1 = require("./sessionSummaryGate");
39
75
  const aidenPrompt_1 = __importDefault(require("./aidenPrompt"));
40
76
  const historyStore_1 = require("./historyStore");
41
77
  const modelMetadata_1 = require("../../core/v4/modelMetadata");
78
+ // v4.1.3-prebump: classify provider errors so the catch path can show
79
+ // a tailored action hint (e.g. groq 413 → "switch to chatgpt-plus")
80
+ // instead of the generic "/model or aiden doctor" line.
81
+ const errors_1 = require("../../providers/v4/errors");
82
+ const sessionDistiller_1 = require("../../core/v4/sessionDistiller");
83
+ const sessionEndCard_1 = require("./display/sessionEndCard");
84
+ const version_1 = require("../../core/version");
85
+ const distillationStore_1 = require("../../core/v4/distillationStore");
86
+ const promotionCandidates_1 = require("../../core/v4/promotionCandidates");
87
+ const promotionPrompt_1 = require("./promotionPrompt");
88
+ const node_path_1 = __importDefault(require("node:path"));
42
89
  const bracketedPaste_1 = require("./bracketedPaste");
43
90
  const pasteCompression_1 = require("./pasteCompression");
44
91
  const pasteIntercept_1 = require("./pasteIntercept");
45
92
  const shellInterpolation_1 = require("./shellInterpolation");
46
93
  const resizeGuard_1 = require("./resizeGuard");
94
+ /**
95
+ * Phase v4.1.2 session-summary-followup: parse the auxiliary client's
96
+ * JSON-array response into a clean `string[]` of bullets. Defensive —
97
+ * tries direct JSON.parse first, then a fenced-code-block strip, then
98
+ * a "first [...] block" extraction. Returns null when nothing usable
99
+ * comes out so the caller can retry once with a stricter prompt.
100
+ *
101
+ * Exported for unit tests.
102
+ */
103
+ function parseSessionBulletsResponse(raw) {
104
+ if (typeof raw !== 'string' || raw.trim().length === 0)
105
+ return null;
106
+ const tryParseArray = (s) => {
107
+ try {
108
+ const parsed = JSON.parse(s);
109
+ if (!Array.isArray(parsed))
110
+ return null;
111
+ const strings = parsed
112
+ .filter((x) => typeof x === 'string')
113
+ .map((x) => x.trim())
114
+ .filter((x) => x.length > 0);
115
+ return strings.length > 0 ? strings : null;
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ };
121
+ // 1. Try the response as-is.
122
+ const direct = tryParseArray(raw.trim());
123
+ if (direct)
124
+ return direct;
125
+ // 2. Strip Markdown code fences if present (```json ... ``` or ``` ... ```).
126
+ const fenceMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
127
+ if (fenceMatch && fenceMatch[1]) {
128
+ const inFence = tryParseArray(fenceMatch[1].trim());
129
+ if (inFence)
130
+ return inFence;
131
+ }
132
+ // 3. Extract the first balanced [...] block from anywhere in the text.
133
+ const bracketStart = raw.indexOf('[');
134
+ const bracketEnd = raw.lastIndexOf(']');
135
+ if (bracketStart >= 0 && bracketEnd > bracketStart) {
136
+ const slice = raw.slice(bracketStart, bracketEnd + 1);
137
+ const extracted = tryParseArray(slice);
138
+ if (extracted)
139
+ return extracted;
140
+ }
141
+ return null;
142
+ }
47
143
  /**
48
144
  * Tier-3.1 helper: render a slash-command label honouring the
49
145
  * `AIDEN_UI_ICONS` opt-in. Default OFF — emoji icons are gated to
@@ -55,18 +151,60 @@ function renderCommandLabel(cmd) {
55
151
  ? `${cmd.icon} /${cmd.name}`
56
152
  : `/${cmd.name}`;
57
153
  }
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
154
  const STATUS_BAR_WIDTH = 10;
155
+ /**
156
+ * Phase v4.1.2-memory-AB: hard cap on the session distillation
157
+ * auxiliary call. Default 4000 ms — comfortable headroom for
158
+ * chatgpt-plus (typical ~1-2s), generous for groq (typical <1s).
159
+ * Override via `AIDEN_SUMMARY_TIMEOUT_MS` env var for power users.
160
+ * Above this we abandon the LLM half (still write a deterministic-
161
+ * only distillation so the session isn't lost) and exit honestly.
162
+ */
163
+ /**
164
+ * v4.1.3-essentials distillation-fix: bumped 4000 → 12000ms in
165
+ * lockstep with `sessionDistiller.DEFAULT_TIMEOUT_MS`. Same
166
+ * rationale — chatgpt-plus Codex cold-start latency for 800-token
167
+ * summaries regularly exceeds 4s, killing the distillation +
168
+ * promotion-prompt path. Env override `AIDEN_SUMMARY_TIMEOUT_MS`
169
+ * still respected.
170
+ */
171
+ const SUMMARY_TIMEOUT_MS_DEFAULT = 12000;
172
+ function resolveSummaryTimeoutMs() {
173
+ const raw = process.env.AIDEN_SUMMARY_TIMEOUT_MS;
174
+ if (!raw)
175
+ return SUMMARY_TIMEOUT_MS_DEFAULT;
176
+ const parsed = Number.parseInt(raw, 10);
177
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : SUMMARY_TIMEOUT_MS_DEFAULT;
178
+ }
179
+ /**
180
+ * v4.1.3-prebump: map a providerBootSelector precedence-case label to
181
+ * a human-readable hint rendered under the boot card's status pills.
182
+ *
183
+ * Returns `null` for the explicit-selection cases (`cli-flag`, with-or-
184
+ * without -partial) where the source isn't surprising. Annotates the
185
+ * persisted-config / auto-priority / hardcoded-fallback paths so users
186
+ * understand "why this provider, why now".
187
+ *
188
+ * Pure helper — exported for unit testing.
189
+ */
190
+ function bootSourceLabel(source) {
191
+ switch (source) {
192
+ case 'persisted-config':
193
+ return '(persisted from prior session — /model to change)';
194
+ case 'config-partial':
195
+ return '(partial config + auto-resolved companion)';
196
+ case 'auto-priority':
197
+ return '(auto-picked — first authed provider)';
198
+ case 'hardcoded-fallback':
199
+ return '(no authed providers — using legacy default)';
200
+ case 'cli-flag':
201
+ case 'cli-flag-partial':
202
+ // Explicit CLI override — user knows why; no annotation.
203
+ return null;
204
+ default:
205
+ return null;
206
+ }
207
+ }
70
208
  class ChatSession {
71
209
  constructor(opts) {
72
210
  this.opts = opts;
@@ -88,6 +226,38 @@ class ChatSession {
88
226
  // provider used last turn (so a switch surfaces as `groq ──→ together`).
89
227
  this.lastTurnElapsedMs = 0;
90
228
  this.lastFooterProvider = null;
229
+ /**
230
+ * Phase v4.1.2-memory-AB:
231
+ * Accumulated tool-call trace across every `runConversation` call
232
+ * in this ChatSession instance. Fed to the session distiller at
233
+ * exit to derive deterministic fields (files_touched, tools_used).
234
+ * Reset only when ChatSession itself is re-instantiated.
235
+ */
236
+ this.sessionToolTrace = [];
237
+ /**
238
+ * Phase v4.1.2-memory-AB:
239
+ * Idempotency flag. Set ONLY after a successful summary write
240
+ * (verified-on-disk via MemoryGuard). A failed or timed-out attempt
241
+ * leaves this `false` so the next exit path retries — matches the
242
+ * "honest by design / best-effort, log clearly" stance.
243
+ * Scoped to ChatSession instance lifetime (no DB persistence).
244
+ */
245
+ this.summarized = false;
246
+ /**
247
+ * Phase v4.1.2-memory-D:
248
+ * Last successful distillation, cached so the promotion-prompt flow
249
+ * (`/quit` path only — SIGINT/SIGTERM skip) can extract candidates
250
+ * without re-driving the auxiliary LLM. Mirrors `summarized` —
251
+ * populated alongside it after a verified write.
252
+ */
253
+ this.lastDistillation = null;
254
+ /**
255
+ * Absolute path the most recent distillation JSON was written to.
256
+ * Captured at write-time and surfaced in the session-end card so the
257
+ * user has a concrete artifact to inspect or feed to recall_session.
258
+ * Null when the write failed or no distillation has been produced.
259
+ */
260
+ this.lastDistillationPath = null;
91
261
  this.currentProviderId = opts.initialProviderId;
92
262
  this.currentModelId = opts.initialModelId;
93
263
  this.modelMetadata = opts.modelMetadata ?? new modelMetadata_1.ModelMetadata();
@@ -129,6 +299,11 @@ class ChatSession {
129
299
  paths: this.opts.paths,
130
300
  });
131
301
  this.opts.agent.setProvider(adapter);
302
+ // Phase v4.1.2-bug2: keep the prompt's Runtime slot in lockstep
303
+ // with the routed provider. Without this, the agent's adapter
304
+ // swaps correctly but its system prompt keeps self-describing as
305
+ // the boot-time provider/model for the rest of the session.
306
+ this.opts.agent.setActiveModel(providerId, modelId);
132
307
  this.currentProviderId = providerId;
133
308
  this.currentModelId = modelId;
134
309
  }
@@ -152,15 +327,57 @@ class ChatSession {
152
327
  }
153
328
  // 2. Boxed startup card.
154
329
  await this.renderStartupCard();
155
- // 3. Optional SIGINT handler.
330
+ // 3. Optional SIGINT / SIGTERM handlers.
331
+ //
332
+ // Phase v4.1.2-memory-AB: SIGINT used to do `process.exit(0)` directly,
333
+ // bypassing session_summary + the new distillation file. The Ctrl-C
334
+ // path is the most common premature exit, so it's now hooked too.
335
+ // Both signals route to the same async-with-timeout helper; on
336
+ // timeout (default 4s, override AIDEN_SUMMARY_TIMEOUT_MS) the exit
337
+ // proceeds anyway with a dim log line — honest about the skip.
156
338
  let sigintHandler = null;
339
+ let sigtermHandler = null;
340
+ let exitHandler = null;
157
341
  if (this.opts.installSignalHandler !== false) {
158
- sigintHandler = () => {
342
+ const makeHandler = (sig) => async () => {
159
343
  this.opts.display.write('\n');
344
+ this.opts.display.dim(`Got ${sig.toUpperCase()} — saving session before exit…`);
345
+ try {
346
+ await this.maybeAutoSummarizeWithTimeout(sig);
347
+ }
348
+ catch (err) {
349
+ this.opts.display.warn(`Session summary skipped on ${sig}: ${err.message}`);
350
+ }
351
+ // v4.1.3-repl-polish: render session-end card before farewell when
352
+ // a distillation was written this session. Pass the on-disk path
353
+ // so the card surfaces the artifact location to the user.
354
+ if (this.lastDistillation) {
355
+ for (const line of (0, sessionEndCard_1.renderSessionEndCard)(this.lastDistillation, (t, k) => this.opts.display.applyColors(t, k), this.lastDistillationPath)) {
356
+ this.opts.display.write(line + '\n');
357
+ }
358
+ }
160
359
  this.opts.display.dim('Goodbye.');
161
360
  process.exit(0);
162
361
  };
362
+ sigintHandler = makeHandler('sigint');
363
+ sigtermHandler = makeHandler('sigterm');
163
364
  process.on('SIGINT', sigintHandler);
365
+ process.on('SIGTERM', sigtermHandler);
366
+ // Last-resort safety net: synchronous-only hook, so we can't run
367
+ // the auxiliary call here. Just log when we exited without
368
+ // summarizing so the user knows where to look for missing data.
369
+ exitHandler = () => {
370
+ if (!this.summarized) {
371
+ // Best-effort one-liner — stderr because stdout may be torn
372
+ // down already.
373
+ try {
374
+ process.stderr.write('[aiden] process exiting without session summary — ' +
375
+ 'distillation file not written for this session.\n');
376
+ }
377
+ catch { /* nothing to do */ }
378
+ }
379
+ };
380
+ process.on('exit', exitHandler);
164
381
  }
165
382
  // 4. Main loop.
166
383
  // Tier-3.1.1: feed the new aidenPrompt with live slash commands +
@@ -254,8 +471,31 @@ class ChatSession {
254
471
  // Phase 18: raw text prompt for /auth login OAuth code paste.
255
472
  prompt: (msg) => promptApi.readLine(msg),
256
473
  });
257
- if (result.exit)
474
+ if (result.exit) {
475
+ // Phase v4.1.2 alive-core / Phase v4.1.2-memory-AB:
476
+ // auto-trigger session distillation on /quit when the
477
+ // session was substantive (≥3 user turns). SIGINT and
478
+ // SIGTERM now also hit this path via their own handlers
479
+ // above; the in-memory `summarized` flag prevents double-
480
+ // writes. The /quit path tags exit_path='quit' so the
481
+ // distillation file records which exit class fired.
482
+ await this.maybeAutoSummarizeWithTimeout('quit');
483
+ // Phase v4.1.2-memory-D: promotion prompt — only on /quit,
484
+ // NEVER from signal handlers (async stdin in a signal
485
+ // handler context is unsafe). Distillation files from
486
+ // SIGINT-exited sessions stay on disk; their candidates
487
+ // surface on the next `/quit` only if the conversation
488
+ // is resumed in the same process (not today's behavior),
489
+ // otherwise they're skipped — documented in commit.
490
+ await this.maybeRunPromotion(promptApi);
491
+ // v4.1.3-repl-polish: session-end card before farewell.
492
+ if (this.lastDistillation) {
493
+ for (const line of (0, sessionEndCard_1.renderSessionEndCard)(this.lastDistillation, (t, k) => this.opts.display.applyColors(t, k), this.lastDistillationPath)) {
494
+ this.opts.display.write(line + '\n');
495
+ }
496
+ }
258
497
  break;
498
+ }
259
499
  if (result.clearHistory)
260
500
  this.history = [];
261
501
  // Phase 23.6 — v3 doesn't print a status footer after slash
@@ -268,6 +508,10 @@ class ChatSession {
268
508
  finally {
269
509
  if (sigintHandler)
270
510
  process.off('SIGINT', sigintHandler);
511
+ if (sigtermHandler)
512
+ process.off('SIGTERM', sigtermHandler);
513
+ if (exitHandler)
514
+ process.off('exit', exitHandler);
271
515
  if (pasteEnabled)
272
516
  (0, bracketedPaste_1.disableBracketedPaste)(stdout);
273
517
  restorePasteInterceptor();
@@ -275,6 +519,336 @@ class ChatSession {
275
519
  }
276
520
  }
277
521
  // ── Inner: a single agent turn ─────────────────────────────────────
522
+ /**
523
+ * Phase v4.1.2 alive-core (refined v4.1.2-followup-2): auto-trigger
524
+ * `session_summary` on /quit when the session was substantive
525
+ * (≥3 user turns). The synthetic prompt forces the model to call
526
+ * the tool — prose-only responses are not acceptable.
527
+ *
528
+ * Every non-success path is logged explicitly so users always know
529
+ * what happened:
530
+ * - threshold-skip → log: "session too short, skipping summary"
531
+ * - unconfigured-skip → log: "no provider, skipping summary"
532
+ * - tool-not-called (model returned prose) → log a clear warning
533
+ * - tool-errored (throw) → log the error verbatim
534
+ * - tool-succeeded → log the absolute MEMORY.md path
535
+ *
536
+ * Post-run verification: compare MEMORY.md size+mtime before vs
537
+ * after the synthetic turn. If unchanged, the model didn't actually
538
+ * fire the tool and the user gets a "run /session-summary manually
539
+ * next time" hint.
540
+ *
541
+ * SIGINT and crash paths skip this method entirely because the
542
+ * signal handler does process.exit(0) before this slash-command
543
+ * branch runs.
544
+ */
545
+ /**
546
+ * Phase v4.1.2-memory-AB: combined Phase A (reliable session-end
547
+ * firing) + Phase B (structured distillation) entry point.
548
+ *
549
+ * Drives one auxiliary-LLM call, produces a SessionDistillation,
550
+ * writes the distillation JSON to <paths.root>/distillations/, AND
551
+ * writes the bullets-only summary to MEMORY.md via the existing
552
+ * sessionSummaryTool — both artifacts populated from the single
553
+ * LLM call (no extra cost over the previous Path D).
554
+ *
555
+ * Idempotency: `this.summarized` is set to true ONLY on full
556
+ * success (MEMORY.md write verified). Failed or timed-out attempts
557
+ * leave the flag false so the next exit path retries. Lightweight
558
+ * in-memory flag pattern — clears on normal completion, only set
559
+ * after a fully verified write.
560
+ *
561
+ * Timeout: SUMMARY_TIMEOUT_MS_DEFAULT (4s) override via env var.
562
+ * On timeout the LLM result is treated as empty → distillation
563
+ * file written with `partial: true` + deterministic fields only;
564
+ * MEMORY.md not updated (no bullets to write).
565
+ *
566
+ * Honest logging: every skip / timeout / partial path produces a
567
+ * user-visible dim or warn line. No silent drops.
568
+ */
569
+ async maybeAutoSummarizeWithTimeout(exitPath) {
570
+ // Idempotency check first — cheapest possible bail.
571
+ if (this.summarized) {
572
+ this.opts.display.dim(`Session already summarized; skipping ${exitPath} re-fire.`);
573
+ return;
574
+ }
575
+ const userTurns = this.history.filter((m) => m.role === 'user').length;
576
+ const memoryPath = this.opts.paths?.memoryMd;
577
+ const gate = (0, sessionSummaryGate_1.shouldAutoSummarize)({
578
+ userTurns,
579
+ unconfigured: !!this.opts.unconfigured,
580
+ memoryPath,
581
+ });
582
+ if (gate.fire === false) {
583
+ switch (gate.reason) {
584
+ case 'short':
585
+ this.opts.display.dim(`Skipping session summary — only ${userTurns} user turn(s), need ${sessionSummaryGate_1.SESSION_SUMMARY_MIN_TURNS}+.`);
586
+ return;
587
+ case 'unconfigured':
588
+ this.opts.display.dim('Skipping session summary — no provider configured.');
589
+ return;
590
+ case 'no-paths':
591
+ this.opts.display.dim('Skipping session summary — no aiden paths wired (test mode?).');
592
+ return;
593
+ }
594
+ }
595
+ if (!this.opts.auxiliaryClient || !this.opts.memoryGuard || !this.opts.memoryManager) {
596
+ this.opts.display.warn('Skipping session summary — auxiliary client / memory plumbing not wired ' +
597
+ '(this is normal in test mode; real CLI sessions get all three).');
598
+ return;
599
+ }
600
+ const timeoutMs = resolveSummaryTimeoutMs();
601
+ const memoryPathSafe = memoryPath;
602
+ this.opts.display.dim(`Generating session distillation via auxiliary client (timeout ${timeoutMs}ms)…`);
603
+ // Snapshot MEMORY.md state to detect post-write whether the write
604
+ // actually advanced the file — preserves the verify-on-disk check
605
+ // from the pre-AB path.
606
+ const before = await this.snapshotMemoryStat(memoryPathSafe);
607
+ // Single auxiliary call → SessionDistillation. distillSession
608
+ // owns its own internal timeout, so we don't need an outer race
609
+ // here; the deterministic fields populate regardless of LLM
610
+ // outcome (so even a full timeout produces a useful artifact).
611
+ let dist;
612
+ try {
613
+ dist = await (0, sessionDistiller_1.distillSession)({
614
+ sessionId: this.sessionId ?? `unbound-${Date.now()}`,
615
+ startedAt: new Date(this.startedAt).toISOString(),
616
+ exitPath,
617
+ userTurns,
618
+ messages: this.history,
619
+ toolTrace: this.sessionToolTrace,
620
+ auxiliaryClient: this.opts.auxiliaryClient,
621
+ timeoutMs,
622
+ // v4.1.3-essentials distillation-fix: route the new
623
+ // diagnostic signal to a dim line so the user can see WHICH
624
+ // of the three failure classes fired (timeout / call-fail /
625
+ // unparseable JSON). Before this hook, all three converged
626
+ // on a silent `partial:true` and the downstream "no bullets"
627
+ // warning didn't distinguish them.
628
+ onDiagnostic: (msg) => {
629
+ this.opts.display.dim(`[distill] ${msg}`);
630
+ },
631
+ });
632
+ }
633
+ catch (err) {
634
+ this.opts.display.warn(`Session distillation failed: ${err.message}. ` +
635
+ `MEMORY.md unchanged at: ${memoryPathSafe}`);
636
+ return;
637
+ }
638
+ // Persist the distillation JSON. Failures are recorded into the
639
+ // slice3 subsystem health surface (when the agent wires one) and
640
+ // logged here; they don't block the MEMORY.md write.
641
+ if (this.opts.paths?.root) {
642
+ const dir = node_path_1.default.join(this.opts.paths.root, 'distillations');
643
+ try {
644
+ const file = await (0, distillationStore_1.writeDistillation)(dir, dist);
645
+ this.lastDistillationPath = file;
646
+ this.opts.display.dim(`Session distillation${dist.partial ? ' (partial)' : ''} saved to ${file}`);
647
+ }
648
+ catch (err) {
649
+ this.opts.display.warn(`Distillation write failed: ${err.message}. ` +
650
+ `(Continuing to MEMORY.md update.)`);
651
+ }
652
+ }
653
+ // Update MEMORY.md `## Recent sessions` via the existing tool — no
654
+ // change to its on-disk shape (back-compat per slice's hard
655
+ // constraint). Skip when bullets are empty (full LLM timeout) —
656
+ // a zero-bullet entry would just be noise in MEMORY.md.
657
+ if (dist.bullets.length === 0) {
658
+ this.opts.display.warn(`Session summary skipped MEMORY.md update — auxiliary returned no bullets ` +
659
+ `(distillation file may still have deterministic fields).`);
660
+ return;
661
+ }
662
+ try {
663
+ const { sessionSummaryTool } = await Promise.resolve().then(() => __importStar(require('../../tools/v4/memory/sessionSummary')));
664
+ const result = await sessionSummaryTool.execute({ bullets: dist.bullets, trigger: 'auto-quit' }, {
665
+ cwd: process.cwd(),
666
+ paths: this.opts.paths,
667
+ memory: this.opts.memoryManager,
668
+ memoryGuard: this.opts.memoryGuard,
669
+ });
670
+ if (!result.success) {
671
+ this.opts.display.warn(`Session summary failed: ${result.error ?? 'unknown error'}. ` +
672
+ `MEMORY.md may be unchanged at: ${memoryPathSafe}`);
673
+ return;
674
+ }
675
+ }
676
+ catch (err) {
677
+ this.opts.display.warn(`Session summary failed during write: ${err.message}. ` +
678
+ `MEMORY.md unchanged at: ${memoryPathSafe}`);
679
+ return;
680
+ }
681
+ const after = await this.snapshotMemoryStat(memoryPathSafe);
682
+ if ((0, sessionSummaryGate_1.memoryGrewBetween)(before, after)) {
683
+ this.opts.display.dim(`Session summary saved to ${memoryPathSafe}`);
684
+ // Mark summarized ONLY after both writes verified — partial
685
+ // states leave the flag false so the next exit path retries.
686
+ this.summarized = true;
687
+ // Phase v4.1.2-memory-D: cache the distillation for the promotion
688
+ // flow. The /quit handler (and only /quit) consults this to build
689
+ // candidates without re-driving the auxiliary LLM.
690
+ this.lastDistillation = dist;
691
+ }
692
+ else {
693
+ this.opts.display.warn(`Session summary write completed but MEMORY.md size+mtime did not advance. ` +
694
+ `Check ${memoryPathSafe} manually.`);
695
+ }
696
+ }
697
+ /**
698
+ * Phase v4.1.2-memory-D: promotion-prompt flow.
699
+ *
700
+ * Called from the `/quit` path ONLY (NOT from SIGINT/SIGTERM
701
+ * handlers — async stdin can't be safely driven from a signal
702
+ * handler context). Builds candidates from `this.history` +
703
+ * `this.lastDistillation`, dedups against the existing
704
+ * `## Durable facts` section in MEMORY.md, prompts the user,
705
+ * persists approved selections.
706
+ *
707
+ * Gates (any false → silent no-op):
708
+ * - this.summarized (need a fresh distillation)
709
+ * - this.lastDistillation (set alongside summarized)
710
+ * - this.opts.memoryManager (real CLI sessions only)
711
+ * - this.opts.memoryGuard (real CLI sessions only)
712
+ *
713
+ * UX rules per Phase D's Q5 first-run experience:
714
+ * - 0 candidates AND 0 totalBeforeDedup → completely silent
715
+ * - 0 candidates AFTER dedup, but some were dropped → dim line
716
+ * "N candidates already in durable facts — nothing new to promote"
717
+ * - >0 candidates → prompt for approval, write approved
718
+ */
719
+ async maybeRunPromotion(api) {
720
+ if (!this.summarized || !this.lastDistillation)
721
+ return;
722
+ if (!this.opts.memoryManager || !this.opts.memoryGuard)
723
+ return;
724
+ let existingBody;
725
+ try {
726
+ existingBody = await (0, promotionPrompt_1.readExistingDurableFactsBody)(this.opts.memoryManager);
727
+ }
728
+ catch (err) {
729
+ this.opts.display.warn(`Could not read existing durable facts: ${err.message}. ` +
730
+ `Promotion skipped.`);
731
+ return;
732
+ }
733
+ const built = (0, promotionCandidates_1.extractCandidates)(this.history, this.lastDistillation, existingBody);
734
+ // Silent on truly empty sessions; reward the user on "all already saved".
735
+ if (built.candidates.length === 0) {
736
+ if (built.totalBeforeDedup === 0) {
737
+ return; // no signals + no distillation gold to promote — silent
738
+ }
739
+ if (built.dedupedAgainstExisting > 0) {
740
+ this.opts.display.dim(`${built.dedupedAgainstExisting} candidate${built.dedupedAgainstExisting === 1 ? '' : 's'} ` +
741
+ `already in durable facts — nothing new to promote.`);
742
+ }
743
+ return;
744
+ }
745
+ let approved;
746
+ try {
747
+ approved = await (0, promotionPrompt_1.promptForApproval)(api, this.opts.display, built.candidates);
748
+ }
749
+ catch (err) {
750
+ // The prompt API throwing is rare (broken stdin, etc.) — log
751
+ // and skip; no auto-write on error per "opt-in by design".
752
+ this.opts.display.warn(`Promotion prompt failed: ${err.message}. ` +
753
+ `Nothing was written to durable facts.`);
754
+ return;
755
+ }
756
+ if (approved.length === 0)
757
+ return; // user replied skip / none / unparseable
758
+ try {
759
+ const result = await (0, promotionPrompt_1.writeApprovedDurableFacts)(this.opts.memoryManager, this.opts.memoryGuard, approved);
760
+ if (result.ok && result.verified) {
761
+ this.opts.display.dim(`Promoted ${approved.length} fact${approved.length === 1 ? '' : 's'} ` +
762
+ `to MEMORY.md \`## Durable facts\`.`);
763
+ }
764
+ else {
765
+ this.opts.display.warn(`Durable-facts write completed but did not verify: ` +
766
+ `${result.reason ?? 'unknown'}. Inspect MEMORY.md manually.`);
767
+ }
768
+ }
769
+ catch (err) {
770
+ this.opts.display.warn(`Durable-facts write failed: ${err.message}. ` +
771
+ `MEMORY.md may be unchanged.`);
772
+ }
773
+ }
774
+ /**
775
+ * Phase v4.1.2 session-summary-followup: ask the auxiliary client
776
+ * for a JSON array of 5 session-summary bullets. One retry on
777
+ * malformed output with a stricter "JSON only" reminder, then we
778
+ * surface the failure honestly via the caller's warn() log.
779
+ *
780
+ * Returns `null` when both attempts fail to yield a valid array.
781
+ */
782
+ async requestSessionBulletsFromAuxiliary() {
783
+ const aux = this.opts.auxiliaryClient;
784
+ const transcript = this.buildSessionTranscriptForSummary();
785
+ const promptStrict = (extraNote) => [
786
+ 'Summarize this session in EXACTLY 5 short bullets. Focus on:',
787
+ '- what we worked on',
788
+ '- decisions made',
789
+ '- files / commits changed',
790
+ '- problems solved',
791
+ '- open items',
792
+ '',
793
+ 'Respond with ONLY a JSON array of 5 strings. No prose. No explanation. ' +
794
+ 'No code fences. No leading or trailing text.',
795
+ '',
796
+ 'Example: ["Shipped v4.1.1 to npm", "Diagnosed OAuth bug", "Patched tool schema", "Added doctor --providers", "Queued auxiliary fallback"]',
797
+ '',
798
+ extraNote,
799
+ '',
800
+ 'Session transcript:',
801
+ transcript,
802
+ ].filter((s) => s.length > 0).join('\n');
803
+ const attempt = async (note) => {
804
+ const res = await aux.call({
805
+ purpose: 'session_summary',
806
+ prompt: promptStrict(note),
807
+ maxTokens: 800,
808
+ timeoutMs: 30000,
809
+ });
810
+ return parseSessionBulletsResponse(res.content);
811
+ };
812
+ const first = await attempt('');
813
+ if (first)
814
+ return first;
815
+ const second = await attempt('STRICT: Your previous response was not parseable. Return ONLY the JSON array, nothing else.');
816
+ return second;
817
+ }
818
+ /**
819
+ * Compress recent history into a transcript blob the auxiliary
820
+ * client can summarise. Caps to the last 30 messages so the
821
+ * auxiliary prompt stays under typical small-model context limits;
822
+ * the auxiliary's `maxTokens: 800` output budget bounds the cost.
823
+ */
824
+ buildSessionTranscriptForSummary() {
825
+ const recent = this.history.slice(-30);
826
+ const lines = [];
827
+ for (const m of recent) {
828
+ const role = m.role === 'user' ? 'USER' : m.role === 'assistant' ? 'AIDEN' : m.role.toUpperCase();
829
+ const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
830
+ // Truncate any single message to 800 chars so a giant paste
831
+ // doesn't blow the prompt budget.
832
+ const trimmed = content.length > 800 ? `${content.slice(0, 800)}…` : content;
833
+ lines.push(`${role}: ${trimmed}`);
834
+ }
835
+ return lines.join('\n\n');
836
+ }
837
+ /**
838
+ * Read MEMORY.md size + mtime for the pre/post-write comparison in
839
+ * `maybeAutoSummarize`. Missing file is normalised to zeros so the
840
+ * "did MEMORY.md grow" comparison is well-defined even on fresh installs.
841
+ */
842
+ async snapshotMemoryStat(p) {
843
+ try {
844
+ const { promises: fsPromises } = await Promise.resolve().then(() => __importStar(require('node:fs')));
845
+ const stat = await fsPromises.stat(p);
846
+ return { size: stat.size, mtime: stat.mtimeMs };
847
+ }
848
+ catch {
849
+ return { size: 0, mtime: 0 };
850
+ }
851
+ }
278
852
  async runAgentTurn(userInput) {
279
853
  // Phase 30.2.1 — explore mode: short-circuit BEFORE building the
280
854
  // turn-status spinner / agent call. The wizard skipped, so there's
@@ -368,6 +942,12 @@ class ChatSession {
368
942
  // Unverified writes get a quieter line so the user knows the model
369
943
  // tried but the round-trip didn't confirm.
370
944
  renderMemoryConfirmations(result.toolCallTrace, this.opts.display);
945
+ // Phase v4.1.2-memory-AB: accumulate the turn's tool-call trace
946
+ // so the session distiller can derive deterministic fields
947
+ // (files_touched / tools_used) at exit.
948
+ if (result.toolCallTrace && result.toolCallTrace.length > 0) {
949
+ this.sessionToolTrace.push(...result.toolCallTrace);
950
+ }
371
951
  // When streaming was active and emitted the final content already,
372
952
  // skip the markdown re-render — we'd otherwise duplicate text.
373
953
  if (result.finalContent && !streamingActive) {
@@ -390,7 +970,42 @@ class ChatSession {
390
970
  if (streamingActive)
391
971
  this.opts.display.streamComplete();
392
972
  const msg = err?.message ?? String(err);
393
- this.opts.display.printError(msg, 'Run `/model` to switch providers or `aiden doctor` to diagnose.');
973
+ // v4.1.3-prebump: classify the error so the suggestion below
974
+ // points at the actual fix instead of the generic "/model or
975
+ // doctor" line. 413 / 429 / auth get tailored hints; everything
976
+ // else keeps the legacy fallback. Use the live providerId so
977
+ // the user sees WHICH provider blew up (matters when fallback
978
+ // adapters rotate slots mid-turn).
979
+ const cls = (0, errors_1.classifyProviderError)(err);
980
+ const tailored = (0, errors_1.suggestForErrorClass)(cls, this.currentProviderId);
981
+ // v4.1.3-essentials: on `auth` class errors we have enough state
982
+ // (which provider, what to run) to render a capability card —
983
+ // structured "what auth's missing, what you can still do, how to
984
+ // fix" is more useful than the bare message + one-line hint.
985
+ // Other classes keep the printError single-line surface; their
986
+ // hints are already specific.
987
+ if (cls === 'auth') {
988
+ const p = this.currentProviderId;
989
+ this.opts.display.printError(msg);
990
+ this.opts.display.capabilityCard({
991
+ title: `${p} authentication required`,
992
+ canStill: [
993
+ 'Continue chatting if a non-auth provider is configured (run `/model`)',
994
+ 'Run `/auth status` to see which providers are signed in',
995
+ 'Run `aiden doctor --providers` for a fuller liveness probe',
996
+ ],
997
+ cannotReliably: [
998
+ `Call ${p} until credentials are refreshed`,
999
+ 'Trust any cached responses that depended on this provider',
1000
+ ],
1001
+ fix: `Run \`/auth login ${p}\` if it's an OAuth provider, or set the ` +
1002
+ `relevant API key env var. Then retry — no need to restart Aiden.`,
1003
+ });
1004
+ }
1005
+ else {
1006
+ this.opts.display.printError(msg, tailored
1007
+ ?? 'Run `/model` to switch providers or `aiden doctor` to diagnose.');
1008
+ }
394
1009
  this.setStatusState({ kind: 'ready' });
395
1010
  this.lastTurnElapsedMs = Date.now() - turnStartedAt;
396
1011
  }
@@ -444,7 +1059,7 @@ class ChatSession {
444
1059
  const cols = display.cols();
445
1060
  const isNarrow = cols < 60;
446
1061
  const showEnvCapBlock = cols >= 70;
447
- const version = AIDEN_VERSION;
1062
+ const version = version_1.VERSION;
448
1063
  display.write('\n');
449
1064
  if (isNarrow) {
450
1065
  // Compact — single-line text logo + one-line capability summary.
@@ -462,13 +1077,26 @@ class ChatSession {
462
1077
  display.write('\n');
463
1078
  }
464
1079
  // Status pills.
1080
+ // Phase v4.1.2-version-display: append the running version as the
1081
+ // fifth pill so users see what they're on without invoking
1082
+ // `aiden --version`. Sourced from the build-injected core/version.ts.
465
1083
  display.write(display.statusPillsRow({
466
1084
  coreOnline: true,
467
1085
  mode: 'auto',
468
1086
  model: this.currentModelId,
469
1087
  memoryActive: true,
470
1088
  providerOk: !this.opts.unconfigured,
1089
+ version: version_1.VERSION,
471
1090
  }) + '\n');
1091
+ // v4.1.3-prebump: dim source annotation under the pills row so the
1092
+ // user can see WHY this provider/model was chosen — closes the
1093
+ // information gap that made Case 3 (persisted-config) look like a
1094
+ // bug ("why is it still on groq when I auth'd chatgpt-plus?"). One
1095
+ // line, dim, only when the source is informative.
1096
+ const sourceLabel = bootSourceLabel(this.opts.initialBootSource);
1097
+ if (sourceLabel) {
1098
+ display.write(` ${display.muted(sourceLabel)}\n`);
1099
+ }
472
1100
  // Tier-3.1b: rule + environment/capabilities block + rule + scroll
473
1101
  // + bottom prompt hint. Skipped at <70 cols to keep the narrow
474
1102
  // boot card from wrapping into noise.