tokenmaw 0.3.0 → 0.4.1

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
@@ -17,7 +17,10 @@ Inside the TUI:
17
17
  - `/model` selects the default model for the current session.
18
18
  - `/agents` shows the effective Agent Specs and their sources.
19
19
  - `/sessions` switches sessions; `/new` creates one.
20
+ - `/cd <path>` switches the working directory (relative paths resolve against the current one). The status bar always shows the current directory; `/cd` with no argument or `/pwd` prints it. Switching reloads the project's `.coder/agents` specs and `AGENTS.md`, and tool path authorization follows the new root.
21
+ - `!<command>` runs a shell command directly in the current workspace, outside the agent loop: the prompt turns into `$` and the command text is highlighted while typing, the command and its output stream inline into the conversation transcript (never sent to the model or fed back into context), long-running commands animate their ellipsis, and `Ctrl+C` stops a running command. `/`-commands stay reserved for the built-in slash commands.
20
22
  - `Ctrl+K` opens the command palette.
23
+ - `/worktree <name>` creates (or reopens) an isolated git worktree under `.coder/worktrees/<name>` on branch `maw/<name>` and moves this session into it, so parallel sessions stop colliding on one working tree. `/worktree-list` shows status, `/worktree-exit` returns to the main checkout (the worktree stays on disk), `/worktree-remove <name>` drops a clean worktree (the branch is never deleted). A `.worktreeinclude` file copies gitignored files (e.g. `.env`) into new worktrees. Non-interactive equivalent: `maw --worktree [name]`.
21
24
  - `Ctrl+B` toggles Agent Activity.
22
25
  - `Ctrl+J` or `Alt+Enter` inserts a newline; `Enter` sends. Chinese text wraps by terminal width.
23
26
  - `Up` / `Down` browse input history; `Ctrl+U` clears the draft.
@@ -28,7 +31,10 @@ Inside the TUI:
28
31
  - `F2` (or `/select`) optionally releases app mouse capture for the terminal's native selection. `F2` again restores app clicks, drag selection and wheel scrolling. `Ctrl+Y` and `PageUp` / `PageDown` also work without the mouse.
29
32
  - `Ctrl+X` or `/cancel` stops the current session's agents; send another message to continue.
30
33
  - `/compact` summarizes and archives older context of the main agent; an optional argument focuses the digest (e.g. `/compact file changes and pending work`).
31
- - `Ctrl+C` exits. Runtime errors appear in the conversation.
34
+ - `/btw <question>` opens a side conversation forked from the current session (full context included) and sends your question there. `/back` or `Ctrl+C` returns to the main conversation; side sessions are marked `[side]` in the status bar and the `/sessions` list.
35
+ - `/fork` copies the current conversation into a new saved session. `/sessions` lists both; the original stays untouched.
36
+ - `/goal <text>` sets a standing goal for the session: it is injected into every agent's prompt until cleared, shows in the status bar, and survives across sessions. `/goal clear` removes it.
37
+ - `Ctrl+C` copies a selection; without a selection it arms a quit confirmation — press again within 2 seconds to exit. Inside a side conversation, `Ctrl+C` returns to the parent session instead. Runtime errors appear in the conversation.
32
38
 
33
39
  For a non-interactive run:
34
40
 
@@ -49,7 +55,7 @@ Interactive mode requires a terminal. Non-interactive runs report agent failures
49
55
 
50
56
  ## Architecture
51
57
 
52
- The user talks to `main`, whose primary responsibility is responsive conversation and coordination. Execution tasks, including saving an HTML page, are delegated to coordinators by default; coordinators select specialists. Main yields after handing off work and is automatically resumed by agent results. Its broad tool access remains available for bounded checks and fallback, subject to the configured workspace policy. The scheduler reserves user-facing capacity independently of the background concurrency limit.
58
+ The user talks to `main`, whose primary responsibility is responsive conversation and forward progress. Main handles small, local, and well-scoped work directly. It delegates only genuinely multi-file, ambiguous, or independently parallel work; coordinators select specialists only when that extra coordination is useful. Main remains available while background work runs and is resumed by verified agent results. The scheduler reserves user-facing capacity independently of the background concurrency limit.
53
59
 
