aiden-runtime 4.9.1 → 4.9.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.
@@ -291,6 +291,18 @@ exports.default = (0, core_1.createPrompt)((config, done) => {
291
291
  line = `${message} ${theme.style.answer(value)}`;
292
292
  }
293
293
  else {
294
+ // v4.9.2 Slice 2 (commit 0d0668f1) attempted to fix cursor
295
+ // misalignment by post-pending cursorBackward(ghost.length).
296
+ // Live-REPL diagnostic proved the fix is structurally inert:
297
+ // @inquirer/core's screen-manager.js:56 appends an ABSOLUTE
298
+ // cursorTo(this.cursorPos.cols) AFTER our content, overriding
299
+ // any cursor-positioning escape we emit inline. The real fix
300
+ // requires either rendering the ghost via a side-channel post-
301
+ // render write or moving it out of the inline line entirely —
302
+ // both need the proper save/restore refactor scheduled for v4.10
303
+ // once the prompt has a real screen-manager-aware test harness.
304
+ // Reverted here so the shipped v4.9.2 doesn't carry a "fix" that
305
+ // doesn't fix anything. Bug D status: known, deferred.
294
306
  const ghostStr = ghost ? dim(ghost) : '';
295
307
  line = `${prefix} ${message}${value}${ghostStr}`;
296
308
  }
@@ -82,6 +82,7 @@ const progressBar_1 = require("./display/progressBar");
82
82
  const uiBuild_1 = require("./uiBuild");
83
83
  const sessionSummaryGate_1 = require("./sessionSummaryGate");
84
84
  const aidenPrompt_1 = __importDefault(require("./aidenPrompt"));
85
+ const confirmPrompt_1 = require("./confirmPrompt");
85
86
  const historyStore_1 = require("./historyStore");
86
87
  const modelMetadata_1 = require("../../core/v4/modelMetadata");
87
88
  // v4.1.3-prebump: classify provider errors so the catch path can show
@@ -496,17 +497,21 @@ class ChatSession {
496
497
  agent: this.opts.agent,
497
498
  pluginLoader: this.opts.pluginLoader,
498
499
  channelManager: this.opts.channelManager,
499
- confirm: async (msg) => {
500
- // Phase 17.1: bug was reading `this.opts.promptApi?` which is
501
- // undefined when no override is passed; the chain silently
502
- // resolved to undefined returned false → "Grant cancelled"
503
- // before the user could type anything. Use the resolved local
504
- // promptApi (which falls back to readline-default) instead.
505
- const r = await promptApi.readLine(msg);
506
- if (typeof r !== 'string')
507
- return false;
508
- return /^(y|yes)$/i.test(r.trim());
509
- },
500
+ // v4.9.2 Slice 3 — UX-rebuilt confirmation primitive.
501
+ // The stdin/keypress mechanics worked correctly all along;
502
+ // users simply couldn't see the prompt was open. The
503
+ // extracted `runConfirm` helper now owns the canonical
504
+ // y/N hint, the warn-tinted '?' glyph, the
505
+ // suggestionsDisabled flag (so confirmations skip ghost-
506
+ // match against outer chat history), and the per-input
507
+ // honest cancellation message.
508
+ //
509
+ // Phase 17.1 anchor: the previous primitive read
510
+ // `this.opts.promptApi?` (undefined → silently returned
511
+ // false → "Grant cancelled" before user could type) —
512
+ // fixed by routing through the resolved local `promptApi`.
513
+ // That fix stands; Slice 3 adds the UX layer on top.
514
+ confirm: (msg) => (0, confirmPrompt_1.runConfirm)(msg, promptApi, this.opts.display),
510
515
  // Phase 18: raw text prompt for /auth login OAuth code paste.
511
516
  prompt: (msg) => promptApi.readLine(msg),
512
517
  });
