claude-threads 1.20.1 → 1.21.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/CHANGELOG.md +33 -0
- package/dist/index.js +369 -57
- package/dist/mcp/mcp-server.js +291 -26
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,39 @@ 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.1] - 2026-08-07
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **A respawning session can no longer be torn down by its own dying Claude process.** Two stacked races made `!cd` / `!permissions` / worktree respawns flaky (seen as the recurring `!cd should restart Claude CLI` failure on main's Integration Tests): a last event flushed by the dying process ran `resetSessionActivity`, which unconditionally flipped the session's `restarting` state back to `active` — so when the old process's exit landed, `handleExit` mistook it for the current process dying and did a full session teardown mid-restart. `resetSessionActivity` now leaves `restarting`/`cancelling` states alone, `handleExit` ignores exits from a CLI instance that is no longer the session's current one (the exit event can be delivered after the respawn already swapped in the new instance), and a successful respawn transitions to `active` explicitly instead of relying on the old exit's side effect. `ClaudeCli.kill()` gained an integration-test caller trace, mirroring the existing `sendMessage` one — attributing kills is the key question when debugging these races in CI logs.
|
|
12
|
+
- **`checkGitHubCli` unit tests no longer spawn the real `gh` CLI.** The two subprocess probes each carry a 5s timeout, overrunning bun's 5s per-test budget on a slow runner (the flaky `checkGitHubCli` timeout on main's CI). The exec call is now injectable and the tests cover all three outcomes (installed+authenticated, not installed, not authenticated) deterministically.
|
|
13
|
+
- **Integration: the `!cd` confirmation wait no longer matches the user's own message.** The loose `/changed|directory|\/tmp/i` pattern matched the `!cd /tmp` command itself, ending the test while the respawn was still in flight — `afterEach`'s `killAllSessions` then raced the restart and leaked an orphaned mock CLI into the next test.
|
|
14
|
+
|
|
15
|
+
## [1.21.0] - 2026-08-07
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **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.
|
|
19
|
+
- **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.
|
|
20
|
+
- **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.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **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.
|
|
24
|
+
- **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.
|
|
25
|
+
|
|
26
|
+
### Hardening
|
|
27
|
+
Three adversarial review rounds ran over this change; everything they confirmed is fixed here, with red-green tests.
|
|
28
|
+
|
|
29
|
+
- **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.)
|
|
30
|
+
- **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`.
|
|
31
|
+
- **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.
|
|
32
|
+
- **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.
|
|
33
|
+
- **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.
|
|
34
|
+
- **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.
|
|
35
|
+
- **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.
|
|
36
|
+
- **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.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- **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`).
|
|
40
|
+
|
|
8
41
|
## [1.20.1] - 2026-08-06
|
|
9
42
|
|
|
10
43
|
### Added
|
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
|
}
|
|
@@ -56307,6 +56341,12 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
56307
56341
|
const pid = proc.pid;
|
|
56308
56342
|
this.process = null;
|
|
56309
56343
|
this.log.debug(`Killing Claude process (pid=${pid})`);
|
|
56344
|
+
if (process.env.INTEGRATION_TEST === "1") {
|
|
56345
|
+
const stack = new Error().stack?.split(`
|
|
56346
|
+
`).slice(2, 7).join(" > ").replace(/\s+at\s+/g, " < ") ?? "?";
|
|
56347
|
+
process.stderr.write(`[claude-cli kill pid=${pid}] | ${stack}
|
|
56348
|
+
`);
|
|
56349
|
+
}
|
|
56310
56350
|
return new Promise((resolve4) => {
|
|
56311
56351
|
this.log.debug("Sending first SIGINT");
|
|
56312
56352
|
proc.kill("SIGINT");
|
|
@@ -56322,12 +56362,22 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
56322
56362
|
proc.kill("SIGTERM");
|
|
56323
56363
|
} catch {}
|
|
56324
56364
|
}, 2000);
|
|
56325
|
-
|
|
56326
|
-
this.log.debug(`Claude process
|
|
56365
|
+
const settle = (reason) => {
|
|
56366
|
+
this.log.debug(`Claude process gone (${reason})`);
|
|
56327
56367
|
clearTimeout(secondSigint);
|
|
56328
56368
|
clearTimeout(forceKillTimeout);
|
|
56369
|
+
clearTimeout(lastResort);
|
|
56329
56370
|
resolve4();
|
|
56330
|
-
}
|
|
56371
|
+
};
|
|
56372
|
+
const lastResort = setTimeout(() => {
|
|
56373
|
+
try {
|
|
56374
|
+
this.log.warn("Claude process did not exit after SIGTERM — sending SIGKILL");
|
|
56375
|
+
proc.kill("SIGKILL");
|
|
56376
|
+
} catch {}
|
|
56377
|
+
settle("kill timeout");
|
|
56378
|
+
}, 5000);
|
|
56379
|
+
proc.once("close", (code) => settle(`closed, code=${code}`));
|
|
56380
|
+
proc.once("error", () => settle("spawn error"));
|
|
56331
56381
|
});
|
|
56332
56382
|
}
|
|
56333
56383
|
interrupt() {
|
|
@@ -58482,7 +58532,10 @@ function resetSessionActivity(session) {
|
|
|
58482
58532
|
session.lastActivityAt = new Date;
|
|
58483
58533
|
session.timeoutWarningPosted = false;
|
|
58484
58534
|
session.lifecyclePostId = undefined;
|
|
58485
|
-
|
|
58535
|
+
const state = session.lifecycle.state;
|
|
58536
|
+
if (state !== "restarting" && state !== "cancelling") {
|
|
58537
|
+
transitionTo(session, "active");
|
|
58538
|
+
}
|
|
58486
58539
|
if (session.worktreeInfo?.worktreePath) {
|
|
58487
58540
|
updateWorktreeActivity(session.worktreeInfo.worktreePath, session.sessionId);
|
|
58488
58541
|
}
|
|
@@ -62597,9 +62650,9 @@ _Reported via claude-threads bug report feature_`);
|
|
|
62597
62650
|
function escapeShell(str2) {
|
|
62598
62651
|
return str2.replace(/"/g, "\\\"");
|
|
62599
62652
|
}
|
|
62600
|
-
function checkGitHubCli() {
|
|
62653
|
+
function checkGitHubCli(exec2 = execSync2) {
|
|
62601
62654
|
try {
|
|
62602
|
-
|
|
62655
|
+
exec2("gh --version", {
|
|
62603
62656
|
encoding: "utf-8",
|
|
62604
62657
|
timeout: 5000,
|
|
62605
62658
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -62612,7 +62665,7 @@ function checkGitHubCli() {
|
|
|
62612
62665
|
};
|
|
62613
62666
|
}
|
|
62614
62667
|
try {
|
|
62615
|
-
|
|
62668
|
+
exec2("gh auth status", {
|
|
62616
62669
|
encoding: "utf-8",
|
|
62617
62670
|
timeout: 5000,
|
|
62618
62671
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -63986,11 +64039,26 @@ var bashToolFormatter = {
|
|
|
63986
64039
|
};
|
|
63987
64040
|
// src/operations/tool-formatters/task-tools.ts
|
|
63988
64041
|
var taskToolsFormatter = {
|
|
63989
|
-
toolNames: [
|
|
63990
|
-
|
|
64042
|
+
toolNames: [
|
|
64043
|
+
"TodoWrite",
|
|
64044
|
+
"TaskCreate",
|
|
64045
|
+
"TaskUpdate",
|
|
64046
|
+
"TaskGet",
|
|
64047
|
+
"TaskList",
|
|
64048
|
+
"Task",
|
|
64049
|
+
"EnterPlanMode",
|
|
64050
|
+
"ExitPlanMode",
|
|
64051
|
+
"AskUserQuestion"
|
|
64052
|
+
],
|
|
64053
|
+
format(toolName, input, options) {
|
|
63991
64054
|
const { formatter } = options;
|
|
63992
64055
|
switch (toolName) {
|
|
63993
64056
|
case "TodoWrite":
|
|
64057
|
+
case "TaskCreate":
|
|
64058
|
+
case "TaskUpdate":
|
|
64059
|
+
return { display: null, hidden: true };
|
|
64060
|
+
case "TaskGet":
|
|
64061
|
+
case "TaskList":
|
|
63994
64062
|
return { display: null, hidden: true };
|
|
63995
64063
|
case "Task":
|
|
63996
64064
|
return { display: null, hidden: true };
|
|
@@ -63999,8 +64067,25 @@ var taskToolsFormatter = {
|
|
|
63999
64067
|
display: `\uD83D\uDCCB ${formatter.formatBold("Planning...")}`,
|
|
64000
64068
|
permissionText: `\uD83D\uDCCB ${formatter.formatBold("Planning...")}`
|
|
64001
64069
|
};
|
|
64002
|
-
case "ExitPlanMode":
|
|
64003
|
-
|
|
64070
|
+
case "ExitPlanMode": {
|
|
64071
|
+
const plan = typeof input.plan === "string" ? input.plan : "";
|
|
64072
|
+
const points = Array.from(plan);
|
|
64073
|
+
const truncated = points.length > 1500;
|
|
64074
|
+
let preview = truncated ? `${points.slice(0, 1500).join("")}
|
|
64075
|
+
…` : plan;
|
|
64076
|
+
if (truncated) {
|
|
64077
|
+
const fenceCount = (preview.match(/^```/gm) || []).length;
|
|
64078
|
+
if (fenceCount % 2 === 1)
|
|
64079
|
+
preview += "\n```";
|
|
64080
|
+
}
|
|
64081
|
+
return {
|
|
64082
|
+
display: null,
|
|
64083
|
+
hidden: true,
|
|
64084
|
+
permissionText: preview ? `\uD83D\uDCCB ${formatter.formatBold("Plan approval requested")}
|
|
64085
|
+
|
|
64086
|
+
${preview}` : `\uD83D\uDCCB ${formatter.formatBold("Plan approval requested")}`
|
|
64087
|
+
};
|
|
64088
|
+
}
|
|
64004
64089
|
case "AskUserQuestion":
|
|
64005
64090
|
return { display: null, hidden: true };
|
|
64006
64091
|
default:
|
|
@@ -64539,9 +64624,14 @@ function createStatusUpdateOp(sessionId, options) {
|
|
|
64539
64624
|
}
|
|
64540
64625
|
// src/operations/transformer.ts
|
|
64541
64626
|
function transformEvent(event, ctx) {
|
|
64627
|
+
if (event.parent_tool_use_id) {
|
|
64628
|
+
return [];
|
|
64629
|
+
}
|
|
64542
64630
|
switch (event.type) {
|
|
64543
64631
|
case "assistant":
|
|
64544
64632
|
return transformAssistant(event, ctx);
|
|
64633
|
+
case "user":
|
|
64634
|
+
return transformUser(event, ctx);
|
|
64545
64635
|
case "tool_use":
|
|
64546
64636
|
return transformToolUse(event, ctx);
|
|
64547
64637
|
case "tool_result":
|
|
@@ -64583,6 +64673,9 @@ function transformAssistant(event, ctx) {
|
|
|
64583
64673
|
if (result.display && !result.hidden) {
|
|
64584
64674
|
flushTextBuffer();
|
|
64585
64675
|
operations.push(createAppendContentOp(ctx.sessionId, result.display, true));
|
|
64676
|
+
if (block.id) {
|
|
64677
|
+
ctx.toolStartTimes.set(block.id, Date.now());
|
|
64678
|
+
}
|
|
64586
64679
|
}
|
|
64587
64680
|
}
|
|
64588
64681
|
} else if (block.type === "thinking" && block.thinking) {
|
|
@@ -64597,8 +64690,70 @@ function transformAssistant(event, ctx) {
|
|
|
64597
64690
|
}
|
|
64598
64691
|
}
|
|
64599
64692
|
flushTextBuffer();
|
|
64693
|
+
const taskListOps = operations.filter((op) => op.type === "task_list");
|
|
64694
|
+
if (taskListOps.length > 1) {
|
|
64695
|
+
const last = taskListOps[taskListOps.length - 1];
|
|
64696
|
+
return operations.filter((op) => op.type !== "task_list" || op === last);
|
|
64697
|
+
}
|
|
64698
|
+
return operations;
|
|
64699
|
+
}
|
|
64700
|
+
function transformUser(event, ctx) {
|
|
64701
|
+
const msg = event.message;
|
|
64702
|
+
if (!Array.isArray(msg?.content)) {
|
|
64703
|
+
return [];
|
|
64704
|
+
}
|
|
64705
|
+
const operations = [];
|
|
64706
|
+
let indicatorCount = 0;
|
|
64707
|
+
for (const block of msg.content) {
|
|
64708
|
+
if (block?.type !== "tool_result" || !block.tool_use_id)
|
|
64709
|
+
continue;
|
|
64710
|
+
if (ctx.taskTracker.hasPendingCreate(block.tool_use_id)) {
|
|
64711
|
+
const resolution = ctx.taskTracker.resolveCreatedId(block.tool_use_id, toolResultContentText(block.content), block.is_error === true);
|
|
64712
|
+
if (resolution === "removed" || resolution === "merged") {
|
|
64713
|
+
const action = ctx.taskTracker.allCompleted ? "complete" : "update";
|
|
64714
|
+
operations.push(createTaskListOp(ctx.sessionId, action, ctx.taskTracker.toTaskItems()));
|
|
64715
|
+
}
|
|
64716
|
+
}
|
|
64717
|
+
if (ctx.toolStartTimes.has(block.tool_use_id)) {
|
|
64718
|
+
operations.push(createResultIndicatorOp(block.tool_use_id, block.is_error === true, ctx));
|
|
64719
|
+
indicatorCount++;
|
|
64720
|
+
}
|
|
64721
|
+
}
|
|
64722
|
+
const taskListOps = operations.filter((op) => op.type === "task_list");
|
|
64723
|
+
if (taskListOps.length > 1) {
|
|
64724
|
+
const last = taskListOps[taskListOps.length - 1];
|
|
64725
|
+
const coalesced = operations.filter((op) => op.type !== "task_list" || op === last);
|
|
64726
|
+
operations.length = 0;
|
|
64727
|
+
operations.push(...coalesced);
|
|
64728
|
+
}
|
|
64729
|
+
if (indicatorCount > 0) {
|
|
64730
|
+
operations.push(createFlushOp(ctx.sessionId, "tool_complete"));
|
|
64731
|
+
}
|
|
64600
64732
|
return operations;
|
|
64601
64733
|
}
|
|
64734
|
+
function toolResultContentText(content) {
|
|
64735
|
+
if (typeof content === "string")
|
|
64736
|
+
return content;
|
|
64737
|
+
if (Array.isArray(content)) {
|
|
64738
|
+
return content.map((b) => b && typeof b === "object" && typeof b.text === "string" ? b.text : "").join(`
|
|
64739
|
+
`);
|
|
64740
|
+
}
|
|
64741
|
+
return "";
|
|
64742
|
+
}
|
|
64743
|
+
function createResultIndicatorOp(toolUseId, isError, ctx) {
|
|
64744
|
+
let elapsed = "";
|
|
64745
|
+
const startTime = ctx.toolStartTimes.get(toolUseId);
|
|
64746
|
+
if (startTime) {
|
|
64747
|
+
const secs = Math.round((Date.now() - startTime) / 1000);
|
|
64748
|
+
if (secs >= 3) {
|
|
64749
|
+
elapsed = ` (${secs}s)`;
|
|
64750
|
+
}
|
|
64751
|
+
ctx.toolStartTimes.delete(toolUseId);
|
|
64752
|
+
}
|
|
64753
|
+
const icon = isError ? "❌" : "✓";
|
|
64754
|
+
const errorNote = isError ? " Error" : "";
|
|
64755
|
+
return createAppendContentOp(ctx.sessionId, ` ↳ ${icon}${errorNote}${elapsed}`, true);
|
|
64756
|
+
}
|
|
64602
64757
|
function transformToolUse(event, ctx) {
|
|
64603
64758
|
const tool = event.tool_use;
|
|
64604
64759
|
if (tool.id) {
|
|
@@ -64623,23 +64778,10 @@ function transformToolResult(event, ctx) {
|
|
|
64623
64778
|
return [];
|
|
64624
64779
|
}
|
|
64625
64780
|
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;
|
|
64781
|
+
return [
|
|
64782
|
+
createResultIndicatorOp(result.tool_use_id || "", result.is_error === true, ctx),
|
|
64783
|
+
createFlushOp(ctx.sessionId, "tool_complete")
|
|
64784
|
+
];
|
|
64643
64785
|
}
|
|
64644
64786
|
function transformResult(event, ctx) {
|
|
64645
64787
|
const operations = [];
|
|
@@ -64656,6 +64798,10 @@ function handleSpecialTool(toolName, toolUseId, input, ctx) {
|
|
|
64656
64798
|
switch (toolName) {
|
|
64657
64799
|
case "TodoWrite":
|
|
64658
64800
|
return handleTodoWrite(input, ctx);
|
|
64801
|
+
case "TaskCreate":
|
|
64802
|
+
return handleTaskCreate(toolUseId, input, ctx);
|
|
64803
|
+
case "TaskUpdate":
|
|
64804
|
+
return handleTaskUpdate(input, ctx);
|
|
64659
64805
|
case "Task":
|
|
64660
64806
|
return handleTaskStart(toolUseId, input, ctx);
|
|
64661
64807
|
case "AskUserQuestion":
|
|
@@ -64673,10 +64819,22 @@ function handleTodoWrite(input, ctx) {
|
|
|
64673
64819
|
status: t.status,
|
|
64674
64820
|
activeForm: t.activeForm
|
|
64675
64821
|
}));
|
|
64822
|
+
ctx.taskTracker.clear();
|
|
64676
64823
|
const allCompleted = tasks.every((t) => t.status === "completed");
|
|
64677
64824
|
const action = allCompleted ? "complete" : "update";
|
|
64678
64825
|
return [createTaskListOp(ctx.sessionId, action, tasks)];
|
|
64679
64826
|
}
|
|
64827
|
+
function handleTaskCreate(toolUseId, input, ctx) {
|
|
64828
|
+
ctx.taskTracker.create(toolUseId, input);
|
|
64829
|
+
return [createTaskListOp(ctx.sessionId, "update", ctx.taskTracker.toTaskItems())];
|
|
64830
|
+
}
|
|
64831
|
+
function handleTaskUpdate(input, ctx) {
|
|
64832
|
+
const changed = ctx.taskTracker.update(input);
|
|
64833
|
+
if (!changed)
|
|
64834
|
+
return [];
|
|
64835
|
+
const action = ctx.taskTracker.allCompleted ? "complete" : "update";
|
|
64836
|
+
return [createTaskListOp(ctx.sessionId, action, ctx.taskTracker.toTaskItems())];
|
|
64837
|
+
}
|
|
64680
64838
|
function handleTaskStart(toolUseId, input, ctx) {
|
|
64681
64839
|
const description = input.description || input.prompt || "Subagent";
|
|
64682
64840
|
const subagentType = input.subagent_type || "general-purpose";
|
|
@@ -64710,6 +64868,102 @@ function truncateAtWord2(text, maxLength) {
|
|
|
64710
64868
|
}
|
|
64711
64869
|
return truncated + "...";
|
|
64712
64870
|
}
|
|
64871
|
+
// src/operations/task-tracker.ts
|
|
64872
|
+
var CREATED_RESULT_RE = /Task #(\S+) created/;
|
|
64873
|
+
|
|
64874
|
+
class TaskTracker {
|
|
64875
|
+
tasks = [];
|
|
64876
|
+
pendingCreates = new Map;
|
|
64877
|
+
unmatchedCreateResults = 0;
|
|
64878
|
+
create(toolUseId, input) {
|
|
64879
|
+
const task = {
|
|
64880
|
+
subject: typeof input.subject === "string" ? input.subject : "Task",
|
|
64881
|
+
activeForm: typeof input.activeForm === "string" ? input.activeForm : undefined,
|
|
64882
|
+
status: "pending"
|
|
64883
|
+
};
|
|
64884
|
+
this.tasks.push(task);
|
|
64885
|
+
if (toolUseId) {
|
|
64886
|
+
this.pendingCreates.set(toolUseId, task);
|
|
64887
|
+
}
|
|
64888
|
+
}
|
|
64889
|
+
hasPendingCreate(toolUseId) {
|
|
64890
|
+
return this.pendingCreates.has(toolUseId);
|
|
64891
|
+
}
|
|
64892
|
+
resolveCreatedId(toolUseId, resultContent, isError = false) {
|
|
64893
|
+
const task = this.pendingCreates.get(toolUseId);
|
|
64894
|
+
if (!task)
|
|
64895
|
+
return "ignored";
|
|
64896
|
+
this.pendingCreates.delete(toolUseId);
|
|
64897
|
+
const match = isError ? null : CREATED_RESULT_RE.exec(resultContent);
|
|
64898
|
+
if (!match) {
|
|
64899
|
+
if (!isError)
|
|
64900
|
+
this.unmatchedCreateResults++;
|
|
64901
|
+
this.tasks = this.tasks.filter((t) => t !== task);
|
|
64902
|
+
return "removed";
|
|
64903
|
+
}
|
|
64904
|
+
const placeholder = this.tasks.find((t) => t !== task && t.taskId === match[1]);
|
|
64905
|
+
if (placeholder) {
|
|
64906
|
+
task.status = placeholder.status;
|
|
64907
|
+
if (placeholder.subject !== `Task #${match[1]}`)
|
|
64908
|
+
task.subject = placeholder.subject;
|
|
64909
|
+
if (placeholder.activeForm && !task.activeForm)
|
|
64910
|
+
task.activeForm = placeholder.activeForm;
|
|
64911
|
+
this.tasks = this.tasks.filter((t) => t !== placeholder);
|
|
64912
|
+
}
|
|
64913
|
+
task.taskId = match[1];
|
|
64914
|
+
return placeholder ? "merged" : "resolved";
|
|
64915
|
+
}
|
|
64916
|
+
consumeUnmatchedCreateResultFlag() {
|
|
64917
|
+
if (this.unmatchedCreateResults === 0)
|
|
64918
|
+
return false;
|
|
64919
|
+
this.unmatchedCreateResults = 0;
|
|
64920
|
+
return true;
|
|
64921
|
+
}
|
|
64922
|
+
update(input) {
|
|
64923
|
+
const taskId = typeof input.taskId === "string" ? input.taskId : typeof input.taskId === "number" ? String(input.taskId) : undefined;
|
|
64924
|
+
if (!taskId)
|
|
64925
|
+
return false;
|
|
64926
|
+
let task = this.tasks.find((t) => t.taskId === taskId);
|
|
64927
|
+
const status = typeof input.status === "string" ? input.status : undefined;
|
|
64928
|
+
if (status === "deleted") {
|
|
64929
|
+
if (!task)
|
|
64930
|
+
return false;
|
|
64931
|
+
this.tasks = this.tasks.filter((t) => t !== task);
|
|
64932
|
+
return true;
|
|
64933
|
+
}
|
|
64934
|
+
if (!task) {
|
|
64935
|
+
task = { taskId, subject: `Task #${taskId}`, status: "pending", isPlaceholder: true };
|
|
64936
|
+
this.tasks.push(task);
|
|
64937
|
+
}
|
|
64938
|
+
if (typeof input.subject === "string")
|
|
64939
|
+
task.subject = input.subject;
|
|
64940
|
+
if (typeof input.activeForm === "string")
|
|
64941
|
+
task.activeForm = input.activeForm;
|
|
64942
|
+
if (status === "pending" || status === "in_progress" || status === "completed") {
|
|
64943
|
+
task.status = status;
|
|
64944
|
+
}
|
|
64945
|
+
return true;
|
|
64946
|
+
}
|
|
64947
|
+
toTaskItems() {
|
|
64948
|
+
return this.tasks.map((t) => ({
|
|
64949
|
+
content: t.subject,
|
|
64950
|
+
status: t.status,
|
|
64951
|
+
activeForm: t.activeForm ?? t.subject
|
|
64952
|
+
}));
|
|
64953
|
+
}
|
|
64954
|
+
get isEmpty() {
|
|
64955
|
+
return this.tasks.length === 0;
|
|
64956
|
+
}
|
|
64957
|
+
get allCompleted() {
|
|
64958
|
+
return this.tasks.length > 0 && this.tasks.every((t) => t.status === "completed") && this.tasks.some((t) => !t.isPlaceholder);
|
|
64959
|
+
}
|
|
64960
|
+
clear() {
|
|
64961
|
+
this.tasks = [];
|
|
64962
|
+
this.pendingCreates.clear();
|
|
64963
|
+
this.unmatchedCreateResults = 0;
|
|
64964
|
+
}
|
|
64965
|
+
}
|
|
64966
|
+
|
|
64713
64967
|
// src/operations/executors/base.ts
|
|
64714
64968
|
class BaseExecutor {
|
|
64715
64969
|
state;
|
|
@@ -66462,6 +66716,7 @@ class MessageManager {
|
|
|
66462
66716
|
startTypingCallback;
|
|
66463
66717
|
emitSessionUpdateCallback;
|
|
66464
66718
|
toolStartTimes = new Map;
|
|
66719
|
+
taskTracker = new TaskTracker;
|
|
66465
66720
|
flushTimer = null;
|
|
66466
66721
|
static DEFAULT_FLUSH_DELAY_MS = 500;
|
|
66467
66722
|
flushDelayMs;
|
|
@@ -66532,10 +66787,14 @@ class MessageManager {
|
|
|
66532
66787
|
sessionId: this.sessionId,
|
|
66533
66788
|
formatter: this.platform.getFormatter(),
|
|
66534
66789
|
toolStartTimes: this.toolStartTimes,
|
|
66790
|
+
taskTracker: this.taskTracker,
|
|
66535
66791
|
detailed: true,
|
|
66536
66792
|
worktreeInfo: this.worktreePath && this.worktreeBranch ? { path: this.worktreePath, branch: this.worktreeBranch } : undefined
|
|
66537
66793
|
};
|
|
66538
66794
|
const ops = transformEvent(event, transformCtx);
|
|
66795
|
+
if (this.taskTracker.consumeUnmatchedCreateResultFlag()) {
|
|
66796
|
+
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.");
|
|
66797
|
+
}
|
|
66539
66798
|
if (ops.length === 0) {
|
|
66540
66799
|
if (event.type !== "system") {
|
|
66541
66800
|
logger.debug(`No operations from event: ${event.type}`);
|
|
@@ -66919,9 +67178,14 @@ class MessageManager {
|
|
|
66919
67178
|
logger.debug("Reaction not handled by any executor");
|
|
66920
67179
|
return false;
|
|
66921
67180
|
}
|
|
67181
|
+
clearClaudeSessionState() {
|
|
67182
|
+
this.toolStartTimes.clear();
|
|
67183
|
+
this.taskTracker.clear();
|
|
67184
|
+
}
|
|
66922
67185
|
reset() {
|
|
66923
67186
|
this.cancelScheduledFlush();
|
|
66924
67187
|
this.toolStartTimes.clear();
|
|
67188
|
+
this.taskTracker.clear();
|
|
66925
67189
|
this.contentExecutor.reset();
|
|
66926
67190
|
this.taskListExecutor.reset();
|
|
66927
67191
|
this.questionApprovalExecutor.reset();
|
|
@@ -67957,7 +68221,7 @@ async function createAndSwitchToWorktree(session, branch, username, options) {
|
|
|
67957
68221
|
if (session.claude.isRunning()) {
|
|
67958
68222
|
options.stopTyping(session);
|
|
67959
68223
|
transitionTo(session, "restarting");
|
|
67960
|
-
session.claude.kill();
|
|
68224
|
+
await session.claude.kill();
|
|
67961
68225
|
await options.flush(session);
|
|
67962
68226
|
const newSessionId = randomUUID2();
|
|
67963
68227
|
session.claudeSessionId = newSessionId;
|
|
@@ -67977,10 +68241,15 @@ async function createAndSwitchToWorktree(session, branch, username, options) {
|
|
|
67977
68241
|
resume: false,
|
|
67978
68242
|
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
68243
|
};
|
|
67980
|
-
session.
|
|
67981
|
-
|
|
67982
|
-
session.claude
|
|
67983
|
-
|
|
68244
|
+
session.messageManager?.clearClaudeSessionState();
|
|
68245
|
+
const newClaude = new ClaudeCli(cliOptions);
|
|
68246
|
+
session.claude = newClaude;
|
|
68247
|
+
newClaude.on("event", (e) => options.handleEvent(session.sessionId, e));
|
|
68248
|
+
newClaude.on("exit", (code) => options.handleExit(session.sessionId, code, newClaude));
|
|
68249
|
+
newClaude.start();
|
|
68250
|
+
if (isSessionRestarting(session)) {
|
|
68251
|
+
transitionTo(session, "active");
|
|
68252
|
+
}
|
|
67984
68253
|
}
|
|
67985
68254
|
await options.updateSessionHeader(session);
|
|
67986
68255
|
await post(session, "success", `${fmt.formatBold("Joined existing worktree")} for branch ${fmt.formatCode(branch)}
|
|
@@ -68049,7 +68318,7 @@ ${fmt.formatItalic("Claude Code restarted in the worktree")}`);
|
|
|
68049
68318
|
if (session.claude.isRunning()) {
|
|
68050
68319
|
options.stopTyping(session);
|
|
68051
68320
|
transitionTo(session, "restarting");
|
|
68052
|
-
session.claude.kill();
|
|
68321
|
+
await session.claude.kill();
|
|
68053
68322
|
await options.flush(session);
|
|
68054
68323
|
const newSessionId = randomUUID2();
|
|
68055
68324
|
session.claudeSessionId = newSessionId;
|
|
@@ -68069,10 +68338,15 @@ ${fmt.formatItalic("Claude Code restarted in the worktree")}`);
|
|
|
68069
68338
|
resume: false,
|
|
68070
68339
|
appendSystemPrompt: await buildAppendSystemPrompt(session.platform, session.platformId, worktreePath, session.threadId, session.startedBy, session.sessionAllowedUsers, options.appendSystemPrompt ?? "", options.githubEmailsStore, { omitSessionContext: !needsTitlePrompt, userAttribution: session.userAttribution })
|
|
68071
68340
|
};
|
|
68072
|
-
session.
|
|
68073
|
-
|
|
68074
|
-
session.claude
|
|
68075
|
-
|
|
68341
|
+
session.messageManager?.clearClaudeSessionState();
|
|
68342
|
+
const newClaude = new ClaudeCli(cliOptions);
|
|
68343
|
+
session.claude = newClaude;
|
|
68344
|
+
newClaude.on("event", (e) => options.handleEvent(session.sessionId, e));
|
|
68345
|
+
newClaude.on("exit", (code) => options.handleExit(session.sessionId, code, newClaude));
|
|
68346
|
+
newClaude.start();
|
|
68347
|
+
if (isSessionRestarting(session)) {
|
|
68348
|
+
transitionTo(session, "active");
|
|
68349
|
+
}
|
|
68076
68350
|
}
|
|
68077
68351
|
await options.updateSessionHeader(session);
|
|
68078
68352
|
const shortWorktreePath = shortenPath(worktreePath, undefined, { path: worktreePath, branch });
|
|
@@ -68342,6 +68616,9 @@ function extractAndUpdatePullRequest(text, session, ctx) {
|
|
|
68342
68616
|
ctx.ops.updateSessionHeader(session).catch(() => {});
|
|
68343
68617
|
}
|
|
68344
68618
|
}
|
|
68619
|
+
function isSidechainEvent(event) {
|
|
68620
|
+
return Boolean(event.parent_tool_use_id);
|
|
68621
|
+
}
|
|
68345
68622
|
function handleEventPreProcessing(session, event, ctx) {
|
|
68346
68623
|
session.threadLogger?.logEvent(event);
|
|
68347
68624
|
resetSessionActivity(session);
|
|
@@ -68363,6 +68640,16 @@ function handleEventPreProcessing(session, event, ctx) {
|
|
|
68363
68640
|
handleCompactionComplete(session, e.compact_metadata, ctx);
|
|
68364
68641
|
}
|
|
68365
68642
|
}
|
|
68643
|
+
if (event.type === "assistant" && !isSidechainEvent(event)) {
|
|
68644
|
+
const msg = event.message;
|
|
68645
|
+
if (Array.isArray(msg?.content)) {
|
|
68646
|
+
for (const block of msg.content) {
|
|
68647
|
+
if (block.type === "tool_use" && block.name) {
|
|
68648
|
+
trackEvent(session, "tool_use", block.name);
|
|
68649
|
+
}
|
|
68650
|
+
}
|
|
68651
|
+
}
|
|
68652
|
+
}
|
|
68366
68653
|
if (event.type === "tool_use") {
|
|
68367
68654
|
const tool = event.tool_use;
|
|
68368
68655
|
trackEvent(session, "tool_use", tool.name);
|
|
@@ -68374,7 +68661,9 @@ function handleEventPostProcessing(session, event, ctx) {
|
|
|
68374
68661
|
for (const block of msg?.content || []) {
|
|
68375
68662
|
if (block.type === "text" && block.text) {
|
|
68376
68663
|
extractAndUpdatePullRequest(block.text, session, ctx);
|
|
68377
|
-
|
|
68664
|
+
if (!isSidechainEvent(event)) {
|
|
68665
|
+
detectAndExecuteClaudeCommands(block.text, session, ctx);
|
|
68666
|
+
}
|
|
68378
68667
|
}
|
|
68379
68668
|
}
|
|
68380
68669
|
}
|
|
@@ -68384,6 +68673,16 @@ function handleEventPostProcessing(session, event, ctx) {
|
|
|
68384
68673
|
ctx.ops.emitSessionUpdate(session.sessionId, { status: getSessionStatus(session) });
|
|
68385
68674
|
updateUsageStats(session, event, ctx);
|
|
68386
68675
|
}
|
|
68676
|
+
if (event.type === "user" && !isSidechainEvent(event)) {
|
|
68677
|
+
const msg = event.message;
|
|
68678
|
+
if (Array.isArray(msg?.content)) {
|
|
68679
|
+
for (const block of msg.content) {
|
|
68680
|
+
if (block.type === "tool_result" && block.is_error) {
|
|
68681
|
+
trackEvent(session, "tool_error", "Tool execution failed");
|
|
68682
|
+
}
|
|
68683
|
+
}
|
|
68684
|
+
}
|
|
68685
|
+
}
|
|
68387
68686
|
if (event.type === "tool_result") {
|
|
68388
68687
|
const result = event.tool_result;
|
|
68389
68688
|
if (result.is_error) {
|
|
@@ -68943,17 +69242,26 @@ function commonRestartCliOptions(session, ctx) {
|
|
|
68943
69242
|
async function restartClaudeSession(session, cliOptions, ctx, actionName) {
|
|
68944
69243
|
ctx.ops.stopTyping(session);
|
|
68945
69244
|
transitionTo(session, "restarting");
|
|
68946
|
-
session.claude.kill();
|
|
69245
|
+
await session.claude.kill();
|
|
68947
69246
|
await ctx.ops.flush(session);
|
|
68948
|
-
|
|
68949
|
-
|
|
68950
|
-
|
|
68951
|
-
|
|
69247
|
+
if (!cliOptions.resume) {
|
|
69248
|
+
session.messageManager?.clearClaudeSessionState();
|
|
69249
|
+
}
|
|
69250
|
+
const newClaude = new ClaudeCli(cliOptions);
|
|
69251
|
+
session.claude = newClaude;
|
|
69252
|
+
newClaude.on("event", (e) => ctx.ops.handleEvent(session.sessionId, e));
|
|
69253
|
+
newClaude.on("exit", (code) => ctx.ops.handleExit(session.sessionId, code, newClaude));
|
|
69254
|
+
newClaude.on("rate-limit", (hit) => handleRateLimit(session, hit, ctx));
|
|
68952
69255
|
try {
|
|
68953
|
-
|
|
69256
|
+
newClaude.start();
|
|
69257
|
+
if (isSessionRestarting(session)) {
|
|
69258
|
+
transitionTo(session, "active");
|
|
69259
|
+
}
|
|
68954
69260
|
return true;
|
|
68955
69261
|
} catch (err) {
|
|
68956
|
-
|
|
69262
|
+
if (isSessionRestarting(session)) {
|
|
69263
|
+
transitionTo(session, "active");
|
|
69264
|
+
}
|
|
68957
69265
|
await logAndNotify(err, { action: actionName, session });
|
|
68958
69266
|
return false;
|
|
68959
69267
|
}
|
|
@@ -70066,7 +70374,7 @@ async function startSession(options, username, displayName, replyToPostId, platf
|
|
|
70066
70374
|
await ctx.ops.updateStickyMessage();
|
|
70067
70375
|
ctx.ops.startTyping(session);
|
|
70068
70376
|
claude.on("event", (e) => ctx.ops.handleEvent(sessionId, e));
|
|
70069
|
-
claude.on("exit", (code) => ctx.ops.handleExit(sessionId, code));
|
|
70377
|
+
claude.on("exit", (code) => ctx.ops.handleExit(sessionId, code, claude));
|
|
70070
70378
|
claude.on("rate-limit", (hit) => handleRateLimit(session, hit, ctx));
|
|
70071
70379
|
try {
|
|
70072
70380
|
claude.start();
|
|
@@ -70261,7 +70569,7 @@ Please start a new session.`), { action: "Post resume failure notification" });
|
|
|
70261
70569
|
ctx.ops.emitSessionAdd(session);
|
|
70262
70570
|
keepAlive.sessionStarted();
|
|
70263
70571
|
claude.on("event", (e) => ctx.ops.handleEvent(sessionId, e));
|
|
70264
|
-
claude.on("exit", (code) => ctx.ops.handleExit(sessionId, code));
|
|
70572
|
+
claude.on("exit", (code) => ctx.ops.handleExit(sessionId, code, claude));
|
|
70265
70573
|
claude.on("rate-limit", (hit) => handleRateLimit(session, hit, ctx));
|
|
70266
70574
|
try {
|
|
70267
70575
|
claude.start();
|
|
@@ -70349,7 +70657,7 @@ async function resumePausedSession(threadId, message, files, ctx, username) {
|
|
|
70349
70657
|
log31.warn(`Failed to resume session ${shortId}..., could not send message`);
|
|
70350
70658
|
}
|
|
70351
70659
|
}
|
|
70352
|
-
async function handleExit(sessionId, code, ctx) {
|
|
70660
|
+
async function handleExit(sessionId, code, ctx, source) {
|
|
70353
70661
|
const session = mutableSessions(ctx).get(sessionId);
|
|
70354
70662
|
const shortId = sessionId.substring(0, 8);
|
|
70355
70663
|
sessionLog6(session).debug(`handleExit called code=${code} isShuttingDown=${ctx.state.isShuttingDown}`);
|
|
@@ -70357,6 +70665,10 @@ async function handleExit(sessionId, code, ctx) {
|
|
|
70357
70665
|
log31.debug(`Session ${shortId}... not found (already cleaned up)`);
|
|
70358
70666
|
return;
|
|
70359
70667
|
}
|
|
70668
|
+
if (source && session.claude !== source) {
|
|
70669
|
+
sessionLog6(session).debug(`Ignoring exit from replaced Claude process`);
|
|
70670
|
+
return;
|
|
70671
|
+
}
|
|
70360
70672
|
if (isSessionRestarting(session)) {
|
|
70361
70673
|
sessionLog6(session).debug(`Restarting, skipping cleanup`);
|
|
70362
70674
|
transitionTo(session, "active");
|
|
@@ -71080,7 +71392,7 @@ class SessionManager extends EventEmitter4 {
|
|
|
71080
71392
|
updateSessionHeader: (s) => this.updateSessionHeader(s),
|
|
71081
71393
|
updateStickyMessage: () => this.updateStickyMessage(),
|
|
71082
71394
|
handleEvent: (sid, e) => this.handleEvent(sid, e),
|
|
71083
|
-
handleExit: (sid, code) => this.handleExit(sid, code),
|
|
71395
|
+
handleExit: (sid, code, source) => this.handleExit(sid, code, source),
|
|
71084
71396
|
killSession: (tid) => this.killSession(tid),
|
|
71085
71397
|
shouldPromptForWorktree: (s) => this.shouldPromptForWorktree(s),
|
|
71086
71398
|
postWorktreePrompt: (s, r) => this.postWorktreePrompt(s, r),
|
|
@@ -71184,8 +71496,8 @@ class SessionManager extends EventEmitter4 {
|
|
|
71184
71496
|
session.messageManager.handleEvent(event);
|
|
71185
71497
|
handleEventPostProcessing(session, event, this.getContext());
|
|
71186
71498
|
}
|
|
71187
|
-
async handleExit(sessionId, code) {
|
|
71188
|
-
await handleExit(sessionId, code, this.getContext());
|
|
71499
|
+
async handleExit(sessionId, code, source) {
|
|
71500
|
+
await handleExit(sessionId, code, this.getContext(), source);
|
|
71189
71501
|
}
|
|
71190
71502
|
startTyping(session) {
|
|
71191
71503
|
const wasTyping = session.timers.typingTimer !== null;
|
|
@@ -71700,7 +72012,7 @@ class SessionManager extends EventEmitter4 {
|
|
|
71700
72012
|
worktreeMode: this.worktreeMode,
|
|
71701
72013
|
permissionTimeoutMs: this.limits.permissionTimeoutSeconds * 1000,
|
|
71702
72014
|
handleEvent: (tid, e) => this.handleEvent(tid, e),
|
|
71703
|
-
handleExit: (tid, code) => this.handleExit(tid, code),
|
|
72015
|
+
handleExit: (tid, code, source) => this.handleExit(tid, code, source),
|
|
71704
72016
|
updateSessionHeader: (s) => this.updateSessionHeader(s),
|
|
71705
72017
|
flush: async (s) => {
|
|
71706
72018
|
if (s.messageManager) {
|
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
|
}
|
|
@@ -56018,6 +56263,12 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
56018
56263
|
const pid = proc.pid;
|
|
56019
56264
|
this.process = null;
|
|
56020
56265
|
this.log.debug(`Killing Claude process (pid=${pid})`);
|
|
56266
|
+
if (process.env.INTEGRATION_TEST === "1") {
|
|
56267
|
+
const stack = new Error().stack?.split(`
|
|
56268
|
+
`).slice(2, 7).join(" > ").replace(/\s+at\s+/g, " < ") ?? "?";
|
|
56269
|
+
process.stderr.write(`[claude-cli kill pid=${pid}] | ${stack}
|
|
56270
|
+
`);
|
|
56271
|
+
}
|
|
56021
56272
|
return new Promise((resolve5) => {
|
|
56022
56273
|
this.log.debug("Sending first SIGINT");
|
|
56023
56274
|
proc.kill("SIGINT");
|
|
@@ -56033,12 +56284,22 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
56033
56284
|
proc.kill("SIGTERM");
|
|
56034
56285
|
} catch {}
|
|
56035
56286
|
}, 2000);
|
|
56036
|
-
|
|
56037
|
-
this.log.debug(`Claude process
|
|
56287
|
+
const settle = (reason) => {
|
|
56288
|
+
this.log.debug(`Claude process gone (${reason})`);
|
|
56038
56289
|
clearTimeout(secondSigint);
|
|
56039
56290
|
clearTimeout(forceKillTimeout);
|
|
56291
|
+
clearTimeout(lastResort);
|
|
56040
56292
|
resolve5();
|
|
56041
|
-
}
|
|
56293
|
+
};
|
|
56294
|
+
const lastResort = setTimeout(() => {
|
|
56295
|
+
try {
|
|
56296
|
+
this.log.warn("Claude process did not exit after SIGTERM — sending SIGKILL");
|
|
56297
|
+
proc.kill("SIGKILL");
|
|
56298
|
+
} catch {}
|
|
56299
|
+
settle("kill timeout");
|
|
56300
|
+
}, 5000);
|
|
56301
|
+
proc.once("close", (code) => settle(`closed, code=${code}`));
|
|
56302
|
+
proc.once("error", () => settle("spawn error"));
|
|
56042
56303
|
});
|
|
56043
56304
|
}
|
|
56044
56305
|
interrupt() {
|
|
@@ -58181,6 +58442,10 @@ async function handlePermissionWith(toolName, toolInput, cfg) {
|
|
|
58181
58442
|
mcpLogger.debug(`Skipping standard prompt for ${toolName} (handler enforces its own gate)`);
|
|
58182
58443
|
return { behavior: "allow", updatedInput: toolInput };
|
|
58183
58444
|
}
|
|
58445
|
+
if (toolName === "AskUserQuestion") {
|
|
58446
|
+
mcpLogger.debug("Auto-allowing AskUserQuestion (question UI is handled by the main bot)");
|
|
58447
|
+
return { behavior: "allow", updatedInput: toolInput };
|
|
58448
|
+
}
|
|
58184
58449
|
if (cfg.getAllowAll()) {
|
|
58185
58450
|
mcpLogger.debug(`Auto-allowing ${toolName} (allow all active)`);
|
|
58186
58451
|
return { behavior: "allow", updatedInput: toolInput };
|
package/package.json
CHANGED