aiden-runtime 4.1.0 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +89 -33
  2. package/dist/cli/v4/aidenCLI.js +162 -11
  3. package/dist/cli/v4/callbacks.js +5 -2
  4. package/dist/cli/v4/chatSession.js +525 -15
  5. package/dist/cli/v4/commands/auth.js +6 -3
  6. package/dist/cli/v4/commands/help.js +4 -0
  7. package/dist/cli/v4/commands/index.js +10 -1
  8. package/dist/cli/v4/commands/reloadSoul.js +37 -0
  9. package/dist/cli/v4/commands/update.js +102 -0
  10. package/dist/cli/v4/defaultSoul.js +68 -2
  11. package/dist/cli/v4/display.js +28 -10
  12. package/dist/cli/v4/doctor.js +173 -1
  13. package/dist/cli/v4/doctorLiveness.js +384 -0
  14. package/dist/cli/v4/promotionPrompt.js +202 -0
  15. package/dist/cli/v4/providerBootSelector.js +144 -0
  16. package/dist/cli/v4/sessionSummaryGate.js +66 -0
  17. package/dist/cli/v4/toolPreview.js +139 -0
  18. package/dist/core/v4/aidenAgent.js +91 -29
  19. package/dist/core/v4/capabilities.js +89 -0
  20. package/dist/core/v4/contextCompressor.js +25 -8
  21. package/dist/core/v4/distillationIndex.js +167 -0
  22. package/dist/core/v4/distillationStore.js +98 -0
  23. package/dist/core/v4/logger/logger.js +40 -9
  24. package/dist/core/v4/promotionCandidates.js +234 -0
  25. package/dist/core/v4/promptBuilder.js +145 -1
  26. package/dist/core/v4/sessionDistiller.js +405 -0
  27. package/dist/core/v4/skillMining/extractorPrompt.js +28 -21
  28. package/dist/core/v4/skillMining/proposalBuilder.js +3 -2
  29. package/dist/core/v4/skillMining/skillMiner.js +43 -6
  30. package/dist/core/v4/skillOutcomeTracker.js +323 -0
  31. package/dist/core/v4/subsystemHealth.js +143 -0
  32. package/dist/core/v4/update/executeInstall.js +233 -0
  33. package/dist/core/version.js +1 -1
  34. package/dist/moat/dangerousPatterns.js +1 -1
  35. package/dist/moat/memoryGuard.js +111 -0
  36. package/dist/moat/skillTeacher.js +14 -5
  37. package/dist/providers/v4/chatCompletionsAdapter.js +9 -0
  38. package/dist/providers/v4/codexResponsesAdapter.js +7 -2
  39. package/dist/providers/v4/errors.js +67 -1
  40. package/dist/providers/v4/modelDefaults.js +65 -0
  41. package/dist/providers/v4/ollamaPromptToolsAdapter.js +9 -2
  42. package/dist/providers/v4/registry.js +9 -2
  43. package/dist/providers/v4/runtimeResolver.js +6 -0
  44. package/dist/tools/v4/index.js +57 -1
  45. package/dist/tools/v4/memory/memoryRemove.js +57 -2
  46. package/dist/tools/v4/memory/sessionSummary.js +151 -0
  47. package/dist/tools/v4/sessions/recallSession.js +163 -0
  48. package/dist/tools/v4/sessions/sessionSearch.js +5 -1
  49. package/dist/tools/v4/subagent/subagentFanout.js +24 -0
  50. package/dist/tools/v4/system/_psHelpers.js +55 -0
  51. package/dist/tools/v4/system/aidenSelfUpdate.js +162 -0
  52. package/dist/tools/v4/system/appClose.js +79 -0
  53. package/dist/tools/v4/system/appLaunch.js +92 -0
  54. package/dist/tools/v4/system/clipboardRead.js +54 -0
  55. package/dist/tools/v4/system/clipboardWrite.js +84 -0
  56. package/dist/tools/v4/system/mediaKey.js +78 -0
  57. package/dist/tools/v4/system/osProcessList.js +99 -0
  58. package/dist/tools/v4/system/screenshot.js +106 -0
  59. package/dist/tools/v4/system/volumeSet.js +157 -0
  60. package/package.json +4 -1
  61. package/skills/system_control.md +135 -69
@@ -232,12 +232,19 @@ exports.PROVIDER_REGISTRY = {
232
232
  apiMode: 'chat_completions',
233
233
  baseUrl: 'https://api.deepseek.com/v1',
234
234
  apiKeyEnvVar: 'DEEPSEEK_API_KEY',
235
- description: 'DeepSeek direct API — V3 chat + R1 reasoning.',
235
+ description: 'DeepSeek direct API — V4 Pro reasoning flagship + legacy aliases.',
236
236
  tier: 'paid',
237
237
  hasFreeTier: false,
238
238
  docsUrl: 'https://api-docs.deepseek.com/',
239
239
  supportsToolCalling: true,
240
- modelIds: ['deepseek-chat', 'deepseek-reasoner'],
240
+ // Phase v4.1.2-deepseek: `deepseek-v4-pro` prepended as the new
241
+ // flagship — becomes the auto-pick default for new users via
242
+ // pickProbeModel(). Legacy `deepseek-chat` and `deepseek-reasoner`
243
+ // retained for back-compat (still functional aliases of the V4
244
+ // flash family per DeepSeek docs; deprecated-but-live). Removal
245
+ // is its own deprecation slice. Per-call thinking + reasoning
246
+ // _effort defaults for v4-pro live in providers/v4/modelDefaults.ts.
247
+ modelIds: ['deepseek-v4-pro', 'deepseek-chat', 'deepseek-reasoner'],
241
248
  },
