@gkorepanov/ccodex 0.5.0-rc.9 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +159 -157
- package/config/compatibility.json +1 -1
- package/dist/claude/delegation.d.ts +10 -0
- package/dist/claude/delegation.js +41 -0
- package/dist/claude/delegation.js.map +1 -0
- package/dist/claude/native/catalog.d.ts +5 -0
- package/dist/claude/native/catalog.js +26 -17
- package/dist/claude/native/catalog.js.map +1 -1
- package/dist/claude/native/history.js +4 -3
- package/dist/claude/native/history.js.map +1 -1
- package/dist/claude/native/pages.d.ts +2 -0
- package/dist/claude/native/pages.js +6 -1
- package/dist/claude/native/pages.js.map +1 -1
- package/dist/claude/native/projector.js +33 -12
- package/dist/claude/native/projector.js.map +1 -1
- package/dist/claude/native/records.js +19 -0
- package/dist/claude/native/records.js.map +1 -1
- package/dist/claude/native/search.d.ts +7 -0
- package/dist/claude/native/search.js +79 -0
- package/dist/claude/native/search.js.map +1 -0
- package/dist/claude/native/summary.d.ts +6 -1
- package/dist/claude/native/summary.js +24 -6
- package/dist/claude/native/summary.js.map +1 -1
- package/dist/claude/peers.d.ts +2 -0
- package/dist/claude/peers.js +13 -4
- package/dist/claude/peers.js.map +1 -1
- package/dist/claude/processes.d.ts +2 -0
- package/dist/claude/processes.js +15 -0
- package/dist/claude/processes.js.map +1 -1
- package/dist/claude/sdk.d.ts +6 -5
- package/dist/claude/sdk.js +15 -8
- package/dist/claude/sdk.js.map +1 -1
- package/dist/claude/session.d.ts +25 -5
- package/dist/claude/session.js +134 -29
- package/dist/claude/session.js.map +1 -1
- package/dist/claude/threads.d.ts +43 -3
- package/dist/claude/threads.js +252 -59
- package/dist/claude/threads.js.map +1 -1
- package/dist/claude/toolMapper.d.ts +7 -0
- package/dist/claude/toolMapper.js +14 -1
- package/dist/claude/toolMapper.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/daemon/daemon.d.ts +3 -0
- package/dist/daemon/daemon.js +6 -3
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/daemon/supervisor.d.ts +3 -0
- package/dist/daemon/supervisor.js +2 -0
- package/dist/daemon/supervisor.js.map +1 -1
- package/dist/desktop/stdioFrontend.js +1 -1
- package/dist/desktop/stdioFrontend.js.map +1 -1
- package/dist/gateway/catalog.js +35 -20
- package/dist/gateway/catalog.js.map +1 -1
- package/dist/gateway/connection.d.ts +2 -0
- package/dist/gateway/connection.js +14 -2
- package/dist/gateway/connection.js.map +1 -1
- package/dist/gateway/lineage.d.ts +9 -1
- package/dist/gateway/lineage.js +134 -37
- package/dist/gateway/lineage.js.map +1 -1
- package/dist/gateway/server.d.ts +2 -1
- package/dist/gateway/server.js +24 -12
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/status.d.ts +15 -5
- package/dist/gateway/status.js +142 -47
- package/dist/gateway/status.js.map +1 -1
- package/dist/gateway/stock.js +17 -2
- package/dist/gateway/stock.js.map +1 -1
- package/dist/instructions.d.ts +4 -0
- package/dist/instructions.js +13 -0
- package/dist/instructions.js.map +1 -0
- package/dist/management/commands.js +19 -2
- package/dist/management/commands.js.map +1 -1
- package/dist/meta.d.ts +39 -11
- package/dist/meta.js +59 -28
- package/dist/meta.js.map +1 -1
- package/dist/protocol/codex.d.ts +1 -3
- package/dist/protocol/codex.js +1 -4
- package/dist/protocol/codex.js.map +1 -1
- package/dist/protocol/turnPagination.d.ts +27 -2
- package/dist/protocol/turnPagination.js +46 -18
- package/dist/protocol/turnPagination.js.map +1 -1
- package/examples/config.toml +6 -2
- package/instructions/ccodex_extra_claude_instructions.md +20 -0
- package/instructions/ccodex_extra_common_instructions.md +23 -0
- package/legal/THIRD_PARTY_NOTICES.md +9 -9
- package/package.json +6 -5
- package/scripts/migrate-0.4-to-0.5.mjs +29 -18
package/dist/claude/threads.js
CHANGED
|
@@ -6,15 +6,17 @@ import { deleteSession, forkSession, renameSession } from "@anthropic-ai/claude-
|
|
|
6
6
|
import { v7 as uuidv7 } from "uuid";
|
|
7
7
|
import { packageVersion } from "../management/commands.js";
|
|
8
8
|
import { invalidParams, invalidRequest, requestedModel } from "../protocol/codex.js";
|
|
9
|
-
import { anchorCursor, historyCursors, paginateItems, paginateTurns, startedTurn } from "../protocol/turnPagination.js";
|
|
9
|
+
import { anchorCursor, historyCursors, occurrencesPage, paginateItems, paginateTurns, startedTurn, turnOccurrences } from "../protocol/turnPagination.js";
|
|
10
|
+
import { claudeEffort, ULTRA } from "./delegation.js";
|
|
10
11
|
import { normalizeUserInput } from "./inputMapper.js";
|
|
11
12
|
import { claudeModelLabel, modelCatalogValue, normalizeClaudeModelIdentifier } from "./modelSelection.js";
|
|
12
13
|
import { NativeSessionCatalog } from "./native/catalog.js";
|
|
14
|
+
import { firstMatch } from "./native/search.js";
|
|
13
15
|
import { nativeThread } from "./native/projector.js";
|
|
14
16
|
import { projectSubagents } from "./native/subagents.js";
|
|
15
17
|
import { readTranscriptRecords } from "./native/records.js";
|
|
16
|
-
import { summarizeTranscript, userText } from "./native/summary.js";
|
|
17
|
-
import { codexPermissions, mapClaudeModel, mapSkill, permissionSettings, withProbeQuery } from "./sdk.js";
|
|
18
|
+
import { preview, summarizeTranscript, userText } from "./native/summary.js";
|
|
19
|
+
import { claudeMode, codexPermissions, mapClaudeModel, mapSkill, permissionSettings, withProbeQuery } from "./sdk.js";
|
|
18
20
|
import { killProcesses, sessionProcesses } from "./processes.js";
|
|
19
21
|
import { ClaudeSession } from "./session.js";
|
|
20
22
|
/** Claude's standard context window, until a session of the model reports its own. */
|
|
@@ -29,6 +31,13 @@ const IDLE_MS = Number(process.env.CCODEX_E2E_IDLE_MS) || 30 * 60_000;
|
|
|
29
31
|
const RESUME_WARM_MS = Number(process.env.CCODEX_E2E_RESUME_WARM_MS) || 10_000;
|
|
30
32
|
/** Claude processes kept at most (each holds its session in memory: 250–550 MB); tests lower it. */
|
|
31
33
|
const MAX_PROCESSES = Number(process.env.CCODEX_E2E_MAX_PROCESSES) || 10;
|
|
34
|
+
/** A chat's permissions and plan mode from the mode its transcript last recorded (in plan mode, meta.json keeps its own). */
|
|
35
|
+
function recordedPermissions(threadId, recorded, meta) {
|
|
36
|
+
if (recorded !== "plan")
|
|
37
|
+
return { permissionMode: (recorded ?? "default"), plan: false };
|
|
38
|
+
const kept = meta.plan(threadId);
|
|
39
|
+
return { permissionMode: (kept?.permissionMode ?? "default"), plan: kept?.plan ?? true };
|
|
40
|
+
}
|
|
32
41
|
/** The Claude side of the gateway: catalog of native sessions, live sessions, side chats, models, skills. */
|
|
33
42
|
export class ClaudeThreads {
|
|
34
43
|
config;
|
|
@@ -39,6 +48,9 @@ export class ClaudeThreads {
|
|
|
39
48
|
sides = new Map();
|
|
40
49
|
/** `agent-<id>` sub-agent thread → the native session whose transcript directory holds it. */
|
|
41
50
|
subagentRoots = new Map();
|
|
51
|
+
/** Threads rolled back but not continued yet: their kept history ends at this record. A restart meanwhile shows
|
|
52
|
+
* the rolled-back messages again. */
|
|
53
|
+
leaves = new Map();
|
|
42
54
|
/** Sub-agents spawned live, shown until Claude has written their transcript. */
|
|
43
55
|
spawnedSubagents = new Map();
|
|
44
56
|
/** Context window of each Claude model, as its sessions report it. */
|
|
@@ -48,6 +60,11 @@ export class ClaudeThreads {
|
|
|
48
60
|
models_;
|
|
49
61
|
defaultModel = null;
|
|
50
62
|
rateLimitWindows = new Map();
|
|
63
|
+
usage;
|
|
64
|
+
/** Why Claude's plan limits could not be read, when they could not. */
|
|
65
|
+
usageError;
|
|
66
|
+
/** Weekly windows of single models (e.g. Fable), from Claude's `/usage` data. */
|
|
67
|
+
modelLimits = [];
|
|
51
68
|
stopWatching;
|
|
52
69
|
onTurnCompleted;
|
|
53
70
|
constructor(config, gateway, logger) {
|
|
@@ -78,7 +95,7 @@ export class ClaudeThreads {
|
|
|
78
95
|
if (!known.has(id) && !this.sessions.has(id) && !this.gateway.meta.hidden(id)) {
|
|
79
96
|
this.gateway.broadcast("thread/started", { thread: this.decorate(nativeThread(id, header, { status: this.status(id) })) });
|
|
80
97
|
}
|
|
81
|
-
else if (known.has(id) && known.get(id) !== name && name) {
|
|
98
|
+
else if (known.has(id) && known.get(id) !== name && name && !this.gateway.meta.hidden(id)) {
|
|
82
99
|
this.gateway.emit(id, "thread/name/updated", { threadId: id, threadName: name });
|
|
83
100
|
}
|
|
84
101
|
known.set(id, name);
|
|
@@ -95,7 +112,7 @@ export class ClaudeThreads {
|
|
|
95
112
|
/** CPU seconds each command of a loaded session used, and since when unchanged. */
|
|
96
113
|
cpuSeen = new Map();
|
|
97
114
|
sweeper;
|
|
98
|
-
/** What CCodex last did to each chat's process and commands, and why (shown by /
|
|
115
|
+
/** What CCodex last did to each chat's process and commands, and why (shown by /cc). */
|
|
99
116
|
actions = new Map();
|
|
100
117
|
act(session, event, text, pids) {
|
|
101
118
|
this.logger.info(event, { threadId: session.threadId, pids });
|
|
@@ -135,7 +152,7 @@ export class ClaudeThreads {
|
|
|
135
152
|
this.act(session, "claude.tasks.hung", `stopped its background tasks: their commands used no CPU for ${idle}`, pids);
|
|
136
153
|
void session.stopTasks().catch(() => killProcesses(pids));
|
|
137
154
|
}
|
|
138
|
-
else if (session.quiet(now, IDLE_MS) && !this.gateway.subscribers(session.threadId)) {
|
|
155
|
+
else if (session.quiet(now, IDLE_MS) && !this.gateway.subscribers(session.threadId).size) {
|
|
139
156
|
this.act(session, "claude.unloaded", `unloaded the chat: nobody had it open and it was quiet for ${idle}`, pids);
|
|
140
157
|
this.sessions.delete(session.threadId);
|
|
141
158
|
void session.unload().then(() => killProcesses(pids ?? []));
|
|
@@ -190,20 +207,34 @@ export class ClaudeThreads {
|
|
|
190
207
|
return {
|
|
191
208
|
...summary,
|
|
192
209
|
aiTitle: this.gateway.titles.naming(summary.sessionId) ? null : summary.aiTitle,
|
|
193
|
-
model: session?.settings.model ??
|
|
194
|
-
reasoningEffort: session?.settings.effort ?? summary
|
|
210
|
+
model: session?.settings.model ?? this.recordedModel(summary),
|
|
211
|
+
reasoningEffort: session?.settings.effort ?? this.recordedEffort(summary),
|
|
195
212
|
};
|
|
196
213
|
}
|
|
214
|
+
/** A chat's model from its transcript; in plan mode meta.json's (Claude plans a Haiku chat on Sonnet, see Meta.setPlan). */
|
|
215
|
+
recordedModel(summary) {
|
|
216
|
+
const planned = summary.permissionMode === "plan" ? this.gateway.meta.plan(summary.sessionId)?.model : undefined;
|
|
217
|
+
return planned ?? (summary.model && this.pickerModel(summary.model));
|
|
218
|
+
}
|
|
219
|
+
/** A chat's effort from its transcript (Claude records `ultra` as the max it runs at, told to delegate); in plan mode meta.json's. */
|
|
220
|
+
recordedEffort(summary) {
|
|
221
|
+
if (summary.permissionMode === "plan")
|
|
222
|
+
return this.gateway.meta.plan(summary.sessionId)?.effort ?? null;
|
|
223
|
+
return summary.delegating && summary.reasoningEffort === claudeEffort(ULTRA) ? ULTRA : summary.reasoningEffort;
|
|
224
|
+
}
|
|
197
225
|
/** Transcripts name the resolved model (`claude-haiku-4-5-…`); the picker (and a live session) its value (`haiku`). */
|
|
198
226
|
pickerModel(model) {
|
|
199
227
|
const id = normalizeClaudeModelIdentifier(model);
|
|
200
228
|
return this.pickerValues.get(id) ?? id;
|
|
201
229
|
}
|
|
202
230
|
/** Every native session as a list row (sub-agents excluded; they are listed through their parent). */
|
|
231
|
+
search(term) {
|
|
232
|
+
return this.catalog.search(this.config.claudeBinary, term);
|
|
233
|
+
}
|
|
203
234
|
threads() {
|
|
204
235
|
const rows = this.catalog.sessions().map((summary) => this.decorate(nativeThread(summary.sessionId, this.headerOf(summary, this.sessions.get(summary.sessionId)), { status: this.status(summary.sessionId) })));
|
|
205
236
|
for (const session of this.sessions.values()) {
|
|
206
|
-
if (!this.catalog.get(session.threadId))
|
|
237
|
+
if (session.started && !this.catalog.get(session.threadId))
|
|
207
238
|
rows.push(this.decorate(this.freshThread(session)));
|
|
208
239
|
}
|
|
209
240
|
return rows;
|
|
@@ -224,7 +255,7 @@ export class ClaudeThreads {
|
|
|
224
255
|
return nativeThread(session.threadId, {
|
|
225
256
|
cwd: session.settings.cwd, gitBranch: null, createdAt: now, updatedAt: now, preview: "",
|
|
226
257
|
customTitle: this.pendingNames.get(session.threadId) ?? null, aiTitle: null, model: session.settings.model, reasoningEffort: session.settings.effort,
|
|
227
|
-
serviceTier: session.settings.fast ? "fast" : null, permissionMode: session.settings
|
|
258
|
+
serviceTier: session.settings.fast ? "fast" : null, permissionMode: claudeMode(session.settings), cliVersion: null, goal: null,
|
|
228
259
|
}, { status: this.status(session.threadId) });
|
|
229
260
|
}
|
|
230
261
|
// ---- reads ----
|
|
@@ -233,7 +264,7 @@ export class ClaudeThreads {
|
|
|
233
264
|
await this.catalog.refresh();
|
|
234
265
|
if (!this.catalog.get(threadId))
|
|
235
266
|
return undefined;
|
|
236
|
-
return this.catalog.projection(threadId, this.
|
|
267
|
+
return this.catalog.projection(threadId, this.leaves.get(threadId));
|
|
237
268
|
}
|
|
238
269
|
subagentRoot(threadId) {
|
|
239
270
|
const known = this.subagentRoots.get(threadId);
|
|
@@ -286,7 +317,7 @@ export class ClaudeThreads {
|
|
|
286
317
|
subagentSpawned(session, item, running) {
|
|
287
318
|
const childId = item.receiverThreadIds[0];
|
|
288
319
|
const now = Math.floor(Date.now() / 1000);
|
|
289
|
-
const header = { ...summarizeTranscript([]), cwd: session.settings.cwd, preview: item.prompt ?? "", model: item.model, createdAt: now, updatedAt: now };
|
|
320
|
+
const header = { ...summarizeTranscript([]), cwd: session.settings.cwd, preview: preview(item.prompt ?? ""), model: item.model, createdAt: now, updatedAt: now };
|
|
290
321
|
const thread = nativeThread(childId, header, {
|
|
291
322
|
status: { type: "active", activeFlags: [] },
|
|
292
323
|
subagent: { parentThreadId: session.threadId, depth: 1, nickname: `${item.agentsStates[childId].message} [${claudeModelLabel(item.model ?? "Claude")}]` },
|
|
@@ -403,7 +434,7 @@ export class ClaudeThreads {
|
|
|
403
434
|
return !threadId.startsWith("agent-") && !this.sides.has(threadId) && this.catalog.has(threadId);
|
|
404
435
|
}
|
|
405
436
|
pages(threadId) {
|
|
406
|
-
return this.catalog.pages(threadId, this.
|
|
437
|
+
return this.catalog.pages(threadId, this.leaves.get(threadId));
|
|
407
438
|
}
|
|
408
439
|
/** The running turn is the newest one, before Claude writes any of it too (a turn it goes on in after an answer). */
|
|
409
440
|
running(threadId, turnId) {
|
|
@@ -454,6 +485,22 @@ export class ClaudeThreads {
|
|
|
454
485
|
: await pages.since(source, anchor.anchor);
|
|
455
486
|
return paginateTurns(this.withLive(threadId, window.turns), params);
|
|
456
487
|
}
|
|
488
|
+
/** Stock's `thread/searchOccurrences`: history is read back only to the first message holding the term (ripgrep finds it). */
|
|
489
|
+
async searchOccurrences(threadId, params) {
|
|
490
|
+
if (!String(params.searchTerm ?? "").trim())
|
|
491
|
+
throw invalidRequest("thread/searchOccurrences requires a non-empty searchTerm");
|
|
492
|
+
if (!this.catalog.get(threadId))
|
|
493
|
+
await this.catalog.refresh();
|
|
494
|
+
let turns;
|
|
495
|
+
if (this.paged(threadId)) {
|
|
496
|
+
const { pages, source } = this.pages(threadId);
|
|
497
|
+
const at = await firstMatch(this.config.claudeBinary, source.path, params.searchTerm);
|
|
498
|
+
turns = this.withLive(threadId, at === undefined ? [] : (await pages.reaching(source, at)).turns);
|
|
499
|
+
}
|
|
500
|
+
else
|
|
501
|
+
turns = (await this.read(threadId)).turns;
|
|
502
|
+
return occurrencesPage(turnOccurrences(turns, params.searchTerm), params);
|
|
503
|
+
}
|
|
457
504
|
async itemsPage(threadId, params) {
|
|
458
505
|
if (!this.catalog.get(threadId))
|
|
459
506
|
await this.catalog.refresh();
|
|
@@ -481,10 +528,10 @@ export class ClaudeThreads {
|
|
|
481
528
|
const summary = this.catalog.get(threadId);
|
|
482
529
|
return {
|
|
483
530
|
cwd: summary?.cwd ?? process.cwd(),
|
|
484
|
-
model: summary
|
|
485
|
-
effort: summary
|
|
531
|
+
model: (summary && this.recordedModel(summary)) || this.defaultModel,
|
|
532
|
+
effort: summary ? this.recordedEffort(summary) : null,
|
|
486
533
|
fast: summary?.serviceTier === "fast",
|
|
487
|
-
|
|
534
|
+
...recordedPermissions(threadId, summary?.permissionMode, this.gateway.meta),
|
|
488
535
|
};
|
|
489
536
|
}
|
|
490
537
|
/** Common fields of thread/start|resume|fork responses and thread/settings/updated. */
|
|
@@ -521,7 +568,7 @@ export class ClaudeThreads {
|
|
|
521
568
|
effort: settings.effort,
|
|
522
569
|
summary: null,
|
|
523
570
|
collaborationMode: {
|
|
524
|
-
mode: settings.
|
|
571
|
+
mode: settings.plan ? "plan" : "default",
|
|
525
572
|
settings: { model: response.model, reasoning_effort: settings.effort, developer_instructions: null },
|
|
526
573
|
},
|
|
527
574
|
multiAgentMode: "explicitRequestOnly",
|
|
@@ -547,14 +594,14 @@ export class ClaudeThreads {
|
|
|
547
594
|
return session;
|
|
548
595
|
if (!this.catalog.get(threadId))
|
|
549
596
|
throw invalidParams(`thread not found: ${threadId}`);
|
|
550
|
-
const leaf = this.
|
|
597
|
+
const leaf = this.leaves.get(threadId);
|
|
551
598
|
session = new ClaudeSession(this, threadId, this.settings(threadId), { exists: true, ...(leaf ? { resumeAt: leaf } : {}) });
|
|
552
599
|
this.sessions.set(threadId, session);
|
|
553
600
|
return session;
|
|
554
601
|
}
|
|
555
602
|
/** The session continued from the rollback leaf: its new records end the history from now on. */
|
|
556
603
|
resumedAtLeaf(threadId) {
|
|
557
|
-
this.
|
|
604
|
+
this.leaves.delete(threadId);
|
|
558
605
|
}
|
|
559
606
|
turnCompleted(session, turnId) {
|
|
560
607
|
const threadId = session.threadId;
|
|
@@ -567,6 +614,9 @@ export class ClaudeThreads {
|
|
|
567
614
|
const goal = this.goal(threadId);
|
|
568
615
|
if (JSON.stringify(goal) === before)
|
|
569
616
|
return;
|
|
617
|
+
// A goal replaced meanwhile (edited, paused, cleared) is no news: Desktop would clear the new goal for its completion.
|
|
618
|
+
if (goal && session.goalObjective !== undefined && goal.objective !== session.goalObjective)
|
|
619
|
+
return;
|
|
570
620
|
if (goal)
|
|
571
621
|
this.gateway.emit(threadId, "thread/goal/updated", { threadId, turnId, goal });
|
|
572
622
|
else
|
|
@@ -574,13 +624,22 @@ export class ClaudeThreads {
|
|
|
574
624
|
});
|
|
575
625
|
this.onTurnCompleted?.(threadId, turnId);
|
|
576
626
|
}
|
|
577
|
-
/**
|
|
627
|
+
/**
|
|
628
|
+
* Claude's native `/goal` as a Codex ThreadGoal (Claude tracks no budget or time); a paused one is kept in meta.json.
|
|
629
|
+
* The goal last sent to Claude is the chat's: Claude records a `/goal` a moment after taking it.
|
|
630
|
+
*/
|
|
578
631
|
goal(threadId) {
|
|
579
|
-
const
|
|
632
|
+
const paused = this.gateway.meta.pausedGoal(threadId);
|
|
633
|
+
const recorded = this.catalog.get(threadId)?.goal;
|
|
634
|
+
const sent = this.sessions.get(threadId)?.goalObjective;
|
|
635
|
+
const now = Math.floor(Date.now() / 1000);
|
|
636
|
+
const goal = paused ? { ...paused, met: false }
|
|
637
|
+
: sent === undefined || sent === recorded?.objective ? recorded
|
|
638
|
+
: sent && { objective: sent, createdAt: now, updatedAt: now, met: false };
|
|
580
639
|
if (!goal)
|
|
581
640
|
return null;
|
|
582
641
|
return {
|
|
583
|
-
threadId, objective: goal.objective, status: goal.met ? "complete" : "active", tokenBudget: null, tokensUsed: 0,
|
|
642
|
+
threadId, objective: goal.objective, status: paused ? "paused" : goal.met ? "complete" : "active", tokenBudget: null, tokensUsed: 0,
|
|
584
643
|
timeUsedSeconds: Math.max(0, goal.updatedAt - goal.createdAt), createdAt: goal.createdAt, updatedAt: goal.updatedAt,
|
|
585
644
|
};
|
|
586
645
|
}
|
|
@@ -588,14 +647,26 @@ export class ClaudeThreads {
|
|
|
588
647
|
// Sub-agent activity is shown by the parent's collabAgentToolCall item; child history is read from disk.
|
|
589
648
|
}
|
|
590
649
|
onRateLimit(info) {
|
|
591
|
-
if (!info
|
|
650
|
+
if (!info)
|
|
592
651
|
return;
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
652
|
+
// A turn's event carries every plan window Claude knows in `unifiedWindows` (utilization as a fraction); `status`,
|
|
653
|
+
// and `utilization` if any, are about the window `rateLimitType` names. A window without one keeps the last known.
|
|
654
|
+
const windows = { ...info.unifiedWindows };
|
|
655
|
+
if (info.rateLimitType)
|
|
656
|
+
windows[info.rateLimitType] = { resetsAt: info.resetsAt, utilization: info.utilization, ...windows[info.rateLimitType] };
|
|
657
|
+
for (const [type, window] of Object.entries(windows)) {
|
|
658
|
+
const status = type === info.rateLimitType ? info.status : "allowed";
|
|
659
|
+
const usedPercent = typeof window.utilization === "number" ? Math.round(window.utilization * 100)
|
|
660
|
+
: status === "rejected" ? 100 : this.rateLimitWindows.get(type)?.usedPercent;
|
|
661
|
+
if (usedPercent === undefined)
|
|
662
|
+
continue;
|
|
663
|
+
this.rateLimitWindows.set(type, {
|
|
664
|
+
status,
|
|
665
|
+
usedPercent,
|
|
666
|
+
windowDurationMins: WINDOW_MINUTES[type] ?? null,
|
|
667
|
+
resetsAt: typeof window.resetsAt === "number" ? window.resetsAt : null,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
599
670
|
const snapshot = this.rateLimitSnapshot();
|
|
600
671
|
for (const connection of this.gateway.connections) {
|
|
601
672
|
if (connection.provider === "claude")
|
|
@@ -617,10 +688,44 @@ export class ClaudeThreads {
|
|
|
617
688
|
rateLimitReachedType: [...this.rateLimitWindows.values()].some((value) => value.status === "rejected") ? "rate_limit_reached" : null,
|
|
618
689
|
};
|
|
619
690
|
}
|
|
691
|
+
/** Claude's plan limits: read from Claude's `/usage` data at most once a minute, and as turns report them. */
|
|
620
692
|
async rateLimits() {
|
|
693
|
+
if (!this.usage || Date.now() - this.usage.at > 60_000)
|
|
694
|
+
this.usage = { at: Date.now(), done: this.readUsage() };
|
|
695
|
+
await this.usage.done;
|
|
621
696
|
const snapshot = this.rateLimitSnapshot();
|
|
622
697
|
return { rateLimits: snapshot, rateLimitsByLimitId: { claude: snapshot } };
|
|
623
698
|
}
|
|
699
|
+
async readUsage() {
|
|
700
|
+
try {
|
|
701
|
+
const usage = await withProbeQuery(this.config, undefined, (probe) => probe.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET({ skipBehaviors: true }));
|
|
702
|
+
// No windows at all: claude.ai's usage endpoint failed (e.g. rate-limited) and Claude had nothing recent cached.
|
|
703
|
+
if (usage.rate_limits_available && !usage.rate_limits)
|
|
704
|
+
throw new Error("claude.ai's usage endpoint did not answer");
|
|
705
|
+
this.usageError = usage.rate_limits_available ? undefined : "no plan limits for this login (API key or cloud provider)";
|
|
706
|
+
// Absent when Claude answers from its cached data: the last known stay.
|
|
707
|
+
if (usage.rate_limits?.model_scoped)
|
|
708
|
+
this.modelLimits = usage.rate_limits.model_scoped.filter((row) => typeof row.utilization === "number").map((row) => ({
|
|
709
|
+
name: row.display_name,
|
|
710
|
+
window: { usedPercent: Math.round(row.utilization), windowDurationMins: 10_080, resetsAt: row.resets_at ? Math.floor(Date.parse(row.resets_at) / 1000) : null },
|
|
711
|
+
}));
|
|
712
|
+
for (const type of ["five_hour", "seven_day", "seven_day_opus", "seven_day_sonnet"]) {
|
|
713
|
+
const window = usage.rate_limits?.[type];
|
|
714
|
+
if (typeof window?.utilization !== "number")
|
|
715
|
+
continue;
|
|
716
|
+
this.rateLimitWindows.set(type, {
|
|
717
|
+
status: window.utilization >= 100 ? "rejected" : "allowed",
|
|
718
|
+
usedPercent: Math.round(window.utilization),
|
|
719
|
+
windowDurationMins: WINDOW_MINUTES[type],
|
|
720
|
+
resetsAt: window.resets_at ? Math.floor(Date.parse(window.resets_at) / 1000) : null,
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
catch (error) {
|
|
725
|
+
this.usageError = `limits unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
726
|
+
this.logger.warn("claude.usage.unavailable", { error: this.usageError });
|
|
727
|
+
}
|
|
728
|
+
}
|
|
624
729
|
rateLimitWindowsText() {
|
|
625
730
|
return [...this.rateLimitWindows.entries()].map(([key, value]) => `${key}: ${value.usedPercent}% used${value.resetsAt ? `, resets ${new Date(value.resetsAt * 1000).toISOString().slice(0, 16).replace("T", " ")} UTC` : ""} (${value.status})`);
|
|
626
731
|
}
|
|
@@ -637,7 +742,9 @@ export class ClaudeThreads {
|
|
|
637
742
|
}
|
|
638
743
|
cachedModels() {
|
|
639
744
|
try {
|
|
640
|
-
|
|
745
|
+
const cache = JSON.parse(readFileSync(this.modelsPath, "utf8"));
|
|
746
|
+
// As stock's models cache: one another version wrote (another Claude, another model list) is stale.
|
|
747
|
+
return cache.version === packageVersion() ? cache.models : undefined;
|
|
641
748
|
}
|
|
642
749
|
catch {
|
|
643
750
|
return undefined;
|
|
@@ -645,9 +752,17 @@ export class ClaudeThreads {
|
|
|
645
752
|
}
|
|
646
753
|
/** Asks Claude for its models and keeps them for the next start. */
|
|
647
754
|
async probeModels() {
|
|
648
|
-
const models = await withProbeQuery(this.config, undefined, (probe) =>
|
|
755
|
+
const models = await withProbeQuery(this.config, undefined, async (probe) => {
|
|
756
|
+
const models = await probe.supportedModels();
|
|
757
|
+
// Claude's own context budget per model: its window, capped by settings such as CLAUDE_CODE_AUTO_COMPACT_WINDOW.
|
|
758
|
+
for (const model of models) {
|
|
759
|
+
await probe.setModel(model.value);
|
|
760
|
+
model.contextWindow = (await probe.getContextUsage({ detail: "summary" })).maxTokens;
|
|
761
|
+
}
|
|
762
|
+
return models;
|
|
763
|
+
});
|
|
649
764
|
const temporary = `${this.modelsPath}.${process.pid}.tmp`;
|
|
650
|
-
void writeFile(temporary, JSON.stringify(models), { mode: 0o600 }).then(() => rename(temporary, this.modelsPath)).catch(() => undefined);
|
|
765
|
+
void writeFile(temporary, JSON.stringify({ version: packageVersion(), models }), { mode: 0o600 }).then(() => rename(temporary, this.modelsPath)).catch(() => undefined);
|
|
651
766
|
return this.useModels(models);
|
|
652
767
|
}
|
|
653
768
|
useModels(models) {
|
|
@@ -656,14 +771,37 @@ export class ClaudeThreads {
|
|
|
656
771
|
for (const model of models) {
|
|
657
772
|
if (model.value !== "default" && model.resolvedModel)
|
|
658
773
|
this.pickerValues.set(normalizeClaudeModelIdentifier(model.resolvedModel), modelCatalogValue(model));
|
|
774
|
+
if (!model.contextWindow)
|
|
775
|
+
continue;
|
|
776
|
+
this.contextWindows.set(modelCatalogValue(model), model.contextWindow);
|
|
777
|
+
if (model.resolvedModel)
|
|
778
|
+
this.contextWindows.set(normalizeClaudeModelIdentifier(model.resolvedModel), model.contextWindow);
|
|
659
779
|
}
|
|
660
|
-
|
|
780
|
+
const mapped = models.filter((model) => model.value !== "default").map((model) => mapClaudeModel(model, this.config.modelPrefix));
|
|
781
|
+
for (const [index, model] of models.filter((model) => model.value !== "default").entries()) {
|
|
782
|
+
if (!model.supportedEffortLevels?.length)
|
|
783
|
+
continue;
|
|
784
|
+
const effort = mapped[index].defaultReasoningEffort;
|
|
785
|
+
this.defaultEfforts.set(modelCatalogValue(model), effort);
|
|
786
|
+
if (model.resolvedModel)
|
|
787
|
+
this.defaultEfforts.set(normalizeClaudeModelIdentifier(model.resolvedModel), effort);
|
|
788
|
+
}
|
|
789
|
+
return mapped;
|
|
790
|
+
}
|
|
791
|
+
/** The effort Claude runs at: the chat's, else its model's default as the picker shows it (Claude's own may differ). */
|
|
792
|
+
effort(settings) {
|
|
793
|
+
return settings.effort ?? this.defaultEfforts.get(normalizeClaudeModelIdentifier(settings.model || this.defaultModel || "")) ?? null;
|
|
794
|
+
}
|
|
795
|
+
/** The context window Claude works with for a model (picker value or resolved id; none: the default model). */
|
|
796
|
+
contextWindow(model) {
|
|
797
|
+
return this.contextWindows.get(normalizeClaudeModelIdentifier(model || this.defaultModel || "")) ?? DEFAULT_CONTEXT_WINDOW;
|
|
661
798
|
}
|
|
662
799
|
modelLabel(model) {
|
|
663
800
|
return model ? claudeModelLabel(model) : "Claude (default)";
|
|
664
801
|
}
|
|
665
802
|
pendingNames = new Map();
|
|
666
803
|
pickerValues = new Map();
|
|
804
|
+
defaultEfforts = new Map();
|
|
667
805
|
skillCache = new Map();
|
|
668
806
|
async skills(cwds) {
|
|
669
807
|
const entries = await Promise.all(cwds.map(async (cwd) => {
|
|
@@ -695,7 +833,10 @@ export class ClaudeThreads {
|
|
|
695
833
|
}
|
|
696
834
|
async sideTurn(side, params) {
|
|
697
835
|
const input = normalizeUserInput(params.input ?? []);
|
|
698
|
-
|
|
836
|
+
// Desktop's description turn (after a rename) wants JSON matching its schema; Claude's side question answers in prose.
|
|
837
|
+
const schema = params.outputSchema;
|
|
838
|
+
const typed = input.flatMap((item) => item.type === "text" ? [item.text] : []).join("\n");
|
|
839
|
+
const question = schema ? `${typed}\n\nAnswer with only a JSON object matching this JSON Schema, nothing else:\n${JSON.stringify(schema)}` : typed;
|
|
699
840
|
const id = randomUUID();
|
|
700
841
|
const started = Math.floor(Date.now() / 1000);
|
|
701
842
|
const turn = { id, items: [], itemsView: "full", status: "inProgress", error: null, startedAt: started, completedAt: null, durationMs: null };
|
|
@@ -714,6 +855,8 @@ export class ClaudeThreads {
|
|
|
714
855
|
text = await this.session(side.sourceId).askSideQuestion(history.length
|
|
715
856
|
? `Earlier in this side conversation:\n${history.join("\n")}\n\nNew question: ${question}`
|
|
716
857
|
: question);
|
|
858
|
+
if (schema)
|
|
859
|
+
text = JSON.stringify(JSON.parse(text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1)));
|
|
717
860
|
}
|
|
718
861
|
catch (error) {
|
|
719
862
|
text = `Side question failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
@@ -744,6 +887,7 @@ export class ClaudeThreads {
|
|
|
744
887
|
case "thread/read": return { thread: params.includeTurns ? await this.read(threadId).then(({ thread, turns }) => ({ ...thread, turns })) : await this.thread(threadId) };
|
|
745
888
|
case "thread/turns/list": return this.turnsPage(threadId, params);
|
|
746
889
|
case "thread/items/list": return this.itemsPage(threadId, params);
|
|
890
|
+
case "thread/searchOccurrences": return this.searchOccurrences(threadId, params);
|
|
747
891
|
case "turn/start": {
|
|
748
892
|
this.gateway.subscribe(threadId, connection);
|
|
749
893
|
return { turn: await this.session(threadId).startTurn(params) };
|
|
@@ -774,10 +918,7 @@ export class ClaudeThreads {
|
|
|
774
918
|
return {};
|
|
775
919
|
}
|
|
776
920
|
case "thread/settings/update": {
|
|
777
|
-
|
|
778
|
-
if (await session.updateSettings(params)) {
|
|
779
|
-
this.gateway.emit(threadId, "thread/settings/updated", { threadId, threadSettings: this.threadSettings(session.settings) });
|
|
780
|
-
}
|
|
921
|
+
await this.session(threadId).updateSettings(params);
|
|
781
922
|
return {};
|
|
782
923
|
}
|
|
783
924
|
// Claude's settings have no per-turn scope: the running turn's change stays for the chat.
|
|
@@ -800,9 +941,11 @@ export class ClaudeThreads {
|
|
|
800
941
|
return { goal: goal?.status === "complete" ? null : goal };
|
|
801
942
|
}
|
|
802
943
|
case "thread/goal/set": {
|
|
944
|
+
await this.catalog.refresh();
|
|
803
945
|
const now = Math.floor(Date.now() / 1000);
|
|
804
946
|
const current = this.goal(threadId);
|
|
805
947
|
if (params.objective) {
|
|
948
|
+
this.gateway.meta.setPausedGoal(threadId, null);
|
|
806
949
|
const session = this.session(threadId);
|
|
807
950
|
const goal = {
|
|
808
951
|
threadId, objective: params.objective, status: "active", tokenBudget: null, tokensUsed: 0, timeUsedSeconds: 0,
|
|
@@ -811,21 +954,41 @@ export class ClaudeThreads {
|
|
|
811
954
|
// Like stock, the goal's turn starts after the answer: Desktop shows the goal message itself on the answer.
|
|
812
955
|
setImmediate(() => {
|
|
813
956
|
this.gateway.emit(threadId, "thread/goal/updated", { threadId, turnId: null, goal });
|
|
814
|
-
session.
|
|
957
|
+
session.goal(params.objective, current?.status === "active")
|
|
815
958
|
.catch((error) => this.logger.warn("claude.goal.start-failed", { threadId, error: String(error) }));
|
|
816
959
|
});
|
|
817
960
|
return { goal };
|
|
818
961
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
962
|
+
// Claude's `/goal` has no pause: pausing clears it there (stopping its turn) and keeps it in meta.json; resuming
|
|
963
|
+
// sets it again.
|
|
964
|
+
const pausing = params.status === "paused" && current?.status === "active";
|
|
965
|
+
const resuming = params.status === "active" && current?.status === "paused";
|
|
966
|
+
if (!pausing && !resuming)
|
|
967
|
+
return { goal: current };
|
|
968
|
+
const goal = { ...current, status: params.status, updatedAt: now, timeUsedSeconds: Math.max(0, now - current.createdAt) };
|
|
969
|
+
this.gateway.meta.setPausedGoal(threadId, pausing ? { objective: goal.objective, createdAt: goal.createdAt, updatedAt: now } : null);
|
|
970
|
+
setImmediate(() => {
|
|
971
|
+
this.gateway.emit(threadId, "thread/goal/updated", { threadId, turnId: null, goal });
|
|
972
|
+
this.session(threadId).goal(pausing ? "clear" : goal.objective, pausing)
|
|
973
|
+
.catch((error) => this.logger.warn("claude.goal.update-failed", { threadId, error: String(error) }));
|
|
974
|
+
});
|
|
975
|
+
return { goal };
|
|
822
976
|
}
|
|
823
977
|
case "thread/goal/clear": {
|
|
978
|
+
await this.catalog.refresh();
|
|
824
979
|
const goal = this.goal(threadId);
|
|
825
|
-
if (goal
|
|
826
|
-
|
|
827
|
-
this.gateway.
|
|
828
|
-
|
|
980
|
+
if (!goal)
|
|
981
|
+
return { cleared: false };
|
|
982
|
+
this.gateway.meta.setPausedGoal(threadId, null);
|
|
983
|
+
setImmediate(() => {
|
|
984
|
+
this.gateway.emit(threadId, "thread/goal/cleared", { threadId });
|
|
985
|
+
// Claude drops a met goal itself.
|
|
986
|
+
if (goal.status === "active") {
|
|
987
|
+
this.session(threadId).goal("clear", true)
|
|
988
|
+
.catch((error) => this.logger.warn("claude.goal.clear-failed", { threadId, error: String(error) }));
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
return { cleared: true };
|
|
829
992
|
}
|
|
830
993
|
case "thread/queue/list": return { data: this.sessions.get(threadId)?.queued ?? [], nextCursor: null };
|
|
831
994
|
case "thread/queue/add": {
|
|
@@ -845,7 +1008,7 @@ export class ClaudeThreads {
|
|
|
845
1008
|
case "thread/backgroundTerminals/list": {
|
|
846
1009
|
const session = this.sessions.get(threadId);
|
|
847
1010
|
return {
|
|
848
|
-
data:
|
|
1011
|
+
data: (session?.backgroundTasks ?? []).filter((task) => task.taskType === "local_bash").map((task) => ({
|
|
849
1012
|
itemId: task.toolUseId ?? task.taskId, processId: task.taskId, command: task.description, cwd: session.settings.cwd,
|
|
850
1013
|
osPid: null, cpuPercent: null, rssKb: null,
|
|
851
1014
|
})),
|
|
@@ -881,6 +1044,7 @@ export class ClaudeThreads {
|
|
|
881
1044
|
}
|
|
882
1045
|
case "thread/turns/list": return paginateTurns(side.turns, params);
|
|
883
1046
|
case "thread/items/list": return paginateItems(side.turns, params);
|
|
1047
|
+
case "thread/searchOccurrences": return occurrencesPage(turnOccurrences(side.turns, params.searchTerm), params);
|
|
884
1048
|
case "thread/unsubscribe":
|
|
885
1049
|
case "thread/delete":
|
|
886
1050
|
case "thread/archive":
|
|
@@ -908,13 +1072,14 @@ export class ClaudeThreads {
|
|
|
908
1072
|
async startThread(connection, params) {
|
|
909
1073
|
await this.models().catch(() => undefined);
|
|
910
1074
|
const settings = this.settingsFrom(params, {
|
|
911
|
-
cwd: params.cwd ?? process.cwd(), model: this.defaultModel, effort: null, fast: false, permissionMode: "default",
|
|
1075
|
+
cwd: params.cwd ?? process.cwd(), model: this.defaultModel, effort: null, fast: false, permissionMode: "default", plan: false,
|
|
912
1076
|
});
|
|
913
1077
|
const session = this.create(settings);
|
|
914
1078
|
const threadId = session.threadId;
|
|
915
1079
|
this.gateway.subscribe(threadId, connection);
|
|
916
1080
|
const thread = this.decorate(this.freshThread(session));
|
|
917
|
-
|
|
1081
|
+
// After the response, as stock: Desktop hides its new-chat draft's `thread/started` only once it knows the id.
|
|
1082
|
+
setImmediate(() => this.gateway.emit(threadId, "thread/started", { thread }));
|
|
918
1083
|
this.gateway.titles.track(threadId);
|
|
919
1084
|
this.prewarm(session);
|
|
920
1085
|
return { thread, ...this.settingsResponse(settings) };
|
|
@@ -947,7 +1112,7 @@ export class ClaudeThreads {
|
|
|
947
1112
|
const { turns, usage } = await this.newestTurns(threadId, 1);
|
|
948
1113
|
// Like stock, the context meter follows a resume (Desktop's /status reads it).
|
|
949
1114
|
if (usage?.last) {
|
|
950
|
-
const modelContextWindow = this.
|
|
1115
|
+
const modelContextWindow = this.contextWindow(thread.model?.slice(this.config.modelPrefix.length));
|
|
951
1116
|
setImmediate(() => this.gateway.emit(threadId, "thread/tokenUsage/updated", { threadId, turnId: turns.at(-1)?.id ?? null, tokenUsage: { ...usage, modelContextWindow } }));
|
|
952
1117
|
}
|
|
953
1118
|
// A sub-agent has no session of its own: it shows the model and directory it runs with.
|
|
@@ -959,7 +1124,7 @@ export class ClaudeThreads {
|
|
|
959
1124
|
const response = {
|
|
960
1125
|
thread: { ...thread, turns: params.excludeTurns ? [] : turns },
|
|
961
1126
|
...this.settingsResponse(settings),
|
|
962
|
-
collaborationMode:
|
|
1127
|
+
collaborationMode: this.threadSettings(settings).collaborationMode,
|
|
963
1128
|
initialTurnsPage: params.initialTurnsPage ? await this.turnsPage(threadId, { ...params.initialTurnsPage, threadId }) : null,
|
|
964
1129
|
...historyCursors(turns),
|
|
965
1130
|
};
|
|
@@ -980,6 +1145,33 @@ export class ClaudeThreads {
|
|
|
980
1145
|
this.gateway.emit(threadId, "thread/name/updated", { threadId, threadName: name });
|
|
981
1146
|
return {};
|
|
982
1147
|
}
|
|
1148
|
+
/** The goal of a Claude backend the chat switches away from, out of Claude (a running goal turn stops). */
|
|
1149
|
+
async takeGoal(threadId) {
|
|
1150
|
+
await this.catalog.refresh();
|
|
1151
|
+
const goal = this.goal(threadId);
|
|
1152
|
+
if (goal?.status === "active")
|
|
1153
|
+
await this.session(threadId).goal("clear", true);
|
|
1154
|
+
this.gateway.meta.setPausedGoal(threadId, null);
|
|
1155
|
+
return goal?.status === "complete" ? null : goal;
|
|
1156
|
+
}
|
|
1157
|
+
/** A goal the chat brings to this Claude backend as it switches (or keeps as a switch fails). */
|
|
1158
|
+
async giveGoal(threadId, goal) {
|
|
1159
|
+
if (goal.status === "paused")
|
|
1160
|
+
this.gateway.meta.setPausedGoal(threadId, { objective: goal.objective, createdAt: goal.createdAt, updatedAt: goal.updatedAt });
|
|
1161
|
+
else
|
|
1162
|
+
await this.session(threadId).goal(goal.objective, false);
|
|
1163
|
+
}
|
|
1164
|
+
/** A Claude backend the chat switched away from: nothing of it runs on (background tasks, process). */
|
|
1165
|
+
async retire(threadId) {
|
|
1166
|
+
const session = this.sessions.get(threadId);
|
|
1167
|
+
if (!session)
|
|
1168
|
+
return;
|
|
1169
|
+
this.sessions.delete(threadId);
|
|
1170
|
+
await session.stopTasks();
|
|
1171
|
+
const pids = sessionProcesses().filter((process) => process.session === threadId).map((process) => process.pid);
|
|
1172
|
+
await session.unload();
|
|
1173
|
+
killProcesses(pids);
|
|
1174
|
+
}
|
|
983
1175
|
/** Removes a session with its transcript (also what a failed switch to Claude leaves behind). */
|
|
984
1176
|
async discard(threadId) {
|
|
985
1177
|
await this.sessions.get(threadId)?.unload();
|
|
@@ -1017,7 +1209,7 @@ export class ClaudeThreads {
|
|
|
1017
1209
|
}
|
|
1018
1210
|
const upTo = params.lastTurnId ? await this.boundaryBefore(sourceId, params.lastTurnId, true)
|
|
1019
1211
|
: params.beforeTurnId ? await this.boundaryBefore(sourceId, params.beforeTurnId, false)
|
|
1020
|
-
: this.
|
|
1212
|
+
: this.leaves.get(sourceId) ?? null;
|
|
1021
1213
|
const summary = this.catalog.get(sourceId);
|
|
1022
1214
|
if (!summary)
|
|
1023
1215
|
throw invalidParams(`thread not found: ${sourceId}`);
|
|
@@ -1027,7 +1219,7 @@ export class ClaudeThreads {
|
|
|
1027
1219
|
const thread = await this.thread(sessionId);
|
|
1028
1220
|
const turns = params.excludeTurns ? [] : (await this.newestTurns(sessionId, 25)).turns;
|
|
1029
1221
|
const forked = { ...thread, forkedFromId: sourceId };
|
|
1030
|
-
this.gateway.emit(sessionId, "thread/started", { thread: forked });
|
|
1222
|
+
setImmediate(() => this.gateway.emit(sessionId, "thread/started", { thread: forked }));
|
|
1031
1223
|
const settings = this.settingsFrom(params, this.settings(sourceId));
|
|
1032
1224
|
this.sessions.set(sessionId, new ClaudeSession(this, sessionId, settings, { exists: true }));
|
|
1033
1225
|
return { thread: { ...forked, turns }, ...this.settingsResponse(settings) };
|
|
@@ -1040,7 +1232,7 @@ export class ClaudeThreads {
|
|
|
1040
1232
|
await session?.unload();
|
|
1041
1233
|
this.sessions.delete(threadId);
|
|
1042
1234
|
if (leaf) {
|
|
1043
|
-
this.
|
|
1235
|
+
this.leaves.set(threadId, leaf);
|
|
1044
1236
|
return;
|
|
1045
1237
|
}
|
|
1046
1238
|
// Everything rolled back: Claude neither resumes nor starts anew a transcript without messages, so the session
|
|
@@ -1048,7 +1240,7 @@ export class ClaudeThreads {
|
|
|
1048
1240
|
const name = this.catalog.get(threadId).customTitle;
|
|
1049
1241
|
await deleteSession(threadId);
|
|
1050
1242
|
this.catalog.dropPages(threadId);
|
|
1051
|
-
this.
|
|
1243
|
+
this.leaves.delete(threadId);
|
|
1052
1244
|
await this.catalog.refresh();
|
|
1053
1245
|
this.sessions.set(threadId, new ClaudeSession(this, threadId, settings, { exists: false }));
|
|
1054
1246
|
if (name)
|
|
@@ -1114,15 +1306,16 @@ export class ClaudeThreads {
|
|
|
1114
1306
|
effort: settings.effort,
|
|
1115
1307
|
fast: settings.fast,
|
|
1116
1308
|
permissionMode: settings.permissionMode,
|
|
1309
|
+
plan: settings.plan,
|
|
1310
|
+
cwd: settings.cwd,
|
|
1117
1311
|
loaded: session !== undefined,
|
|
1118
1312
|
process: session?.loaded ?? false,
|
|
1119
1313
|
actions: this.actions.get(threadId) ?? [],
|
|
1120
|
-
|
|
1314
|
+
turnId: session?.turn?.id ?? null,
|
|
1121
1315
|
usage: session?.totalUsage,
|
|
1122
|
-
contextWindow: session?.
|
|
1316
|
+
contextWindow: this.contextWindow(session?.liveModel ?? settings.model),
|
|
1123
1317
|
lastUsage: session?.lastUsage,
|
|
1124
|
-
|
|
1125
|
-
backgroundTasks: session?.tasks.size ?? 0,
|
|
1318
|
+
backgroundTasks: session?.backgroundTasks.length ?? 0,
|
|
1126
1319
|
};
|
|
1127
1320
|
}
|
|
1128
1321
|
/** The summary of Claude's latest compaction if nothing was said since ("" otherwise). */
|