@matterailab/orbcode 0.3.3 → 0.4.0

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 CHANGED
@@ -185,10 +185,9 @@ sessions.
185
185
  | ------------------------- | ------- | ---------- | ---------------------- |
186
186
  | `axon-eido-3-code-pro` | 400k | 64k | $3/M in · $9/M out |
187
187
  | `axon-eido-3-code-mini` | 400k | 64k | $1.5/M in · $4.5/M out |
188
- | `axon-code-2-5-pro` | 400k | 64k | $2/M in · $6/M out |
189
- | `axon-code-2-5-mini` | 400k | 64k | free |
188
+ | `axon-eido-3-flash` | 200k | 64k | free |
190
189
 
191
- `axon-eido-3-code-mini` is the default. All four support native JSON tool calls
190
+ `axon-eido-3-code-mini` is the default. All three support native JSON tool calls
192
191
  and image input. Cost comes from the API's usage chunks (`is_byok`-aware) and is
193
192
  shown in the status bar.
194
193
 
@@ -298,6 +297,7 @@ MatterAI gateway untouched.
298
297
  | `/status` | version, model, account, gateway, context usage, cost, approval modes |
299
298
  | `/usage` | fetch plan usage |
300
299
  | `/init` | analyze the codebase and create/improve `AGENTS.md` in the repo's `.orb/` directory |
300
+ | `/task` | reference a previous session as context for the current task — opens a picker if no ID is given |
301
301
  | `/link` | link other repos on your machine so changes here are checked against them (enter a folder path) |
302
302
  | `/mcp` | manage MCP servers — enable, disable, reconnect, view status & tool counts |
303
303
  | `/login` | start the browser sign-in flow |