242
249
  mistral: {
243
250
  id: 'mistral',
@@ -39,6 +39,7 @@ const chatCompletionsAdapter_1 = require("./chatCompletionsAdapter");
39
39
  const anthropicAdapter_1 = require("./anthropicAdapter");
40
40
  const codexResponsesAdapter_1 = require("./codexResponsesAdapter");
41
41
  const ollamaPromptToolsAdapter_1 = require("./ollamaPromptToolsAdapter");
42
+ const modelDefaults_1 = require("./modelDefaults");
42
43
  const tokenStore_1 = require("../../core/v4/auth/tokenStore");
43
44
  class RuntimeResolver {
44
45
  constructor(credentialResolver) {
@@ -61,6 +62,11 @@ class RuntimeResolver {
61
62
  model: model.id,
62
63
  providerName: entry.id,
63
64
  extraHeaders: entry.extraHeaders,
65
+ // Phase v4.1.2-deepseek: per-model body defaults (e.g.
66
+ // DeepSeek V4-Pro's mandatory thinking + reasoning_effort).
67
+ // Undefined for models without registered defaults — adapter
68
+ // skips the merge in that case.
69
+ defaultExtraBody: (0, modelDefaults_1.getModelDefaults)(entry.id, model.id)?.extraBody,
64
70
  });
65
71
  case 'anthropic_messages':
66
72
  if (!credentials.apiKey) {
@@ -21,7 +21,7 @@
21
21
  * Status: PHASE 8.
22
22
  */
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.memoryRemoveTool = exports.memoryReplaceTool = exports.memoryAddTool = exports.processWaitTool = exports.processKillTool = exports.processLogReadTool = exports.processListTool = exports.processSpawnTool = exports.executeCodeTool = exports.shellExecTool = exports.naturalEventsTool = exports.nowPlayingTool = exports.systemInfoTool = exports.makeLookupToolSchema = exports.skillManageTool = exports.skillViewTool = exports.skillsListTool = exports.sessionListTool = exports.sessionSearchTool = exports.browserCloseTool = exports.browserScrollTool = exports.browserFillTool = exports.browserTypeTool = exports.browserClickTool = exports.browserNavigateTool = exports.browserGetUrlTool = exports.browserExtractTool = exports.browserScreenshotTool = exports.fileCopyTool = exports.fileMoveTool = exports.fileDeleteTool = exports.filePatchTool = exports.fileWriteTool = exports.fileListTool = exports.fileReadTool = exports.deepResearchTool = exports.webPageTool = exports.webFetchTool = exports.webSearchTool = exports.makeSubagentFanoutTool = void 0;
24
+ exports.sessionSummaryTool = exports.memoryRemoveTool = exports.memoryReplaceTool = exports.memoryAddTool = exports.processWaitTool = exports.processKillTool = exports.processLogReadTool = exports.processListTool = exports.processSpawnTool = exports.executeCodeTool = exports.shellExecTool = exports.clipboardWriteTool = exports.clipboardReadTool = exports.appCloseTool = exports.appLaunchTool = exports.volumeSetTool = exports.mediaKeyTool = exports.osProcessListTool = exports.screenshotTool = exports.naturalEventsTool = exports.nowPlayingTool = exports.systemInfoTool = exports.makeLookupToolSchema = exports.skillManageTool = exports.skillViewTool = exports.skillsListTool = exports.sessionListTool = exports.sessionSearchTool = exports.browserCloseTool = exports.browserScrollTool = exports.browserFillTool = exports.browserTypeTool = exports.browserClickTool = exports.browserNavigateTool = exports.browserGetUrlTool = exports.browserExtractTool = exports.browserScreenshotTool = exports.fileCopyTool = exports.fileMoveTool = exports.fileDeleteTool = exports.filePatchTool = exports.fileWriteTool = exports.fileListTool = exports.fileReadTool = exports.deepResearchTool = exports.webPageTool = exports.webFetchTool = exports.webSearchTool = exports.makeSubagentFanoutTool = void 0;
25
25
  exports.registerReadOnlyTools = registerReadOnlyTools;
26
26
  exports.registerWriteTools = registerWriteTools;
27
27
  exports.registerAllTools = registerAllTools;
@@ -49,6 +49,7 @@ const browserScroll_1 = require("./browser/browserScroll");
49
49
  const browserClose_1 = require("./browser/browserClose");
50
50
  const sessionSearch_1 = require("./sessions/sessionSearch");
51
51
  const sessionList_1 = require("./sessions/sessionList");
52
+ const recallSession_1 = require("./sessions/recallSession");
52
53
  const skillsList_1 = require("./skills/skillsList");
53
54
  const skillView_1 = require("./skills/skillView");
54
55
  const skillManage_1 = require("./skills/skillManage");
@@ -56,6 +57,18 @@ const lookupToolSchema_1 = require("./skills/lookupToolSchema");
56
57
  const systemInfo_1 = require("./system/systemInfo");
57
58
  const nowPlaying_1 = require("./system/nowPlaying");
58
59
  const naturalEvents_1 = require("./system/naturalEvents");
60
+ // Phase v4.1.2-followup-3 computer-control bundle.
61
+ const screenshot_1 = require("./system/screenshot");
62
+ const osProcessList_1 = require("./system/osProcessList");
63
+ const mediaKey_1 = require("./system/mediaKey");
64
+ const volumeSet_1 = require("./system/volumeSet");
65
+ const appLaunch_1 = require("./system/appLaunch");
66
+ const appClose_1 = require("./system/appClose");
67
+ const clipboardRead_1 = require("./system/clipboardRead");
68
+ const clipboardWrite_1 = require("./system/clipboardWrite");
69
+ // Phase v4.1.2-update — natural-language self-update entry point.
70
+ // Routes through the same shared executeInstall executor as `/update install`.
71
+ const aidenSelfUpdate_1 = require("./system/aidenSelfUpdate");
59
72
  const shellExec_1 = require("./terminal/shellExec");
60
73
  const executeCode_1 = require("./executeCode");
61
74
  const processSpawn_1 = require("./process/processSpawn");
@@ -66,6 +79,7 @@ const processWait_1 = require("./process/processWait");
66
79
  const memoryAdd_1 = require("./memory/memoryAdd");
67
80
  const memoryReplace_1 = require("./memory/memoryReplace");
68
81
  const memoryRemove_1 = require("./memory/memoryRemove");
82
+ const sessionSummary_1 = require("./memory/sessionSummary");
69
83
  const subagentFanout_1 = require("./subagent/subagentFanout");
70
84
  /**
71
85
  * Register every read-only tool into `registry`. The
@@ -94,11 +108,21 @@ function registerReadOnlyTools(registry) {
94
108
  registry.register(browserGetUrl_1.browserGetUrlTool);
95
109
  registry.register(sessionSearch_1.sessionSearchTool);
96
110
  registry.register(sessionList_1.sessionListTool);
111
+ // Phase v4.1.2-memory-C: recall_session reads SessionDistillation
112
+ // files written by Phase A+B. Sits alongside session_search — the
113
+ // two have distinct purposes (FTS5-over-messages vs ranked
114
+ // distillation summaries); descriptions force the right model
115
+ // choice.
116
+ registry.register(recallSession_1.recallSessionTool);
97
117
  registry.register(skillsList_1.skillsListTool);
98
118
  registry.register(skillView_1.skillViewTool);
99
119
  registry.register(systemInfo_1.systemInfoTool);
100
120
  registry.register(nowPlaying_1.nowPlayingTool);
101
121
  registry.register(naturalEvents_1.naturalEventsTool);
122
+ // Phase v4.1.2-followup-3 — computer-control read-only tools.
123
+ registry.register(screenshot_1.screenshotTool);
124
+ registry.register(osProcessList_1.osProcessListTool);
125
+ registry.register(clipboardRead_1.clipboardReadTool);
102
126
  registry.register((0, lookupToolSchema_1.makeLookupToolSchema)(registry));
103
127
  // Phase v4.1-subagent — register a stub for subagent_fanout so its
104
128
  // schema is visible to the agent loop, the MCP server, and the
@@ -158,9 +182,22 @@ function registerWriteTools(registry) {
158
182
  registry.register(memoryAdd_1.memoryAddTool);
159
183
  registry.register(memoryReplace_1.memoryReplaceTool);
160
184
  registry.register(memoryRemove_1.memoryRemoveTool);
185
+ // Phase v4.1.2 alive-core: cross-session continuity via /quit auto-summary.
186
+ registry.register(sessionSummary_1.sessionSummaryTool);
161
187
  // Phase 10: skill_manage — mutating, also goes through the approval
162
188
  // engine. skills_list / skill_view stay in registerReadOnlyTools.
163
189
  registry.register(skillManage_1.skillManageTool);
190
+ // Phase v4.1.2-update: natural-language entry to the same install
191
+ // executor that /update install uses. Two-step confirmation gate
192
+ // (confirm:false → status; confirm:true → install).
193
+ registry.register(aidenSelfUpdate_1.aidenSelfUpdateTool);
194
+ // Phase v4.1.2-followup-3 — computer-control mutating tools. All
195
+ // route through the approval engine like every other write.
196
+ registry.register(mediaKey_1.mediaKeyTool);
197
+ registry.register(volumeSet_1.volumeSetTool);
198
+ registry.register(appLaunch_1.appLaunchTool);
199
+ registry.register(appClose_1.appCloseTool);
200
+ registry.register(clipboardWrite_1.clipboardWriteTool);
164
201
  }
165
202
  /** Register every v4 tool. Most callers want this. */
166
203
  function registerAllTools(registry) {
@@ -227,6 +264,23 @@ var nowPlaying_2 = require("./system/nowPlaying");
227
264
  Object.defineProperty(exports, "nowPlayingTool", { enumerable: true, get: function () { return nowPlaying_2.nowPlayingTool; } });
228
265
  var naturalEvents_2 = require("./system/naturalEvents");
229
266
  Object.defineProperty(exports, "naturalEventsTool", { enumerable: true, get: function () { return naturalEvents_2.naturalEventsTool; } });
267
+ // Phase v4.1.2-followup-3 computer-control bundle exports.
268
+ var screenshot_2 = require("./system/screenshot");
269
+ Object.defineProperty(exports, "screenshotTool", { enumerable: true, get: function () { return screenshot_2.screenshotTool; } });
270
+ var osProcessList_2 = require("./system/osProcessList");
271
+ Object.defineProperty(exports, "osProcessListTool", { enumerable: true, get: function () { return osProcessList_2.osProcessListTool; } });
272
+ var mediaKey_2 = require("./system/mediaKey");
273
+ Object.defineProperty(exports, "mediaKeyTool", { enumerable: true, get: function () { return mediaKey_2.mediaKeyTool; } });
274
+ var volumeSet_2 = require("./system/volumeSet");
275
+ Object.defineProperty(exports, "volumeSetTool", { enumerable: true, get: function () { return volumeSet_2.volumeSetTool; } });
276
+ var appLaunch_2 = require("./system/appLaunch");
277
+ Object.defineProperty(exports, "appLaunchTool", { enumerable: true, get: function () { return appLaunch_2.appLaunchTool; } });
278
+ var appClose_2 = require("./system/appClose");
279
+ Object.defineProperty(exports, "appCloseTool", { enumerable: true, get: function () { return appClose_2.appCloseTool; } });
280
+ var clipboardRead_2 = require("./system/clipboardRead");
281
+ Object.defineProperty(exports, "clipboardReadTool", { enumerable: true, get: function () { return clipboardRead_2.clipboardReadTool; } });
282
+ var clipboardWrite_2 = require("./system/clipboardWrite");
283
+ Object.defineProperty(exports, "clipboardWriteTool", { enumerable: true, get: function () { return clipboardWrite_2.clipboardWriteTool; } });
230
284
  var shellExec_2 = require("./terminal/shellExec");
231
285
  Object.defineProperty(exports, "shellExecTool", { enumerable: true, get: function () { return shellExec_2.shellExecTool; } });
232
286
  var executeCode_2 = require("./executeCode");
@@ -247,3 +301,5 @@ var memoryReplace_2 = require("./memory/memoryReplace");
247
301
  Object.defineProperty(exports, "memoryReplaceTool", { enumerable: true, get: function () { return memoryReplace_2.memoryReplaceTool; } });
248
302
  var memoryRemove_2 = require("./memory/memoryRemove");
249
303
  Object.defineProperty(exports, "memoryRemoveTool", { enumerable: true, get: function () { return memoryRemove_2.memoryRemoveTool; } });
304
+ var sessionSummary_2 = require("./memory/sessionSummary");
305
+ Object.defineProperty(exports, "sessionSummaryTool", { enumerable: true, get: function () { return sessionSummary_2.sessionSummaryTool; } });
@@ -12,14 +12,37 @@
12
12
  * Returns `verified: true` only after the post-write read confirms
13
13
  * the text is gone from the file.
14
14
  *
15
- * Status: PHASE 9.
15
+ * Phase v4.1.2-bug-X: user-approved durable facts (anything in
16
+ * MEMORY.md `## Durable facts`) are protected from autonomous
17
+ * deletion. A subsequent session on a weak model (llama-3.3 in the
18
+ * smoke test) called memory_remove on a Phase-D-promoted fact with
19
+ * reasoning "outdated" — violating Phase D's opt-in trust contract.
20
+ *
21
+ * The guard is STRICT: if the requested substring appears ANYWHERE
22
+ * in the `## Durable facts` body, the call is rejected. Substring
23
+ * removal operates whole-file, so partial protection would still
24
+ * nuke the durable copy as side-effect. Hard rejection (vs propose-
25
+ * and-defer) is honest: model must surface to user; only the user
26
+ * (editing MEMORY.md directly, or via a future `/forget` slash
27
+ * command) can revoke durable content.
28
+ *
29
+ * Status: PHASE 9 (PHASE v4.1.2-bug-X: section protection).
16
30
  */
17
31
  Object.defineProperty(exports, "__esModule", { value: true });
18
32
  exports.memoryRemoveTool = void 0;
33
+ const memoryGuard_1 = require("../../../moat/memoryGuard");
34
+ /** Section in MEMORY.md that Phase D promotion writes to. */
35
+ const DURABLE_FACTS_HEADER = '## Durable facts';
19
36
  exports.memoryRemoveTool = {
20
37
  schema: {
21
38
  name: 'memory_remove',
22
- description: 'Remove an entry from MEMORY.md or USER.md by substring match. Returns verified=true only after the change is confirmed on disk.',
39
+ description: 'Remove an entry from MEMORY.md or USER.md by substring match. ' +
40
+ 'CANNOT remove entries in MEMORY.md `## Durable facts` — those are ' +
41
+ 'user-approved facts the user explicitly promoted; only the user ' +
42
+ 'can revoke them by editing MEMORY.md directly. If you think a ' +
43
+ 'durable fact is outdated, surface that to the user and ask them ' +
44
+ 'to confirm; do not propose autonomous removal. Returns ' +
45
+ 'verified=true only after the change is confirmed on disk.',
23
46
  inputSchema: {
24
47
  type: 'object',
25
48
  properties: {
@@ -42,6 +65,38 @@ exports.memoryRemoveTool = {
42
65
  }
43
66
  const file = args.file === 'user' ? 'user' : 'memory';
44
67
  const text = String(args.text ?? '');
68
+ // Phase v4.1.2-bug-X: durable-section protection. Only applies
69
+ // to MEMORY.md (USER.md has no section structure today). The
70
+ // check requires snapshot access — when ctx.memory is not
71
+ // wired (test contexts, future surface refactors), we fall
72
+ // through to the existing guardedRemove behavior. Real CLI
73
+ // sessions wire memoryManager, so production paths get the
74
+ // guard. Documented intentional fall-through.
75
+ if (file === 'memory' && ctx.memory) {
76
+ try {
77
+ const snap = await ctx.memory.loadSnapshot();
78
+ const memoryMd = snap?.memoryMd ?? '';
79
+ if (memoryMd && (0, memoryGuard_1.containsInSection)(memoryMd, text, DURABLE_FACTS_HEADER)) {
80
+ const preview = text.length > 60
81
+ ? `${text.slice(0, 60)}…`
82
+ : text;
83
+ return {
84
+ success: false,
85
+ verified: false,
86
+ error: `Cannot remove "${preview}" — it's in MEMORY.md ` +
87
+ `\`${DURABLE_FACTS_HEADER}\`, which holds user-approved facts. ` +
88
+ `Only the user can revoke these. Ask them to confirm removal ` +
89
+ `in their next message; do not propose autonomous deletion.`,
90
+ file,
91
+ protectedSection: DURABLE_FACTS_HEADER,
92
+ };
93
+ }
94
+ }
95
+ catch {
96
+ // Snapshot read failed — fall through to old behavior rather
97
+ // than block legitimate removals on transient I/O errors.
98
+ }
99
+ }
45
100
  const r = await ctx.memoryGuard.guardedRemove(file, text);
46
101
  return {
47
102
  success: r.ok,
@@ -0,0 +1,151 @@
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
+ * tools/v4/memory/sessionSummary.ts — Phase v4.1.2 alive-core.
10
+ *
11
+ * `session_summary` — append a five-bullet summary of the current
12
+ * session to MEMORY.md under a `## Recent sessions` section.
13
+ *
14
+ * Why this exists: until v4.1.2, "what did we work on last session?"
15
+ * was unanswerable — MEMORY.md held durable facts but no rolling
16
+ * conversation log. After this tool runs at /quit (or on demand), the
17
+ * NEXT session's PromptBuilder injects MEMORY.md as a slot, and the
18
+ * model can read the summary back as ambient context.
19
+ *
20
+ * Design:
21
+ *
22
+ * - The model is responsible for generating the five bullets. It
23
+ * already has the full conversation context in its message
24
+ * history, so this tool's job is *persistence* — not LLM dispatch.
25
+ * This avoids threading the AuxiliaryClient into ToolContext just
26
+ * for one tool and keeps the verify-on-disk contract clean.
27
+ *
28
+ * - Section rotation: append the new entry at the top of the
29
+ * section (most-recent-first), keep the most recent 10, drop the
30
+ * rest. Bound the size so MEMORY.md doesn't grow indefinitely.
31
+ *
32
+ * - Write goes through `MemoryGuard.replaceSection`, which preserves
33
+ * the standard `verified: true` contract that
34
+ * HonestyEnforcement relies on.
35
+ */
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.sessionSummaryTool = void 0;
38
+ const RECENT_SESSIONS_HEADER = '## Recent sessions';
39
+ const MAX_RECENT_ENTRIES = 10;
40
+ /**
41
+ * Render one summary entry: timestamp header + bullets. Trims and
42
+ * normalises so two adjacent entries don't collide on whitespace.
43
+ */
44
+ function formatEntry(bullets, when) {
45
+ const stamp = when.toISOString().replace(/\.\d+Z$/, 'Z'); // second precision
46
+ const cleaned = bullets
47
+ .map((b) => b.trim())
48
+ .filter((b) => b.length > 0)
49
+ .map((b) => (b.startsWith('-') ? b : `- ${b}`));
50
+ return [`### ${stamp}`, ...cleaned].join('\n');
51
+ }
52
+ /**
53
+ * Split an existing Recent-sessions body into entries (each headed by
54
+ * a `### ` timestamp line). The most-recent entry is index 0.
55
+ */
56
+ function parseEntries(body) {
57
+ const trimmed = body.trim();
58
+ if (!trimmed)
59
+ return [];
60
+ // Split on `### ` at line start; first chunk is empty when the body
61
+ // starts with the marker, which we filter out.
62
+ const parts = trimmed
63
+ .split(/^### /m)
64
+ .map((p) => p.trim())
65
+ .filter((p) => p.length > 0)
66
+ .map((p) => `### ${p}`);
67
+ return parts;
68
+ }
69
+ exports.sessionSummaryTool = {
70
+ schema: {
71
+ name: 'session_summary',
72
+ description: 'Append a five-bullet summary of the current session to MEMORY.md ' +
73
+ '(under "## Recent sessions"). The next session will see it as ambient ' +
74
+ 'context. Call this at the end of a meaningful session, or right before ' +
75
+ 'the user types /quit. You craft the bullets — be concise and concrete: ' +
76
+ 'what we worked on, decisions made, files changed, problems solved, open items.',
77
+ inputSchema: {
78
+ type: 'object',
79
+ properties: {
80
+ bullets: {
81
+ type: 'array',
82
+ description: 'Exactly five concise bullets (3-15 words each) summarising the session. ' +
83
+ 'Focus on what will be useful for the next session.',
84
+ items: {
85
+ type: 'string',
86
+ description: 'One bullet. Plain prose; leading "- " optional (added if missing).',
87
+ },
88
+ },
89
+ trigger: {
90
+ type: 'string',
91
+ enum: ['manual', 'auto-quit'],
92
+ description: 'Diagnostic only — whether the model invoked this directly ' +
93
+ '("manual") or the REPL auto-triggered it on /quit ("auto-quit"). ' +
94
+ 'Defaults to "manual" when omitted.',
95
+ },
96
+ },
97
+ required: ['bullets'],
98
+ },
99
+ },
100
+ category: 'write',
101
+ mutates: true,
102
+ toolset: 'memory',
103
+ async execute(args, ctx) {
104
+ if (!ctx.memoryGuard) {
105
+ return { success: false, error: 'memory guard not configured' };
106
+ }
107
+ if (!ctx.memory) {
108
+ return { success: false, error: 'memory manager not configured' };
109
+ }
110
+ const rawBullets = Array.isArray(args.bullets) ? args.bullets : [];
111
+ const bullets = rawBullets
112
+ .filter((b) => typeof b === 'string')
113
+ .map((b) => b.trim())
114
+ .filter((b) => b.length > 0);
115
+ if (bullets.length === 0) {
116
+ return {
117
+ success: false,
118
+ error: 'session_summary requires at least one non-empty bullet',
119
+ };
120
+ }
121
+ const now = new Date();
122
+ const newEntry = formatEntry(bullets, now);
123
+ // Read current MEMORY.md to find existing Recent-sessions body.
124
+ const snap = await ctx.memory.loadSnapshot();
125
+ const memoryMd = snap.memoryMd ?? '';
126
+ // Pull existing section body (if any). The header consumes its
127
+ // line; the capture group grabs everything until the next h2 or
128
+ // EOF. Whitespace-only bodies are captured as empty after trim.
129
+ const headerEscaped = RECENT_SESSIONS_HEADER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
130
+ // Note: NO `m` flag — we want `$` to mean end-of-string, not
131
+ // end-of-line. With `m`, the lookahead `$` matches before every
132
+ // newline and we capture only the first body line instead of the
133
+ // whole section.
134
+ const sectionRe = new RegExp(`${headerEscaped}[^\\n]*\\n([\\s\\S]*?)(?=\\n## |$)`);
135
+ const match = memoryMd.match(sectionRe);
136
+ const existingBody = match ? (match[1] ?? '').trim() : '';
137
+ const existingEntries = parseEntries(existingBody);
138
+ // Most-recent-first ordering, capped to 10.
139
+ const combined = [newEntry, ...existingEntries].slice(0, MAX_RECENT_ENTRIES);
140
+ const newBody = combined.join('\n\n');
141
+ const result = await ctx.memoryGuard.replaceSection('memory', RECENT_SESSIONS_HEADER, newBody);
142
+ return {
143
+ success: result.ok,
144
+ verified: result.verified,
145
+ error: result.ok ? undefined : result.reason,
146
+ entries: combined.length,
147
+ trigger: args.trigger ?? 'manual',
148
+ timestamp: now.toISOString(),
149
+ };
150
+ },
151
+ };
@@ -0,0 +1,163 @@
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
+ * tools/v4/sessions/recallSession.ts — Phase v4.1.2-memory-C.
10
+ *
11
+ * `recall_session` — return ranked SessionDistillation summaries for
12
+ * past sessions matching the user's query (or just the most recent N
13
+ * when no query is supplied).
14
+ *
15
+ * Coexists with `session_search`:
16
+ * - session_search → FTS5 over message TEXT in SessionStore.
17
+ * Returns per-message snippets. Use when the user wants the exact
18
+ * words of a past message.
19
+ * - recall_session → ranked DISTILLATIONS by TOPIC. Returns
20
+ * structured per-session summaries (decisions, open items, files
21
+ * touched). Use when the user wants context on what HAPPENED in
22
+ * past sessions.
23
+ *
24
+ * Index strategy: scan-all. Reads every distillation JSON from
25
+ * `<paths.root>/distillations/` per query. Expected file count is
26
+ * <1000 per user; sub-100ms at that scale. When telemetry shows
27
+ * latency >500ms, the escalation path is direct migration to SQLite
28
+ * FTS5 — JSON-index intermediate is intentionally skipped.
29
+ */
30
+ var __importDefault = (this && this.__importDefault) || function (mod) {
31
+ return (mod && mod.__esModule) ? mod : { "default": mod };
32
+ };
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.__testFs = exports.recallSessionTool = void 0;
35
+ exports.getDistillationsDir = getDistillationsDir;
36
+ const node_path_1 = __importDefault(require("node:path"));
37
+ const node_fs_1 = require("node:fs");
38
+ const distillationStore_1 = require("../../../core/v4/distillationStore");
39
+ const distillationIndex_1 = require("../../../core/v4/distillationIndex");
40
+ const DEFAULT_LIMIT = 5;
41
+ const MAX_LIMIT = 25;
42
+ exports.recallSessionTool = {
43
+ schema: {
44
+ name: 'recall_session',
45
+ description: 'Recall past SESSIONS by topic. Returns ranked summaries — ' +
46
+ 'decisions made, files touched, open items, tool usage — from ' +
47
+ 'previously persisted session distillations. ' +
48
+ 'For the EXACT WORDS of a past message, call `session_search` ' +
49
+ 'instead (FTS5 over message text). For "what happened" / "what ' +
50
+ 'did we work on" / "what was unfinished", use this tool.',
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: {
54
+ query: {
55
+ type: 'string',
56
+ description: 'Optional keyword filter. Case-insensitive substring match ' +
57
+ 'across keywords, bullets, decisions, open_items, and ' +
58
+ 'tool names. Omit to get the most recent sessions.',
59
+ },
60
+ limit: {
61
+ type: 'number',
62
+ description: `Maximum number of matches to return. Default ${DEFAULT_LIMIT}, max ${MAX_LIMIT}.`,
63
+ },
64
+ days: {
65
+ type: 'number',
66
+ description: 'Optional recency window in days. Drops distillations older ' +
67
+ 'than this before ranking. Omit for no time filter.',
68
+ },
69
+ include_full: {
70
+ type: 'boolean',
71
+ description: 'When true, each match also carries tools_used + keywords ' +
72
+ '(useful when the agent needs granular tool history). ' +
73
+ 'Default false to keep responses compact.',
74
+ },
75
+ },
76
+ },
77
+ },
78
+ category: 'read',
79
+ mutates: false,
80
+ toolset: 'sessions',
81
+ async execute(args, ctx) {
82
+ if (!ctx.paths?.root) {
83
+ return {
84
+ success: false,
85
+ error: 'recall_session requires resolved aiden paths',
86
+ };
87
+ }
88
+ const dir = node_path_1.default.join(ctx.paths.root, 'distillations');
89
+ // Read everything off disk first. Each failure (malformed JSON,
90
+ // EACCES on individual files) is skipped silently; the diagnostic
91
+ // for "files exist but couldn't be read" is the gap between
92
+ // scanned (id count) and dists.length (parse-success count).
93
+ let ids;
94
+ try {
95
+ ids = await (0, distillationStore_1.listDistillationIds)(dir);
96
+ }
97
+ catch (err) {
98
+ const code = err.code;
99
+ if (code === 'ENOENT') {
100
+ // No distillations directory yet — first-run case is
101
+ // success with zero matches, NOT a failure.
102
+ return {
103
+ success: true,
104
+ query: typeof args.query === 'string' ? args.query : undefined,
105
+ matches: [],
106
+ total_found: 0,
107
+ scanned: 0,
108
+ };
109
+ }
110
+ return {
111
+ success: false,
112
+ error: `Failed to enumerate distillations: ${err.message}`,
113
+ };
114
+ }
115
+ const dists = [];
116
+ for (const id of ids) {
117
+ try {
118
+ const d = await (0, distillationStore_1.readDistillation)(dir, id);
119
+ if (d)
120
+ dists.push(d);
121
+ }
122
+ catch {
123
+ // One bad file shouldn't prevent the agent from seeing the
124
+ // rest. The user can diagnose via `aiden doctor` (the file
125
+ // is still on disk).
126
+ }
127
+ }
128
+ const recallQuery = {
129
+ query: typeof args.query === 'string' ? args.query : undefined,
130
+ limit: typeof args.limit === 'number' ? args.limit : undefined,
131
+ days: typeof args.days === 'number' ? args.days : undefined,
132
+ include_full: args.include_full === true,
133
+ };
134
+ const ranked = (0, distillationIndex_1.rankDistillations)(dists, recallQuery);
135
+ return {
136
+ success: true,
137
+ query: recallQuery.query,
138
+ matches: ranked.matches,
139
+ total_found: ranked.total_found,
140
+ // scanned reflects files we attempted to read — useful diagnostic.
141
+ // If scanned > ranked.total_found AND dists.length < scanned, the
142
+ // delta is malformed files; the agent can suggest running aiden
143
+ // doctor to inspect.
144
+ scanned: ids.length,
145
+ };
146
+ // Note re: subsystem health — wire-up happens at the runtime
147
+ // construction layer (cli/v4/aidenCLI.ts) where the registry is
148
+ // built. The tool itself stays pure of registry-knowledge for
149
+ // testability; the registry caller decides whether to wrap the
150
+ // file-read errors in a tracker.
151
+ },
152
+ };
153
+ // Expose the directory path for runtime wire-up. Tools that want to
154
+ // register recall_session with the slice3 SubsystemHealthRegistry
155
+ // pass this helper to the tracker so they get health snapshots without
156
+ // hard-coding the path in two places.
157
+ function getDistillationsDir(rootDir) {
158
+ return node_path_1.default.join(rootDir, 'distillations');
159
+ }
160
+ // Re-export read-only fs surface used by tests under controlled
161
+ // fixtures. Production code never imports from here; the tool calls
162
+ // fs directly.
163
+ exports.__testFs = node_fs_1.promises;
@@ -22,7 +22,11 @@ const MAX_LIMIT = 50;
22
22
  exports.sessionSearchTool = {
23
23
  schema: {
24
24
  name: 'session_search',
25
- description: 'Search past conversation sessions by keyword (FTS5 full-text). Returns matching message snippets with the session id and timestamp. Use to recall something the user said in an earlier conversation.',
25
+ description: 'Search past conversation MESSAGES by keyword (FTS5 full-text). ' +
26
+ 'Returns matching message SNIPPETS with the session id and timestamp. ' +
27
+ 'Use when you need the exact words a past message contained. ' +
28
+ 'For topic-level recall of what happened in past sessions (decisions, ' +
29
+ 'files touched, open items), call `recall_session` instead.',
26
30
  inputSchema: {
27
31
  type: 'object',
28
32
  properties: {
@@ -73,6 +73,30 @@ function makeSubagentFanoutTool(factory) {
73
73
  tasks: {
74
74
  type: 'array',
75
75
  description: 'Per-child task list (partition mode only). Length must equal n.',
76
+ // Schema mirrors PartitionTask interface in
77
+ // core/v4/subagent/fanout.ts:70-75. If you change one, change
78
+ // the other. OpenAI Codex backend strictly validates schemas
79
+ // and rejects `type: "array"` declarations missing `items`,
80
+ // so the inner shape must be explicit here.
81
+ items: {
82
+ type: 'object',
83
+ description: 'One unit of work for a partition-mode child.',
84
+ properties: {
85
+ goal: {
86
+ type: 'string',
87
+ description: 'The task this child should accomplish.',
88
+ },
89
+ context: {
90
+ type: 'string',
91
+ description: 'Optional shared context for the child.',
92
+ },
93
+ role: {
94
+ type: 'string',
95
+ description: 'Optional role tag, diagnostic only.',
96
+ },
97
+ },
98
+ required: ['goal'],
99
+ },
76
100
  },
77
101
  merge: {
78
102
  type: 'string',
@@ -0,0 +1,55 @@
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
+ * tools/v4/system/_psHelpers.ts — Phase v4.1.2-followup-3.
10
+ *
11
+ * Shared utilities for the computer-control tool family. Each tool
12
+ * (screenshot / os_process_list / media_key / volume_set / app_launch /
13
+ * app_close / clipboard_read / clipboard_write) gates on `win32` and
14
+ * shells out to PowerShell. The gate + exec boilerplate is identical
15
+ * across all eight tools — extracted here so the per-tool files stay
16
+ * focused on the one PowerShell snippet that matters.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.isWindows = exports.execAsync = void 0;
20
+ exports.windowsOnlyError = windowsOnlyError;
21
+ exports.runPowerShell = runPowerShell;
22
+ const node_child_process_1 = require("node:child_process");
23
+ const node_util_1 = require("node:util");
24
+ exports.execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
25
+ /**
26
+ * Standard "not supported on this platform" error payload. Surfaces a
27
+ * link the user can file an issue against rather than pretending the
28
+ * call quietly no-op'd.
29
+ */
30
+ function windowsOnlyError(toolName) {
31
+ return {
32
+ success: false,
33
+ error: `Tool '${toolName}' is Windows-only in v4.1.2. macOS/Linux ` +
34
+ `support tracked at github.com/taracodlabs/aiden — please file an ` +
35
+ `issue if needed. (Detected platform: ${process.platform})`,
36
+ };
37
+ }
38
+ /**
39
+ * Run a PowerShell snippet and return stdout. Defaults to a 15-second
40
+ * timeout — caller passes a different one when a slower operation
41
+ * (screenshot, app launch) is expected.
42
+ *
43
+ * Single source of truth for the `shell: 'powershell.exe'` invocation
44
+ * shape so future powershell-CLI / `pwsh` migration is one-line.
45
+ */
46
+ async function runPowerShell(script, options = {}) {
47
+ const opts = {
48
+ shell: 'powershell.exe',
49
+ timeout: options.timeoutMs ?? 15000,
50
+ maxBuffer: (options.maxBufferMb ?? 4) * 1024 * 1024,
51
+ };
52
+ return await (0, exports.execAsync)(script, opts);
53
+ }
54
+ const isWindows = () => process.platform === 'win32';
55
+ exports.isWindows = isWindows;