@nghyane/arcane 0.1.16 → 0.1.17

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 (43) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +7 -15
  3. package/src/config/settings-schema.ts +19 -46
  4. package/src/config/settings.ts +0 -1
  5. package/src/exa/mcp-client.ts +57 -2
  6. package/src/internal-urls/docs-index.generated.ts +1 -2
  7. package/src/internal-urls/index.ts +2 -4
  8. package/src/internal-urls/router.ts +2 -2
  9. package/src/internal-urls/types.ts +2 -2
  10. package/src/mcp/oauth-flow.ts +1 -1
  11. package/src/modes/controllers/command-controller.ts +4 -44
  12. package/src/patch/hashline.ts +42 -0
  13. package/src/prompts/system/system-prompt.md +14 -10
  14. package/src/prompts/thread-extract.md +16 -0
  15. package/src/prompts/tools/render-mermaid.md +9 -0
  16. package/src/sdk.ts +1 -19
  17. package/src/session/agent-session.ts +4 -3
  18. package/src/session/retry-utils.ts +1 -1
  19. package/src/session/session-index.ts +329 -0
  20. package/src/slash-commands/builtin-registry.ts +0 -16
  21. package/src/task/index.ts +1 -1
  22. package/src/tools/ask.ts +9 -6
  23. package/src/tools/bash-skill-urls.ts +3 -3
  24. package/src/tools/create-tools.ts +26 -0
  25. package/src/tools/find-thread.ts +120 -0
  26. package/src/tools/index.ts +5 -0
  27. package/src/tools/read-thread.ts +409 -0
  28. package/src/tools/read.ts +2 -2
  29. package/src/tools/render-mermaid.ts +68 -0
  30. package/src/tools/save-memory.ts +182 -0
  31. package/src/web/search/index.ts +2 -0
  32. package/src/web/search/provider.ts +3 -0
  33. package/src/web/search/providers/anthropic.ts +1 -0
  34. package/src/web/search/providers/gemini.ts +122 -37
  35. package/src/web/search/providers/kagi.ts +163 -0
  36. package/src/web/search/types.ts +1 -0
  37. package/src/internal-urls/memory-protocol.ts +0 -133
  38. package/src/memories/index.ts +0 -1099
  39. package/src/memories/storage.ts +0 -563
  40. package/src/prompts/memories/consolidation.md +0 -30
  41. package/src/prompts/memories/read_path.md +0 -11
  42. package/src/prompts/memories/stage_one_input.md +0 -6
  43. package/src/prompts/memories/stage_one_system.md +0 -21
