@gkorepanov/ccodex 0.5.0-rc.8 → 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 +255 -60
- 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) {
|
|
@@ -418,8 +449,10 @@ export class ClaudeThreads {
|
|
|
418
449
|
items: turn.items.map((item) => running.has(item.id) ? { ...item, status: "inProgress", exitCode: null, aggregatedOutput: null, durationMs: null } : item),
|
|
419
450
|
})) : [...history];
|
|
420
451
|
const live = session?.liveTurn();
|
|
452
|
+
// Stock's stale turns: one the transcript leaves unfinished with none running here (its Claude died with a daemon
|
|
453
|
+
// restart) was interrupted. A sub-agent runs in its parent's session.
|
|
421
454
|
if (!live)
|
|
422
|
-
return turns;
|
|
455
|
+
return threadId.startsWith("agent-") ? turns : turns.map((turn) => turn.status === "inProgress" ? { ...turn, status: "interrupted" } : turn);
|
|
423
456
|
const index = turns.findIndex((turn) => turn.id === live.id);
|
|
424
457
|
if (index >= 0)
|
|
425
458
|
turns = [...turns.slice(0, index), { ...turns[index], status: "inProgress", completedAt: null }];
|
|
@@ -452,6 +485,22 @@ export class ClaudeThreads {
|
|
|
452
485
|
: await pages.since(source, anchor.anchor);
|
|
453
486
|
return paginateTurns(this.withLive(threadId, window.turns), params);
|
|
454
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
|
+
}
|
|
455
504
|
async itemsPage(threadId, params) {
|
|
456
505
|
if (!this.catalog.get(threadId))
|
|
457
506
|
await this.catalog.refresh();
|
|
@@ -479,10 +528,10 @@ export class ClaudeThreads {
|
|
|
479
528
|
const summary = this.catalog.get(threadId);
|
|
480
529
|
return {
|
|
481
530
|
cwd: summary?.cwd ?? process.cwd(),
|
|
482
|
-
model: summary
|
|
483
|
-
effort: summary
|
|
531
|
+
model: (summary && this.recordedModel(summary)) || this.defaultModel,
|
|
532
|
+
effort: summary ? this.recordedEffort(summary) : null,
|
|
484
533
|
fast: summary?.serviceTier === "fast",
|
|
485
|
-
|
|
534
|
+
...recordedPermissions(threadId, summary?.permissionMode, this.gateway.meta),
|
|
486
535
|
};
|
|
487
536
|
}
|
|
488
537
|
/** Common fields of thread/start|resume|fork responses and thread/settings/updated. */
|
|
@@ -519,7 +568,7 @@ export class ClaudeThreads {
|
|
|
519
568
|
effort: settings.effort,
|
|
520
569
|
summary: null,
|
|
521
570
|
collaborationMode: {
|
|
522
|
-
mode: settings.
|
|
571
|
+
mode: settings.plan ? "plan" : "default",
|
|
523
572
|
settings: { model: response.model, reasoning_effort: settings.effort, developer_instructions: null },
|
|
524
573
|
},
|
|
525
574
|
multiAgentMode: "explicitRequestOnly",
|
|
@@ -545,14 +594,14 @@ export class ClaudeThreads {
|
|
|
545
594
|
return session;
|
|
546
595
|
if (!this.catalog.get(threadId))
|
|
547
596
|
throw invalidParams(`thread not found: ${threadId}`);
|
|
548
|
-
const leaf = this.
|
|
597
|
+
const leaf = this.leaves.get(threadId);
|
|
549
598
|
session = new ClaudeSession(this, threadId, this.settings(threadId), { exists: true, ...(leaf ? { resumeAt: leaf } : {}) });
|
|
550
599
|
this.sessions.set(threadId, session);
|
|
551
600
|
return session;
|
|
552
601
|
}
|
|
553
602
|
/** The session continued from the rollback leaf: its new records end the history from now on. */
|
|
554
603
|
resumedAtLeaf(threadId) {
|
|
555
|
-
this.
|
|
604
|
+
this.leaves.delete(threadId);
|
|
556
605
|
}
|
|
557
606
|
turnCompleted(session, turnId) {
|
|
558
607
|
const threadId = session.threadId;
|
|
@@ -565,6 +614,9 @@ export class ClaudeThreads {
|
|
|
565
614
|
const goal = this.goal(threadId);
|
|
566
615
|
if (JSON.stringify(goal) === before)
|
|
567
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;
|
|
568
620
|
if (goal)
|
|
569
621
|
this.gateway.emit(threadId, "thread/goal/updated", { threadId, turnId, goal });
|
|
570
622
|
else
|
|
@@ -572,13 +624,22 @@ export class ClaudeThreads {
|
|
|
572
624
|
});
|
|
573
625
|
this.onTurnCompleted?.(threadId, turnId);
|
|
574
626
|
}
|
|
575
|
-
/**
|
|
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
|
+
*/
|
|
576
631
|
goal(threadId) {
|
|
577
|
-
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 };
|
|
578
639
|
if (!goal)
|
|
579
640
|
return null;
|
|
580
641
|
return {
|
|
581
|
-
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,
|
|
582
643
|
timeUsedSeconds: Math.max(0, goal.updatedAt - goal.createdAt), createdAt: goal.createdAt, updatedAt: goal.updatedAt,
|
|
583
644
|
};
|
|
584
645
|
}
|
|
@@ -586,14 +647,26 @@ export class ClaudeThreads {
|
|
|
586
647
|
// Sub-agent activity is shown by the parent's collabAgentToolCall item; child history is read from disk.
|
|
587
648
|
}
|
|
588
649
|
onRateLimit(info) {
|
|
589
|
-
if (!info
|
|
650
|
+
if (!info)
|
|
590
651
|
return;
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
+
}
|
|
597
670
|
const snapshot = this.rateLimitSnapshot();
|
|
598
671
|
for (const connection of this.gateway.connections) {
|
|
599
672
|
if (connection.provider === "claude")
|
|
@@ -615,10 +688,44 @@ export class ClaudeThreads {
|
|
|
615
688
|
rateLimitReachedType: [...this.rateLimitWindows.values()].some((value) => value.status === "rejected") ? "rate_limit_reached" : null,
|
|
616
689
|
};
|
|
617
690
|
}
|
|
691
|
+
/** Claude's plan limits: read from Claude's `/usage` data at most once a minute, and as turns report them. */
|
|
618
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;
|
|
619
696
|
const snapshot = this.rateLimitSnapshot();
|
|
620
697
|
return { rateLimits: snapshot, rateLimitsByLimitId: { claude: snapshot } };
|
|
621
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
|
+
}
|
|
622
729
|
rateLimitWindowsText() {
|
|
623
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})`);
|
|
624
731
|
}
|
|
@@ -635,7 +742,9 @@ export class ClaudeThreads {
|
|
|
635
742
|
}
|
|
636
743
|
cachedModels() {
|
|
637
744
|
try {
|
|
638
|
-
|
|
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;
|
|
639
748
|
}
|
|
640
749
|
catch {
|
|
641
750
|
return undefined;
|
|
@@ -643,9 +752,17 @@ export class ClaudeThreads {
|
|
|
643
752
|
}
|
|
644
753
|
/** Asks Claude for its models and keeps them for the next start. */
|
|
645
754
|
async probeModels() {
|
|
646
|
-
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
|
+
});
|
|
647
764
|
const temporary = `${this.modelsPath}.${process.pid}.tmp`;
|
|
648
|
-
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);
|
|
649
766
|
return this.useModels(models);
|
|
650
767
|
}
|
|
651
768
|
useModels(models) {
|
|
@@ -654,14 +771,37 @@ export class ClaudeThreads {
|
|
|
654
771
|
for (const model of models) {
|
|
655
772
|
if (model.value !== "default" && model.resolvedModel)
|
|
656
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);
|
|
657
779
|
}
|
|
658
|
-
|
|
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;
|
|
659
798
|
}
|
|
660
799
|
modelLabel(model) {
|
|
661
800
|
return model ? claudeModelLabel(model) : "Claude (default)";
|
|
662
801
|
}
|
|
663
802
|
pendingNames = new Map();
|
|
664
803
|
pickerValues = new Map();
|
|
804
|
+
defaultEfforts = new Map();
|
|
665
805
|
skillCache = new Map();
|
|
666
806
|
async skills(cwds) {
|
|
667
807
|
const entries = await Promise.all(cwds.map(async (cwd) => {
|
|
@@ -693,7 +833,10 @@ export class ClaudeThreads {
|
|
|
693
833
|
}
|
|
694
834
|
async sideTurn(side, params) {
|
|
695
835
|
const input = normalizeUserInput(params.input ?? []);
|
|
696
|
-
|
|
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;
|
|
697
840
|
const id = randomUUID();
|
|
698
841
|
const started = Math.floor(Date.now() / 1000);
|
|
699
842
|
const turn = { id, items: [], itemsView: "full", status: "inProgress", error: null, startedAt: started, completedAt: null, durationMs: null };
|
|
@@ -712,6 +855,8 @@ export class ClaudeThreads {
|
|
|
712
855
|
text = await this.session(side.sourceId).askSideQuestion(history.length
|
|
713
856
|
? `Earlier in this side conversation:\n${history.join("\n")}\n\nNew question: ${question}`
|
|
714
857
|
: question);
|
|
858
|
+
if (schema)
|
|
859
|
+
text = JSON.stringify(JSON.parse(text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1)));
|
|
715
860
|
}
|
|
716
861
|
catch (error) {
|
|
717
862
|
text = `Side question failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
@@ -742,6 +887,7 @@ export class ClaudeThreads {
|
|
|
742
887
|
case "thread/read": return { thread: params.includeTurns ? await this.read(threadId).then(({ thread, turns }) => ({ ...thread, turns })) : await this.thread(threadId) };
|
|
743
888
|
case "thread/turns/list": return this.turnsPage(threadId, params);
|
|
744
889
|
case "thread/items/list": return this.itemsPage(threadId, params);
|
|
890
|
+
case "thread/searchOccurrences": return this.searchOccurrences(threadId, params);
|
|
745
891
|
case "turn/start": {
|
|
746
892
|
this.gateway.subscribe(threadId, connection);
|
|
747
893
|
return { turn: await this.session(threadId).startTurn(params) };
|
|
@@ -772,10 +918,7 @@ export class ClaudeThreads {
|
|
|
772
918
|
return {};
|
|
773
919
|
}
|
|
774
920
|
case "thread/settings/update": {
|
|
775
|
-
|
|
776
|
-
if (await session.updateSettings(params)) {
|
|
777
|
-
this.gateway.emit(threadId, "thread/settings/updated", { threadId, threadSettings: this.threadSettings(session.settings) });
|
|
778
|
-
}
|
|
921
|
+
await this.session(threadId).updateSettings(params);
|
|
779
922
|
return {};
|
|
780
923
|
}
|
|
781
924
|
// Claude's settings have no per-turn scope: the running turn's change stays for the chat.
|
|
@@ -798,9 +941,11 @@ export class ClaudeThreads {
|
|
|
798
941
|
return { goal: goal?.status === "complete" ? null : goal };
|
|
799
942
|
}
|
|
800
943
|
case "thread/goal/set": {
|
|
944
|
+
await this.catalog.refresh();
|
|
801
945
|
const now = Math.floor(Date.now() / 1000);
|
|
802
946
|
const current = this.goal(threadId);
|
|
803
947
|
if (params.objective) {
|
|
948
|
+
this.gateway.meta.setPausedGoal(threadId, null);
|
|
804
949
|
const session = this.session(threadId);
|
|
805
950
|
const goal = {
|
|
806
951
|
threadId, objective: params.objective, status: "active", tokenBudget: null, tokensUsed: 0, timeUsedSeconds: 0,
|
|
@@ -809,21 +954,41 @@ export class ClaudeThreads {
|
|
|
809
954
|
// Like stock, the goal's turn starts after the answer: Desktop shows the goal message itself on the answer.
|
|
810
955
|
setImmediate(() => {
|
|
811
956
|
this.gateway.emit(threadId, "thread/goal/updated", { threadId, turnId: null, goal });
|
|
812
|
-
session.
|
|
957
|
+
session.goal(params.objective, current?.status === "active")
|
|
813
958
|
.catch((error) => this.logger.warn("claude.goal.start-failed", { threadId, error: String(error) }));
|
|
814
959
|
});
|
|
815
960
|
return { goal };
|
|
816
961
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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 };
|
|
820
976
|
}
|
|
821
977
|
case "thread/goal/clear": {
|
|
978
|
+
await this.catalog.refresh();
|
|
822
979
|
const goal = this.goal(threadId);
|
|
823
|
-
if (goal
|
|
824
|
-
|
|
825
|
-
this.gateway.
|
|
826
|
-
|
|
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 };
|
|
827
992
|
}
|
|
828
993
|
case "thread/queue/list": return { data: this.sessions.get(threadId)?.queued ?? [], nextCursor: null };
|
|
829
994
|
case "thread/queue/add": {
|
|
@@ -843,7 +1008,7 @@ export class ClaudeThreads {
|
|
|
843
1008
|
case "thread/backgroundTerminals/list": {
|
|
844
1009
|
const session = this.sessions.get(threadId);
|
|
845
1010
|
return {
|
|
846
|
-
data:
|
|
1011
|
+
data: (session?.backgroundTasks ?? []).filter((task) => task.taskType === "local_bash").map((task) => ({
|
|
847
1012
|
itemId: task.toolUseId ?? task.taskId, processId: task.taskId, command: task.description, cwd: session.settings.cwd,
|
|
848
1013
|
osPid: null, cpuPercent: null, rssKb: null,
|
|
849
1014
|
})),
|
|
@@ -879,6 +1044,7 @@ export class ClaudeThreads {
|
|
|
879
1044
|
}
|
|
880
1045
|
case "thread/turns/list": return paginateTurns(side.turns, params);
|
|
881
1046
|
case "thread/items/list": return paginateItems(side.turns, params);
|
|
1047
|
+
case "thread/searchOccurrences": return occurrencesPage(turnOccurrences(side.turns, params.searchTerm), params);
|
|
882
1048
|
case "thread/unsubscribe":
|
|
883
1049
|
case "thread/delete":
|
|
884
1050
|
case "thread/archive":
|
|
@@ -906,13 +1072,14 @@ export class ClaudeThreads {
|
|
|
906
1072
|
async startThread(connection, params) {
|
|
907
1073
|
await this.models().catch(() => undefined);
|
|
908
1074
|
const settings = this.settingsFrom(params, {
|
|
909
|
-
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,
|
|
910
1076
|
});
|
|
911
1077
|
const session = this.create(settings);
|
|
912
1078
|
const threadId = session.threadId;
|
|
913
1079
|
this.gateway.subscribe(threadId, connection);
|
|
914
1080
|
const thread = this.decorate(this.freshThread(session));
|
|
915
|
-
|
|
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 }));
|
|
916
1083
|
this.gateway.titles.track(threadId);
|
|
917
1084
|
this.prewarm(session);
|
|
918
1085
|
return { thread, ...this.settingsResponse(settings) };
|
|
@@ -945,7 +1112,7 @@ export class ClaudeThreads {
|
|
|
945
1112
|
const { turns, usage } = await this.newestTurns(threadId, 1);
|
|
946
1113
|
// Like stock, the context meter follows a resume (Desktop's /status reads it).
|
|
947
1114
|
if (usage?.last) {
|
|
948
|
-
const modelContextWindow = this.
|
|
1115
|
+
const modelContextWindow = this.contextWindow(thread.model?.slice(this.config.modelPrefix.length));
|
|
949
1116
|
setImmediate(() => this.gateway.emit(threadId, "thread/tokenUsage/updated", { threadId, turnId: turns.at(-1)?.id ?? null, tokenUsage: { ...usage, modelContextWindow } }));
|
|
950
1117
|
}
|
|
951
1118
|
// A sub-agent has no session of its own: it shows the model and directory it runs with.
|
|
@@ -957,7 +1124,7 @@ export class ClaudeThreads {
|
|
|
957
1124
|
const response = {
|
|
958
1125
|
thread: { ...thread, turns: params.excludeTurns ? [] : turns },
|
|
959
1126
|
...this.settingsResponse(settings),
|
|
960
|
-
collaborationMode:
|
|
1127
|
+
collaborationMode: this.threadSettings(settings).collaborationMode,
|
|
961
1128
|
initialTurnsPage: params.initialTurnsPage ? await this.turnsPage(threadId, { ...params.initialTurnsPage, threadId }) : null,
|
|
962
1129
|
...historyCursors(turns),
|
|
963
1130
|
};
|
|
@@ -978,6 +1145,33 @@ export class ClaudeThreads {
|
|
|
978
1145
|
this.gateway.emit(threadId, "thread/name/updated", { threadId, threadName: name });
|
|
979
1146
|
return {};
|
|
980
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
|
+
}
|
|
981
1175
|
/** Removes a session with its transcript (also what a failed switch to Claude leaves behind). */
|
|
982
1176
|
async discard(threadId) {
|
|
983
1177
|
await this.sessions.get(threadId)?.unload();
|
|
@@ -1015,7 +1209,7 @@ export class ClaudeThreads {
|
|
|
1015
1209
|
}
|
|
1016
1210
|
const upTo = params.lastTurnId ? await this.boundaryBefore(sourceId, params.lastTurnId, true)
|
|
1017
1211
|
: params.beforeTurnId ? await this.boundaryBefore(sourceId, params.beforeTurnId, false)
|
|
1018
|
-
: this.
|
|
1212
|
+
: this.leaves.get(sourceId) ?? null;
|
|
1019
1213
|
const summary = this.catalog.get(sourceId);
|
|
1020
1214
|
if (!summary)
|
|
1021
1215
|
throw invalidParams(`thread not found: ${sourceId}`);
|
|
@@ -1025,7 +1219,7 @@ export class ClaudeThreads {
|
|
|
1025
1219
|
const thread = await this.thread(sessionId);
|
|
1026
1220
|
const turns = params.excludeTurns ? [] : (await this.newestTurns(sessionId, 25)).turns;
|
|
1027
1221
|
const forked = { ...thread, forkedFromId: sourceId };
|
|
1028
|
-
this.gateway.emit(sessionId, "thread/started", { thread: forked });
|
|
1222
|
+
setImmediate(() => this.gateway.emit(sessionId, "thread/started", { thread: forked }));
|
|
1029
1223
|
const settings = this.settingsFrom(params, this.settings(sourceId));
|
|
1030
1224
|
this.sessions.set(sessionId, new ClaudeSession(this, sessionId, settings, { exists: true }));
|
|
1031
1225
|
return { thread: { ...forked, turns }, ...this.settingsResponse(settings) };
|
|
@@ -1038,7 +1232,7 @@ export class ClaudeThreads {
|
|
|
1038
1232
|
await session?.unload();
|
|
1039
1233
|
this.sessions.delete(threadId);
|
|
1040
1234
|
if (leaf) {
|
|
1041
|
-
this.
|
|
1235
|
+
this.leaves.set(threadId, leaf);
|
|
1042
1236
|
return;
|
|
1043
1237
|
}
|
|
1044
1238
|
// Everything rolled back: Claude neither resumes nor starts anew a transcript without messages, so the session
|
|
@@ -1046,7 +1240,7 @@ export class ClaudeThreads {
|
|
|
1046
1240
|
const name = this.catalog.get(threadId).customTitle;
|
|
1047
1241
|
await deleteSession(threadId);
|
|
1048
1242
|
this.catalog.dropPages(threadId);
|
|
1049
|
-
this.
|
|
1243
|
+
this.leaves.delete(threadId);
|
|
1050
1244
|
await this.catalog.refresh();
|
|
1051
1245
|
this.sessions.set(threadId, new ClaudeSession(this, threadId, settings, { exists: false }));
|
|
1052
1246
|
if (name)
|
|
@@ -1112,15 +1306,16 @@ export class ClaudeThreads {
|
|
|
1112
1306
|
effort: settings.effort,
|
|
1113
1307
|
fast: settings.fast,
|
|
1114
1308
|
permissionMode: settings.permissionMode,
|
|
1309
|
+
plan: settings.plan,
|
|
1310
|
+
cwd: settings.cwd,
|
|
1115
1311
|
loaded: session !== undefined,
|
|
1116
1312
|
process: session?.loaded ?? false,
|
|
1117
1313
|
actions: this.actions.get(threadId) ?? [],
|
|
1118
|
-
|
|
1314
|
+
turnId: session?.turn?.id ?? null,
|
|
1119
1315
|
usage: session?.totalUsage,
|
|
1120
|
-
contextWindow: session?.
|
|
1316
|
+
contextWindow: this.contextWindow(session?.liveModel ?? settings.model),
|
|
1121
1317
|
lastUsage: session?.lastUsage,
|
|
1122
|
-
|
|
1123
|
-
backgroundTasks: session?.tasks.size ?? 0,
|
|
1318
|
+
backgroundTasks: session?.backgroundTasks.length ?? 0,
|
|
1124
1319
|
};
|
|
1125
1320
|
}
|
|
1126
1321
|
/** The summary of Claude's latest compaction if nothing was said since ("" otherwise). */
|