@@ -2085,9 +2090,15 @@ function createDefaultPromptApi(opts = {}) {
2085
2090
  // aidenPrompt component (ghost text + slash dropdown + history nav).
2086
2091
  const useLegacyPrompt = (0, uiBuild_1.isNoUiMode)() || !opts.commands;
2087
2092
  return {
2088
- async readLine(prompt) {
2093
+ async readLine(prompt, readOpts) {
2089
2094
  try {
2090
- if (useLegacyPrompt) {
2095
+ // v4.9.2 Slice 3 — confirmation prompts (suggestionsDisabled)
2096
+ // always route through the legacy inquirer input path. No
2097
+ // ghost-text (would autocomplete from outer chat history —
2098
+ // wrong context for a y/n question), no slash dropdown
2099
+ // (irrelevant for confirmations). Inquirer's plain input is
2100
+ // the well-tested baseline for single-shot questions.
2101
+ if (readOpts?.suggestionsDisabled || useLegacyPrompt) {
2091
2102
  return (await inq.input({ message: prompt, theme: promptTheme })) ?? '';
2092
2103
  }
2093
2104
  // Fetch history just-in-time so each read sees the latest
@@ -255,10 +255,9 @@ async function telegramRemove(ctx) {
255
255
  return;
256
256
  }
257
257
  const proceed = await confirm('Remove the Telegram bot token? This stops polling.');
258
- if (!proceed) {
259
- display.dim(' Cancelled.');
258
+ // v4.9.2 Slice 3 — confirm() now owns the rejection message.
259
+ if (!proceed)
260
260
  return;
261
- }
262
261
  // Stop the live adapter first so polling actually halts even if the
263
262
  // .env write fails for some reason.
264
263
  const manager = ctx.channelManager;
@@ -297,10 +296,9 @@ async function telegramTakeover(ctx) {
297
296
  const proceed = confirm
298
297
  ? await confirm('Take over Telegram polling? This will boot any other Aiden instance off the bot.')
299
298
  : true;
300
- if (!proceed) {
301
- display.dim(' Cancelled.');
299
+ // v4.9.2 Slice 3 — confirm() now owns the rejection message.
300
+ if (!proceed)
302
301
  return;
303
- }
304
302
  const spinner = display.startSpinner('Reclaiming Telegram polling…');
305
303
  let result;
306
304
  try {
@@ -327,8 +327,13 @@ async function cmdRemove(ctx, args) {
327
327
  else if (ctx.confirm) {
328
328
  ok = await ctx.confirm(question);
329
329
  }
330
+ // v4.9.2 Slice 3 — when ctx.confirm was the source, the primitive
331
+ // already printed a per-input rejection message. The ctx.prompt
332
+ // branch above does its own y/N parsing, so it still owns its own
333
+ // "Cancelled." line.
330
334
  if (!ok) {
331
- ctx.display.dim('Cancelled.');
335
+ if (ctx.prompt)
336
+ ctx.display.dim('Cancelled.');
332
337
  return;
333
338
  }
334
339
  if ((0, cronManager_1.deleteJob)(job.id)) {
@@ -85,7 +85,7 @@ function collectDoctorChecks(rootDir) {
85
85
  detail: `current=${ver} latest=${migrations_1.LATEST_SCHEMA_VERSION}`,
86
86
  fixable: false });
87
87
  // 3. Recent incarnation
88
- const inc = db.prepare(`SELECT incarnation_id, started_at, ended_at, exit_reason FROM daemon_incarnations
88
+ const inc = db.prepare(`SELECT incarnation_id, started_at, ended_at, exit_reason FROM daemon_incarnations
89
89
  ORDER BY started_at DESC LIMIT 1`).get();
90
90
  if (!inc) {
91
91
  checks.push({ name: 'recent incarnation', status: 'warn',
@@ -100,7 +100,7 @@ function collectDoctorChecks(rootDir) {
100
100
  }
101
101
  // 4. Recent crashes (24h)
102
102
  const sinceIso = new Date(Date.now() - TWENTY_FOUR_HOURS_MS).toISOString();
103
- const crashes = db.prepare(`SELECT COUNT(*) AS c FROM daemon_incarnations
103
+ const crashes = db.prepare(`SELECT COUNT(*) AS c FROM daemon_incarnations
104
104
  WHERE exit_reason = 'crash' AND started_at > ?`).get(sinceIso);
105
105
  checks.push({ name: 'recent crashes (24h)',
106
106
  status: crashes.c === 0 ? 'ok' : crashes.c > 3 ? 'warn' : 'ok',
@@ -111,7 +111,7 @@ function collectDoctorChecks(rootDir) {
111
111
  if (tableExists(db, 'run_attempts')) {
112
112
  const currentInc = inc?.incarnation_id ?? '';
113
113
  const cutoffIso = new Date(Date.now() - 30 * 60 * 1000).toISOString();
114
- stuckAttempts = db.prepare(`SELECT COUNT(*) AS c FROM run_attempts
114
+ stuckAttempts = db.prepare(`SELECT COUNT(*) AS c FROM run_attempts
115
115
  WHERE status='running' AND incarnation_id != ? AND started_at < ?`).get(currentInc, cutoffIso).c;
116
116
  }
117
117
  checks.push({ name: 'stuck attempts',
@@ -125,7 +125,7 @@ function collectDoctorChecks(rootDir) {
125
125
  let orphanSpans = 0;
126
126
  if (tableExists(db, 'spans')) {
127
127
  const currentInc = inc?.incarnation_id ?? '';
128
- orphanSpans = db.prepare(`SELECT COUNT(*) AS c FROM spans
128
+ orphanSpans = db.prepare(`SELECT COUNT(*) AS c FROM spans
129
129
  WHERE status IS NULL AND ended_at IS NULL AND incarnation_id != ?`).get(currentInc).c;
130
130
  }
131
131
  checks.push({ name: 'orphan spans',
@@ -147,7 +147,7 @@ function collectDoctorChecks(rootDir) {
147
147
  // 8. Stale-claimed trigger events
148
148
  let staleClaimed = 0;
149
149
  if (tableExists(db, 'trigger_events')) {
150
- staleClaimed = db.prepare(`SELECT COUNT(*) AS c FROM trigger_events
150
+ staleClaimed = db.prepare(`SELECT COUNT(*) AS c FROM trigger_events
151
151
  WHERE status='claimed' AND claim_expires_at IS NOT NULL AND claim_expires_at < ?`).get(Date.now()).c;
152
152
  }
153
153
  checks.push({ name: 'stale-claimed trigger events',
@@ -150,7 +150,7 @@ function readSnapshot() {
150
150
  // Recent runs (last 3).
151
151
  const recentRuns = (() => {
152
152
  try {
153
- const rows = db.prepare(`SELECT id, status, finish_reason, started_at, completed_at FROM runs
153
+ const rows = db.prepare(`SELECT id, status, finish_reason, started_at, completed_at FROM runs
154
154
  ORDER BY id DESC LIMIT 3`).all();
155
155
  return rows.map((r) => ({
156
156
  id: r.id,
@@ -266,8 +266,8 @@ async function wireSubagentFanout(opts) {
266
266
  // WAL-coexistence model as REPL — connection.ts caches per-path.
267
267
  const mcpInstanceId = `mcp-${(0, node_crypto_1.randomUUID)().slice(0, 8)}`;
268
268
  const mcpDb = (0, daemon_1.openDaemonDb)((0, daemon_1.daemonDbPath)(opts.paths.root));
269
- mcpDb.prepare(`INSERT OR IGNORE INTO daemon_instances
270
- (instance_id, pid, hostname, started_at, last_heartbeat, version)
269
+ mcpDb.prepare(`INSERT OR IGNORE INTO daemon_instances
270
+ (instance_id, pid, hostname, started_at, last_heartbeat, version)
271
271
  VALUES (?, ?, ?, ?, ?, ?)`).run(mcpInstanceId, process.pid, node_os_1.default.hostname(), Date.now(), Date.now(), version_1.VERSION);
272
272
  const mcpRunStore = (0, daemon_1.createRunStore)({ db: mcpDb });
273
273
  // v4.6 Phase 3b — self-improvement loop singleton against the
@@ -161,10 +161,9 @@ exports.plugins = {
161
161
  ctx.display.write('\n');
162
162
  const confirmFn = ctx.confirm ?? (async () => false);
163
163
  const allow = await confirmFn(`Install ${manifest.name} with the listed permissions? [y/N] `);
164
- if (!allow) {
165
- ctx.display.dim('Install cancelled.');
164
+ // v4.9.2 Slice 3 — confirm() now owns the rejection message.
165
+ if (!allow)
166
166
  return {};
167
- }
168
167
  // Copy into the user plugins dir.
169
168
  const dst = node_path_1.default.join(ctx.paths.pluginsDir, manifest.name);
170
169
  try {
@@ -258,10 +257,9 @@ exports.plugins = {
258
257
  const allow = await confirmFn(isUpgrade
259
258
  ? `Grant the listed permissions (including ${newPerms.length} new)? [y/N] `
260
259
  : `Grant the listed permissions? [y/N] `);
261
- if (!allow) {
262
- ctx.display.dim('Grant cancelled.');
260
+ // v4.9.2 Slice 3 — confirm() now owns the rejection message.
261
+ if (!allow)
263
262
  return {};
264
- }
265
263
  await (0, plugins_1.saveGrantedPermissions)(dir, entry.manifest.permissions);
266
264
  // Reload so the new state takes effect.
267
265
  await ctx.pluginLoader.teardown();
@@ -79,9 +79,9 @@ async function runTriggerSubcommand(action, args, argv, opts = {}) {
79
79
  spec.paths = spec.paths.map((p) => node_path_1.default.resolve(p));
80
80
  const id = (0, node_crypto_1.randomUUID)();
81
81
  const now = Date.now();
82
- db.prepare(`INSERT INTO triggers
83
- (id, source, name, spec_json, enabled, prompt_template, deliver_only,
84
- created_at, updated_at)
82
+ db.prepare(`INSERT INTO triggers
83
+ (id, source, name, spec_json, enabled, prompt_template, deliver_only,
84
+ created_at, updated_at)
85
85
  VALUES (?, 'file', ?, ?, ?, ?, 0, ?, ?)`).run(id, a.name, JSON.stringify(spec), a.disabled ? 0 : 1, spec.promptTemplate ?? null, now, now);
86
86
  out(`trigger added: ${id} (${a.name})\n`);
87
87
  out('Restart the daemon to activate the watcher.\n');
@@ -182,11 +182,11 @@ async function runTriggerSubcommand(action, args, argv, opts = {}) {
182
182
  return 1;
183
183
  }
184
184
  const prefix = `trigger:${trig.source}:${id}:`;
185
- const rows = db.prepare(`SELECT re.ts, re.kind, re.payload, r.id AS run_id
186
- FROM run_events re
187
- JOIN runs r ON re.run_id = r.id
188
- WHERE r.session_id LIKE ?
189
- ORDER BY re.ts DESC
185
+ const rows = db.prepare(`SELECT re.ts, re.kind, re.payload, r.id AS run_id
186
+ FROM run_events re
187
+ JOIN runs r ON re.run_id = r.id
188
+ WHERE r.session_id LIKE ?
189
+ ORDER BY re.ts DESC
190
190
  LIMIT 50`).all(`${prefix}%`);
191
191
  if (rows.length === 0) {
192
192
  out(`No run events recorded for trigger ${id} (${trig.name}).\n`);
@@ -213,10 +213,10 @@ async function runTriggerSubcommand(action, args, argv, opts = {}) {
213
213
  return 1;
214
214
  }
215
215
  const prefix = `trigger:${trig.source}:${id}:`;
216
- const rows = db.prepare(`SELECT id, status, finish_reason, started_at, completed_at
217
- FROM runs
218
- WHERE session_id LIKE ?
219
- ORDER BY started_at DESC
216
+ const rows = db.prepare(`SELECT id, status, finish_reason, started_at, completed_at
217
+ FROM runs
218
+ WHERE session_id LIKE ?
219
+ ORDER BY started_at DESC
220
220
  LIMIT 50`).all(`${prefix}%`);
221
221
  if (rows.length === 0) {
222
222
  out(`No runs recorded for trigger ${id} (${trig.name}).\n`);
@@ -261,9 +261,9 @@ function runAddWebhook(db, argv, out, err) {
261
261
  });
262
262
  const id = (0, node_crypto_1.randomUUID)();
263
263
  const now = Date.now();
264
- db.prepare(`INSERT INTO triggers
265
- (id, source, name, spec_json, enabled, prompt_template, deliver_only,
266
- created_at, updated_at)
264
+ db.prepare(`INSERT INTO triggers
265
+ (id, source, name, spec_json, enabled, prompt_template, deliver_only,
266
+ created_at, updated_at)
267
267
  VALUES (?, 'webhook', ?, ?, ?, ?, ?, ?, ?)`).run(id, a.name, JSON.stringify(spec), a.disabled ? 0 : 1, spec.promptTemplate ?? null, spec.deliverOnly ? 1 : 0, now, now);
268
268
  const cfg = (0, daemon_1.getDaemonConfig)();
269
269
  const host = process.env.AIDEN_DAEMON_BIND ?? '127.0.0.1';
@@ -359,9 +359,9 @@ async function runAddEmail(db, argv, out, err) {
359
359
  }
360
360
  const id = (0, node_crypto_1.randomUUID)();
361
361
  const now = Date.now();
362
- db.prepare(`INSERT INTO triggers
363
- (id, source, name, spec_json, enabled, prompt_template, deliver_only,
364
- created_at, updated_at)
362
+ db.prepare(`INSERT INTO triggers
363
+ (id, source, name, spec_json, enabled, prompt_template, deliver_only,
364
+ created_at, updated_at)
365
365
  VALUES (?, 'email', ?, ?, ?, ?, ?, ?, ?)`).run(id, a.name, JSON.stringify(spec), a.disabled ? 0 : 1, spec.promptTemplate ?? null, spec.deliverOnly ? 1 : 0, now, now);
366
366
  out(`trigger added: ${id} (${a.name})\n`);
367
367
  out(`imap host: ${spec.imap.host}:${spec.imap.port}${spec.imap.tls ? ' (TLS)' : ''}\n`);
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * cli/v4/confirmPrompt.ts — v4.9.2 SLICE 3.
10
+ *
11
+ * The slash-command `ctx.confirm()` primitive, extracted from the
12
+ * chatSession closure so it has one source of truth + is unit-testable
13
+ * without spinning up a full REPL.
14
+ *
15
+ * Behaviour:
16
+ * - Canonicalises the y/n hint: strips any caller-appended ` (y/N) `
17
+ * / ` [y/N] ` / ` (Y/n) ` so the primitive can append exactly one
18
+ * ` (y/N) ` in canonical lowercase-y / capital-N form.
19
+ * - Prefixes with a warn-tinted `?` glyph so the confirmation chrome
20
+ * is visually distinct from the main ▲ chat prompt (Slice 3 root
21
+ * cause: users couldn't tell a prompt was open).
22
+ * - Routes through `promptApi.readLine` with `suggestionsDisabled:true`
23
+ * so the inquirer-input path runs (no ghost-text from outer chat
24
+ * history, no slash dropdown — irrelevant for y/n).
25
+ * - Emits a per-input cancellation reason:
26
+ * empty / Enter alone → "Cancelled (press 'y' to confirm; …)"
27
+ * 'n' / 'no' → "Cancelled." (deliberate decline)
28
+ * other non-y → `Cancelled ("<x>" not recognized — …)`
29
+ * null / non-string → "Cancelled (no input)."
30
+ * Callers no longer print their own "Cancelled." line — the
31
+ * primitive owns the rejection message.
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.runConfirm = runConfirm;
35
+ /** Strip any caller-appended `(y/N)` / `[y/N]` / `(Y/n)` so we can
36
+ * re-append the canonical hint without duplication. */
37
+ const TRAILING_YN_HINT_RE = /\s*[\[(](y\/[nN]|Y\/n)[\])]\s*$/i;
38
+ /**
39
+ * Run a single confirmation prompt. Resolves to `true` on `y` / `yes`
40
+ * (case insensitive, trimmed); `false` on anything else, with a
41
+ * specific cancellation line written to `display.dim()`.
42
+ *
43
+ * Never throws — readLine errors and non-string returns degrade to
44
+ * `false` with an honest "no input" reason.
45
+ */
46
+ async function runConfirm(msg, promptApi, display) {
47
+ const stripped = msg.replace(TRAILING_YN_HINT_RE, '').trimEnd();
48
+ const decorated = `${display.paint('?', 'warn')} ${stripped} (y/N) `;
49
+ const r = await promptApi.readLine(decorated, { suggestionsDisabled: true });
50
+ if (typeof r !== 'string') {
51
+ display.dim('Cancelled (no input).');
52
+ return false;
53
+ }
54
+ const trimmed = r.trim();
55
+ if (/^(y|yes)$/i.test(trimmed))
56
+ return true;
57
+ if (trimmed === '') {
58
+ display.dim(`Cancelled (press 'y' to confirm; Enter alone = no).`);
59
+ }
60
+ else if (/^(n|no)$/i.test(trimmed)) {
61
+ display.dim('Cancelled.');
62
+ }
63
+ else {
64
+ display.dim(`Cancelled ("${trimmed}" not recognized — expected y/yes/n/no).`);
65
+ }
66
+ return false;
67
+ }