@@ -566,6 +566,37 @@ export function validateLineRef(ref: { line: number; hash: string }, fileLines:
566
566
  // Edit Application
567
567
  // ═══════════════════════════════════════════════════════════════════════════
568
568
 
569
+ /**
570
+ * Detect suspicious Unicode escape placeholders in edit lines.
571
+ * LLMs sometimes emit literal `\uDDDD` strings instead of actual Unicode characters.
572
+ * Returns a warning message if detected, undefined otherwise.
573
+ */
574
+ function detectUnicodeEscapePlaceholders(lines: string[]): string | undefined {
575
+ for (const line of lines) {
576
+ if (/\\u[0-9A-Fa-f]{4}/.test(line)) {
577
+ return "Warning: edit content contains literal Unicode escape sequences (\\uXXXX). These may be intended as actual Unicode characters.";
578
+ }
579
+ }
580
+ return undefined;
581
+ }
582
+
583
+ /**
584
+ * Auto-correct escaped tab indentation in edit lines.
585
+ * When enabled via ARCANE_HASHLINE_AUTOCORRECT_ESCAPED_TABS=1, replaces
586
+ * leading `\\t` sequences (literal backslash-t from JSON) with real tab characters.
587
+ */
588
+ function autocorrectEscapedTabs(lines: string[]): string[] {
589
+ if (Bun.env.ARCANE_HASHLINE_AUTOCORRECT_ESCAPED_TABS !== "1") {
590
+ return lines;
591
+ }
592
+ return lines.map(line => {
593
+ const match = line.match(/^((?:\\t)+)/);
594
+ if (!match) return line;
595
+ const tabCount = match[1].length / 2; // each \\t is 2 chars
596
+ return "\t".repeat(tabCount) + line.slice(match[1].length);
597
+ });
598
+ }
599
+
569
600
  /**
570
601
  * Apply an array of hashline edits to file content.
571
602
  *
@@ -599,6 +630,16 @@ export function applyHashlineEdits(
599
630
 
600
631
  const autocorrect = Bun.env.ARCANE_HL_AUTOCORRECT === "1";
601
632
 
633
+ // Collect warnings and auto-correct edit content
634
+ const warnings: string[] = [];
635
+ for (const edit of edits) {
636
+ const unicodeWarning = detectUnicodeEscapePlaceholders(edit.content);
637
+ if (unicodeWarning && !warnings.includes(unicodeWarning)) {
638
+ warnings.push(unicodeWarning);
639
+ }
640
+ edit.content = autocorrectEscapedTabs(edit.content);
641
+ }
642
+
602
643
  function collectExplicitlyTouchedLines(): Set<number> {
603
644
  const touched = new Set<number>();
604
645
  for (const edit of edits) {
@@ -914,6 +955,7 @@ export function applyHashlineEdits(
914
955
  return {
915
956
  content: finalContent,
916
957
  firstChangedLine,
958
+ ...(warnings.length > 0 ? { warnings } : {}),
917
959
  ...(noopEdits.length > 0 ? { noopEdits } : {}),
918
960
  };
919
961
 
@@ -162,6 +162,20 @@ Best practices:
162
162
  - Run multiple sub-agents concurrently if tasks are independent with disjoint write targets.
163
163
  {{/has}}
164
164
 
165
+ ### Cross-session Knowledge
166
+
167
+ Tools: `find_thread`, `read_thread`, `save_memory`
168
+ **Proactive search triggers** — use `find_thread` when:
169
+ - User mentions past work: "we did this before", "last time", "in a previous session"
170
+ - User asks "what did we do about X" or "how did we solve Y"
171
+ - Task seems related to work that may have been done before
172
+ - Handoff context references a parent thread and you need more detail
173
+ **Do NOT search when:**
174
+ - Question is about current session context
175
+ - Generic coding question with no project-specific history
176
+ - User explicitly provides all needed context
177
+ **save_memory**: only when user says "remember this" or states a clear preference. If unsure, ask.
178
+
165
179
  ### Verification
166
180
  After completing changes, verify using commands from AGENTS.md or the project's config. Format → typecheck/lint → test (if relevant) → build (if required).
167
181
  Report evidence concisely: counts, pass/fail, error summary.
@@ -252,16 +266,6 @@ Scan descriptions vs task domain — read skill if ≥50% likely relevant.
252
266
  </rules>
253
267
  {{/if}}
254
268
 
255
- {{#if memories.length}}
256
- <memories>
257
- {{#each memories}}
258
- <memory path="{{path}}">
259
- {{content}}
260
- </memory>
261
- {{/each}}
262
- </memories>
263
- {{/if}}
264
-
265
269
  {{#if preloadedSkills.length}}
266
270
  {{#each preloadedSkills}}
267
271
  <skill name="{{name}}">
@@ -0,0 +1,16 @@
1
+ You are helping extract relevant information from a conversation thread based on a goal.
2
+
3
+ ## Task
4
+
5
+ I am providing a conversation thread rendered as markdown, along with a goal describing what information to extract.
6
+
7
+ Your job is to:
8
+ 1. Analyze the thread content
9
+ 2. Identify information that is relevant to the goal
10
+ 3. Extract and preserve those relevant parts with full fidelity
11
+
12
+ ## Rules
13
+ - Be concise but complete — include all relevant details
14
+ - Preserve code snippets, file paths, commands, and decisions exactly as they appear
15
+ - Omit pleasantries, failed attempts, and thinking-out-loud unless the goal asks for them
16
+ - If nothing relevant is found, say so briefly
@@ -0,0 +1,9 @@
1
+ Convert Mermaid graph source into ASCII diagram output.
2
+
3
+ Parameters:
4
+ - `mermaid` (required): Mermaid graph text to render.
5
+ - `config` (optional): JSON render configuration (spacing and layout options).
6
+ Behavior:
7
+ - Returns ASCII diagram text.
8
+ - Saves full ASCII output to an artifact URL (`artifact://<id>`) when artifact storage is available.
9
+ - Returns an error when the Mermaid input is invalid or rendering fails.
package/src/sdk.ts CHANGED
@@ -42,13 +42,11 @@ import {
42
42
  ArtifactProtocolHandler,
43
43
  DocsProtocolHandler,
44
44
  InternalUrlRouter,
45
- MemoryProtocolHandler,
46
45
  RuleProtocolHandler,
47
46
  SkillProtocolHandler,
48
47
  } from "./internal-urls";
49
48
  import { disposeAllKernelSessions } from "./ipy/executor";
50
49
  import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } from "./mcp";
51
- import { buildMemoryToolDeveloperInstructions, getMemoryRoot, startMemoryStartupTask } from "./memories";
52
50
  import { collectEnvSecrets, loadSecrets, obfuscateMessages, SecretObfuscator } from "./secrets";
53
51
  import { AgentSession } from "./session/agent-session";
54
52
  import { AuthStorage } from "./session/auth-storage";
@@ -735,7 +733,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
735
733
  settings,
736
734
  };
737
735
 
738
- // Initialize internal URL router for internal protocols (agent://, artifact://, memory://, skill://, rule://)
736
+ // Initialize internal URL router for internal protocols (agent://, artifact://, skill://, rule://)
739
737
  const internalRouter = new InternalUrlRouter();
740
738
  const getArtifactsDir = () => {
741
739
  const sessionFile = sessionManager.getSessionFile();
@@ -743,11 +741,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
743
741
  };
744
742
  internalRouter.register(new AgentProtocolHandler({ getArtifactsDir }));
745
743
  internalRouter.register(new ArtifactProtocolHandler({ getArtifactsDir }));
746
- internalRouter.register(
747
- new MemoryProtocolHandler({
748
- getMemoryRoot: () => getMemoryRoot(agentDir, settings.getCwd()),
749
- }),
750
- );
751
744
  internalRouter.register(
752
745
  new SkillProtocolHandler({
753
746
  getSkills: () => skills,
@@ -1027,7 +1020,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1027
1020
 
1028
1021
  const rebuildSystemPrompt = async (toolNames: string[], tools: Map<string, AgentTool>): Promise<string> => {
1029
1022
  toolContextStore.setToolNames(toolNames);
1030
- const memoryInstructions = await buildMemoryToolDeveloperInstructions(agentDir, settings);
1031
1023
  const defaultPrompt = await buildSystemPromptInternal({
1032
1024
  cwd,
1033
1025
  skills,
@@ -1037,7 +1029,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1037
1029
  toolNames,
1038
1030
  rules: rulebookRules,
1039
1031
  skillsSettings: settings.getGroup("skills") as SkillsSettings,
1040
- appendSystemPrompt: memoryInstructions,
1041
1032
  });
1042
1033
 
1043
1034
  if (options.systemPrompt === undefined) {
@@ -1054,7 +1045,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1054
1045
  rules: rulebookRules,
1055
1046
  skillsSettings: settings.getGroup("skills") as SkillsSettings,
1056
1047
  customPrompt: options.systemPrompt,
1057
- appendSystemPrompt: memoryInstructions,
1058
1048
  });
1059
1049
  }
1060
1050
  return options.systemPrompt(defaultPrompt);
@@ -1258,14 +1248,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1258
1248
  }
1259
1249
  }
1260
1250
 
1261
- startMemoryStartupTask({
1262
- session,
1263
- settings,
1264
- modelRegistry,
1265
- agentDir,
1266
- isSubagent,
1267
- });
1268
-
1269
1251
  return {
1270
1252
  session,
1271
1253
  extensionsResult,
@@ -2067,9 +2067,10 @@ Be thorough - include exact file paths, function names, error messages, and tech
2067
2067
  return undefined;
2068
2068
  }
2069
2069
 
2070
- // Start a new session
2070
+ // Start a new session with parent reference
2071
+ const parentThreadId = this.sessionManager.getSessionId();
2071
2072
  await this.sessionManager.flush();
2072
- await this.sessionManager.newSession();
2073
+ await this.sessionManager.newSession({ parentSession: parentThreadId });
2073
2074
  this.agent.reset();
2074
2075
  this.agent.sessionId = this.sessionManager.getSessionId();
2075
2076
  this.#steeringMessages = [];
@@ -2078,7 +2079,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
2078
2079
  this.#todoReminderCount = 0;
2079
2080
 
2080
2081
  // Inject the handoff document as a custom message
2081
- const handoffContent = `<handoff-context>\n${handoffText}\n</handoff-context>\n\nThe above is a handoff document from a previous session. Use this context to continue the work seamlessly.`;
2082
+ const handoffContent = `<handoff-context thread="${parentThreadId}">\n${handoffText}\n</handoff-context>\n\nThe above is a handoff document from thread \`${parentThreadId}\`. Use this context to continue the work seamlessly. If you need additional details not covered above, use \`read_thread("${parentThreadId}", "your specific question")\` to query the original session.`;
2082
2083
  this.sessionManager.appendCustomMessageEntry("handoff", handoffContent, true);
2083
2084
 
2084
2085
  // Rebuild agent messages from session
@@ -17,7 +17,7 @@ export function isRetryableErrorMessage(errorMessage: string): boolean {
17
17
  * Check if an error message indicates a usage/billing limit (non-transient).
18
18
  */
19
19
  export function isUsageLimitErrorMessage(errorMessage: string): boolean {
20
- return /usage.?limit|usage_limit_reached|limit_reached/i.test(errorMessage);
20
+ return /usage.?limit|usage_limit_reached|limit_reached|quota.?exhaust/i.test(errorMessage);
21
21
  }
22
22
 
23
23
  /**
@@ -0,0 +1,329 @@
1
+ import { Database, type Statement } from "bun:sqlite";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { logger, parseJsonlLenient } from "@nghyane/arcane-utils";
5
+ import { getAgentDir } from "@nghyane/arcane-utils/dirs";
6
+
7
+ export interface SessionIndexEntry {
8
+ sessionId: string;
9
+ title: string;
10
+ firstMessage: string;
11
+ files: string;
12
+ cwd: string;
13
+ createdAt: number;
14
+ messageCount: number;
15
+ }
16
+
17
+ export interface SessionSearchResult {
18
+ threadId: string;
19
+ title: string;
20
+ date: string;
21
+ messageCount: number;
22
+ snippet: string;
23
+ }
24
+
25
+ interface SearchRow {
26
+ session_id: string;
27
+ title: string;
28
+ created_at: number;
29
+ message_count: number;
30
+ snippet: string;
31
+ }
32
+
33
+ const FILE_PATH_PARAMS = new Set(["path", "file", "filePath", "glob", "pattern", "command_working_directory"]);
34
+
35
+ export class SessionIndex {
36
+ #db: Database;
37
+ static #instance?: SessionIndex;
38
+
39
+ #upsertStmt: Statement;
40
+ #hasStmt: Statement;
41
+
42
+ private constructor(dbPath: string) {
43
+ const dir = path.dirname(dbPath);
44
+ fs.mkdirSync(dir, { recursive: true });
45
+
46
+ this.#db = new Database(dbPath);
47
+
48
+ this.#db.exec(`
49
+ PRAGMA journal_mode=WAL;
50
+ PRAGMA synchronous=NORMAL;
51
+ PRAGMA busy_timeout=5000;
52
+
53
+ CREATE TABLE IF NOT EXISTS session_index (
54
+ session_id TEXT PRIMARY KEY,
55
+ title TEXT,
56
+ first_message TEXT,
57
+ files TEXT,
58
+ cwd TEXT,
59
+ created_at INTEGER,
60
+ message_count INTEGER
61
+ );
62
+
63
+ CREATE VIRTUAL TABLE IF NOT EXISTS session_fts USING fts5(
64
+ title, first_message, files,
65
+ content='session_index', content_rowid='rowid'
66
+ );
67
+
68
+ CREATE TRIGGER IF NOT EXISTS session_index_ai AFTER INSERT ON session_index BEGIN
69
+ INSERT INTO session_fts(rowid, title, first_message, files)
70
+ VALUES (new.rowid, new.title, new.first_message, new.files);
71
+ END;
72
+
73
+ CREATE TRIGGER IF NOT EXISTS session_index_ad AFTER DELETE ON session_index BEGIN
74
+ INSERT INTO session_fts(session_fts, rowid, title, first_message, files)
75
+ VALUES ('delete', old.rowid, old.title, old.first_message, old.files);
76
+ END;
77
+
78
+ CREATE TRIGGER IF NOT EXISTS session_index_au AFTER UPDATE ON session_index BEGIN
79
+ INSERT INTO session_fts(session_fts, rowid, title, first_message, files)
80
+ VALUES ('delete', old.rowid, old.title, old.first_message, old.files);
81
+ INSERT INTO session_fts(rowid, title, first_message, files)
82
+ VALUES (new.rowid, new.title, new.first_message, new.files);
83
+ END;
84
+ `);
85
+
86
+ this.#upsertStmt = this.#db.prepare(
87
+ "INSERT OR REPLACE INTO session_index (session_id, title, first_message, files, cwd, created_at, message_count) VALUES (?, ?, ?, ?, ?, ?, ?)",
88
+ );
89
+ this.#hasStmt = this.#db.prepare("SELECT 1 FROM session_index WHERE session_id = ?");
90
+ }
91
+
92
+ static open(dbPath: string = path.join(getAgentDir(), "session-index.db")): SessionIndex {
93
+ if (!SessionIndex.#instance) {
94
+ SessionIndex.#instance = new SessionIndex(dbPath);
95
+ }
96
+ return SessionIndex.#instance;
97
+ }
98
+
99
+ upsert(entry: SessionIndexEntry): void {
100
+ try {
101
+ this.#upsertStmt.run(
102
+ entry.sessionId,
103
+ entry.title,
104
+ entry.firstMessage,
105
+ entry.files,
106
+ entry.cwd,
107
+ entry.createdAt,
108
+ entry.messageCount,
109
+ );
110
+ } catch (error) {
111
+ logger.error("SessionIndex upsert failed", { error: String(error) });
112
+ }
113
+ }
114
+
115
+ has(sessionId: string): boolean {
116
+ return this.#hasStmt.get(sessionId) != null;
117
+ }
118
+
119
+ search(query: string, limit: number): SessionSearchResult[] {
120
+ const safeLimit = Math.min(Math.max(1, Math.floor(limit)), 50);
121
+ const { ftsQuery, afterTs, beforeTs } = this.#parseQuery(query);
122
+ if (!ftsQuery) return [];
123
+
124
+ const conditions = ["session_fts MATCH ?"];
125
+ const params: (string | number)[] = [ftsQuery];
126
+
127
+ if (afterTs != null) {
128
+ conditions.push("si.created_at >= ?");
129
+ params.push(afterTs);
130
+ }
131
+ if (beforeTs != null) {
132
+ conditions.push("si.created_at <= ?");
133
+ params.push(beforeTs);
134
+ }
135
+ params.push(safeLimit);
136
+
137
+ const sql = `SELECT si.session_id, si.title, si.created_at, si.message_count,
138
+ snippet(session_fts, -1, '<match>', '</match>', '...', 32) as snippet
139
+ FROM session_fts f
140
+ JOIN session_index si ON si.rowid = f.rowid
141
+ WHERE ${conditions.join(" AND ")}
142
+ ORDER BY si.created_at DESC
143
+ LIMIT ?`;
144
+
145
+ try {
146
+ const rows = this.#db.prepare(sql).all(...params) as SearchRow[];
147
+ return rows.map(row => ({
148
+ threadId: row.session_id,
149
+ title: row.title || "Untitled",
150
+ date: new Date(row.created_at * 1000).toISOString().slice(0, 10),
151
+ messageCount: row.message_count,
152
+ snippet: row.snippet || "",
153
+ }));
154
+ } catch (error) {
155
+ logger.error("SessionIndex search failed", { error: String(error) });
156
+ return [];
157
+ }
158
+ }
159
+
160
+ async indexSessionFile(filePath: string): Promise<void> {
161
+ try {
162
+ const content = await Bun.file(filePath).text();
163
+ const entries = parseJsonlLenient<Record<string, unknown>>(content);
164
+ if (entries.length === 0) return;
165
+
166
+ const header = entries.find(e => e.type === "session") as
167
+ | { type: string; id?: string; title?: string; cwd?: string; timestamp?: string }
168
+ | undefined;
169
+ if (!header?.id) return;
170
+
171
+ let firstMessage = "";
172
+ let messageCount = 0;
173
+ const fileSet = new Set<string>();
174
+
175
+ for (const entry of entries) {
176
+ if (entry.type !== "message") continue;
177
+ const msg = entry.message as { role?: string; content?: unknown } | undefined;
178
+ if (!msg?.role) continue;
179
+
180
+ messageCount++;
181
+
182
+ if (msg.role === "user" && !firstMessage) {
183
+ firstMessage = extractTextContent(msg.content);
184
+ }
185
+
186
+ if (msg.role === "assistant") {
187
+ extractFilePaths(msg.content, fileSet);
188
+ }
189
+ }
190
+
191
+ const title = header.title || firstMessage.slice(0, 100) || "Untitled";
192
+ const createdAt = header.timestamp ? Math.floor(new Date(header.timestamp).getTime() / 1000) : 0;
193
+
194
+ this.upsert({
195
+ sessionId: header.id,
196
+ title,
197
+ firstMessage: firstMessage.slice(0, 500),
198
+ files: [...fileSet].join(" "),
199
+ cwd: header.cwd || "",
200
+ createdAt,
201
+ messageCount,
202
+ });
203
+ } catch (error) {
204
+ logger.warn("SessionIndex indexSessionFile failed", { path: filePath, error: String(error) });
205
+ }
206
+ }
207
+
208
+ async indexAllSessions(sessionsDir?: string): Promise<void> {
209
+ const dir = sessionsDir ?? path.join(getAgentDir(), "sessions");
210
+ let subdirs: string[];
211
+ try {
212
+ subdirs = fs.readdirSync(dir);
213
+ } catch {
214
+ return;
215
+ }
216
+
217
+ for (const subdir of subdirs) {
218
+ const subdirPath = path.join(dir, subdir);
219
+ let stat: fs.Stats;
220
+ try {
221
+ stat = fs.statSync(subdirPath);
222
+ } catch {
223
+ continue;
224
+ }
225
+ if (!stat.isDirectory()) continue;
226
+
227
+ let files: string[];
228
+ try {
229
+ files = fs.readdirSync(subdirPath).filter(f => f.endsWith(".jsonl"));
230
+ } catch {
231
+ continue;
232
+ }
233
+
234
+ for (const file of files) {
235
+ const filePath = path.join(subdirPath, file);
236
+ try {
237
+ const firstLine = await Bun.file(filePath).text();
238
+ const headerLine = firstLine.split("\n")[0];
239
+ if (!headerLine) continue;
240
+ const header = JSON.parse(headerLine) as { id?: string };
241
+ if (!header.id) continue;
242
+ if (this.has(header.id)) continue;
243
+ await this.indexSessionFile(filePath);
244
+ } catch {}
245
+ }
246
+ }
247
+ }
248
+
249
+ #parseQuery(query: string): { ftsQuery: string | null; afterTs: number | null; beforeTs: number | null } {
250
+ let afterTs: number | null = null;
251
+ let beforeTs: number | null = null;
252
+
253
+ const remaining = query.replace(/\b(after|before):(\S+)/g, (_, dir: string, val: string) => {
254
+ const ts = this.#parseDate(val);
255
+ if (ts != null) {
256
+ if (dir === "after") afterTs = ts;
257
+ else beforeTs = ts;
258
+ }
259
+ return "";
260
+ });
261
+
262
+ const ftsQuery = this.#buildFtsQuery(remaining);
263
+ return { ftsQuery, afterTs, beforeTs };
264
+ }
265
+
266
+ #parseDate(value: string): number | null {
267
+ const relMatch = value.match(/^(\d+)([dwm])$/);
268
+ if (relMatch) {
269
+ const n = Number.parseInt(relMatch[1], 10);
270
+ const unit = relMatch[2];
271
+ const now = Date.now();
272
+ let ms = 0;
273
+ if (unit === "d") ms = n * 86400_000;
274
+ else if (unit === "w") ms = n * 7 * 86400_000;
275
+ else if (unit === "m") ms = n * 30 * 86400_000;
276
+ return Math.floor((now - ms) / 1000);
277
+ }
278
+
279
+ const d = new Date(value);
280
+ if (!Number.isNaN(d.getTime())) {
281
+ return Math.floor(d.getTime() / 1000);
282
+ }
283
+ return null;
284
+ }
285
+
286
+ #buildFtsQuery(query: string): string | null {
287
+ const tokens = query
288
+ .trim()
289
+ .split(/\s+/)
290
+ .map(t => t.trim())
291
+ .filter(Boolean);
292
+
293
+ if (tokens.length === 0) return null;
294
+
295
+ return tokens
296
+ .map(token => {
297
+ const escaped = token.replace(/"/g, '""');
298
+ return `"${escaped}"*`;
299
+ })
300
+ .join(" ");
301
+ }
302
+ }
303
+
304
+ function extractTextContent(content: unknown): string {
305
+ if (typeof content === "string") return content;
306
+ if (Array.isArray(content)) {
307
+ for (const block of content) {
308
+ if (block && typeof block === "object" && "type" in block && block.type === "text" && "text" in block) {
309
+ return String(block.text);
310
+ }
311
+ }
312
+ }
313
+ return "";
314
+ }
315
+
316
+ function extractFilePaths(content: unknown, fileSet: Set<string>): void {
317
+ if (!Array.isArray(content)) return;
318
+ for (const block of content) {
319
+ if (!block || typeof block !== "object") continue;
320
+ if (!("type" in block) || block.type !== "toolCall") continue;
321
+ const args = "arguments" in block ? (block.arguments as Record<string, unknown>) : null;
322
+ if (!args) continue;
323
+ for (const [key, val] of Object.entries(args)) {
324
+ if (FILE_PATH_PARAMS.has(key) && typeof val === "string" && val.length > 0) {
325
+ fileSet.add(val);
326
+ }
327
+ }
328
+ }
329
+ }
@@ -349,22 +349,6 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
349
349
  runtime.ctx.editor.setText("");
350
350
  },
351
351
  },
352
- {
353
- name: "memory",
354
- description: "Inspect and operate memory maintenance",
355
- subcommands: [
356
- { name: "view", description: "Show current memory injection payload" },
357
- { name: "clear", description: "Clear persisted memory data and artifacts" },
358
- { name: "reset", description: "Alias for clear" },
359
- { name: "enqueue", description: "Enqueue memory consolidation maintenance" },
360
- { name: "rebuild", description: "Alias for enqueue" },
361
- ],
362
- allowArgs: true,
363
- handle: async (command, runtime) => {
364
- runtime.ctx.editor.setText("");
365
- await runtime.ctx.handleMemoryCommand(command.text);
366
- },
367
- },
368
352
  {
369
353
  name: "move",
370
354
  description: "Move session to a different working directory",
package/src/task/index.ts CHANGED
@@ -189,7 +189,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
189
189
  modelRegistry: this.session.subagentContext?.modelRegistry,
190
190
  settings: this.session.settings,
191
191
  mcpManager: this.session.subagentContext?.mcpManager,
192
- contextFiles: this.session.contextFiles,
192
+ contextFiles: this.session.contextFiles?.filter(f => !f.path.endsWith("AGENTS.md")),
193
193
  skills: this.session.skills,
194
194
  promptTemplates: this.session.promptTemplates,
195
195
  });
package/src/tools/ask.ts CHANGED
@@ -111,14 +111,15 @@ interface UIContext {
111
111
  select(
112
112
  prompt: string,
113
113
  options: string[],
114
- options_?: { initialIndex?: number; timeout?: number; outline?: boolean },
114
+ options_?: { initialIndex?: number; timeout?: number; outline?: boolean; signal?: AbortSignal },
115
115
  ): Promise<string | undefined>;
116
- input(prompt: string): Promise<string | undefined>;
116
+ input(prompt: string, placeholder?: string, options_?: { signal?: AbortSignal }): Promise<string | undefined>;
117
117
  }
118
118
 
119
119
  interface AskQuestionOptions {
120
120
  /** Timeout in milliseconds, null/undefined to disable */
121
121
  timeout?: number | null;
122
+ signal?: AbortSignal;
122
123
  }
