claude-threads 1.20.0 → 1.21.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/CHANGELOG.md +31 -0
- package/README.md +21 -0
- package/dist/index.js +317 -32
- package/dist/mcp/mcp-server.js +285 -26
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.21.0] - 2026-08-07
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Task list display works again on modern Claude CLIs.** Claude Code moved task tracking from `TodoWrite` (whole list per call) to the incremental `TaskCreate`/`TaskUpdate` tools; verified against CLI 2.1.223, `TodoWrite` is never emitted anymore, so the bot's live task list in the thread had silently gone dark. A new per-session `TaskTracker` accumulates the incremental calls (a task's real id is only revealed by its tool result, which arrives later inside a `user` event) and feeds the existing task-list pipeline. `TaskUpdate` on an unknown id (tasks created before a resume, or by a subagent) shows a placeholder rather than nothing; `status: "deleted"` removes the task. `TodoWrite` still works for older CLIs. `TaskGet`/`TaskList` (read-only queries) are hidden from chat.
|
|
12
|
+
- **Tool completion indicators (`↳ ✓` / `↳ ❌ Error`) fire again.** The real CLI delivers tool results as `tool_result` blocks inside `user` events; the bot only handled a legacy top-level `tool_result` event shape that modern CLIs never emit — so per-tool completion/error indicators, elapsed times, and the tool-completion flush never triggered. The transformer now processes `user` events. Indicators are only emitted for tools that were actually displayed (hidden tools like `TaskCreate` no longer risk orphaned indicators), and elapsed time works on the real event flow (start times were previously only recorded on the legacy shape). Bug-report context tracking (tool uses/errors) was extended to the real event shapes; the legacy shapes remain handled for old captures and test fixtures.
|
|
13
|
+
- **Questions no longer trigger a duplicate permission prompt.** On modern CLIs `AskUserQuestion` routes through the `--permission-prompt-tool`, so next to the bot's proper question UI a generic "Permission requested: AskUserQuestion 👍✅👎" post appeared — and approving it just resolved the tool as unanswered. The MCP permission server now auto-allows `AskUserQuestion`; the question UI and reaction-answer flow are unchanged. Known limitation, verified on 2.1.223: with `--dangerously-skip-permissions` (bypass mode) the CLI does not expose `AskUserQuestion` at all, so bypass sessions cannot receive interactive questions — that is CLI behavior, not a bot regression. Also relevant: since CLI 2.1.200 an unanswered question no longer auto-continues after an idle timeout, which suits chat (users react late) — the previous auto-continue could race a slow reaction.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- **Structured rate-limit detection.** Modern CLIs emit a `rate_limit_event` with `rate_limit_info: {status, resetsAt, rateLimitType}` on every turn. The bot now feeds **`status: "rejected"`** events into the existing account-cooldown path (`resetsAt` epoch-seconds, sanity-bounded to at most 8 days out; slightly-past values from clock skew clamp to a brief cooldown), complementing the stderr phrase-scraping which remains for older CLIs. The predicate is strict equality on `rejected` — the SDK's status union is `allowed | allowed_warning | rejected`, and `allowed_warning` (~70%+ utilization, request went through) must not bench a healthy account: cooldowns only ever extend, so one warning would have parked the account until its weekly reset.
|
|
17
|
+
- **The MCP plan-approval prompt now shows the plan.** On modern CLIs `ExitPlanMode` routes through the permission prompt; it used to render as a bare "Permission requested: ExitPlanMode" with no plan content. The prompt now carries the plan text (truncated at 1500 code points, with an unclosed code fence re-balanced so the reaction legend stays outside it; untruncated plans render exactly as authored). Known remaining conflict, left as follow-up work: the MCP prompt and the bot's plan-approval UI are two competing approval surfaces — reacting on the bot's UI alone lets the MCP prompt time out and deny the plan.
|
|
18
|
+
|
|
19
|
+
### Hardening
|
|
20
|
+
Three adversarial review rounds ran over this change; everything they confirmed is fixed here, with red-green tests.
|
|
21
|
+
|
|
22
|
+
- **A resumed session can no longer lose its restored task list.** Task state is in-memory, so after a bot restart the resumed CLI session references task ids the fresh tracker has never seen. A single post-resume `TaskUpdate(completed)` created one completed placeholder, `allCompleted` fired on it, and the executor deleted the correctly-restored task-list post while reporting full completion. `allCompleted` now requires at least one task the tracker actually saw created, and a failed-create refresh on an empty tracker emits `update` (never `complete`) for the same reason. (Persisting tracker state across restarts, which would also restore full names instead of `Task #N` placeholders, is left as follow-up work.)
|
|
23
|
+
- **Fresh-CLI restarts clear accumulated task state — safely.** `!cd` and worktree switches respawn Claude as a fresh session whose task numbering restarts at #1; stale `TaskTracker` entries would collide with the new ids and corrupt updates. `MessageManager.clearClaudeSessionState()` now runs on every `resume: false` respawn (resume restarts like `!permissions` keep their state). The respawn sites await the old process before clearing, and `kill()` was reworked to resolve on stdio close (late-buffered stream-json can arrive after `exit`) with an escalation to SIGKILL and a bounded timeout — previously a failed spawn or a SIGTERM-immune process could leave `kill()` pending forever, freezing the session in `restarting`.
|
|
24
|
+
- **Failed `TaskCreate` calls no longer leave ghost tasks.** A create whose tool result is an error (or doesn't carry the expected "Task #N created" text — e.g. a future CLI rewording) removes the task and refreshes the display; previously it stayed forever as an un-updatable pending row that also pinned `allCompleted` at false. A non-error result with unexpected wording additionally logs a warning — that wording is the one CLI dependency whose drift would silently darken the task display again.
|
|
25
|
+
- **Sidechain events are filtered everywhere.** Events carrying `parent_tool_use_id` (subagent activity forwarded by some CLI versions) no longer reach the transformer — a subagent's `TaskCreate`/`TaskUpdate` calls would otherwise permanently pollute the main thread's task list with colliding ids — and the event handlers apply the same filter: subagent text no longer executes `!cd`-style Claude commands against the main session, and subagent tool uses/errors no longer pollute bug-report context. PR-URL detection intentionally still sees subagent text.
|
|
26
|
+
- **Task-list updates are coalesced per event.** A parallel burst of N `TaskCreate` calls in one assistant event — or N of their results failing in one `user` event — now yields one task-post update (the final snapshot) instead of N back-to-back `updatePost` calls that could trip platform rate limits.
|
|
27
|
+
- **Rate-limit cooldown precision.** A reset-less hit (the 1-hour default guess) is ignored while a cooldown from an explicit reset time is running, so the guess can't stretch a known-shorter deadline — but reset-less repeats during a guess-based cooldown still extend it (the documented behavior), and an explicit reset that arrives too late to re-emit still records its explicitness so later guesses defer to it regardless of arrival order.
|
|
28
|
+
- **Placeholder merge on late id resolution.** If a `TaskUpdate` for id N arrives before the corresponding create's result resolves that id, the placeholder and the real task are merged into one row (adopting the update's status and any rename) and the display refreshes immediately.
|
|
29
|
+
- **Tracker robustness.** Numeric `taskId` values are coerced instead of silently freezing the displayed list; multi-MB tool results are no longer copied when no `TaskCreate` is pending; and a session mixing both task dialects no longer flip-flops — the full-list `TodoWrite` clears the incremental tracker.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- **Verified against Claude CLI 2.1.223** (previously 2.1.116); suggested install version in error messages and onboarding updated. The compatible range is unchanged (`>=2.0.74 <2.2.0`).
|
|
33
|
+
|
|
34
|
+
## [1.20.1] - 2026-08-06
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- **"How it compares" section on the website and README** - A short, respectful comparison with the other ways to drive a coding agent from chat: Anthropic's own Claude Code in Slack / Claude Tag, OpenAI's Codex in Slack, Cursor in Slack, Cognition's Devin, and community Slack bots. The framing is a single axis — where the session runs (vendor cloud vs. your machine) — with claude-threads positioned as the local, self-hosted option that also speaks Mattermost. A closing note names the strongest differentiators beyond that: in-thread permission approvals, multiplayer sessions, the multi-account pool, and no GitHub assumption. The website's "Why I built this" note about Anthropic's integration was updated to match (it is no longer Enterprise-gated: Pro/Max plans have Claude Code in Slack, Team/Enterprise have Claude Tag).
|
|
38
|
+
|
|
8
39
|
## [1.20.0] - 2026-08-06
|
|
9
40
|
|
|
10
41
|
### Added
|
package/README.md
CHANGED
|
@@ -170,6 +170,27 @@ Restrict who can use the bot during setup, or reconfigure later with `claude-thr
|
|
|
170
170
|
|
|
171
171
|
Who may do what (start sessions, react to permission prompts, approve guest messages) is written down in the [authorization model](https://github.com/anneschuth/claude-threads/blob/main/SECURITY.md).
|
|
172
172
|
|
|
173
|
+
## How it compares
|
|
174
|
+
|
|
175
|
+
Driving a coding agent from chat is a busy space: Anthropic, OpenAI, Cursor, and Cognition each ship their own integration, and other open-source projects exist too — several of them excellent. The main difference is where the session actually runs.
|
|
176
|
+
|
|
177
|
+
| Product | Sessions run on | Chat platforms | Account |
|
|
178
|
+
| :----------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------- | :---------------------------------------------- | :-------------------------------------------- |
|
|
179
|
+
| **claude-threads** (open source, Apache-2.0) | Your machine — local checkout, your MCP servers and plugins | Slack + Mattermost, multiple workspaces at once | Any Claude subscription or API key |
|
|
180
|
+
| [Claude Code in Slack](https://code.claude.com/docs/en/slack) / [Claude Tag](https://claude.com/docs/claude-tag/overview) (Anthropic) | Anthropic's cloud, against GitHub repos | Slack | Claude Pro/Max; Claude Tag on Team/Enterprise |
|
|
181
|
+
| [Codex in Slack](https://slack.com/marketplace/A09F5C369E3-openai-codex) (OpenAI) | OpenAI's Codex cloud | Slack | ChatGPT plan |
|
|
182
|
+
| [Cursor in Slack](https://cursor.com/docs/integrations/slack) | Cursor's cloud agents | Slack | Cursor plan |
|
|
183
|
+
| [Devin](https://devin.ai/) (Cognition) | Cognition's cloud | Slack (also Linear and Jira tickets) | Devin plan |
|
|
184
|
+
| Community bots such as [claude-code-slack-bot](https://github.com/mpociot/claude-code-slack-bot) | Your machine | Slack | Claude subscription or API key |
|
|
185
|
+
|
|
186
|
+
The cloud products share a working style: you delegate from a thread, the work runs in a vendor-hosted sandbox, and progress updates and a pull request come back — Claude Tag additionally stays in the conversation as a shared teammate the whole channel can talk to. If your repositories live on GitHub and you want nothing to host, they are a good choice; Anthropic's own integration in particular runs the same Claude Code this bot does.
|
|
187
|
+
|
|
188
|
+
claude-threads makes the opposite choice: the whole session stays in the thread and on your hardware. Output streams live, permission prompts land as emoji reactions, invited teammates type into the same session, and Claude works against a local checkout with whatever MCP servers and plugins you already configured. That fit matters when chat is self-hosted Mattermost, when code cannot leave the building, or when you want a session in chat to behave exactly like the one at your desk. The trade-off: you run the bot yourself, on a machine that has the repository.
|
|
189
|
+
|
|
190
|
+
A few more things set it apart. The full Claude Code permission model lives in the thread: every tool use can prompt for 👍/✅/👎 approval, with three modes switchable mid-session. Sessions are multiplayer: `!invite` teammates while Claude works, messages from uninvited users are gated behind approval, and commits credit everyone involved as co-author. An optional [account pool](https://github.com/anneschuth/claude-threads/blob/main/docs/CONFIGURATION.md#claude-accounts-optional-multi-account-mode) routes each new session to the Claude subscription with the most headroom and cools down rate-limited ones. And nothing assumes GitHub — the session works against any local checkout, whatever its host.
|
|
191
|
+
|
|
192
|
+
This table is current as of August 2026. All of these products move quickly — if it has gone stale, [open an issue](https://github.com/anneschuth/claude-threads/issues).
|
|
193
|
+
|
|
173
194
|
## Documentation
|
|
174
195
|
|
|
175
196
|
- **[Setup Guide](https://github.com/anneschuth/claude-threads/blob/main/SETUP_GUIDE.md)** - Creating the bot account on Mattermost or Slack, step by step
|
package/dist/index.js
CHANGED
|
@@ -51448,7 +51448,7 @@ function validateClaudeCli() {
|
|
|
51448
51448
|
version: result.version,
|
|
51449
51449
|
compatible: false,
|
|
51450
51450
|
message: `Claude CLI version ${result.version} is not compatible. Required: ${CLAUDE_CLI_VERSION_RANGE}
|
|
51451
|
-
` + `Install a compatible version: npm install -g @anthropic-ai/claude-code@2.1.
|
|
51451
|
+
` + `Install a compatible version: npm install -g @anthropic-ai/claude-code@2.1.223`,
|
|
51452
51452
|
rawOutput: result.rawOutput ?? undefined
|
|
51453
51453
|
};
|
|
51454
51454
|
}
|
|
@@ -51698,7 +51698,7 @@ async function runOnboarding(reconfigure = false) {
|
|
|
51698
51698
|
console.log(dim(` ⚠️ Claude Code CLI ${claudeCheck.version} is not compatible`));
|
|
51699
51699
|
console.log("");
|
|
51700
51700
|
console.log(dim(` Install a compatible version:`));
|
|
51701
|
-
console.log(dim(" npm install -g @anthropic-ai/claude-code@2.1.
|
|
51701
|
+
console.log(dim(" npm install -g @anthropic-ai/claude-code@2.1.223"));
|
|
51702
51702
|
} else {
|
|
51703
51703
|
console.log(dim(" ⚠️ Claude Code CLI found but version could not be determined"));
|
|
51704
51704
|
if (claudeCheck.rawOutput) {
|
|
@@ -55918,6 +55918,25 @@ function extractResetAt(text, now) {
|
|
|
55918
55918
|
}
|
|
55919
55919
|
return;
|
|
55920
55920
|
}
|
|
55921
|
+
function parseRateLimitEvent(event, now = Date.now()) {
|
|
55922
|
+
const info = event?.rate_limit_info;
|
|
55923
|
+
if (!info || typeof info !== "object")
|
|
55924
|
+
return { detected: false };
|
|
55925
|
+
const { status, resetsAt } = info;
|
|
55926
|
+
if (status !== "rejected")
|
|
55927
|
+
return { detected: false };
|
|
55928
|
+
const PAST_SKEW_TOLERANCE_MS = 2 * 60000;
|
|
55929
|
+
let resetAtEpochMs;
|
|
55930
|
+
if (typeof resetsAt === "number") {
|
|
55931
|
+
const ms = resetsAt * 1000;
|
|
55932
|
+
if (ms > now && ms - now < 8 * 86400000) {
|
|
55933
|
+
resetAtEpochMs = ms;
|
|
55934
|
+
} else if (ms <= now && now - ms < PAST_SKEW_TOLERANCE_MS) {
|
|
55935
|
+
resetAtEpochMs = now + 60000;
|
|
55936
|
+
}
|
|
55937
|
+
}
|
|
55938
|
+
return { detected: true, matched: `rate_limit_event status=${status}`, resetAtEpochMs };
|
|
55939
|
+
}
|
|
55921
55940
|
|
|
55922
55941
|
// src/claude/cli.ts
|
|
55923
55942
|
var log8 = createLogger("claude");
|
|
@@ -56054,6 +56073,7 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
56054
56073
|
stderrBuffer = "";
|
|
56055
56074
|
mcpConfigTempFile = null;
|
|
56056
56075
|
lastEmittedRateLimitDeadline = 0;
|
|
56076
|
+
lastEmittedHitHadExplicitReset = false;
|
|
56057
56077
|
log;
|
|
56058
56078
|
constructor(options) {
|
|
56059
56079
|
super();
|
|
@@ -56107,6 +56127,7 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
56107
56127
|
totalStderrBytes -= this.stderrBuffer.length;
|
|
56108
56128
|
this.stderrBuffer = "";
|
|
56109
56129
|
this.lastEmittedRateLimitDeadline = 0;
|
|
56130
|
+
this.lastEmittedHitHadExplicitReset = false;
|
|
56110
56131
|
cleanupBrowserBridgeSockets();
|
|
56111
56132
|
const claudePath = getClaudePath();
|
|
56112
56133
|
const args = [
|
|
@@ -56256,18 +56277,31 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
56256
56277
|
if (event.type === "result" && isErrorResultEvent(event)) {
|
|
56257
56278
|
this.maybeEmitRateLimit(trimmed);
|
|
56258
56279
|
}
|
|
56280
|
+
if (event.type === "rate_limit_event") {
|
|
56281
|
+
this.maybeEmitRateLimitHit(parseRateLimitEvent(event));
|
|
56282
|
+
}
|
|
56259
56283
|
} catch {}
|
|
56260
56284
|
}
|
|
56261
56285
|
}
|
|
56262
56286
|
maybeEmitRateLimit(text) {
|
|
56263
|
-
|
|
56287
|
+
this.maybeEmitRateLimitHit(detectRateLimit(text));
|
|
56288
|
+
}
|
|
56289
|
+
maybeEmitRateLimitHit(hit) {
|
|
56264
56290
|
if (!hit.detected)
|
|
56265
56291
|
return;
|
|
56292
|
+
if (!hit.resetAtEpochMs && this.lastEmittedHitHadExplicitReset && this.lastEmittedRateLimitDeadline > Date.now()) {
|
|
56293
|
+
return;
|
|
56294
|
+
}
|
|
56266
56295
|
const newDeadline = cooldownDeadline(hit);
|
|
56267
56296
|
const MIN_ADVANCE_MS = 60000;
|
|
56268
|
-
if (newDeadline - this.lastEmittedRateLimitDeadline < MIN_ADVANCE_MS)
|
|
56297
|
+
if (newDeadline - this.lastEmittedRateLimitDeadline < MIN_ADVANCE_MS) {
|
|
56298
|
+
if (hit.resetAtEpochMs !== undefined) {
|
|
56299
|
+
this.lastEmittedHitHadExplicitReset = true;
|
|
56300
|
+
}
|
|
56269
56301
|
return;
|
|
56302
|
+
}
|
|
56270
56303
|
this.lastEmittedRateLimitDeadline = newDeadline;
|
|
56304
|
+
this.lastEmittedHitHadExplicitReset = hit.resetAtEpochMs !== undefined;
|
|
56271
56305
|
this.log.warn(`Rate limit detected: ${hit.matched ?? "(no match text)"}`);
|
|
56272
56306
|
this.emit("rate-limit", hit);
|
|
56273
56307
|
}
|
|
@@ -56322,12 +56356,22 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
56322
56356
|
proc.kill("SIGTERM");
|
|
56323
56357
|
} catch {}
|
|
56324
56358
|
}, 2000);
|
|
56325
|
-
|
|
56326
|
-
this.log.debug(`Claude process
|
|
56359
|
+
const settle = (reason) => {
|
|
56360
|
+
this.log.debug(`Claude process gone (${reason})`);
|
|
56327
56361
|
clearTimeout(secondSigint);
|
|
56328
56362
|
clearTimeout(forceKillTimeout);
|
|
56363
|
+
clearTimeout(lastResort);
|
|
56329
56364
|
resolve4();
|
|
56330
|
-
}
|
|
56365
|
+
};
|
|
56366
|
+
const lastResort = setTimeout(() => {
|
|
56367
|
+
try {
|
|
56368
|
+
this.log.warn("Claude process did not exit after SIGTERM — sending SIGKILL");
|
|
56369
|
+
proc.kill("SIGKILL");
|
|
56370
|
+
} catch {}
|
|
56371
|
+
settle("kill timeout");
|
|
56372
|
+
}, 5000);
|
|
56373
|
+
proc.once("close", (code) => settle(`closed, code=${code}`));
|
|
56374
|
+
proc.once("error", () => settle("spawn error"));
|
|
56331
56375
|
});
|
|
56332
56376
|
}
|
|
56333
56377
|
interrupt() {
|
|
@@ -63986,11 +64030,26 @@ var bashToolFormatter = {
|
|
|
63986
64030
|
};
|
|
63987
64031
|
// src/operations/tool-formatters/task-tools.ts
|
|
63988
64032
|
var taskToolsFormatter = {
|
|
63989
|
-
toolNames: [
|
|
63990
|
-
|
|
64033
|
+
toolNames: [
|
|
64034
|
+
"TodoWrite",
|
|
64035
|
+
"TaskCreate",
|
|
64036
|
+
"TaskUpdate",
|
|
64037
|
+
"TaskGet",
|
|
64038
|
+
"TaskList",
|
|
64039
|
+
"Task",
|
|
64040
|
+
"EnterPlanMode",
|
|
64041
|
+
"ExitPlanMode",
|
|
64042
|
+
"AskUserQuestion"
|
|
64043
|
+
],
|
|
64044
|
+
format(toolName, input, options) {
|
|
63991
64045
|
const { formatter } = options;
|
|
63992
64046
|
switch (toolName) {
|
|
63993
64047
|
case "TodoWrite":
|
|
64048
|
+
case "TaskCreate":
|
|
64049
|
+
case "TaskUpdate":
|
|
64050
|
+
return { display: null, hidden: true };
|
|
64051
|
+
case "TaskGet":
|
|
64052
|
+
case "TaskList":
|
|
63994
64053
|
return { display: null, hidden: true };
|
|
63995
64054
|
case "Task":
|
|
63996
64055
|
return { display: null, hidden: true };
|
|
@@ -63999,8 +64058,25 @@ var taskToolsFormatter = {
|
|
|
63999
64058
|
display: `\uD83D\uDCCB ${formatter.formatBold("Planning...")}`,
|
|
64000
64059
|
permissionText: `\uD83D\uDCCB ${formatter.formatBold("Planning...")}`
|
|
64001
64060
|
};
|
|
64002
|
-
case "ExitPlanMode":
|
|
64003
|
-
|
|
64061
|
+
case "ExitPlanMode": {
|
|
64062
|
+
const plan = typeof input.plan === "string" ? input.plan : "";
|
|
64063
|
+
const points = Array.from(plan);
|
|
64064
|
+
const truncated = points.length > 1500;
|
|
64065
|
+
let preview = truncated ? `${points.slice(0, 1500).join("")}
|
|
64066
|
+
…` : plan;
|
|
64067
|
+
if (truncated) {
|
|
64068
|
+
const fenceCount = (preview.match(/^```/gm) || []).length;
|
|
64069
|
+
if (fenceCount % 2 === 1)
|
|
64070
|
+
preview += "\n```";
|
|
64071
|
+
}
|
|
64072
|
+
return {
|
|
64073
|
+
display: null,
|
|
64074
|
+
hidden: true,
|
|
64075
|
+
permissionText: preview ? `\uD83D\uDCCB ${formatter.formatBold("Plan approval requested")}
|
|
64076
|
+
|
|
64077
|
+
${preview}` : `\uD83D\uDCCB ${formatter.formatBold("Plan approval requested")}`
|
|
64078
|
+
};
|
|
64079
|
+
}
|
|
64004
64080
|
case "AskUserQuestion":
|
|
64005
64081
|
return { display: null, hidden: true };
|
|
64006
64082
|
default:
|
|
@@ -64539,9 +64615,14 @@ function createStatusUpdateOp(sessionId, options) {
|
|
|
64539
64615
|
}
|
|
64540
64616
|
// src/operations/transformer.ts
|
|
64541
64617
|
function transformEvent(event, ctx) {
|
|
64618
|
+
if (event.parent_tool_use_id) {
|
|
64619
|
+
return [];
|
|
64620
|
+
}
|
|
64542
64621
|
switch (event.type) {
|
|
64543
64622
|
case "assistant":
|
|
64544
64623
|
return transformAssistant(event, ctx);
|
|
64624
|
+
case "user":
|
|
64625
|
+
return transformUser(event, ctx);
|
|
64545
64626
|
case "tool_use":
|
|
64546
64627
|
return transformToolUse(event, ctx);
|
|
64547
64628
|
case "tool_result":
|
|
@@ -64583,6 +64664,9 @@ function transformAssistant(event, ctx) {
|
|
|
64583
64664
|
if (result.display && !result.hidden) {
|
|
64584
64665
|
flushTextBuffer();
|
|
64585
64666
|
operations.push(createAppendContentOp(ctx.sessionId, result.display, true));
|
|
64667
|
+
if (block.id) {
|
|
64668
|
+
ctx.toolStartTimes.set(block.id, Date.now());
|
|
64669
|
+
}
|
|
64586
64670
|
}
|
|
64587
64671
|
}
|
|
64588
64672
|
} else if (block.type === "thinking" && block.thinking) {
|
|
@@ -64597,8 +64681,70 @@ function transformAssistant(event, ctx) {
|
|
|
64597
64681
|
}
|
|
64598
64682
|
}
|
|
64599
64683
|
flushTextBuffer();
|
|
64684
|
+
const taskListOps = operations.filter((op) => op.type === "task_list");
|
|
64685
|
+
if (taskListOps.length > 1) {
|
|
64686
|
+
const last = taskListOps[taskListOps.length - 1];
|
|
64687
|
+
return operations.filter((op) => op.type !== "task_list" || op === last);
|
|
64688
|
+
}
|
|
64689
|
+
return operations;
|
|
64690
|
+
}
|
|
64691
|
+
function transformUser(event, ctx) {
|
|
64692
|
+
const msg = event.message;
|
|
64693
|
+
if (!Array.isArray(msg?.content)) {
|
|
64694
|
+
return [];
|
|
64695
|
+
}
|
|
64696
|
+
const operations = [];
|
|
64697
|
+
let indicatorCount = 0;
|
|
64698
|
+
for (const block of msg.content) {
|
|
64699
|
+
if (block?.type !== "tool_result" || !block.tool_use_id)
|
|
64700
|
+
continue;
|
|
64701
|
+
if (ctx.taskTracker.hasPendingCreate(block.tool_use_id)) {
|
|
64702
|
+
const resolution = ctx.taskTracker.resolveCreatedId(block.tool_use_id, toolResultContentText(block.content), block.is_error === true);
|
|
64703
|
+
if (resolution === "removed" || resolution === "merged") {
|
|
64704
|
+
const action = ctx.taskTracker.allCompleted ? "complete" : "update";
|
|
64705
|
+
operations.push(createTaskListOp(ctx.sessionId, action, ctx.taskTracker.toTaskItems()));
|
|
64706
|
+
}
|
|
64707
|
+
}
|
|
64708
|
+
if (ctx.toolStartTimes.has(block.tool_use_id)) {
|
|
64709
|
+
operations.push(createResultIndicatorOp(block.tool_use_id, block.is_error === true, ctx));
|
|
64710
|
+
indicatorCount++;
|
|
64711
|
+
}
|
|
64712
|
+
}
|
|
64713
|
+
const taskListOps = operations.filter((op) => op.type === "task_list");
|
|
64714
|
+
if (taskListOps.length > 1) {
|
|
64715
|
+
const last = taskListOps[taskListOps.length - 1];
|
|
64716
|
+
const coalesced = operations.filter((op) => op.type !== "task_list" || op === last);
|
|
64717
|
+
operations.length = 0;
|
|
64718
|
+
operations.push(...coalesced);
|
|
64719
|
+
}
|
|
64720
|
+
if (indicatorCount > 0) {
|
|
64721
|
+
operations.push(createFlushOp(ctx.sessionId, "tool_complete"));
|
|
64722
|
+
}
|
|
64600
64723
|
return operations;
|
|
64601
64724
|
}
|
|
64725
|
+
function toolResultContentText(content) {
|
|
64726
|
+
if (typeof content === "string")
|
|
64727
|
+
return content;
|
|
64728
|
+
if (Array.isArray(content)) {
|
|
64729
|
+
return content.map((b) => b && typeof b === "object" && typeof b.text === "string" ? b.text : "").join(`
|
|
64730
|
+
`);
|
|
64731
|
+
}
|
|
64732
|
+
return "";
|
|
64733
|
+
}
|
|
64734
|
+
function createResultIndicatorOp(toolUseId, isError, ctx) {
|
|
64735
|
+
let elapsed = "";
|
|
64736
|
+
const startTime = ctx.toolStartTimes.get(toolUseId);
|
|
64737
|
+
if (startTime) {
|
|
64738
|
+
const secs = Math.round((Date.now() - startTime) / 1000);
|
|
64739
|
+
if (secs >= 3) {
|
|
64740
|
+
elapsed = ` (${secs}s)`;
|
|
64741
|
+
}
|
|
64742
|
+
ctx.toolStartTimes.delete(toolUseId);
|
|
64743
|
+
}
|
|
64744
|
+
const icon = isError ? "❌" : "✓";
|
|
64745
|
+
const errorNote = isError ? " Error" : "";
|
|
64746
|
+
return createAppendContentOp(ctx.sessionId, ` ↳ ${icon}${errorNote}${elapsed}`, true);
|
|
64747
|
+
}
|
|
64602
64748
|
function transformToolUse(event, ctx) {
|
|
64603
64749
|
const tool = event.tool_use;
|
|
64604
64750
|
if (tool.id) {
|
|
@@ -64623,23 +64769,10 @@ function transformToolResult(event, ctx) {
|
|
|
64623
64769
|
return [];
|
|
64624
64770
|
}
|
|
64625
64771
|
const result = event.tool_result;
|
|
64626
|
-
|
|
64627
|
-
|
|
64628
|
-
|
|
64629
|
-
|
|
64630
|
-
if (startTime) {
|
|
64631
|
-
const secs = Math.round((Date.now() - startTime) / 1000);
|
|
64632
|
-
if (secs >= 3) {
|
|
64633
|
-
elapsed = ` (${secs}s)`;
|
|
64634
|
-
}
|
|
64635
|
-
ctx.toolStartTimes.delete(result.tool_use_id);
|
|
64636
|
-
}
|
|
64637
|
-
}
|
|
64638
|
-
const icon = result.is_error ? "❌" : "✓";
|
|
64639
|
-
const errorNote = result.is_error ? " Error" : "";
|
|
64640
|
-
operations.push(createAppendContentOp(ctx.sessionId, ` ↳ ${icon}${errorNote}${elapsed}`, true));
|
|
64641
|
-
operations.push(createFlushOp(ctx.sessionId, "tool_complete"));
|
|
64642
|
-
return operations;
|
|
64772
|
+
return [
|
|
64773
|
+
createResultIndicatorOp(result.tool_use_id || "", result.is_error === true, ctx),
|
|
64774
|
+
createFlushOp(ctx.sessionId, "tool_complete")
|
|
64775
|
+
];
|
|
64643
64776
|
}
|
|
64644
64777
|
function transformResult(event, ctx) {
|
|
64645
64778
|
const operations = [];
|
|
@@ -64656,6 +64789,10 @@ function handleSpecialTool(toolName, toolUseId, input, ctx) {
|
|
|
64656
64789
|
switch (toolName) {
|
|
64657
64790
|
case "TodoWrite":
|
|
64658
64791
|
return handleTodoWrite(input, ctx);
|
|
64792
|
+
case "TaskCreate":
|
|
64793
|
+
return handleTaskCreate(toolUseId, input, ctx);
|
|
64794
|
+
case "TaskUpdate":
|
|
64795
|
+
return handleTaskUpdate(input, ctx);
|
|
64659
64796
|
case "Task":
|
|
64660
64797
|
return handleTaskStart(toolUseId, input, ctx);
|
|
64661
64798
|
case "AskUserQuestion":
|
|
@@ -64673,10 +64810,22 @@ function handleTodoWrite(input, ctx) {
|
|
|
64673
64810
|
status: t.status,
|
|
64674
64811
|
activeForm: t.activeForm
|
|
64675
64812
|
}));
|
|
64813
|
+
ctx.taskTracker.clear();
|
|
64676
64814
|
const allCompleted = tasks.every((t) => t.status === "completed");
|
|
64677
64815
|
const action = allCompleted ? "complete" : "update";
|
|
64678
64816
|
return [createTaskListOp(ctx.sessionId, action, tasks)];
|
|
64679
64817
|
}
|
|
64818
|
+
function handleTaskCreate(toolUseId, input, ctx) {
|
|
64819
|
+
ctx.taskTracker.create(toolUseId, input);
|
|
64820
|
+
return [createTaskListOp(ctx.sessionId, "update", ctx.taskTracker.toTaskItems())];
|
|
64821
|
+
}
|
|
64822
|
+
function handleTaskUpdate(input, ctx) {
|
|
64823
|
+
const changed = ctx.taskTracker.update(input);
|
|
64824
|
+
if (!changed)
|
|
64825
|
+
return [];
|
|
64826
|
+
const action = ctx.taskTracker.allCompleted ? "complete" : "update";
|
|
64827
|
+
return [createTaskListOp(ctx.sessionId, action, ctx.taskTracker.toTaskItems())];
|
|
64828
|
+
}
|
|
64680
64829
|
function handleTaskStart(toolUseId, input, ctx) {
|
|
64681
64830
|
const description = input.description || input.prompt || "Subagent";
|
|
64682
64831
|
const subagentType = input.subagent_type || "general-purpose";
|
|
@@ -64710,6 +64859,102 @@ function truncateAtWord2(text, maxLength) {
|
|
|
64710
64859
|
}
|
|
64711
64860
|
return truncated + "...";
|
|
64712
64861
|
}
|
|
64862
|
+
// src/operations/task-tracker.ts
|
|
64863
|
+
var CREATED_RESULT_RE = /Task #(\S+) created/;
|
|
64864
|
+
|
|
64865
|
+
class TaskTracker {
|
|
64866
|
+
tasks = [];
|
|
64867
|
+
pendingCreates = new Map;
|
|
64868
|
+
unmatchedCreateResults = 0;
|
|
64869
|
+
create(toolUseId, input) {
|
|
64870
|
+
const task = {
|
|
64871
|
+
subject: typeof input.subject === "string" ? input.subject : "Task",
|
|
64872
|
+
activeForm: typeof input.activeForm === "string" ? input.activeForm : undefined,
|
|
64873
|
+
status: "pending"
|
|
64874
|
+
};
|
|
64875
|
+
this.tasks.push(task);
|
|
64876
|
+
if (toolUseId) {
|
|
64877
|
+
this.pendingCreates.set(toolUseId, task);
|
|
64878
|
+
}
|
|
64879
|
+
}
|
|
64880
|
+
hasPendingCreate(toolUseId) {
|
|
64881
|
+
return this.pendingCreates.has(toolUseId);
|
|
64882
|
+
}
|
|
64883
|
+
resolveCreatedId(toolUseId, resultContent, isError = false) {
|
|
64884
|
+
const task = this.pendingCreates.get(toolUseId);
|
|
64885
|
+
if (!task)
|
|
64886
|
+
return "ignored";
|
|
64887
|
+
this.pendingCreates.delete(toolUseId);
|
|
64888
|
+
const match = isError ? null : CREATED_RESULT_RE.exec(resultContent);
|
|
64889
|
+
if (!match) {
|
|
64890
|
+
if (!isError)
|
|
64891
|
+
this.unmatchedCreateResults++;
|
|
64892
|
+
this.tasks = this.tasks.filter((t) => t !== task);
|
|
64893
|
+
return "removed";
|
|
64894
|
+
}
|
|
64895
|
+
const placeholder = this.tasks.find((t) => t !== task && t.taskId === match[1]);
|
|
64896
|
+
if (placeholder) {
|
|
64897
|
+
task.status = placeholder.status;
|
|
64898
|
+
if (placeholder.subject !== `Task #${match[1]}`)
|
|
64899
|
+
task.subject = placeholder.subject;
|
|
64900
|
+
if (placeholder.activeForm && !task.activeForm)
|
|
64901
|
+
task.activeForm = placeholder.activeForm;
|
|
64902
|
+
this.tasks = this.tasks.filter((t) => t !== placeholder);
|
|
64903
|
+
}
|
|
64904
|
+
task.taskId = match[1];
|
|
64905
|
+
return placeholder ? "merged" : "resolved";
|
|
64906
|
+
}
|
|
64907
|
+
consumeUnmatchedCreateResultFlag() {
|
|
64908
|
+
if (this.unmatchedCreateResults === 0)
|
|
64909
|
+
return false;
|
|
64910
|
+
this.unmatchedCreateResults = 0;
|
|
64911
|
+
return true;
|
|
64912
|
+
}
|
|
64913
|
+
update(input) {
|
|
64914
|
+
const taskId = typeof input.taskId === "string" ? input.taskId : typeof input.taskId === "number" ? String(input.taskId) : undefined;
|
|
64915
|
+
if (!taskId)
|
|
64916
|
+
return false;
|
|
64917
|
+
let task = this.tasks.find((t) => t.taskId === taskId);
|
|
64918
|
+
const status = typeof input.status === "string" ? input.status : undefined;
|
|
64919
|
+
if (status === "deleted") {
|
|
64920
|
+
if (!task)
|
|
64921
|
+
return false;
|
|
64922
|
+
this.tasks = this.tasks.filter((t) => t !== task);
|
|
64923
|
+
return true;
|
|
64924
|
+
}
|
|
64925
|
+
if (!task) {
|
|
64926
|
+
task = { taskId, subject: `Task #${taskId}`, status: "pending", isPlaceholder: true };
|
|
64927
|
+
this.tasks.push(task);
|
|
64928
|
+
}
|
|
64929
|
+
if (typeof input.subject === "string")
|
|
64930
|
+
task.subject = input.subject;
|
|
64931
|
+
if (typeof input.activeForm === "string")
|
|
64932
|
+
task.activeForm = input.activeForm;
|
|
64933
|
+
if (status === "pending" || status === "in_progress" || status === "completed") {
|
|
64934
|
+
task.status = status;
|
|
64935
|
+
}
|
|
64936
|
+
return true;
|
|
64937
|
+
}
|
|
64938
|
+
toTaskItems() {
|
|
64939
|
+
return this.tasks.map((t) => ({
|
|
64940
|
+
content: t.subject,
|
|
64941
|
+
status: t.status,
|
|
64942
|
+
activeForm: t.activeForm ?? t.subject
|
|
64943
|
+
}));
|
|
64944
|
+
}
|
|
64945
|
+
get isEmpty() {
|
|
64946
|
+
return this.tasks.length === 0;
|
|
64947
|
+
}
|
|
64948
|
+
get allCompleted() {
|
|
64949
|
+
return this.tasks.length > 0 && this.tasks.every((t) => t.status === "completed") && this.tasks.some((t) => !t.isPlaceholder);
|
|
64950
|
+
}
|
|
64951
|
+
clear() {
|
|
64952
|
+
this.tasks = [];
|
|
64953
|
+
this.pendingCreates.clear();
|
|
64954
|
+
this.unmatchedCreateResults = 0;
|
|
64955
|
+
}
|
|
64956
|
+
}
|
|
64957
|
+
|
|
64713
64958
|
// src/operations/executors/base.ts
|
|
64714
64959
|
class BaseExecutor {
|
|
64715
64960
|
state;
|
|
@@ -66462,6 +66707,7 @@ class MessageManager {
|
|
|
66462
66707
|
startTypingCallback;
|
|
66463
66708
|
emitSessionUpdateCallback;
|
|
66464
66709
|
toolStartTimes = new Map;
|
|
66710
|
+
taskTracker = new TaskTracker;
|
|
66465
66711
|
flushTimer = null;
|
|
66466
66712
|
static DEFAULT_FLUSH_DELAY_MS = 500;
|
|
66467
66713
|
flushDelayMs;
|
|
@@ -66532,10 +66778,14 @@ class MessageManager {
|
|
|
66532
66778
|
sessionId: this.sessionId,
|
|
66533
66779
|
formatter: this.platform.getFormatter(),
|
|
66534
66780
|
toolStartTimes: this.toolStartTimes,
|
|
66781
|
+
taskTracker: this.taskTracker,
|
|
66535
66782
|
detailed: true,
|
|
66536
66783
|
worktreeInfo: this.worktreePath && this.worktreeBranch ? { path: this.worktreePath, branch: this.worktreeBranch } : undefined
|
|
66537
66784
|
};
|
|
66538
66785
|
const ops = transformEvent(event, transformCtx);
|
|
66786
|
+
if (this.taskTracker.consumeUnmatchedCreateResultFlag()) {
|
|
66787
|
+
logger.warn('TaskCreate result did not match the expected "Task #N created" wording — ' + "task dropped from the displayed list. If this repeats, the Claude CLI " + "likely changed its result text and the task display will be incomplete.");
|
|
66788
|
+
}
|
|
66539
66789
|
if (ops.length === 0) {
|
|
66540
66790
|
if (event.type !== "system") {
|
|
66541
66791
|
logger.debug(`No operations from event: ${event.type}`);
|
|
@@ -66919,9 +67169,14 @@ class MessageManager {
|
|
|
66919
67169
|
logger.debug("Reaction not handled by any executor");
|
|
66920
67170
|
return false;
|
|
66921
67171
|
}
|
|
67172
|
+
clearClaudeSessionState() {
|
|
67173
|
+
this.toolStartTimes.clear();
|
|
67174
|
+
this.taskTracker.clear();
|
|
67175
|
+
}
|
|
66922
67176
|
reset() {
|
|
66923
67177
|
this.cancelScheduledFlush();
|
|
66924
67178
|
this.toolStartTimes.clear();
|
|
67179
|
+
this.taskTracker.clear();
|
|
66925
67180
|
this.contentExecutor.reset();
|
|
66926
67181
|
this.taskListExecutor.reset();
|
|
66927
67182
|
this.questionApprovalExecutor.reset();
|
|
@@ -67957,7 +68212,7 @@ async function createAndSwitchToWorktree(session, branch, username, options) {
|
|
|
67957
68212
|
if (session.claude.isRunning()) {
|
|
67958
68213
|
options.stopTyping(session);
|
|
67959
68214
|
transitionTo(session, "restarting");
|
|
67960
|
-
session.claude.kill();
|
|
68215
|
+
await session.claude.kill();
|
|
67961
68216
|
await options.flush(session);
|
|
67962
68217
|
const newSessionId = randomUUID2();
|
|
67963
68218
|
session.claudeSessionId = newSessionId;
|
|
@@ -67977,6 +68232,7 @@ async function createAndSwitchToWorktree(session, branch, username, options) {
|
|
|
67977
68232
|
resume: false,
|
|
67978
68233
|
appendSystemPrompt: await buildAppendSystemPrompt(session.platform, session.platformId, existing.path, session.threadId, session.startedBy, session.sessionAllowedUsers, options.appendSystemPrompt ?? "", options.githubEmailsStore, { omitSessionContext: !needsTitlePrompt, userAttribution: session.userAttribution })
|
|
67979
68234
|
};
|
|
68235
|
+
session.messageManager?.clearClaudeSessionState();
|
|
67980
68236
|
session.claude = new ClaudeCli(cliOptions);
|
|
67981
68237
|
session.claude.on("event", (e) => options.handleEvent(session.sessionId, e));
|
|
67982
68238
|
session.claude.on("exit", (code) => options.handleExit(session.sessionId, code));
|
|
@@ -68049,7 +68305,7 @@ ${fmt.formatItalic("Claude Code restarted in the worktree")}`);
|
|
|
68049
68305
|
if (session.claude.isRunning()) {
|
|
68050
68306
|
options.stopTyping(session);
|
|
68051
68307
|
transitionTo(session, "restarting");
|
|
68052
|
-
session.claude.kill();
|
|
68308
|
+
await session.claude.kill();
|
|
68053
68309
|
await options.flush(session);
|
|
68054
68310
|
const newSessionId = randomUUID2();
|
|
68055
68311
|
session.claudeSessionId = newSessionId;
|
|
@@ -68069,6 +68325,7 @@ ${fmt.formatItalic("Claude Code restarted in the worktree")}`);
|
|
|
68069
68325
|
resume: false,
|
|
68070
68326
|
appendSystemPrompt: await buildAppendSystemPrompt(session.platform, session.platformId, worktreePath, session.threadId, session.startedBy, session.sessionAllowedUsers, options.appendSystemPrompt ?? "", options.githubEmailsStore, { omitSessionContext: !needsTitlePrompt, userAttribution: session.userAttribution })
|
|
68071
68327
|
};
|
|
68328
|
+
session.messageManager?.clearClaudeSessionState();
|
|
68072
68329
|
session.claude = new ClaudeCli(cliOptions);
|
|
68073
68330
|
session.claude.on("event", (e) => options.handleEvent(session.sessionId, e));
|
|
68074
68331
|
session.claude.on("exit", (code) => options.handleExit(session.sessionId, code));
|
|
@@ -68342,6 +68599,9 @@ function extractAndUpdatePullRequest(text, session, ctx) {
|
|
|
68342
68599
|
ctx.ops.updateSessionHeader(session).catch(() => {});
|
|
68343
68600
|
}
|
|
68344
68601
|
}
|
|
68602
|
+
function isSidechainEvent(event) {
|
|
68603
|
+
return Boolean(event.parent_tool_use_id);
|
|
68604
|
+
}
|
|
68345
68605
|
function handleEventPreProcessing(session, event, ctx) {
|
|
68346
68606
|
session.threadLogger?.logEvent(event);
|
|
68347
68607
|
resetSessionActivity(session);
|
|
@@ -68363,6 +68623,16 @@ function handleEventPreProcessing(session, event, ctx) {
|
|
|
68363
68623
|
handleCompactionComplete(session, e.compact_metadata, ctx);
|
|
68364
68624
|
}
|
|
68365
68625
|
}
|
|
68626
|
+
if (event.type === "assistant" && !isSidechainEvent(event)) {
|
|
68627
|
+
const msg = event.message;
|
|
68628
|
+
if (Array.isArray(msg?.content)) {
|
|
68629
|
+
for (const block of msg.content) {
|
|
68630
|
+
if (block.type === "tool_use" && block.name) {
|
|
68631
|
+
trackEvent(session, "tool_use", block.name);
|
|
68632
|
+
}
|
|
68633
|
+
}
|
|
68634
|
+
}
|
|
68635
|
+
}
|
|
68366
68636
|
if (event.type === "tool_use") {
|
|
68367
68637
|
const tool = event.tool_use;
|
|
68368
68638
|
trackEvent(session, "tool_use", tool.name);
|
|
@@ -68374,7 +68644,9 @@ function handleEventPostProcessing(session, event, ctx) {
|
|
|
68374
68644
|
for (const block of msg?.content || []) {
|
|
68375
68645
|
if (block.type === "text" && block.text) {
|
|
68376
68646
|
extractAndUpdatePullRequest(block.text, session, ctx);
|
|
68377
|
-
|
|
68647
|
+
if (!isSidechainEvent(event)) {
|
|
68648
|
+
detectAndExecuteClaudeCommands(block.text, session, ctx);
|
|
68649
|
+
}
|
|
68378
68650
|
}
|
|
68379
68651
|
}
|
|
68380
68652
|
}
|
|
@@ -68384,6 +68656,16 @@ function handleEventPostProcessing(session, event, ctx) {
|
|
|
68384
68656
|
ctx.ops.emitSessionUpdate(session.sessionId, { status: getSessionStatus(session) });
|
|
68385
68657
|
updateUsageStats(session, event, ctx);
|
|
68386
68658
|
}
|
|
68659
|
+
if (event.type === "user" && !isSidechainEvent(event)) {
|
|
68660
|
+
const msg = event.message;
|
|
68661
|
+
if (Array.isArray(msg?.content)) {
|
|
68662
|
+
for (const block of msg.content) {
|
|
68663
|
+
if (block.type === "tool_result" && block.is_error) {
|
|
68664
|
+
trackEvent(session, "tool_error", "Tool execution failed");
|
|
68665
|
+
}
|
|
68666
|
+
}
|
|
68667
|
+
}
|
|
68668
|
+
}
|
|
68387
68669
|
if (event.type === "tool_result") {
|
|
68388
68670
|
const result = event.tool_result;
|
|
68389
68671
|
if (result.is_error) {
|
|
@@ -68943,8 +69225,11 @@ function commonRestartCliOptions(session, ctx) {
|
|
|
68943
69225
|
async function restartClaudeSession(session, cliOptions, ctx, actionName) {
|
|
68944
69226
|
ctx.ops.stopTyping(session);
|
|
68945
69227
|
transitionTo(session, "restarting");
|
|
68946
|
-
session.claude.kill();
|
|
69228
|
+
await session.claude.kill();
|
|
68947
69229
|
await ctx.ops.flush(session);
|
|
69230
|
+
if (!cliOptions.resume) {
|
|
69231
|
+
session.messageManager?.clearClaudeSessionState();
|
|
69232
|
+
}
|
|
68948
69233
|
session.claude = new ClaudeCli(cliOptions);
|
|
68949
69234
|
session.claude.on("event", (e) => ctx.ops.handleEvent(session.sessionId, e));
|
|
68950
69235
|
session.claude.on("exit", (code) => ctx.ops.handleExit(session.sessionId, code));
|
package/dist/mcp/mcp-server.js
CHANGED
|
@@ -48574,11 +48574,26 @@ var bashToolFormatter = {
|
|
|
48574
48574
|
};
|
|
48575
48575
|
// src/operations/tool-formatters/task-tools.ts
|
|
48576
48576
|
var taskToolsFormatter = {
|
|
48577
|
-
toolNames: [
|
|
48578
|
-
|
|
48577
|
+
toolNames: [
|
|
48578
|
+
"TodoWrite",
|
|
48579
|
+
"TaskCreate",
|
|
48580
|
+
"TaskUpdate",
|
|
48581
|
+
"TaskGet",
|
|
48582
|
+
"TaskList",
|
|
48583
|
+
"Task",
|
|
48584
|
+
"EnterPlanMode",
|
|
48585
|
+
"ExitPlanMode",
|
|
48586
|
+
"AskUserQuestion"
|
|
48587
|
+
],
|
|
48588
|
+
format(toolName, input, options) {
|
|
48579
48589
|
const { formatter } = options;
|
|
48580
48590
|
switch (toolName) {
|
|
48581
48591
|
case "TodoWrite":
|
|
48592
|
+
case "TaskCreate":
|
|
48593
|
+
case "TaskUpdate":
|
|
48594
|
+
return { display: null, hidden: true };
|
|
48595
|
+
case "TaskGet":
|
|
48596
|
+
case "TaskList":
|
|
48582
48597
|
return { display: null, hidden: true };
|
|
48583
48598
|
case "Task":
|
|
48584
48599
|
return { display: null, hidden: true };
|
|
@@ -48587,8 +48602,25 @@ var taskToolsFormatter = {
|
|
|
48587
48602
|
display: `\uD83D\uDCCB ${formatter.formatBold("Planning...")}`,
|
|
48588
48603
|
permissionText: `\uD83D\uDCCB ${formatter.formatBold("Planning...")}`
|
|
48589
48604
|
};
|
|
48590
|
-
case "ExitPlanMode":
|
|
48591
|
-
|
|
48605
|
+
case "ExitPlanMode": {
|
|
48606
|
+
const plan = typeof input.plan === "string" ? input.plan : "";
|
|
48607
|
+
const points = Array.from(plan);
|
|
48608
|
+
const truncated = points.length > 1500;
|
|
48609
|
+
let preview = truncated ? `${points.slice(0, 1500).join("")}
|
|
48610
|
+
…` : plan;
|
|
48611
|
+
if (truncated) {
|
|
48612
|
+
const fenceCount = (preview.match(/^```/gm) || []).length;
|
|
48613
|
+
if (fenceCount % 2 === 1)
|
|
48614
|
+
preview += "\n```";
|
|
48615
|
+
}
|
|
48616
|
+
return {
|
|
48617
|
+
display: null,
|
|
48618
|
+
hidden: true,
|
|
48619
|
+
permissionText: preview ? `\uD83D\uDCCB ${formatter.formatBold("Plan approval requested")}
|
|
48620
|
+
|
|
48621
|
+
${preview}` : `\uD83D\uDCCB ${formatter.formatBold("Plan approval requested")}`
|
|
48622
|
+
};
|
|
48623
|
+
}
|
|
48592
48624
|
case "AskUserQuestion":
|
|
48593
48625
|
return { display: null, hidden: true };
|
|
48594
48626
|
default:
|
|
@@ -49135,9 +49167,14 @@ function createStatusUpdateOp(sessionId, options) {
|
|
|
49135
49167
|
}
|
|
49136
49168
|
// src/operations/transformer.ts
|
|
49137
49169
|
function transformEvent(event, ctx) {
|
|
49170
|
+
if (event.parent_tool_use_id) {
|
|
49171
|
+
return [];
|
|
49172
|
+
}
|
|
49138
49173
|
switch (event.type) {
|
|
49139
49174
|
case "assistant":
|
|
49140
49175
|
return transformAssistant(event, ctx);
|
|
49176
|
+
case "user":
|
|
49177
|
+
return transformUser(event, ctx);
|
|
49141
49178
|
case "tool_use":
|
|
49142
49179
|
return transformToolUse(event, ctx);
|
|
49143
49180
|
case "tool_result":
|
|
@@ -49179,6 +49216,9 @@ function transformAssistant(event, ctx) {
|
|
|
49179
49216
|
if (result.display && !result.hidden) {
|
|
49180
49217
|
flushTextBuffer();
|
|
49181
49218
|
operations.push(createAppendContentOp(ctx.sessionId, result.display, true));
|
|
49219
|
+
if (block.id) {
|
|
49220
|
+
ctx.toolStartTimes.set(block.id, Date.now());
|
|
49221
|
+
}
|
|
49182
49222
|
}
|
|
49183
49223
|
}
|
|
49184
49224
|
} else if (block.type === "thinking" && block.thinking) {
|
|
@@ -49193,8 +49233,70 @@ function transformAssistant(event, ctx) {
|
|
|
49193
49233
|
}
|
|
49194
49234
|
}
|
|
49195
49235
|
flushTextBuffer();
|
|
49236
|
+
const taskListOps = operations.filter((op) => op.type === "task_list");
|
|
49237
|
+
if (taskListOps.length > 1) {
|
|
49238
|
+
const last = taskListOps[taskListOps.length - 1];
|
|
49239
|
+
return operations.filter((op) => op.type !== "task_list" || op === last);
|
|
49240
|
+
}
|
|
49196
49241
|
return operations;
|
|
49197
49242
|
}
|
|
49243
|
+
function transformUser(event, ctx) {
|
|
49244
|
+
const msg = event.message;
|
|
49245
|
+
if (!Array.isArray(msg?.content)) {
|
|
49246
|
+
return [];
|
|
49247
|
+
}
|
|
49248
|
+
const operations = [];
|
|
49249
|
+
let indicatorCount = 0;
|
|
49250
|
+
for (const block of msg.content) {
|
|
49251
|
+
if (block?.type !== "tool_result" || !block.tool_use_id)
|
|
49252
|
+
continue;
|
|
49253
|
+
if (ctx.taskTracker.hasPendingCreate(block.tool_use_id)) {
|
|
49254
|
+
const resolution = ctx.taskTracker.resolveCreatedId(block.tool_use_id, toolResultContentText(block.content), block.is_error === true);
|
|
49255
|
+
if (resolution === "removed" || resolution === "merged") {
|
|
49256
|
+
const action = ctx.taskTracker.allCompleted ? "complete" : "update";
|
|
49257
|
+
operations.push(createTaskListOp(ctx.sessionId, action, ctx.taskTracker.toTaskItems()));
|
|
49258
|
+
}
|
|
49259
|
+
}
|
|
49260
|
+
if (ctx.toolStartTimes.has(block.tool_use_id)) {
|
|
49261
|
+
operations.push(createResultIndicatorOp(block.tool_use_id, block.is_error === true, ctx));
|
|
49262
|
+
indicatorCount++;
|
|
49263
|
+
}
|
|
49264
|
+
}
|
|
49265
|
+
const taskListOps = operations.filter((op) => op.type === "task_list");
|
|
49266
|
+
if (taskListOps.length > 1) {
|
|
49267
|
+
const last = taskListOps[taskListOps.length - 1];
|
|
49268
|
+
const coalesced = operations.filter((op) => op.type !== "task_list" || op === last);
|
|
49269
|
+
operations.length = 0;
|
|
49270
|
+
operations.push(...coalesced);
|
|
49271
|
+
}
|
|
49272
|
+
if (indicatorCount > 0) {
|
|
49273
|
+
operations.push(createFlushOp(ctx.sessionId, "tool_complete"));
|
|
49274
|
+
}
|
|
49275
|
+
return operations;
|
|
49276
|
+
}
|
|
49277
|
+
function toolResultContentText(content) {
|
|
49278
|
+
if (typeof content === "string")
|
|
49279
|
+
return content;
|
|
49280
|
+
if (Array.isArray(content)) {
|
|
49281
|
+
return content.map((b) => b && typeof b === "object" && typeof b.text === "string" ? b.text : "").join(`
|
|
49282
|
+
`);
|
|
49283
|
+
}
|
|
49284
|
+
return "";
|
|
49285
|
+
}
|
|
49286
|
+
function createResultIndicatorOp(toolUseId, isError, ctx) {
|
|
49287
|
+
let elapsed = "";
|
|
49288
|
+
const startTime = ctx.toolStartTimes.get(toolUseId);
|
|
49289
|
+
if (startTime) {
|
|
49290
|
+
const secs = Math.round((Date.now() - startTime) / 1000);
|
|
49291
|
+
if (secs >= 3) {
|
|
49292
|
+
elapsed = ` (${secs}s)`;
|
|
49293
|
+
}
|
|
49294
|
+
ctx.toolStartTimes.delete(toolUseId);
|
|
49295
|
+
}
|
|
49296
|
+
const icon = isError ? "❌" : "✓";
|
|
49297
|
+
const errorNote = isError ? " Error" : "";
|
|
49298
|
+
return createAppendContentOp(ctx.sessionId, ` ↳ ${icon}${errorNote}${elapsed}`, true);
|
|
49299
|
+
}
|
|
49198
49300
|
function transformToolUse(event, ctx) {
|
|
49199
49301
|
const tool = event.tool_use;
|
|
49200
49302
|
if (tool.id) {
|
|
@@ -49219,23 +49321,10 @@ function transformToolResult(event, ctx) {
|
|
|
49219
49321
|
return [];
|
|
49220
49322
|
}
|
|
49221
49323
|
const result = event.tool_result;
|
|
49222
|
-
|
|
49223
|
-
|
|
49224
|
-
|
|
49225
|
-
|
|
49226
|
-
if (startTime) {
|
|
49227
|
-
const secs = Math.round((Date.now() - startTime) / 1000);
|
|
49228
|
-
if (secs >= 3) {
|
|
49229
|
-
elapsed = ` (${secs}s)`;
|
|
49230
|
-
}
|
|
49231
|
-
ctx.toolStartTimes.delete(result.tool_use_id);
|
|
49232
|
-
}
|
|
49233
|
-
}
|
|
49234
|
-
const icon = result.is_error ? "❌" : "✓";
|
|
49235
|
-
const errorNote = result.is_error ? " Error" : "";
|
|
49236
|
-
operations.push(createAppendContentOp(ctx.sessionId, ` ↳ ${icon}${errorNote}${elapsed}`, true));
|
|
49237
|
-
operations.push(createFlushOp(ctx.sessionId, "tool_complete"));
|
|
49238
|
-
return operations;
|
|
49324
|
+
return [
|
|
49325
|
+
createResultIndicatorOp(result.tool_use_id || "", result.is_error === true, ctx),
|
|
49326
|
+
createFlushOp(ctx.sessionId, "tool_complete")
|
|
49327
|
+
];
|
|
49239
49328
|
}
|
|
49240
49329
|
function transformResult(event, ctx) {
|
|
49241
49330
|
const operations = [];
|
|
@@ -49252,6 +49341,10 @@ function handleSpecialTool(toolName, toolUseId, input, ctx) {
|
|
|
49252
49341
|
switch (toolName) {
|
|
49253
49342
|
case "TodoWrite":
|
|
49254
49343
|
return handleTodoWrite(input, ctx);
|
|
49344
|
+
case "TaskCreate":
|
|
49345
|
+
return handleTaskCreate(toolUseId, input, ctx);
|
|
49346
|
+
case "TaskUpdate":
|
|
49347
|
+
return handleTaskUpdate(input, ctx);
|
|
49255
49348
|
case "Task":
|
|
49256
49349
|
return handleTaskStart(toolUseId, input, ctx);
|
|
49257
49350
|
case "AskUserQuestion":
|
|
@@ -49269,10 +49362,22 @@ function handleTodoWrite(input, ctx) {
|
|
|
49269
49362
|
status: t.status,
|
|
49270
49363
|
activeForm: t.activeForm
|
|
49271
49364
|
}));
|
|
49365
|
+
ctx.taskTracker.clear();
|
|
49272
49366
|
const allCompleted = tasks.every((t) => t.status === "completed");
|
|
49273
49367
|
const action = allCompleted ? "complete" : "update";
|
|
49274
49368
|
return [createTaskListOp(ctx.sessionId, action, tasks)];
|
|
49275
49369
|
}
|
|
49370
|
+
function handleTaskCreate(toolUseId, input, ctx) {
|
|
49371
|
+
ctx.taskTracker.create(toolUseId, input);
|
|
49372
|
+
return [createTaskListOp(ctx.sessionId, "update", ctx.taskTracker.toTaskItems())];
|
|
49373
|
+
}
|
|
49374
|
+
function handleTaskUpdate(input, ctx) {
|
|
49375
|
+
const changed = ctx.taskTracker.update(input);
|
|
49376
|
+
if (!changed)
|
|
49377
|
+
return [];
|
|
49378
|
+
const action = ctx.taskTracker.allCompleted ? "complete" : "update";
|
|
49379
|
+
return [createTaskListOp(ctx.sessionId, action, ctx.taskTracker.toTaskItems())];
|
|
49380
|
+
}
|
|
49276
49381
|
function handleTaskStart(toolUseId, input, ctx) {
|
|
49277
49382
|
const description = input.description || input.prompt || "Subagent";
|
|
49278
49383
|
const subagentType = input.subagent_type || "general-purpose";
|
|
@@ -49306,6 +49411,102 @@ function truncateAtWord(text, maxLength) {
|
|
|
49306
49411
|
}
|
|
49307
49412
|
return truncated + "...";
|
|
49308
49413
|
}
|
|
49414
|
+
// src/operations/task-tracker.ts
|
|
49415
|
+
var CREATED_RESULT_RE = /Task #(\S+) created/;
|
|
49416
|
+
|
|
49417
|
+
class TaskTracker {
|
|
49418
|
+
tasks = [];
|
|
49419
|
+
pendingCreates = new Map;
|
|
49420
|
+
unmatchedCreateResults = 0;
|
|
49421
|
+
create(toolUseId, input) {
|
|
49422
|
+
const task = {
|
|
49423
|
+
subject: typeof input.subject === "string" ? input.subject : "Task",
|
|
49424
|
+
activeForm: typeof input.activeForm === "string" ? input.activeForm : undefined,
|
|
49425
|
+
status: "pending"
|
|
49426
|
+
};
|
|
49427
|
+
this.tasks.push(task);
|
|
49428
|
+
if (toolUseId) {
|
|
49429
|
+
this.pendingCreates.set(toolUseId, task);
|
|
49430
|
+
}
|
|
49431
|
+
}
|
|
49432
|
+
hasPendingCreate(toolUseId) {
|
|
49433
|
+
return this.pendingCreates.has(toolUseId);
|
|
49434
|
+
}
|
|
49435
|
+
resolveCreatedId(toolUseId, resultContent, isError = false) {
|
|
49436
|
+
const task = this.pendingCreates.get(toolUseId);
|
|
49437
|
+
if (!task)
|
|
49438
|
+
return "ignored";
|
|
49439
|
+
this.pendingCreates.delete(toolUseId);
|
|
49440
|
+
const match = isError ? null : CREATED_RESULT_RE.exec(resultContent);
|
|
49441
|
+
if (!match) {
|
|
49442
|
+
if (!isError)
|
|
49443
|
+
this.unmatchedCreateResults++;
|
|
49444
|
+
this.tasks = this.tasks.filter((t) => t !== task);
|
|
49445
|
+
return "removed";
|
|
49446
|
+
}
|
|
49447
|
+
const placeholder = this.tasks.find((t) => t !== task && t.taskId === match[1]);
|
|
49448
|
+
if (placeholder) {
|
|
49449
|
+
task.status = placeholder.status;
|
|
49450
|
+
if (placeholder.subject !== `Task #${match[1]}`)
|
|
49451
|
+
task.subject = placeholder.subject;
|
|
49452
|
+
if (placeholder.activeForm && !task.activeForm)
|
|
49453
|
+
task.activeForm = placeholder.activeForm;
|
|
49454
|
+
this.tasks = this.tasks.filter((t) => t !== placeholder);
|
|
49455
|
+
}
|
|
49456
|
+
task.taskId = match[1];
|
|
49457
|
+
return placeholder ? "merged" : "resolved";
|
|
49458
|
+
}
|
|
49459
|
+
consumeUnmatchedCreateResultFlag() {
|
|
49460
|
+
if (this.unmatchedCreateResults === 0)
|
|
49461
|
+
return false;
|
|
49462
|
+
this.unmatchedCreateResults = 0;
|
|
49463
|
+
return true;
|
|
49464
|
+
}
|
|
49465
|
+
update(input) {
|
|
49466
|
+
const taskId = typeof input.taskId === "string" ? input.taskId : typeof input.taskId === "number" ? String(input.taskId) : undefined;
|
|
49467
|
+
if (!taskId)
|
|
49468
|
+
return false;
|
|
49469
|
+
let task = this.tasks.find((t) => t.taskId === taskId);
|
|
49470
|
+
const status = typeof input.status === "string" ? input.status : undefined;
|
|
49471
|
+
if (status === "deleted") {
|
|
49472
|
+
if (!task)
|
|
49473
|
+
return false;
|
|
49474
|
+
this.tasks = this.tasks.filter((t) => t !== task);
|
|
49475
|
+
return true;
|
|
49476
|
+
}
|
|
49477
|
+
if (!task) {
|
|
49478
|
+
task = { taskId, subject: `Task #${taskId}`, status: "pending", isPlaceholder: true };
|
|
49479
|
+
this.tasks.push(task);
|
|
49480
|
+
}
|
|
49481
|
+
if (typeof input.subject === "string")
|
|
49482
|
+
task.subject = input.subject;
|
|
49483
|
+
if (typeof input.activeForm === "string")
|
|
49484
|
+
task.activeForm = input.activeForm;
|
|
49485
|
+
if (status === "pending" || status === "in_progress" || status === "completed") {
|
|
49486
|
+
task.status = status;
|
|
49487
|
+
}
|
|
49488
|
+
return true;
|
|
49489
|
+
}
|
|
49490
|
+
toTaskItems() {
|
|
49491
|
+
return this.tasks.map((t) => ({
|
|
49492
|
+
content: t.subject,
|
|
49493
|
+
status: t.status,
|
|
49494
|
+
activeForm: t.activeForm ?? t.subject
|
|
49495
|
+
}));
|
|
49496
|
+
}
|
|
49497
|
+
get isEmpty() {
|
|
49498
|
+
return this.tasks.length === 0;
|
|
49499
|
+
}
|
|
49500
|
+
get allCompleted() {
|
|
49501
|
+
return this.tasks.length > 0 && this.tasks.every((t) => t.status === "completed") && this.tasks.some((t) => !t.isPlaceholder);
|
|
49502
|
+
}
|
|
49503
|
+
clear() {
|
|
49504
|
+
this.tasks = [];
|
|
49505
|
+
this.pendingCreates.clear();
|
|
49506
|
+
this.unmatchedCreateResults = 0;
|
|
49507
|
+
}
|
|
49508
|
+
}
|
|
49509
|
+
|
|
49309
49510
|
// src/operations/executors/base.ts
|
|
49310
49511
|
class BaseExecutor {
|
|
49311
49512
|
state;
|
|
@@ -51367,6 +51568,7 @@ class MessageManager {
|
|
|
51367
51568
|
startTypingCallback;
|
|
51368
51569
|
emitSessionUpdateCallback;
|
|
51369
51570
|
toolStartTimes = new Map;
|
|
51571
|
+
taskTracker = new TaskTracker;
|
|
51370
51572
|
flushTimer = null;
|
|
51371
51573
|
static DEFAULT_FLUSH_DELAY_MS = 500;
|
|
51372
51574
|
flushDelayMs;
|
|
@@ -51437,10 +51639,14 @@ class MessageManager {
|
|
|
51437
51639
|
sessionId: this.sessionId,
|
|
51438
51640
|
formatter: this.platform.getFormatter(),
|
|
51439
51641
|
toolStartTimes: this.toolStartTimes,
|
|
51642
|
+
taskTracker: this.taskTracker,
|
|
51440
51643
|
detailed: true,
|
|
51441
51644
|
worktreeInfo: this.worktreePath && this.worktreeBranch ? { path: this.worktreePath, branch: this.worktreeBranch } : undefined
|
|
51442
51645
|
};
|
|
51443
51646
|
const ops = transformEvent(event, transformCtx);
|
|
51647
|
+
if (this.taskTracker.consumeUnmatchedCreateResultFlag()) {
|
|
51648
|
+
logger.warn('TaskCreate result did not match the expected "Task #N created" wording — ' + "task dropped from the displayed list. If this repeats, the Claude CLI " + "likely changed its result text and the task display will be incomplete.");
|
|
51649
|
+
}
|
|
51444
51650
|
if (ops.length === 0) {
|
|
51445
51651
|
if (event.type !== "system") {
|
|
51446
51652
|
logger.debug(`No operations from event: ${event.type}`);
|
|
@@ -51824,9 +52030,14 @@ class MessageManager {
|
|
|
51824
52030
|
logger.debug("Reaction not handled by any executor");
|
|
51825
52031
|
return false;
|
|
51826
52032
|
}
|
|
52033
|
+
clearClaudeSessionState() {
|
|
52034
|
+
this.toolStartTimes.clear();
|
|
52035
|
+
this.taskTracker.clear();
|
|
52036
|
+
}
|
|
51827
52037
|
reset() {
|
|
51828
52038
|
this.cancelScheduledFlush();
|
|
51829
52039
|
this.toolStartTimes.clear();
|
|
52040
|
+
this.taskTracker.clear();
|
|
51830
52041
|
this.contentExecutor.reset();
|
|
51831
52042
|
this.taskListExecutor.reset();
|
|
51832
52043
|
this.questionApprovalExecutor.reset();
|
|
@@ -55629,6 +55840,25 @@ function extractResetAt(text, now) {
|
|
|
55629
55840
|
}
|
|
55630
55841
|
return;
|
|
55631
55842
|
}
|
|
55843
|
+
function parseRateLimitEvent(event, now = Date.now()) {
|
|
55844
|
+
const info = event?.rate_limit_info;
|
|
55845
|
+
if (!info || typeof info !== "object")
|
|
55846
|
+
return { detected: false };
|
|
55847
|
+
const { status, resetsAt } = info;
|
|
55848
|
+
if (status !== "rejected")
|
|
55849
|
+
return { detected: false };
|
|
55850
|
+
const PAST_SKEW_TOLERANCE_MS = 2 * 60000;
|
|
55851
|
+
let resetAtEpochMs;
|
|
55852
|
+
if (typeof resetsAt === "number") {
|
|
55853
|
+
const ms = resetsAt * 1000;
|
|
55854
|
+
if (ms > now && ms - now < 8 * 86400000) {
|
|
55855
|
+
resetAtEpochMs = ms;
|
|
55856
|
+
} else if (ms <= now && now - ms < PAST_SKEW_TOLERANCE_MS) {
|
|
55857
|
+
resetAtEpochMs = now + 60000;
|
|
55858
|
+
}
|
|
55859
|
+
}
|
|
55860
|
+
return { detected: true, matched: `rate_limit_event status=${status}`, resetAtEpochMs };
|
|
55861
|
+
}
|
|
55632
55862
|
|
|
55633
55863
|
// src/claude/cli.ts
|
|
55634
55864
|
var log8 = createLogger("claude");
|
|
@@ -55765,6 +55995,7 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
55765
55995
|
stderrBuffer = "";
|
|
55766
55996
|
mcpConfigTempFile = null;
|
|
55767
55997
|
lastEmittedRateLimitDeadline = 0;
|
|
55998
|
+
lastEmittedHitHadExplicitReset = false;
|
|
55768
55999
|
log;
|
|
55769
56000
|
constructor(options) {
|
|
55770
56001
|
super();
|
|
@@ -55818,6 +56049,7 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
55818
56049
|
totalStderrBytes -= this.stderrBuffer.length;
|
|
55819
56050
|
this.stderrBuffer = "";
|
|
55820
56051
|
this.lastEmittedRateLimitDeadline = 0;
|
|
56052
|
+
this.lastEmittedHitHadExplicitReset = false;
|
|
55821
56053
|
cleanupBrowserBridgeSockets();
|
|
55822
56054
|
const claudePath = getClaudePath();
|
|
55823
56055
|
const args = [
|
|
@@ -55967,18 +56199,31 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
55967
56199
|
if (event.type === "result" && isErrorResultEvent(event)) {
|
|
55968
56200
|
this.maybeEmitRateLimit(trimmed);
|
|
55969
56201
|
}
|
|
56202
|
+
if (event.type === "rate_limit_event") {
|
|
56203
|
+
this.maybeEmitRateLimitHit(parseRateLimitEvent(event));
|
|
56204
|
+
}
|
|
55970
56205
|
} catch {}
|
|
55971
56206
|
}
|
|
55972
56207
|
}
|
|
55973
56208
|
maybeEmitRateLimit(text) {
|
|
55974
|
-
|
|
56209
|
+
this.maybeEmitRateLimitHit(detectRateLimit(text));
|
|
56210
|
+
}
|
|
56211
|
+
maybeEmitRateLimitHit(hit) {
|
|
55975
56212
|
if (!hit.detected)
|
|
55976
56213
|
return;
|
|
56214
|
+
if (!hit.resetAtEpochMs && this.lastEmittedHitHadExplicitReset && this.lastEmittedRateLimitDeadline > Date.now()) {
|
|
56215
|
+
return;
|
|
56216
|
+
}
|
|
55977
56217
|
const newDeadline = cooldownDeadline(hit);
|
|
55978
56218
|
const MIN_ADVANCE_MS = 60000;
|
|
55979
|
-
if (newDeadline - this.lastEmittedRateLimitDeadline < MIN_ADVANCE_MS)
|
|
56219
|
+
if (newDeadline - this.lastEmittedRateLimitDeadline < MIN_ADVANCE_MS) {
|
|
56220
|
+
if (hit.resetAtEpochMs !== undefined) {
|
|
56221
|
+
this.lastEmittedHitHadExplicitReset = true;
|
|
56222
|
+
}
|
|
55980
56223
|
return;
|
|
56224
|
+
}
|
|
55981
56225
|
this.lastEmittedRateLimitDeadline = newDeadline;
|
|
56226
|
+
this.lastEmittedHitHadExplicitReset = hit.resetAtEpochMs !== undefined;
|
|
55982
56227
|
this.log.warn(`Rate limit detected: ${hit.matched ?? "(no match text)"}`);
|
|
55983
56228
|
this.emit("rate-limit", hit);
|
|
55984
56229
|
}
|
|
@@ -56033,12 +56278,22 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
56033
56278
|
proc.kill("SIGTERM");
|
|
56034
56279
|
} catch {}
|
|
56035
56280
|
}, 2000);
|
|
56036
|
-
|
|
56037
|
-
this.log.debug(`Claude process
|
|
56281
|
+
const settle = (reason) => {
|
|
56282
|
+
this.log.debug(`Claude process gone (${reason})`);
|
|
56038
56283
|
clearTimeout(secondSigint);
|
|
56039
56284
|
clearTimeout(forceKillTimeout);
|
|
56285
|
+
clearTimeout(lastResort);
|
|
56040
56286
|
resolve5();
|
|
56041
|
-
}
|
|
56287
|
+
};
|
|
56288
|
+
const lastResort = setTimeout(() => {
|
|
56289
|
+
try {
|
|
56290
|
+
this.log.warn("Claude process did not exit after SIGTERM — sending SIGKILL");
|
|
56291
|
+
proc.kill("SIGKILL");
|
|
56292
|
+
} catch {}
|
|
56293
|
+
settle("kill timeout");
|
|
56294
|
+
}, 5000);
|
|
56295
|
+
proc.once("close", (code) => settle(`closed, code=${code}`));
|
|
56296
|
+
proc.once("error", () => settle("spawn error"));
|
|
56042
56297
|
});
|
|
56043
56298
|
}
|
|
56044
56299
|
interrupt() {
|
|
@@ -58181,6 +58436,10 @@ async function handlePermissionWith(toolName, toolInput, cfg) {
|
|
|
58181
58436
|
mcpLogger.debug(`Skipping standard prompt for ${toolName} (handler enforces its own gate)`);
|
|
58182
58437
|
return { behavior: "allow", updatedInput: toolInput };
|
|
58183
58438
|
}
|
|
58439
|
+
if (toolName === "AskUserQuestion") {
|
|
58440
|
+
mcpLogger.debug("Auto-allowing AskUserQuestion (question UI is handled by the main bot)");
|
|
58441
|
+
return { behavior: "allow", updatedInput: toolInput };
|
|
58442
|
+
}
|
|
58184
58443
|
if (cfg.getAllowAll()) {
|
|
58185
58444
|
mcpLogger.debug(`Auto-allowing ${toolName} (allow all active)`);
|
|
58186
58445
|
return { behavior: "allow", updatedInput: toolInput };
|
package/package.json
CHANGED