@@ -95,28 +95,17 @@ export const ANTHROPIC_MODELS = {
95
95
  * interactive picker for now.
96
96
  */
97
97
  export const BUILTIN_AXON_MODELS = {
98
- "axon-code-2-5-mini": {
99
- id: "axon-code-2-5-mini",
100
- name: "Axon Code 2.5 Mini (free)",
101
- description: "Axon Mini is an general purpose super intelligent LLM coding model for low-effort day-to-day tasks",
102
- contextWindow: 400000,
98
+ "axon-eido-3-flash": {
99
+ id: "axon-eido-3-flash",
100
+ name: "Axon Eido 3 Flash (free)",
101
+ description: "Axon Eido is a fast and low cost general purpose model for low-effort day-to-day tasks",
102
+ contextWindow: 200000,
103
103
  maxOutputTokens: 64000,
104
104
  supportsImages: true,
105
- inputPrice: 0.0000005,
106
- outputPrice: 0.0000015,
105
+ inputPrice: 0.0,
106
+ outputPrice: 0.0,
107
107
  free: true,
108
108
  },
109
- "axon-code-2-5-pro": {
110
- id: "axon-code-2-5-pro",
111
- name: "Axon Code 2.5 Pro",
112
- description: "Axon Code 2.5 Pro is the next-generation of Axon Code for coding tasks, currently in experimental stage.",
113
- contextWindow: 400000,
114
- maxOutputTokens: 64000,
115
- supportsImages: true,
116
- inputPrice: 0.000002,
117
- outputPrice: 0.000006,
118
- free: false,
119
- },
120
109
  "axon-eido-3-code-pro": {
121
110
  id: "axon-eido-3-code-pro",
122
111
  name: "Axon Eido 3 Pro",
@@ -14,6 +14,42 @@ import { loadSkills } from "../skills/loader.js";
14
14
  import { renderLinkedReposSection } from "../config/links.js";
15
15
  const MAX_STEPS_PER_TURN = 50;
16
16
  const RESULT_PREVIEW_LINES = 6;
17
+ /** How many times to automatically re-establish a model request that fails
18
+ * before producing any output (transient/connection errors). */
19
+ const MAX_STREAM_RETRIES = 3;
20
+ /** Transient failures worth auto-retrying: any transport/connection error (no
21
+ * usable HTTP status — socket reset, DNS, timeout, TLS drop) plus 5xx/408/429
22
+ * server responses. Real 4xx client errors (auth, bad request) are not retried. */
23
+ function isRetryableStreamError(error) {
24
+ const err = error;
25
+ const status = Number(err?.status ?? err?.code);
26
+ if (Number.isFinite(status) && status !== 0) {
27
+ return status >= 500 || status === 408 || status === 429;
28
+ }
29
+ return true;
30
+ }
31
+ function retryBackoffMs(attempt) {
32
+ return Math.min(500 * 2 ** attempt, 8000);
33
+ }
34
+ /** Sleep that settles early (rejecting with AbortError) if the signal fires, so
35
+ * a user interrupt isn't stuck waiting out a retry backoff. */
36
+ function interruptibleDelay(ms, signal) {
37
+ return new Promise((resolve, reject) => {
38
+ if (signal.aborted) {
39
+ reject(new DOMException("aborted", "AbortError"));
40
+ return;
41
+ }
42
+ const onAbort = () => {
43
+ clearTimeout(timer);
44
+ reject(new DOMException("aborted", "AbortError"));
45
+ };
46
+ const timer = setTimeout(() => {
47
+ signal.removeEventListener("abort", onAbort);
48
+ resolve();
49
+ }, ms);
50
+ signal.addEventListener("abort", onAbort, { once: true });
51
+ });
52
+ }
17
53
  function detectRepo(cwd) {
18
54
  try {
19
55
  const remote = execSync("git config --get remote.origin.url", {
@@ -422,7 +458,13 @@ User time zone: ${timeZone}, UTC${timeZoneOffsetStr}`;
422
458
  },
423
459
  ];
424
460
  let summary = "";
425
- for await (const chunk of this.client.createMessage(this.systemPrompt, request, [], signal)) {
461
+ for await (const chunk of this.streamWithRetry(() => this.client.createMessage(this.systemPrompt, request, [], signal), signal, () => {
462
+ // Compaction only streams text (committed once at the end), so a
463
+ // mid-stream retry just discards the partial summary.
464
+ summary = "";
465
+ onEvent({ type: "stream-reset" });
466
+ return true;
467
+ })) {
426
468
  if (signal.aborted)
427
469
  throw new DOMException("aborted", "AbortError");
428
470
  if (chunk.type === "text") {
@@ -468,28 +510,102 @@ User time zone: ${timeZone}, UTC${timeZoneOffsetStr}`;
468
510
  onEvent({ type: "turn-end" });
469
511
  }
470
512
  }
513
+ /**
514
+ * Consume a model stream, automatically re-establishing the request up to
515
+ * MAX_STREAM_RETRIES times on a transient/connection failure. A user abort is
516
+ * never retried.
517
+ *
518
+ * Before the first chunk of an attempt nothing has streamed, so the retry is
519
+ * always clean. Once chunks have streamed, retrying would duplicate on-screen
520
+ * output — so we only retry mid-stream when the caller supplies `onRestart` and
521
+ * it returns true, meaning it rolled the partial output back (cleared buffers,
522
+ * reset accumulators). If it can't (e.g. a row was already committed), the error
523
+ * propagates.
524
+ */
525
+ async *streamWithRetry(makeStream, signal, onRestart) {
526
+ for (let attempt = 0;; attempt++) {
527
+ let produced = false;
528
+ try {
529
+ for await (const chunk of makeStream()) {
530
+ produced = true;
531
+ yield chunk;
532
+ }
533
+ return;
534
+ }
535
+ catch (error) {
536
+ if (signal.aborted || error.name === "AbortError")
537
+ throw error;
538
+ if (attempt >= MAX_STREAM_RETRIES || !isRetryableStreamError(error))
539
+ throw error;
540
+ // Output already streamed this attempt: only retry if the caller can
541
+ // cleanly roll it back, otherwise a restart would duplicate it.
542
+ if (produced && !(onRestart?.() ?? false))
543
+ throw error;
544
+ const delayMs = retryBackoffMs(attempt);
545
+ this.options.callbacks.onEvent({
546
+ type: "system",
547
+ message: `Connection to the model failed (${error.message}). Retrying ${attempt + 1}/${MAX_STREAM_RETRIES} in ${Math.ceil(delayMs / 1000)}s…`,
548
+ isError: false,
549
+ });
550
+ await interruptibleDelay(delayMs, signal);
551
+ }
552
+ }
553
+ }
471
554
  /** Run one model request + tool execution round. Returns true when the turn is over. */
472
555
  async runStep() {
473
556
  const { onEvent } = this.options.callbacks;
474
557
  const signal = this.abortController.signal;
475
558
  let assistantText = "";
476
- let hadReasoning = false;
559
+ // A reasoning segment is "open" from its first delta until visible content
560
+ // (text or a tool call) begins. We emit reasoning-done at that transition so
561
+ // "Thought for Ns" reflects only the thinking time — not the answer that
562
+ // follows — and the live "Thinking" block stops before the answer streams.
563
+ // A fresh segment can re-open if the model interleaves reasoning with content.
564
+ let reasoningOpen = false;
477
565
  let reasoningStart = 0;
478
566
  let reasoningDetails;
567
+ // Once a reasoning-done row is committed to the transcript we can't roll it
568
+ // back, so a mid-stream retry after that point isn't clean.
569
+ let reasoningRowCommitted = false;
570
+ const finalizeReasoning = () => {
571
+ if (reasoningOpen) {
572
+ reasoningOpen = false;
573
+ reasoningRowCommitted = true;
574
+ onEvent({ type: "reasoning-done", durationMs: Date.now() - reasoningStart });
575
+ }
576
+ };
479
577
  const toolCallsByIndex = new Map();
480
578
  let nextSyntheticIndex = 10000;
481
- const stream = this.client.createMessage(this.systemPrompt, this.outgoingMessages(), getActiveTools(this.mcp), signal);
579
+ // Roll back this step's partial output so streamWithRetry can restart a
580
+ // dropped stream mid-flight. Tools only run after the stream completes, so
581
+ // nothing irreversible has happened yet; the one thing we can't undo is an
582
+ // already-committed reasoning row, so we decline the restart in that case.
583
+ const rollbackForRetry = () => {
584
+ if (reasoningRowCommitted)
585
+ return false;
586
+ assistantText = "";
587
+ reasoningOpen = false;
588
+ reasoningStart = 0;
589
+ reasoningDetails = undefined;
590
+ toolCallsByIndex.clear();
591
+ nextSyntheticIndex = 10000;
592
+ onEvent({ type: "stream-reset" });
593
+ return true;
594
+ };
595
+ const stream = this.streamWithRetry(() => this.client.createMessage(this.systemPrompt, this.outgoingMessages(), getActiveTools(this.mcp), signal), signal, rollbackForRetry);
482
596
  for await (const chunk of stream) {
483
597
  if (signal.aborted)
484
598
  throw new DOMException("aborted", "AbortError");
485
599
  switch (chunk.type) {
486
600
  case "text":
601
+ // Visible content begins — the reasoning phase (if any) is over.
602
+ finalizeReasoning();
487
603
  assistantText += chunk.text;
488
604
  onEvent({ type: "text-delta", text: chunk.text });
489
605
  break;
490
606
  case "reasoning":
491
- if (!hadReasoning) {
492
- hadReasoning = true;
607
+ if (!reasoningOpen) {
608
+ reasoningOpen = true;
493
609
  reasoningStart = Date.now();
494
610
  }
495
611
  onEvent({ type: "reasoning-delta", text: chunk.text });
@@ -499,6 +615,8 @@ User time zone: ${timeZone}, UTC${timeZoneOffsetStr}`;
499
615
  reasoningDetails = chunk.details;
500
616
  break;
501
617
  case "native_tool_calls":
618
+ // A tool call also ends the reasoning phase.
619
+ finalizeReasoning();
502
620
  for (const tc of chunk.toolCalls) {
503
621
  const index = tc.index ?? nextSyntheticIndex++;
504
622
  let pending = toolCallsByIndex.get(index);
@@ -527,9 +645,8 @@ User time zone: ${timeZone}, UTC${timeZoneOffsetStr}`;
527
645
  break;
528
646
  }
529
647
  }
530
- if (hadReasoning) {
531
- onEvent({ type: "reasoning-done", durationMs: Date.now() - reasoningStart });
532
- }
648
+ // A reasoning-only turn (no following text/tool content) still needs closing.
649
+ finalizeReasoning();
533
650
  if (assistantText) {
534
651
  onEvent({ type: "text-done" });
535
652
  }
@@ -32,13 +32,13 @@ You have tools at your disposal to solve the coding task. Follow these rules reg
32
32
 
33
33
  If you intend to call multiple tools and there are no dependencies between the tool calls, make all of the independent tool calls in parallel. Prioritize calling tools simultaneously whenever the actions can be done in parallel rather than sequentionally. For example, when reading 3 files, run 3 tool calls in parallel to read all 3 files into context at the same time. Maximize use of parallel tool calls where possible to increase speed and efficiency. However, if some tool calls depend on previous calls to inform dependent values like the parameters, do NOT call these tools in parallel and instead call them sequentially. Never use placeholders or guess missing parameters in tool calls.
34
34
 
35
- # Maximize Context Understanding
35
+ # Gather Enough Context, Then Act
36
36
 
37
- Be THOROUGH when gathering information. Make sure you have the FULL picture before replying. Use additional tool calls or clarifying questions as needed.
38
- TRACE every symbol back to its definitions and usages so you fully understand it.
39
- Look past the first seemingly relevant result. EXPLORE alternative implementations, edge cases, and varied search terms until you have COMPREHENSIVE coverage of the topic.
37
+ Speed matters: your goal is the correct change in the fewest tool calls, not exhaustive coverage. Scale exploration to the task. A small, localized change typically needs about 3-6 calls locate the code, read the region and its immediate callers, check conventions — while only wide refactors justify long exploration.
40
38
 
41
- If you've performed an edit that may partially fulfill the USER's query, but you're not confident, gather more information or use more tools before ending your turn.
39
+ You have enough context when you know exactly which files and lines to change, you have seen the surrounding code's conventions, and you know how the code you are touching is used. From that point, every further search or read is waste: stop exploring and make the edit. Before each additional call, ask whether its result could change your edit; if not, skip it. Trace only the symbols your change actually depends on, never re-read regions you have already seen, and never re-verify facts you have already established.
40
+
41
+ Never edit code you have not read. If after an edit you are genuinely unsure it fulfills the USER's request, verify that specific doubt with one targeted check — do not relaunch broad exploration.
42
42
 
43
43
  Bias towards not asking the user for help if you can find the answer yourself.
44
44
 
@@ -65,7 +65,7 @@ Your system prompt may include an "Available Skills" section listing skills by n
65
65
 
66
66
  Tools whose names start with \`mcp__\` are provided by external MCP servers the user has configured. They work exactly like native tools — call them with the standard tool call format when the task requires their capabilities. Their descriptions and parameter schemas come from the MCP servers.
67
67
 
68
- CRITICAL: For any task, small or big, you will always and always use the update_todo_list tool to create the TODO list, always keep is upto date with updates to the status and updating/editing the list as needed.`;
68
+ Use the update_todo_list tool to create and maintain a TODO list for any multi-step task (3 or more steps), keeping statuses up to date as you work. For trivial tasks that need only one or two steps, skip the todo list and just do the work.`;
69
69
  const toolGuide = `
70
70
  Common tool calls and explanations
71
71
 
@@ -117,20 +117,15 @@ Common tool calls and explanations
117
117
  }
118
118
  \`\`\`
119
119
 
120
- **Example** (editing across multiple files):
121
- \`\`\`json
122
- {
123
- "edits": [
124
- {"file_path": "/path/to/api.ts", "old_string": "v1", "new_string": "v2"},
125
- {"file_path": "/path/to/config.ts", "old_string": "version: 1", "new_string": "version: 2"}
126
- ]
127
- }
128
- \`\`\`
129
-
130
120
  **Guidance for choosing between file_edit and multi_file_edit**:
131
121
  - 1 edit → \`file_edit\`
132
122
  - 2+ edits → \`multi_file_edit\` (always)
133
123
 
124
+ **Editing discipline (CRITICAL)**:
125
+ - ALWAYS copy \`old_string\` verbatim from a read_file result obtained in the same turn. NEVER reconstruct indentation or whitespace from memory — this is especially important in tab-indented files, where a reconstructed \`old_string\` will silently mismatch.
126
+ - After any successful edit, treat all earlier reads of that file as stale. Re-read the region with read_file before editing the same area of the file again.
127
+ - If one edit in a \`multi_file_edit\` batch fails with a string mismatch, STOP and re-read the file before retrying that edit. Do not guess at a corrected \`old_string\` — guessed corrections compound the mismatch.
128
+
134
129
  ## read_file Tool Usage
135
130
 
136
131
  The \`read_file\` tool reads file contents with optional offset and limit. Use it to examine code before making changes or to discuss specific sections.
@@ -141,33 +136,9 @@ The \`read_file\` tool reads file contents with optional offset and limit. Use i
141
136
  - \`offset\` (optional): Starting line number (1-indexed). Defaults to 1.
142
137
  - \`limit\` (optional): Maximum number of lines to read. If not specified, reads the complete file. Default and maximum limit is 1000 lines.
143
138
 
144
- ### Parameters Schema
145
- \`\`\`typescript
146
- {
147
- file_path: string, // Absolute path to file (required)
148
- offset?: number, // Starting line (1-indexed), defaults to 1
149
- limit?: number // Max lines to read, omit to read entire file
150
- }
151
- \`\`\`
152
-
153
- ### Examples
154
-
155
- **Read entire file:**
156
- \`\`\`json
157
- {
158
- "file_path": "/Users/username/project/src/App.tsx"
159
- }
160
- \`\`\`
161
-
162
- **Read first 50 lines:**
163
- \`\`\`json
164
- {
165
- "file_path": "/Users/username/project/src/App.tsx",
166
- "limit": 50
167
- }
168
- \`\`\`
139
+ ### Example
169
140
 
170
- **Read lines 100-150 (50 lines starting at line 100):**
141
+ **Read lines 100-150:**
171
142
  \`\`\`json
172
143
  {
173
144
  "file_path": "/Users/username/project/src/App.tsx",
@@ -176,43 +147,17 @@ The \`read_file\` tool reads file contents with optional offset and limit. Use i
176
147
  }
177
148
  \`\`\`
178
149
 
179
- ### Workflow: When You Don't Know Line Numbers
180
-
181
- **Step 1:** Use \`search_files\` to find the code:
182
- \`\`\`json
183
- {
184
- "path": "src",
185
- "regex": "function handleSubmit",
186
- "file_pattern": "*.ts"
187
- }
188
- \`\`\`
189
-
190
- **Step 2:** Note the line number from search results (e.g., line 45)
191
-
192
- **Step 3:** Read that section with \`read_file\`:
193
- \`\`\`json
194
- {
195
- "file_path": "/Users/username/project/src/Form.tsx",
196
- "offset": 40,
197
- "limit": 50
198
- }
199
- \`\`\`
150
+ Parameter rules: \`file_path\` must be an absolute path; \`offset\` and \`limit\` must be >= 1 if specified; omit \`limit\` to read from \`offset\` to the end. Call the tool multiple times to read multiple files.
200
151
 
201
- ### Parameter Rules
152
+ CRITICAL: \`offset\` is what targets a region — \`limit\` alone reads the TOP of the file. To inspect line N (e.g. from search results), you MUST pass \`offset\` ≈ N-20 together with \`limit\`. Before sending the call, confirm \`offset\` is present whenever you are aiming at a specific line.
202
153
 
203
- 1. \`file_path\` must be an absolute path
204
- 2. \`offset\` must be >= 1 if specified
205
- 3. \`limit\` must be >= 1 if specified
206
- 4. If \`limit\` is omitted, the entire file is read from \`offset\`
154
+ When you don't know line numbers: use \`search_files\` to locate the code, note the line number from the results, then \`read_file\` that region with surrounding context.
207
155
 
208
- ### Common Patterns
156
+ ### Reading Strategy
209
157
 
210
- | Use Case | Parameters |
211
- |----------|-----------|
212
- | Read entire file | \`file_path\` only |
213
- | Read from start | \`limit: 50\` |
214
- | Read middle section | \`offset: 100, limit: 50\` |
215
- | Read from a specific line to end | \`offset: 200\` |
158
+ - When investigating a bug, read whole functions or logical regions in ONE call rather than small slivers. Prefer one 150-line read over five 30-line reads — fragmented reads lose context and waste calls.
159
+ - Budget your re-reads: if you have already read a region and have not edited it since, work from what you have instead of fetching it again. Re-read only when the file has changed or you genuinely lack the detail.
160
+ - After every read, verify the output matches the parameters you sent. If you meant to read around line N but the result starts at line 1, you omitted \`offset\` re-issue the call with \`offset\` set. NEVER re-read the top of the file expecting a different result.
216
161
 
217
162
 
218
163
  # execute_command
@@ -226,7 +171,9 @@ The tool accepts these parameters:
226
171
  - \`command\` (required): The CLI command to execute. Must be valid for the user's operating system.
227
172
  - \`cwd\` (optional): The working directory to execute the command in. If not provided, the current working directory is used. Ensure this is always an absolute path (starting with \`/\`, or a drive letter like \`C:\\\` on Windows). If you are running the command in the root directly, skip this parameter. The command executor is defaulted to run in the root directory. You already have the Current Workspace Directory in the Environment Details section.
228
173
 
229
- CRITICAL: If the command is a very long running process, prefer to let the user to that they can run it manually in thier terminal. If the user specifically requests to run a long running command, you may proceed.
174
+ CRITICAL: If the command is a very long running process, prefer to let the user know so they can run it manually in their terminal. If the user specifically requests to run a long running command, you may proceed.
175
+
176
+ Command validity rules: a command is never empty, never just \`:\`, never a bare single word with no arguments, and never contains tool-call markup tokens or angle-bracket tags of any kind. Commands must be valid for the user's operating system, shell, and current working directory.
230
177
 
231
178
  ## search_files
232
179
 
@@ -262,90 +209,29 @@ The \`search_files\` tool allows you to search for patterns across files in a di
262
209
  "file_pattern": null
263
210
  }
264
211
 
265
- // Search in JSX/TSX files only
266
- {
267
- "path": "src/components",
268
- "regex": "useState",
269
- "file_pattern": "*.{jsx,tsx}"
270
- }
271
-
272
- // Search in nested directories
273
- {
274
- "path": ".",
275
- "regex": "API_KEY",
276
- "file_pattern": "**/*.env*"
277
- }
278
- \`\`\`
279
-
280
- ### ❌ INCORRECT Examples
281
- \`\`\`json
282
- // WRONG - Unquoted file_pattern (will cause JSON error)
283
- {
284
- "path": "src",
285
- "regex": "import",
286
- "file_pattern": *.js
287
- }
288
-
289
- // WRONG - Missing file_pattern entirely
290
- {
291
- "path": "src",
292
- "regex": "import"
293
- }
294
-
295
- // WRONG - Empty string instead of null
296
- {
297
- "path": "src",
298
- "regex": "import",
299
- "file_pattern": ""
300
- }
301
212
  \`\`\`
302
213
 
303
- ### Regex Pattern Tips
304
-
305
- - Use Rust regex syntax (similar to PCRE)
306
- - Escape special characters: \`\\.\`, \`\\(\`, \`\\[\`, etc.
307
- - Common patterns:
308
- - \`"word"\` - literal match
309
- - \`"\\bword\\b"\` - word boundary match
310
- - \`"function\\s+\\w+"\` - function declarations
311
- - \`"import.*from\\s+['\\"].*['\\"]"\` - import statements
214
+ The regex uses Rust syntax (similar to PCRE); escape special characters like \`\\.\` and \`\\(\`. \`file_pattern\` uses glob syntax: \`"*.ts"\`, \`"*.{jsx,tsx}"\`, \`"**/*.json"\`. When in doubt, use \`null\` to search all files.
312
215
 
313
- ### File Pattern Glob Syntax
216
+ ### Search Hygiene
314
217
 
315
- When using a string value for \`file_pattern\`:
316
- - \`"*.js"\` - All .js files in directory
317
- - \`"*.{js,ts}"\` - All .js and .ts files
318
- - \`"**/*.json"\` - All .json files recursively
319
- - \`"test_*.py"\` - Files starting with test_
320
- - \`"src/**/*.tsx"\` - All .tsx files under src/
321
-
322
- **When in doubt, use \`null\` to search all files.**
323
-
324
- ### Parameter Validation Checklist
325
-
326
- Before submitting, verify:
327
- - ✅ \`path\` is a string (directory path)
328
- - ✅ \`regex\` is a string (valid Rust regex)
329
- - ✅ \`file_pattern\` is EITHER a quoted string OR null
330
- - ✅ All three parameters are present
331
- - ✅ No unquoted glob patterns like \`*.js\`
218
+ - Exclude test, spec, and mock paths from discovery searches by default (\`__tests__\`, \`*.spec.*\`, \`*.test.*\`, \`__mocks__\`) unless the task itself is about tests. They pollute results and bury the implementation you are looking for.
219
+ - Scope \`path\` to the narrowest plausible directory instead of searching from the repository root.
220
+ - If a search returns hundreds of hits, tighten the regex or \`file_pattern\` and search again. Do not scan through the dump.
332
221
 
333
222
  ### Remember
334
223
 
335
224
  **Always quote the file_pattern value or use null. Never use bare/unquoted glob patterns.**
336
225
 
337
- ## execute_command
226
+ ## Verifying tool results and avoiding loops
227
+
228
+ - After EVERY tool call, verify the output actually matches the parameters you sent (correct file, correct line range, correct directory). A result that does not reflect your parameters means the call was malformed — fix the call, do not reason from the bad output.
229
+ - If two consecutive identical tool calls produce identical results, you are in a loop. Change the call or change the strategy. NEVER repeat the same call a third time.
338
230
 
339
- CRITICAL:
340
- 1. A command never starts with \`:\`
341
- 2. A command never contains tool-call markup tokens or angle-bracket tags of any kind
342
- 3. A command is never empty or \`:\`
343
- 4. A command is never a single word or a single word with a space
344
- 5. Commands are always valid for the user's operating system
345
- 6. Commands are always valid for the user's shell
346
- 7. Commands are always valid with executable permissions
347
- 8. Commands are always valid with the user's current working directory
231
+ ## Plan before editing
348
232
 
233
+ - Investigate first, edit second. Once the root cause is confirmed, write out the full change plan — which files, the exact locations, and the edit order — BEFORE touching anything.
234
+ - Then execute the edits in one pass (batched via \`multi_file_edit\`) and verify with a single typecheck/build at the end, rather than alternating between editing and checking.
349
235
 
350
236
  ## update_todo_list
351
237
 
@@ -353,27 +239,14 @@ CRITICAL:
353
239
  Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks.
354
240
 
355
241
  **Checklist Format:**
356
- - Use a single-level markdown checklist (no nesting or subtasks).
357
- - List todos in the intended execution order.
358
- - Status options:
359
- - [ ] Task description (pending)
360
- - [x] Task description (completed)
361
- - [-] Task description (in progress)
362
-
363
- **Status Rules:**
364
- - [ ] = pending (not started)
365
- - [x] = completed (fully finished, no unresolved issues)
366
- - [-] = in_progress (currently being worked on)
242
+ - Use a single-level markdown checklist (no nesting or subtasks), in intended execution order.
243
+ - Statuses: \`[ ]\` pending, \`[x]\` completed (fully finished, no unresolved issues), \`[-]\` in progress.
367
244
 
368
245
  **Core Principles:**
369
- - Before updating, always confirm which todos have been completed since the last update.
370
- - You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress).
371
- - When a new actionable item is discovered during a long or complex task, add it to the todo list immediately.
372
- - Do not remove any unfinished todos unless explicitly instructed.
373
- - Always retain all unfinished tasks, updating their status as needed.
374
- - Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies).
375
- - If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved.
376
- - Remove tasks only if they are no longer relevant or if the user requests deletion.
246
+ - Update multiple statuses in a single call (e.g., mark the previous task completed and the next in progress).
247
+ - Add newly discovered actionable items immediately. Retain all unfinished tasks; remove one only if it is no longer relevant or the user asks.
248
+ - Mark a task completed only when fully accomplished. If blocked, keep it in_progress and add a todo describing what must be resolved.
249
+ - Keep the todo list AHEAD of the work, not behind it: it is a steering tool, not a changelog. Lay out upcoming steps before you start them instead of only recording steps after they are finished.
377
250
 
378
251
  IMPORTANT: Use attempt_completion tool when you have completed the task. This signals that you are done.
379
252
  `;
package/dist/ui/App.js CHANGED
@@ -40,6 +40,10 @@ const SLASH_COMMANDS = [
40
40
  description: "summarize the conversation to free up context",
41
41
  },
42
42
  { name: "/tasks", description: "show the current task list" },
43
+ {
44
+ name: "/task",
45
+ description: "reference a previous task in the current conversation",
46
+ },
43
47
  {
44
48
  name: "/status",
45
49
  description: "show session status (model, context, cost, account)",
@@ -178,6 +182,44 @@ Instructions:
178
182
  5. Finish with a short summary: all findings ordered by severity, and an overall verdict on whether the changes are safe to merge.
179
183
 
180
184
  This is a review only — do NOT modify any files. Present the findings to the user.${userInput ? `\n\nThe user provided the following input with the code-review command:\n${userInput}` : ""}`;
185
+ const MAX_TASK_CONVERSATION_CHARS = 8000;
186
+ function extractConversation(session) {
187
+ const lines = [];
188
+ for (const message of session.messages) {
189
+ if (message.role === "user" && typeof message.content === "string") {
190
+ const match = /<user_query>\n?([\s\S]*?)\n?<\/user_query>/.exec(message.content);
191
+ if (match)
192
+ lines.push(`User: ${match[1]}`);
193
+ }
194
+ else if (message.role === "assistant" &&
195
+ typeof message.content === "string" &&
196
+ message.content.trim()) {
197
+ lines.push(`Assistant: ${message.content}`);
198
+ }
199
+ }
200
+ return lines.join("\n\n");
201
+ }
202
+ function buildTaskReferencePrompt(session) {
203
+ const conversation = extractConversation(session);
204
+ const truncated = conversation.length > MAX_TASK_CONVERSATION_CHARS
205
+ ? conversation.slice(0, MAX_TASK_CONVERSATION_CHARS) +
206
+ "\n\n[... conversation truncated ...]"
207
+ : conversation;
208
+ return `The user has referenced a previous task. Here is the conversation from that task:
209
+
210
+ <previous_task title="${session.title || session.id}">
211
+ ${truncated}
212
+ </previous_task>
213
+
214
+ Please summarize this previous task and add it as a reference for the current task. The summary should capture:
215
+ - What the task was about
216
+ - What was accomplished
217
+ - Key decisions made
218
+ - Files that were created or modified
219
+ - Any remaining work
220
+
221
+ Present the summary in a clear, organized format. This summary will serve as context for the current task.`;
222
+ }
181
223
  let rowCounter = 0;
182
224
  function rowId() {
183
225
  return `row-${rowCounter++}`;
@@ -210,6 +252,7 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
210
252
  const [queuedMessages, setQueuedMessages] = useState([]);
211
253
  const [modelPickerOpen, setModelPickerOpen] = useState(false);
212
254
  const [resumableSessions, setResumableSessions] = useState(null);
255
+ const [taskPickerSessions, setTaskPickerSessions] = useState(null);
213
256
  const [linkManagerOpen, setLinkManagerOpen] = useState(false);
214
257
  const [links, setLinks] = useState([]);
215
258
  const [linkStatus, setLinkStatus] = useState("");
@@ -344,6 +387,17 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
344
387
  setStreamingText("");
345
388
  setBusyLabel("Working");
346
389
  break;
390
+ case "stream-reset":
391
+ // An auto-retry is re-streaming this step from scratch — drop the
392
+ // partial text/reasoning shown for the failed attempt so it doesn't
393
+ // duplicate. (Committed rows are untouched; the agent only resets when
394
+ // nothing has been committed yet.)
395
+ textBufferRef.current = "";
396
+ setStreamingText("");
397
+ reasoningBufferRef.current = "";
398
+ setStreamingReasoning("");
399
+ setBusyLabel("Working");
400
+ break;
347
401
  case "tool-start":
348
402
  setBusyLabel("Working");
349
403
  break;
@@ -485,6 +539,16 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
485
539
  text: `Resumed session: ${session.title || session.id}`,
486
540
  });
487
541
  }, [createAgent, pushRow, resetTranscript]);
542
+ const handleTaskSelect = useCallback((session) => {
543
+ setTaskPickerSessions(null);
544
+ pushRow({
545
+ kind: "user",
546
+ text: `/task (referencing: ${session.title || session.id})`,
547
+ });
548
+ setBusy(true);
549
+ setBusyLabel("Thinking");
550
+ void getAgent().runTurn(buildTaskReferencePrompt(session));
551
+ }, [getAgent, pushRow]);
488
552
  const switchModel = useCallback((modelId) => {
489
553
  const updated = { ...loadSettings(), model: modelId };
490
554
  setSettings(updated);
@@ -607,6 +671,22 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
607
671
  setResumableSessions(sessions);
608
672
  break;
609
673
  }
674
+ case "/task": {
675
+ if (!getAuthToken(settings)) {
676
+ setView("login");
677
+ break;
678
+ }
679
+ const taskSessions = listSessions(process.cwd()).filter((s) => s.id !== agentRef.current?.taskId);
680
+ if (taskSessions.length === 0) {
681
+ pushRow({
682
+ kind: "info",
683
+ text: "No previous tasks found for this directory.",
684
+ });
685
+ break;
686
+ }
687
+ setTaskPickerSessions(taskSessions);
688
+ break;
689
+ }
610
690
  case "/compact":
611
691
  if (!getAuthToken(settings)) {
612
692
  setView("login");
@@ -1021,6 +1101,7 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
1021
1101
  !mcpPickerOpen &&
1022
1102
  !mcpMigrationEntries &&
1023
1103
  !resumableSessions &&
1104
+ !taskPickerSessions &&
1024
1105
  !linkManagerOpen;
1025
1106
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: rows, children: (row) => _jsx(RowView, { row: row }, row.id) }, staticKey), view === "login" ? (_jsx(LoginSection, { onLogin: handleLogin })) : (_jsxs(Box, { flexDirection: "column", children: [updateInfo?.updateAvailable && updateInfo.latest && (_jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: COLORS.warning, paddingX: 2, alignSelf: "flex-start", children: [_jsxs(Text, { color: COLORS.warning, bold: true, children: ["\u2191 Update available: v", updateInfo.current, " \u2192 v", updateInfo.latest] }), _jsxs(Text, { children: ["Run ", _jsx(Text, { color: COLORS.accent, children: "orbcode update" }), " to install the latest version, then relaunch."] })] })), streamingReasoning && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: COLORS.thinking, italic: true, children: "\u2726 Thinking\u2026" }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, italic: true, children: tail(streamingReasoning, expandReasoningRef.current ? 30 : 3) }) })] })), streamingText && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: COLORS.primary, children: "\u25CF " }), streamingText] }) })), taskLines.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: "Tasks" }), taskLines.slice(0, 10).map((line, i) => (_jsx(Text, { dimColor: /^[-*]\s*\[x\]/i.test(line), children: line
1026
1107
  .replace(/^[-*]\s*\[x\]/i, " ■")
@@ -1028,7 +1109,7 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
1028
1109
  .replace(/^[-*]\s*\[ \]/, " □") }, i))), taskLines.length > 10 && (_jsxs(Text, { dimColor: true, children: [" \u2026 ", taskLines.length - 10, " more"] }))] })), modelPickerOpen && (_jsx(Box, { marginTop: 1, children: _jsx(ModelPicker, { currentId: settings.model, onSelect: (modelId) => {
1029
1110
  setModelPickerOpen(false);
1030
1111
  switchModel(modelId);
1031
- }, onCancel: () => setModelPickerOpen(false) }) })), resumableSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: resumableSessions, onSelect: handleResume, onCancel: () => setResumableSessions(null) }) })), linkManagerOpen && (_jsx(Box, { marginTop: 1, children: _jsx(LinkManager, { links: links, status: linkStatus, onAdd: (input) => {
1112
+ }, onCancel: () => setModelPickerOpen(false) }) })), resumableSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: resumableSessions, onSelect: handleResume, onCancel: () => setResumableSessions(null) }) })), taskPickerSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: taskPickerSessions, title: "Reference a previous task", onSelect: handleTaskSelect, onCancel: () => setTaskPickerSessions(null) }) })), linkManagerOpen && (_jsx(Box, { marginTop: 1, children: _jsx(LinkManager, { links: links, status: linkStatus, onAdd: (input) => {
1032
1113
  const result = addLink(process.cwd(), input);
1033
1114
  setLinkStatus(result.message);
1034
1115
  if (result.ok)
@@ -16,7 +16,7 @@ function relativeTime(iso) {
16
16
  const days = Math.round(hours / 24);
17
17
  return `${days}d ago`;
18
18
  }
19
- export function SessionPicker({ sessions, onSelect, onCancel }) {
19
+ export function SessionPicker({ sessions, onSelect, onCancel, title = "Resume a previous session" }) {
20
20
  const [selected, setSelected] = useState(0);
21
21
  useInput((input, key) => {
22
22
  if (key.upArrow) {
@@ -43,7 +43,7 @@ export function SessionPicker({ sessions, onSelect, onCancel }) {
43
43
  });
44
44
  const windowStart = Math.max(0, Math.min(selected - VISIBLE_ROWS + 1, sessions.length - VISIBLE_ROWS));
45
45
  const visible = sessions.slice(windowStart, windowStart + VISIBLE_ROWS);
46
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: COLORS.primary, paddingX: 1, children: [_jsx(Text, { bold: true, color: COLORS.primary, children: "Resume a previous session" }), windowStart > 0 && _jsxs(Text, { dimColor: true, children: [" \u2191 ", windowStart, " more"] }), visible.map((session, i) => {
46
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: COLORS.primary, paddingX: 1, children: [_jsx(Text, { bold: true, color: COLORS.primary, children: title }), windowStart > 0 && _jsxs(Text, { dimColor: true, children: [" \u2191 ", windowStart, " more"] }), visible.map((session, i) => {
47
47
  const index = windowStart + i;
48
48
  const isSelected = index === selected;
49
49
  const userTurns = session.messages.filter((m) => m.role === "user").length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matterailab/orbcode",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "OrbCode CLI — agentic coding in your terminal, powered by Axon models by MatterAI",
5
5
  "type": "module",
6
6
  "bin": {