54
60
  ```text
55
61
  user ↔ main → coordinator(s) → explorer / implement / review / custom agents
@@ -102,6 +108,20 @@ Specs can reduce capabilities but cannot bypass global tool policy, path boundar
102
108
  - New user input interrupts only main's current generation. Background agents keep running until main explicitly redirects or cancels them.
103
109
  - Only main output enters the user-visible conversation.
104
110
  - Sessions and instances persist under `~/.coder/runtime/` and recover after restart.
111
+ - Each turn records requests, input/output tokens, provider-reported cached input, and first-token latency when the backend supplies them. The status bar shows the session aggregate.
112
+ - `AGENT_MAX_CHILDREN_PER_TURN` limits fan-out (default `3`); `AGENT_MAX_CONCURRENT_TURNS` and `AGENT_MAX_DEPTH` provide additional scheduler guardrails.
113
+
114
+ ### Multi-process safety
115
+
116
+ Multiple `maw` processes can run against the same project without silently destroying each other's work:
117
+
118
+ - **Cross-process write locks** — every file write (`edit_file`, `write_file`) and every session is guarded by an O_EXCL lock file under `~/.coder/runtime/locks/`. Lock records carry the holder pid and process start time, so a lock left by a crashed process is detected and taken over automatically, while a live holder is reported by pid (`AGENT_SESSION_LOCK_TIMEOUT_MS` adjusts the session-lock wait, default 5s).
119
+ - **Optimistic conflict detection** — `edit_file` re-verifies inside the lock that the file still matches what the edit was based on; `write_file` refuses to overwrite a file that changed after it was read in this session. Changes from external editors or other tooling surface as explicit errors instead of silent lost updates.
120
+ - **Single writer per session** — opening a session another process is writing succeeds in read-only mode (the status bar shows `[read-only pid <n>]`); submissions, `/goal`, and `/clear` are refused with guidance. `/fork` continues from that point in a writable copy, and once the other writer exits, the session self-heals back to writable on the next message.
121
+ - **Concurrent instance awareness** — live instances register under `~/.coder/runtime/instances/`; the status bar warns when other maw processes share the workspace.
122
+ - **Managed worktrees** — for genuine parallelism, `/worktree <name>` (or `maw --worktree [name]`) isolates each session in its own checkout so conflicts become ordinary merges. Worktrees are `git worktree lock`ed while a session uses them; cleanup removes clean, unlocked ones and never touches dirty checkouts or branches.
123
+
124
+ Provider prompt caching is used when the provider supports it: runtime system prefixes stay stable and live sibling state travels through mailboxes instead of being re-injected into every request. Cross-request response caching is intentionally avoided because coding answers depend on the current workspace and tool results.
105
125
 
106
126
  ### Context compaction
107
127
 
@@ -7,7 +7,6 @@ agents:
7
7
  - '*'
8
8
  ---
9
9
 
10
- You coordinate a complex workstream for the parent agent. Understand the goal, inspect the available agent catalog, and decide your own workflow. Delegate concrete work to the smallest useful set of specialist agents. Use parallel agents only when their work is independent. Send corrections to an existing instance instead of spawning duplicates.
11
-
12
- You do not talk to the user. Report meaningful progress, blockers, and the final integrated result to your parent. Verify claims using specialist results and do not report completion without evidence appropriate to the request. If user input is required, explain the exact decision and choices to the parent agent.
10
+ You coordinate a complex workstream for the parent agent. Understand the goal and choose the smallest workflow that can finish it. Start with one focused explorer or implementer; add another agent only when its work is independent and necessary. As soon as the root cause and edit location are clear, stop exploration and implement. Reuse existing instances instead of spawning duplicates.
13
11
 
12
+ You do not talk to the user. Report concise progress to your parent: conclusion, evidence, changed paths, and next action. Do not paste large tool outputs. Verify claims with focused checks and do not report completion without evidence. If user input is required, explain the exact decision and choices to the parent agent. Stop or cancel redundant agents once one result resolves the question.
package/agents/main.md CHANGED
@@ -10,9 +10,9 @@ agents:
10
10
 
11
11
  You are TokenMaw's user-facing main agent. You are the only agent that talks to the user.
12
12
 
13
- Your primary responsibility is responsiveness to the user, not doing all the work yourself. Answer conversational questions directly. For requests involving implementation, file creation, investigation, research, or verification, briefly acknowledge the concrete task and delegate execution to a coordinator by default, including small tasks such as saving an HTML page. Give it the user's objective, target paths, constraints, and acceptance checks. Do not perform a long sequence of execution tools before delegating.
13
+ Your primary responsibility is responsiveness and forward progress. Answer conversational questions directly. Handle small, local, or well-scoped fixes yourself with the available tools. Delegate only when the task is genuinely multi-file, ambiguous, or benefits from independent parallel work. Never create a coordinator merely to inspect one or two files. When delegating, give one concrete objective, target paths, constraints, and acceptance checks.
14
14
 
15
- Delegation is asynchronous: after assigning work, finish your current response so you remain available to the user. Child results automatically wake you through your mailbox; do not repeatedly call wait_agent or poll status. A handoff acknowledgement is not a completion claim. When the user sends a follow-up, respond promptly and forward relevant changes to the existing coordinator without restarting unrelated background work.
15
+ Delegation is asynchronous: after assigning work, remain available to the user. Child results automatically wake you through your mailbox; do not repeatedly call wait_agent or poll status. A handoff acknowledgement is not a completion claim. When evidence is sufficient, stop further exploration and implement or ask the existing worker to implement. Never restart an equivalent workstream for a follow-up.
16
16
 
17
17
  Your broad tool access is a fallback capability, not the default workflow. Use tools directly for a brief necessary clarification or evidence check, when the user explicitly requests your direct execution, or when delegation is unavailable or has failed. Keep such work bounded and explain a material fallback.
18
18
 
package/dist/backend.js CHANGED
@@ -89,7 +89,11 @@ async function* ollamaStream(config, systemPrompt, messages, tools, signal) {
89
89
  yield { content: null, toolCalls: obj.message.tool_calls, done: false };
90
90
  }
91
91
  if (obj.done) {
92
- yield { content: null, done: true };
92
+ const stats = obj;
93
+ yield { content: null, done: true, usage: (stats.prompt_eval_count !== undefined || stats.eval_count !== undefined) ? {
94
+ inputTokens: stats.prompt_eval_count,
95
+ outputTokens: stats.eval_count,
96
+ } : undefined };
93
97
  return;
94
98
  }
95
99
  }
@@ -200,6 +204,7 @@ async function* openaiStream(config, systemPrompt, messages, tools, signal) {
200
204
  model: config.model,
201
205
  stream: true,
202
206
  messages: convertToOpenAIMessages(systemPrompt, messages),
207
+ stream_options: { include_usage: true },
203
208
  };
204
209
  applyOpenAIRequestOptions(body, config);
205
210
  const openaiTools = convertToolsToOpenAI(tools);
@@ -245,6 +250,13 @@ async function* openaiStream(config, systemPrompt, messages, tools, signal) {
245
250
  continue;
246
251
  try {
247
252
  const parsed = JSON.parse(trimmed.slice(6));
253
+ if (parsed.usage)
254
+ yield { content: null, done: false, usage: {
255
+ inputTokens: parsed.usage.prompt_tokens,
256
+ outputTokens: parsed.usage.completion_tokens,
257
+ cachedInputTokens: parsed.usage.prompt_tokens_details?.cached_tokens,
258
+ reasoningTokens: parsed.usage.completion_tokens_details?.reasoning_tokens,
259
+ } };
248
260
  const choice = parsed.choices?.[0];
249
261
  if (!choice)
250
262
  continue;
@@ -495,6 +507,24 @@ async function* anthropicStream(config, systemPrompt, messages, tools, signal) {
495
507
  yield { content: null, toolCalls: [toolCall], done: false };
496
508
  continue;
497
509
  }
510
+ if (event === 'message_start' && (parsed.message?.usage?.input_tokens !== undefined)) {
511
+ const usage = parsed.message.usage;
512
+ yield { content: null, done: false, usage: {
513
+ inputTokens: usage.input_tokens,
514
+ cachedInputTokens: usage.cache_read_input_tokens,
515
+ cacheCreationInputTokens: usage.cache_creation_input_tokens,
516
+ } };
517
+ }
518
+ if (event === 'message_delta' && parsed.usage) {
519
+ const usage = parsed.usage;
520
+ if (usage.output_tokens !== undefined || usage.cache_read_input_tokens !== undefined || usage.cache_creation_input_tokens !== undefined) {
521
+ yield { content: null, done: false, usage: {
522
+ outputTokens: usage.output_tokens,
523
+ cachedInputTokens: usage.cache_read_input_tokens,
524
+ cacheCreationInputTokens: usage.cache_creation_input_tokens,
525
+ } };
526
+ }
527
+ }
498
528
  if (event === 'message_stop') {
499
529
  yield { content: null, done: true };
500
530
  return;
package/dist/cli.js CHANGED
@@ -6,13 +6,29 @@ import { resolveModelConfig } from './model-config.js';
6
6
  import { defaultPolicy } from './policy.js';
7
7
  import { AgentRegistry } from './runtime/agent-registry.js';
8
8
  import { AgentRuntime } from './runtime/agent-runtime.js';
9
+ import { WorktreeManager } from './runtime/worktree.js';
10
+ import { registerWorkspaceInstance } from './runtime/workspace-instances.js';
9
11
  import { runFullscreenTui } from './ui/fullscreen-tui.js';
12
+ import { checkForUpdate, formatUpdateNotice, offerSelfUpdate } from './update-check.js';
10
13
  import { CODER_VERSION } from './version.js';
11
14
  async function main() {
12
15
  const program = new Command();
13
16
  program.name('maw').description('Document-driven coding agent runtime').version(CODER_VERSION);
14
17
  program.allowExcessArguments(false).showSuggestionAfterError();
15
18
  program.option('--model <name>', 'default model name or .agentrc alias');
19
+ program.option('--worktree [name]', 'start inside an isolated managed git worktree (.coder/worktrees/<name>)');
20
+ // Query npm for a newer release while the command runs; afterwards a y/N
21
+ // prompt offers a self-update (China mirror first) in interactive sessions.
22
+ const updateNotice = checkForUpdate().catch(() => null);
23
+ const showUpdateNotice = async () => {
24
+ const result = await updateNotice;
25
+ if (!result?.updateAvailable || !process.stderr.isTTY)
26
+ return;
27
+ process.stderr.write(`\n${formatUpdateNotice(result)}\n`);
28
+ const message = await offerSelfUpdate(result).catch(() => null);
29
+ if (message)
30
+ process.stderr.write(`\n${message}\n`);
31
+ };
16
32
  let config = await loadConfig();
17
33
  const selectedFromCli = () => program.opts().model;
18
34
  setToolPolicy(defaultPolicy(config.policyLevel ?? 'moderate', process.cwd()));
@@ -23,6 +39,19 @@ async function main() {
23
39
  defaultModel: selectedFromCli() ?? config.model,
24
40
  resolveModel: (alias) => resolveModelConfig(config, alias).config,
25
41
  });
42
+ // Announce this instance so concurrent maw processes in the same workspace
43
+ // can surface a warning (and users can see who else is editing).
44
+ const stopInstanceHeartbeat = await registerWorkspaceInstance(process.cwd());
45
+ // /cd moves the runtime to a new workspace root; the tool policy must follow
46
+ // so read/write authorization keeps covering the new tree.
47
+ runtime.subscribe((event) => {
48
+ if (event.type !== 'workspace_changed')
49
+ return;
50
+ setToolPolicy(defaultPolicy(config.policyLevel ?? 'moderate', event.workspaceRoot));
51
+ void registerWorkspaceInstance(event.workspaceRoot).then((stop) => {
52
+ void stopInstanceHeartbeat().then(stop);
53
+ });
54
+ });
26
55
  const configManager = {
27
56
  getConfig: () => config,
28
57
  saveConfig: async (next) => {
@@ -54,6 +83,7 @@ async function main() {
54
83
  const response = session.messages.slice(submitted + 1).reverse().find((message) => message.role === 'assistant');
55
84
  if (response)
56
85
  process.stdout.write(`${response.content}\n`);
86
+ await showUpdateNotice();
57
87
  }
58
88
  finally {
59
89
  await runtime.shutdown();
@@ -67,12 +97,26 @@ async function main() {
67
97
  for (const spec of runtime.listAgentSpecs()) {
68
98
  process.stdout.write(`${spec.id}\t${spec.scope}\t${spec.model ?? 'inherit'}\t${spec.description}\n`);
69
99
  }
100
+ await showUpdateNotice();
70
101
  await runtime.shutdown();
71
102
  });
72
103
  program.action(async () => {
73
104
  if (!process.stdin.isTTY || !process.stdout.isTTY)
74
105
  throw new Error('Interactive mode requires a terminal. Use maw run --prompt "..." for non-interactive execution.');
75
106
  await runtime.whenReady();
107
+ const worktreeArg = program.opts().worktree;
108
+ if (worktreeArg !== undefined) {
109
+ const manager = new WorktreeManager(process.cwd());
110
+ if (!await manager.isGitRepository()) {
111
+ console.error('--worktree requires a git repository');
112
+ process.exitCode = 1;
113
+ return;
114
+ }
115
+ const name = typeof worktreeArg === 'string' && worktreeArg.trim() ? worktreeArg.trim() : `session-${Date.now()}`;
116
+ const info = await manager.create(name);
117
+ await runtime.changeWorkspace(info.path);
118
+ process.stdout.write(`worktree ready: ${info.path} (${info.branch})\n`);
119
+ }
76
120
  const requested = selectedFromCli();
77
121
  const selected = resolveModelConfig(config, requested);
78
122
  runtime.setDefaultModel(requested ?? config.model);
@@ -86,10 +130,12 @@ async function main() {
86
130
  },
87
131
  configManager,
88
132
  });
133
+ await showUpdateNotice();
89
134
  await runtime.shutdown();
90
135
  });
91
136
  const shutdown = async () => {
92
137
  await runtime.shutdown().catch(() => undefined);
138
+ await stopInstanceHeartbeat().catch(() => undefined);
93
139
  process.exit(0);
94
140
  };
95
141
  process.once('SIGTERM', () => { void shutdown(); });