aiden-runtime 4.1.1 → 4.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -26
- package/dist/cli/v4/aidenCLI.js +169 -9
- package/dist/cli/v4/callbacks.js +20 -2
- package/dist/cli/v4/chatSession.js +644 -16
- package/dist/cli/v4/commands/auth.js +6 -3
- package/dist/cli/v4/commands/doctor.js +23 -27
- package/dist/cli/v4/commands/help.js +4 -0
- package/dist/cli/v4/commands/index.js +10 -1
- package/dist/cli/v4/commands/model.js +30 -1
- package/dist/cli/v4/commands/reloadSoul.js +37 -0
- package/dist/cli/v4/commands/update.js +102 -0
- package/dist/cli/v4/defaultSoul.js +68 -2
- package/dist/cli/v4/display/capabilityCard.js +135 -0
- package/dist/cli/v4/display/sessionEndCard.js +127 -0
- package/dist/cli/v4/display/toolTrail.js +172 -0
- package/dist/cli/v4/display.js +492 -142
- package/dist/cli/v4/doctor.js +472 -58
- package/dist/cli/v4/doctorLiveness.js +65 -10
- package/dist/cli/v4/promotionPrompt.js +332 -0
- package/dist/cli/v4/providerBootSelector.js +144 -0
- package/dist/cli/v4/replyRenderer.js +311 -20
- package/dist/cli/v4/sessionSummaryGate.js +66 -0
- package/dist/cli/v4/skinEngine.js +14 -3
- package/dist/cli/v4/toolPreview.js +153 -0
- package/dist/core/tools/nowPlaying.js +7 -15
- package/dist/core/v4/aidenAgent.js +91 -29
- package/dist/core/v4/capabilities.js +89 -0
- package/dist/core/v4/contextCompressor.js +25 -8
- package/dist/core/v4/distillationIndex.js +167 -0
- package/dist/core/v4/distillationStore.js +98 -0
- package/dist/core/v4/logger/logger.js +40 -9
- package/dist/core/v4/promotionCandidates.js +234 -0
- package/dist/core/v4/promptBuilder.js +145 -1
- package/dist/core/v4/sessionDistiller.js +452 -0
- package/dist/core/v4/skillMining/skillMiner.js +43 -6
- package/dist/core/v4/skillOutcomeTracker.js +323 -0
- package/dist/core/v4/subsystemHealth.js +143 -0
- package/dist/core/v4/toolRegistry.js +16 -1
- package/dist/core/v4/update/executeInstall.js +233 -0
- package/dist/core/version.js +1 -1
- package/dist/moat/memoryGuard.js +111 -0
- package/dist/moat/plannerGuard.js +19 -0
- package/dist/moat/skillTeacher.js +14 -5
- package/dist/providers/v4/chatCompletionsAdapter.js +9 -0
- package/dist/providers/v4/errors.js +112 -4
- package/dist/providers/v4/modelDefaults.js +65 -0
- package/dist/providers/v4/registry.js +9 -2
- package/dist/providers/v4/runtimeResolver.js +6 -0
- package/dist/tools/v4/index.js +80 -1
- package/dist/tools/v4/memory/memoryRemove.js +57 -2
- package/dist/tools/v4/memory/sessionSummary.js +151 -0
- package/dist/tools/v4/sessions/recallSession.js +177 -0
- package/dist/tools/v4/sessions/sessionSearch.js +5 -1
- package/dist/tools/v4/system/_psHelpers.js +123 -0
- package/dist/tools/v4/system/aidenSelfUpdate.js +162 -0
- package/dist/tools/v4/system/appClose.js +79 -0
- package/dist/tools/v4/system/appInput.js +154 -0
- package/dist/tools/v4/system/appLaunch.js +218 -0
- package/dist/tools/v4/system/clipboardRead.js +54 -0
- package/dist/tools/v4/system/clipboardWrite.js +84 -0
- package/dist/tools/v4/system/mediaKey.js +109 -0
- package/dist/tools/v4/system/mediaSessions.js +163 -0
- package/dist/tools/v4/system/mediaTransport.js +211 -0
- package/dist/tools/v4/system/osProcessList.js +99 -0
- package/dist/tools/v4/system/screenshot.js +106 -0
- package/dist/tools/v4/system/volumeSet.js +157 -0
- package/package.json +4 -1
- package/skills/system_control.md +185 -69
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|