123
124
 
124
125
  async function askSingleQuestion(
@@ -158,6 +159,7 @@ async function askSingleQuestion(
158
159
  initialIndex: cursorIndex,
159
160
  timeout: timeout ?? undefined,
160
161
  outline: true,
162
+ signal: options?.signal,
161
163
  });
162
164
  const elapsed = Date.now() - selectionStart;
163
165
  const timedOut = timeout != null && elapsed >= timeout;
@@ -166,7 +168,7 @@ async function askSingleQuestion(
166
168
 
167
169
  if (choice === OTHER_OPTION) {
168
170
  if (!timedOut) {
169
- const input = await ui.input("Enter your response:");
171
+ const input = await ui.input("Enter your response:", undefined, { signal: options?.signal });
170
172
  if (input) customInput = input;
171
173
  }
172
174
  break;
@@ -205,9 +207,10 @@ async function askSingleQuestion(
205
207
  timeout: timeout ?? undefined,
206
208
  initialIndex: recommended,
207
209
  outline: true,
210
+ signal: options?.signal,
208
211
  });
209
212
  if (choice === OTHER_OPTION) {
210
- const input = await ui.input("Enter your response:");
213
+ const input = await ui.input("Enter your response:", undefined, { signal: options?.signal });
211
214
  if (input) customInput = input;
212
215
  } else if (choice) {
213
216
  selectedOptions = [stripRecommendedSuffix(choice)];
@@ -300,7 +303,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails, Them
300
303
  optionLabels,
301
304
  q.multi ?? false,
302
305
  q.recommended,
303
- { timeout },
306
+ { timeout, signal: _signal },
304
307
  );
305
308
 
306
309
  const details: AskToolDetails = {
@@ -335,7 +338,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails, Them
335
338
  optionLabels,
336
339
  q.multi ?? false,
337
340
  q.recommended,
338
- { timeout },
341
+ { timeout, signal: _signal },
339
342
  );
340
343
 
341
344
  results.push({
@@ -9,9 +9,9 @@ const SKILL_URL_PATTERN = /'skill:\/\/[^'\s")`\\]+'|"skill:\/\/[^"\s')`\\]+"|ski
9
9
 
10
10
  /** Regex to find supported internal URL tokens in command text. */
11
11
  const INTERNAL_URL_PATTERN =
12
- /'(?:skill|agent|artifact|plan|memory|rule):\/\/[^'\s")`\\]+'|"(?:skill|agent|artifact|plan|memory|rule):\/\/[^"\s')`\\]+"|(?:skill|agent|artifact|plan|memory|rule):\/\/[^\s'")`\\]+/g;
12
+ /'(?:skill|agent|artifact|plan|rule):\/\/[^'\s")`\\]+'|"(?:skill|agent|artifact|plan|rule):\/\/[^"\s')`\\]+"|(?:skill|agent|artifact|plan|rule):\/\/[^\s'")`\\]+/g;
13
13
 
14
- const SUPPORTED_INTERNAL_SCHEMES = ["skill", "agent", "artifact", "plan", "memory", "rule"] as const;
14
+ const SUPPORTED_INTERNAL_SCHEMES = ["skill", "agent", "artifact", "plan", "rule"] as const;
15
15
 
16
16
  type SupportedInternalScheme = (typeof SUPPORTED_INTERNAL_SCHEMES)[number];
17
17
 
@@ -152,7 +152,7 @@ export function expandSkillUrls(command: string, skills: readonly Skill[]): stri
152
152
 
153
153
  /**
154
154
  * Expand supported internal URLs in a bash command string to shell-escaped absolute paths.
155
- * Supported schemes: skill://, agent://, artifact://, plan://, memory://, rule://
155
+ * Supported schemes: skill://, agent://, artifact://, plan://, rule://
156
156
  */
157
157
  export async function expandInternalUrls(command: string, options: InternalUrlExpansionOptions): Promise<string> {
158
158
  if (!command.includes("://")) return command;