@cjhyy/code-shell-core 0.9.6 → 0.9.7
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/dist/automation/scheduler.d.ts +3 -0
- package/dist/automation/scheduler.js +21 -0
- package/dist/context/manager.d.ts +8 -2
- package/dist/context/manager.js +20 -4
- package/dist/context/notes.d.ts +39 -0
- package/dist/context/notes.js +314 -0
- package/dist/engine/engine.d.ts +5 -4
- package/dist/engine/engine.js +79 -11
- package/dist/engine/run-tooling.d.ts +3 -0
- package/dist/engine/run-tooling.js +25 -23
- package/dist/engine/run-types.d.ts +4 -0
- package/dist/engine/subagent-spawner.d.ts +3 -0
- package/dist/engine/subagent-spawner.js +48 -17
- package/dist/engine/turn-loop.d.ts +10 -0
- package/dist/engine/turn-loop.js +91 -19
- package/dist/engine/types.d.ts +19 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/prompt/section-loader.js +1 -0
- package/dist/prompt/sections/browser.md +4 -2
- package/dist/prompt/sections/context-notes.md +9 -0
- package/dist/protocol/server.d.ts +1 -0
- package/dist/protocol/server.js +207 -19
- package/dist/protocol/types.d.ts +2 -0
- package/dist/session/session-manager.js +8 -7
- package/dist/session/transcript.d.ts +17 -0
- package/dist/session/transcript.js +271 -16
- package/dist/settings/schema.d.ts +9 -0
- package/dist/settings/schema.js +4 -0
- package/dist/themes/paths.js +20 -1
- package/dist/tool-system/browser-bridge.d.ts +3 -1
- package/dist/tool-system/browser-discovery.d.ts +6 -0
- package/dist/tool-system/browser-discovery.js +17 -0
- package/dist/tool-system/builtin/browser-tools.js +12 -8
- package/dist/tool-system/builtin/context-notes.d.ts +12 -0
- package/dist/tool-system/builtin/context-notes.js +188 -0
- package/dist/tool-system/builtin/index.js +47 -0
- package/dist/tool-system/builtin/mcp-tools.d.ts +5 -3
- package/dist/tool-system/builtin/mcp-tools.js +10 -10
- package/dist/tool-system/builtin/tool-search.js +15 -3
- package/dist/tool-system/context.d.ts +9 -0
- package/dist/tool-system/executor.js +5 -3
- package/dist/tool-system/mcp-compat.d.ts +3 -0
- package/dist/tool-system/mcp-compat.js +51 -0
- package/dist/tool-system/mcp-manager.d.ts +27 -26
- package/dist/tool-system/mcp-manager.js +273 -111
- package/dist/tool-system/mcp-workspace.d.ts +18 -0
- package/dist/tool-system/mcp-workspace.js +56 -0
- package/dist/tool-system/permission.d.ts +6 -0
- package/dist/tool-system/permission.js +45 -9
- package/dist/tool-system/plan-mode-allowlist.js +5 -0
- package/dist/tool-system/sandbox/seatbelt.js +71 -2
- package/dist/tool-system/session-tool-host.js +9 -1
- package/dist/types.d.ts +4 -2
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { appendFileSync, chmodSync, closeSync, existsSync, fchmodSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, writeFileSync, } from "node:fs";
|
|
6
6
|
import { dirname } from "node:path";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
7
8
|
import { nanoid } from "nanoid";
|
|
8
9
|
import { logger } from "../logging/logger.js";
|
|
9
10
|
const DEFAULT_CONTEXT_TAIL_SCAN_BYTES = 32 * 1024 * 1024;
|
|
@@ -33,6 +34,131 @@ function appendTranscriptLine(filePath, data) {
|
|
|
33
34
|
const defaultTranscriptWriter = (filePath, data) => {
|
|
34
35
|
appendTranscriptLine(filePath, data);
|
|
35
36
|
};
|
|
37
|
+
function checkpointHash(snapshot) {
|
|
38
|
+
return createHash("sha256").update(JSON.stringify(snapshot)).digest("hex");
|
|
39
|
+
}
|
|
40
|
+
/** Complete, ordered provider pairs are required before a checkpoint can commit. */
|
|
41
|
+
export function hasCompleteContextToolPairs(messages) {
|
|
42
|
+
const pending = new Set();
|
|
43
|
+
const used = new Set();
|
|
44
|
+
for (const message of messages) {
|
|
45
|
+
if (pending.size > 0 &&
|
|
46
|
+
(message.role !== "user" ||
|
|
47
|
+
!Array.isArray(message.content) ||
|
|
48
|
+
message.content.length === 0 ||
|
|
49
|
+
message.content.some((block) => block.type !== "tool_result")))
|
|
50
|
+
return false;
|
|
51
|
+
if (!Array.isArray(message.content))
|
|
52
|
+
continue;
|
|
53
|
+
for (const block of message.content) {
|
|
54
|
+
if (block.type === "tool_use") {
|
|
55
|
+
if (message.role !== "assistant" || !block.id || used.has(block.id))
|
|
56
|
+
return false;
|
|
57
|
+
used.add(block.id);
|
|
58
|
+
pending.add(block.id);
|
|
59
|
+
}
|
|
60
|
+
else if (block.type === "tool_result") {
|
|
61
|
+
if (message.role !== "user" || !block.tool_use_id || !pending.delete(block.tool_use_id)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return pending.size === 0;
|
|
68
|
+
}
|
|
69
|
+
function readCheckpoint(event, precedingEvents) {
|
|
70
|
+
if (event.type !== "context_checkpoint")
|
|
71
|
+
return undefined;
|
|
72
|
+
const data = event.data;
|
|
73
|
+
if (!data ||
|
|
74
|
+
typeof data !== "object" ||
|
|
75
|
+
data.version !== 1 ||
|
|
76
|
+
typeof data.noteId !== "string" ||
|
|
77
|
+
typeof data.coveredThroughEventId !== "string" ||
|
|
78
|
+
!Array.isArray(data.messages) ||
|
|
79
|
+
data.messages.length === 0 ||
|
|
80
|
+
!Array.isArray(data.clientMessageIds))
|
|
81
|
+
return undefined;
|
|
82
|
+
const noteIndex = precedingEvents.findIndex((candidate) => candidate.id === data.noteId);
|
|
83
|
+
const cursorIndex = precedingEvents.findIndex((candidate) => candidate.id === data.coveredThroughEventId);
|
|
84
|
+
const note = precedingEvents[noteIndex];
|
|
85
|
+
if (cursorIndex < 0 ||
|
|
86
|
+
noteIndex <= cursorIndex ||
|
|
87
|
+
note?.type !== "context_note" ||
|
|
88
|
+
!note.data ||
|
|
89
|
+
typeof note.data.text !== "string" ||
|
|
90
|
+
note.data.text.trim().length === 0 ||
|
|
91
|
+
note.data.coveredThroughEventId !== data.coveredThroughEventId)
|
|
92
|
+
return undefined;
|
|
93
|
+
for (const value of data.messages) {
|
|
94
|
+
if (!value || typeof value !== "object")
|
|
95
|
+
return undefined;
|
|
96
|
+
const message = value;
|
|
97
|
+
if (!["user", "assistant", "system", "tool"].includes(message.role))
|
|
98
|
+
return undefined;
|
|
99
|
+
if (!validCheckpointContent(message.content))
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
for (const pair of data.clientMessageIds) {
|
|
104
|
+
if (!Array.isArray(pair) ||
|
|
105
|
+
pair.length !== 2 ||
|
|
106
|
+
typeof pair[0] !== "string" ||
|
|
107
|
+
seen.has(pair[0]) ||
|
|
108
|
+
!Number.isSafeInteger(pair[1]) ||
|
|
109
|
+
pair[1] < 0 ||
|
|
110
|
+
pair[1] >= data.messages.length)
|
|
111
|
+
return undefined;
|
|
112
|
+
seen.add(pair[0]);
|
|
113
|
+
}
|
|
114
|
+
const snapshot = {
|
|
115
|
+
version: 1,
|
|
116
|
+
noteId: data.noteId,
|
|
117
|
+
coveredThroughEventId: data.coveredThroughEventId,
|
|
118
|
+
messages: data.messages,
|
|
119
|
+
clientMessageIds: data.clientMessageIds,
|
|
120
|
+
};
|
|
121
|
+
if (data.checksum !== checkpointHash(snapshot) ||
|
|
122
|
+
!hasCompleteContextToolPairs(snapshot.messages)) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
return snapshot;
|
|
126
|
+
}
|
|
127
|
+
function validCheckpointContent(content, depth = 0) {
|
|
128
|
+
if (typeof content === "string")
|
|
129
|
+
return true;
|
|
130
|
+
if (!Array.isArray(content) || depth > 8)
|
|
131
|
+
return false;
|
|
132
|
+
return content.every((value) => {
|
|
133
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
134
|
+
return false;
|
|
135
|
+
const block = value;
|
|
136
|
+
switch (block.type) {
|
|
137
|
+
case "text":
|
|
138
|
+
case "reasoning":
|
|
139
|
+
return ((block.text === undefined || typeof block.text === "string") &&
|
|
140
|
+
(block.reasoningContent === undefined || typeof block.reasoningContent === "string"));
|
|
141
|
+
case "image":
|
|
142
|
+
return (block.source?.type === "base64" &&
|
|
143
|
+
typeof block.source.media_type === "string" &&
|
|
144
|
+
typeof block.source.data === "string");
|
|
145
|
+
case "tool_use":
|
|
146
|
+
return (depth === 0 &&
|
|
147
|
+
typeof block.id === "string" &&
|
|
148
|
+
(block.name === undefined || typeof block.name === "string") &&
|
|
149
|
+
(block.input === undefined ||
|
|
150
|
+
(block.input !== null &&
|
|
151
|
+
typeof block.input === "object" &&
|
|
152
|
+
!Array.isArray(block.input))));
|
|
153
|
+
case "tool_result":
|
|
154
|
+
return (depth === 0 &&
|
|
155
|
+
typeof block.tool_use_id === "string" &&
|
|
156
|
+
(block.content === undefined || validCheckpointContent(block.content, depth + 1)));
|
|
157
|
+
default:
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
36
162
|
const CONTEXT_EVENT_TYPES = new Set([
|
|
37
163
|
"message",
|
|
38
164
|
"tool_use",
|
|
@@ -47,6 +173,18 @@ function isSyntheticInterruptedToolResult(event) {
|
|
|
47
173
|
event.data.toolName === "unknown" &&
|
|
48
174
|
event.data.error === INTERRUPTED_TOOL_RESULT_ERROR);
|
|
49
175
|
}
|
|
176
|
+
function toolResultContentBlock(event) {
|
|
177
|
+
const { toolCallId, result, error, contentBlocks } = event.data;
|
|
178
|
+
return {
|
|
179
|
+
type: "tool_result",
|
|
180
|
+
tool_use_id: toolCallId,
|
|
181
|
+
content: error
|
|
182
|
+
? `Error: ${error}`
|
|
183
|
+
: Array.isArray(contentBlocks) && contentBlocks.length > 0
|
|
184
|
+
? structuredClone(contentBlocks)
|
|
185
|
+
: (result ?? "(no output)"),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
50
188
|
/**
|
|
51
189
|
* Choose at most one result for every declared tool call. A real late result
|
|
52
190
|
* wins over the legacy synthetic "interrupted" placeholder that an older
|
|
@@ -120,6 +258,40 @@ export class Transcript {
|
|
|
120
258
|
transcript.loadEvents(events);
|
|
121
259
|
return transcript;
|
|
122
260
|
}
|
|
261
|
+
/** Preserve checkpoint provenance when a fork assigns fresh event ids. */
|
|
262
|
+
static remapContextForkReferences(sourceEvents, copiedEvents) {
|
|
263
|
+
if (!sourceEvents.some((event) => event.type === "context_note"))
|
|
264
|
+
return;
|
|
265
|
+
const eventIds = new Map(sourceEvents.map((event, index) => [event.id, copiedEvents[index].id]));
|
|
266
|
+
for (const [index, source] of sourceEvents.entries()) {
|
|
267
|
+
const copied = copiedEvents[index];
|
|
268
|
+
// An inherited note can cite old event ids. Resolve those aliases only
|
|
269
|
+
// against the copied prefix, never by reaching into the parent session.
|
|
270
|
+
copied.data.contextHistorySourceIds = [
|
|
271
|
+
...(Array.isArray(source.data.contextHistorySourceIds)
|
|
272
|
+
? source.data.contextHistorySourceIds.filter((id) => typeof id === "string")
|
|
273
|
+
: []),
|
|
274
|
+
source.id,
|
|
275
|
+
];
|
|
276
|
+
if (source.type === "context_note") {
|
|
277
|
+
const cursor = source.data.coveredThroughEventId;
|
|
278
|
+
if (typeof cursor === "string" && eventIds.has(cursor)) {
|
|
279
|
+
copied.data.coveredThroughEventId = eventIds.get(cursor);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (source.type !== "context_checkpoint")
|
|
283
|
+
continue;
|
|
284
|
+
const snapshot = readCheckpoint(source, sourceEvents.slice(0, index));
|
|
285
|
+
if (!snapshot)
|
|
286
|
+
continue; // Never turn a corrupt source checkpoint into a valid one.
|
|
287
|
+
const noteId = eventIds.get(snapshot.noteId);
|
|
288
|
+
const coveredThroughEventId = eventIds.get(snapshot.coveredThroughEventId);
|
|
289
|
+
if (!noteId || !coveredThroughEventId)
|
|
290
|
+
continue;
|
|
291
|
+
const remapped = { ...snapshot, noteId, coveredThroughEventId };
|
|
292
|
+
Object.assign(copied.data, remapped, { checksum: checkpointHash(remapped) });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
123
295
|
isPersistent() {
|
|
124
296
|
return this.persistent;
|
|
125
297
|
}
|
|
@@ -135,6 +307,41 @@ export class Transcript {
|
|
|
135
307
|
this.flush(event);
|
|
136
308
|
return event;
|
|
137
309
|
}
|
|
310
|
+
/** Failed note/checkpoint writes must never change the active replay. */
|
|
311
|
+
appendContextNote(text, coveredThroughEventId) {
|
|
312
|
+
return this.appendDurableContextEvent("context_note", { text, coveredThroughEventId });
|
|
313
|
+
}
|
|
314
|
+
appendContextCheckpoint(snapshot) {
|
|
315
|
+
const frozen = structuredClone(snapshot);
|
|
316
|
+
const data = { ...frozen, checksum: checkpointHash(frozen) };
|
|
317
|
+
const candidate = {
|
|
318
|
+
id: "validation",
|
|
319
|
+
type: "context_checkpoint",
|
|
320
|
+
timestamp: Date.now(),
|
|
321
|
+
turnNumber: this.currentTurn,
|
|
322
|
+
data,
|
|
323
|
+
};
|
|
324
|
+
if (!readCheckpoint(candidate, this.events))
|
|
325
|
+
return undefined;
|
|
326
|
+
return this.appendDurableContextEvent("context_checkpoint", data);
|
|
327
|
+
}
|
|
328
|
+
appendDurableContextEvent(type, data) {
|
|
329
|
+
// A missing earlier event makes a durable cursor unreliable, even when
|
|
330
|
+
// this particular append would succeed.
|
|
331
|
+
if (this.dirty)
|
|
332
|
+
return undefined;
|
|
333
|
+
const event = {
|
|
334
|
+
id: nanoid(12),
|
|
335
|
+
type,
|
|
336
|
+
timestamp: Date.now(),
|
|
337
|
+
turnNumber: this.currentTurn,
|
|
338
|
+
data,
|
|
339
|
+
};
|
|
340
|
+
if (!this.flush(event))
|
|
341
|
+
return undefined;
|
|
342
|
+
this.events.push(event);
|
|
343
|
+
return event;
|
|
344
|
+
}
|
|
138
345
|
/**
|
|
139
346
|
* Append a chat message to the transcript.
|
|
140
347
|
*
|
|
@@ -288,10 +495,11 @@ export class Transcript {
|
|
|
288
495
|
* payloads, and transport metadata must not leak into them.
|
|
289
496
|
*/
|
|
290
497
|
toMessagesWithIndex() {
|
|
498
|
+
const events = this.contextReplayEvents();
|
|
291
499
|
const messages = [];
|
|
292
500
|
const liveIndexByClientMessageId = new Map();
|
|
293
|
-
const selectedToolResults = preferredToolResults(
|
|
294
|
-
const hasRangeArchive =
|
|
501
|
+
const selectedToolResults = preferredToolResults(events);
|
|
502
|
+
const hasRangeArchive = events.some((e) => e.type === "range_archive");
|
|
295
503
|
const spansByFromId = new Map();
|
|
296
504
|
let openingSpan;
|
|
297
505
|
if (hasRangeArchive) {
|
|
@@ -302,7 +510,7 @@ export class Transcript {
|
|
|
302
510
|
// while scanning forward, so it would never close — silently swallowing
|
|
303
511
|
// the rest of the conversation. Fail open instead: ignore the marker.
|
|
304
512
|
const firstIndexByClientId = new Map();
|
|
305
|
-
for (const [index, event] of
|
|
513
|
+
for (const [index, event] of events.entries()) {
|
|
306
514
|
if (event.type === "message" && typeof event.data.clientMessageId === "string") {
|
|
307
515
|
if (!firstIndexByClientId.has(event.data.clientMessageId)) {
|
|
308
516
|
firstIndexByClientId.set(event.data.clientMessageId, index);
|
|
@@ -310,7 +518,7 @@ export class Transcript {
|
|
|
310
518
|
}
|
|
311
519
|
}
|
|
312
520
|
const presentClientIds = new Set(firstIndexByClientId.keys());
|
|
313
|
-
for (const event of
|
|
521
|
+
for (const event of events) {
|
|
314
522
|
if (event.type !== "range_archive")
|
|
315
523
|
continue;
|
|
316
524
|
const { summary, toClientMessageId, fromClientMessageId } = event.data;
|
|
@@ -351,7 +559,7 @@ export class Transcript {
|
|
|
351
559
|
// preferred) real result landed outside it — would be an orphaned block
|
|
352
560
|
// that breaks provider validation; skip it instead of emitting it.
|
|
353
561
|
const emittedToolUseIds = new Set();
|
|
354
|
-
for (const event of
|
|
562
|
+
for (const event of events) {
|
|
355
563
|
// Span bookkeeping runs on message events only: exit before entry so
|
|
356
564
|
// adjacent spans (A.to === B.from) hand over on the boundary message.
|
|
357
565
|
if (event.type === "message") {
|
|
@@ -387,7 +595,9 @@ export class Transcript {
|
|
|
387
595
|
}
|
|
388
596
|
}
|
|
389
597
|
}
|
|
390
|
-
|
|
598
|
+
// Tool results can be merged into this array below. Never mutate
|
|
599
|
+
// the source event (or a checkpoint) during a read-only replay.
|
|
600
|
+
messages.push({ role: role, content: structuredClone(content) });
|
|
391
601
|
break;
|
|
392
602
|
}
|
|
393
603
|
case "tool_use": {
|
|
@@ -402,18 +612,9 @@ export class Transcript {
|
|
|
402
612
|
!emittedToolUseIds.has(eventToolCallId)) {
|
|
403
613
|
break;
|
|
404
614
|
}
|
|
405
|
-
const { toolCallId, result, error, contentBlocks } = event.data;
|
|
406
615
|
// Find if there's already a user message with tool_results to append to
|
|
407
616
|
const lastMsg = messages[messages.length - 1];
|
|
408
|
-
const block =
|
|
409
|
-
type: "tool_result",
|
|
410
|
-
tool_use_id: toolCallId,
|
|
411
|
-
content: error
|
|
412
|
-
? `Error: ${error}`
|
|
413
|
-
: Array.isArray(contentBlocks) && contentBlocks.length > 0
|
|
414
|
-
? contentBlocks
|
|
415
|
-
: (result ?? "(no output)"),
|
|
416
|
-
};
|
|
617
|
+
const block = toolResultContentBlock(event);
|
|
417
618
|
if (lastMsg?.role === "user" && Array.isArray(lastMsg.content)) {
|
|
418
619
|
lastMsg.content.push(block);
|
|
419
620
|
}
|
|
@@ -449,6 +650,55 @@ export class Transcript {
|
|
|
449
650
|
}
|
|
450
651
|
return { messages, liveIndexByClientMessageId };
|
|
451
652
|
}
|
|
653
|
+
contextReplayEvents() {
|
|
654
|
+
for (let index = this.events.length - 1; index >= 0; index -= 1) {
|
|
655
|
+
const event = this.events[index];
|
|
656
|
+
if (event.type !== "context_checkpoint")
|
|
657
|
+
continue;
|
|
658
|
+
const snapshot = readCheckpoint(event, this.events.slice(0, index));
|
|
659
|
+
if (!snapshot)
|
|
660
|
+
continue; // Corrupt or truncated checkpoints fail open.
|
|
661
|
+
const clientIdByIndex = new Map(snapshot.clientMessageIds.map(([id, i]) => [i, id]));
|
|
662
|
+
const snapshotEvents = snapshot.messages.map((message, messageIndex) => ({
|
|
663
|
+
id: `${event.id}:${messageIndex}`,
|
|
664
|
+
type: "message",
|
|
665
|
+
timestamp: event.timestamp,
|
|
666
|
+
turnNumber: event.turnNumber,
|
|
667
|
+
data: {
|
|
668
|
+
role: message.role,
|
|
669
|
+
content: structuredClone(message.content),
|
|
670
|
+
...(clientIdByIndex.has(messageIndex)
|
|
671
|
+
? { clientMessageId: clientIdByIndex.get(messageIndex) }
|
|
672
|
+
: {}),
|
|
673
|
+
},
|
|
674
|
+
}));
|
|
675
|
+
const tail = this.events.slice(index + 1);
|
|
676
|
+
const lateResults = preferredToolResults([...snapshotEvents, ...tail]);
|
|
677
|
+
const completedIds = new Set();
|
|
678
|
+
for (const snapshotEvent of snapshotEvents) {
|
|
679
|
+
const content = snapshotEvent.data.content;
|
|
680
|
+
if (!Array.isArray(content))
|
|
681
|
+
continue;
|
|
682
|
+
snapshotEvent.data.content = content.map((block) => {
|
|
683
|
+
if (block.type !== "tool_result" || !block.tool_use_id)
|
|
684
|
+
return block;
|
|
685
|
+
completedIds.add(block.tool_use_id);
|
|
686
|
+
const late = lateResults.get(block.tool_use_id);
|
|
687
|
+
// Reconcile late duplicates in the original result position, so a
|
|
688
|
+
// result cannot become orphaned or split an unrelated later batch.
|
|
689
|
+
return late && !isSyntheticInterruptedToolResult(late)
|
|
690
|
+
? toolResultContentBlock(late)
|
|
691
|
+
: block;
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
return [
|
|
695
|
+
...snapshotEvents,
|
|
696
|
+
...tail.filter((candidate) => candidate.type !== "tool_result" ||
|
|
697
|
+
!completedIds.has(candidate.data.toolCallId)),
|
|
698
|
+
];
|
|
699
|
+
}
|
|
700
|
+
return this.events;
|
|
701
|
+
}
|
|
452
702
|
getEvents(type) {
|
|
453
703
|
if (!type)
|
|
454
704
|
return [...this.events];
|
|
@@ -727,6 +977,11 @@ export class Transcript {
|
|
|
727
977
|
return Transcript.loadFromFile(filePath);
|
|
728
978
|
}
|
|
729
979
|
}
|
|
980
|
+
// Checkpoint validity references its note and covered cursor. A bounded
|
|
981
|
+
// range-archive tail may omit either, so use the full loader here.
|
|
982
|
+
if (events.some((event) => event.type === "context_checkpoint")) {
|
|
983
|
+
return Transcript.loadFromFile(filePath);
|
|
984
|
+
}
|
|
730
985
|
let markerIndex = -1;
|
|
731
986
|
let toClientMessageId;
|
|
732
987
|
for (const [index, event] of events.entries()) {
|
|
@@ -405,6 +405,7 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
405
405
|
defaultMode?: "default" | "auto" | "acceptEdits" | "dontAsk" | "bypassPermissions" | "plan" | undefined;
|
|
406
406
|
}>>;
|
|
407
407
|
context: z.ZodDefault<z.ZodObject<{
|
|
408
|
+
strategy: z.ZodOptional<z.ZodEnum<["summary", "notes"]>>;
|
|
408
409
|
maxTokens: z.ZodDefault<z.ZodNumber>;
|
|
409
410
|
compactAtRatio: z.ZodDefault<z.ZodNumber>;
|
|
410
411
|
summarizeAtRatio: z.ZodDefault<z.ZodNumber>;
|
|
@@ -414,8 +415,10 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
414
415
|
compactAtRatio: number;
|
|
415
416
|
summarizeAtRatio: number;
|
|
416
417
|
microcompactFloorRatio: number;
|
|
418
|
+
strategy?: "summary" | "notes" | undefined;
|
|
417
419
|
}, {
|
|
418
420
|
maxTokens?: number | undefined;
|
|
421
|
+
strategy?: "summary" | "notes" | undefined;
|
|
419
422
|
compactAtRatio?: number | undefined;
|
|
420
423
|
summarizeAtRatio?: number | undefined;
|
|
421
424
|
microcompactFloorRatio?: number | undefined;
|
|
@@ -1429,6 +1432,7 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
1429
1432
|
defaultMode?: "default" | "auto" | "acceptEdits" | "dontAsk" | "bypassPermissions" | "plan" | undefined;
|
|
1430
1433
|
}>>;
|
|
1431
1434
|
context: z.ZodDefault<z.ZodObject<{
|
|
1435
|
+
strategy: z.ZodOptional<z.ZodEnum<["summary", "notes"]>>;
|
|
1432
1436
|
maxTokens: z.ZodDefault<z.ZodNumber>;
|
|
1433
1437
|
compactAtRatio: z.ZodDefault<z.ZodNumber>;
|
|
1434
1438
|
summarizeAtRatio: z.ZodDefault<z.ZodNumber>;
|
|
@@ -1438,8 +1442,10 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
1438
1442
|
compactAtRatio: number;
|
|
1439
1443
|
summarizeAtRatio: number;
|
|
1440
1444
|
microcompactFloorRatio: number;
|
|
1445
|
+
strategy?: "summary" | "notes" | undefined;
|
|
1441
1446
|
}, {
|
|
1442
1447
|
maxTokens?: number | undefined;
|
|
1448
|
+
strategy?: "summary" | "notes" | undefined;
|
|
1443
1449
|
compactAtRatio?: number | undefined;
|
|
1444
1450
|
summarizeAtRatio?: number | undefined;
|
|
1445
1451
|
microcompactFloorRatio?: number | undefined;
|
|
@@ -2453,6 +2459,7 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
2453
2459
|
defaultMode?: "default" | "auto" | "acceptEdits" | "dontAsk" | "bypassPermissions" | "plan" | undefined;
|
|
2454
2460
|
}>>;
|
|
2455
2461
|
context: z.ZodDefault<z.ZodObject<{
|
|
2462
|
+
strategy: z.ZodOptional<z.ZodEnum<["summary", "notes"]>>;
|
|
2456
2463
|
maxTokens: z.ZodDefault<z.ZodNumber>;
|
|
2457
2464
|
compactAtRatio: z.ZodDefault<z.ZodNumber>;
|
|
2458
2465
|
summarizeAtRatio: z.ZodDefault<z.ZodNumber>;
|
|
@@ -2462,8 +2469,10 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
2462
2469
|
compactAtRatio: number;
|
|
2463
2470
|
summarizeAtRatio: number;
|
|
2464
2471
|
microcompactFloorRatio: number;
|
|
2472
|
+
strategy?: "summary" | "notes" | undefined;
|
|
2465
2473
|
}, {
|
|
2466
2474
|
maxTokens?: number | undefined;
|
|
2475
|
+
strategy?: "summary" | "notes" | undefined;
|
|
2467
2476
|
compactAtRatio?: number | undefined;
|
|
2468
2477
|
summarizeAtRatio?: number | undefined;
|
|
2469
2478
|
microcompactFloorRatio?: number | undefined;
|
package/dist/settings/schema.js
CHANGED
|
@@ -257,6 +257,10 @@ export const SettingsSchema = z
|
|
|
257
257
|
.default({}),
|
|
258
258
|
context: z
|
|
259
259
|
.object({
|
|
260
|
+
strategy: z
|
|
261
|
+
.enum(["summary", "notes"])
|
|
262
|
+
.optional()
|
|
263
|
+
.describe("上下文策略:notes 使用接续笔记和历史检索;summary 使用现有摘要压缩。未设置时跟随行为配置。"),
|
|
260
264
|
maxTokens: z.number().default(200_000),
|
|
261
265
|
// 压缩阈值(占上下文窗口的比例)。窗口越大可调越高:1M 窗口的模型
|
|
262
266
|
// 把 compactAtRatio 调到 0.95 能少浪费几十万 token。三档须满足
|
package/dist/themes/paths.js
CHANGED
|
@@ -9,8 +9,27 @@ export class ThemeReviewChangedError extends ThemeInstallError {
|
|
|
9
9
|
super("theme content changed since preview");
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Where installed themes live.
|
|
14
|
+
*
|
|
15
|
+
* This used to resolve HOME on its own, so a test that reached it without
|
|
16
|
+
* CODE_SHELL_HOME wrote into the developer's real theme registry — eight
|
|
17
|
+
* "concurrent-N" fixture packs showed up in a real settings picker on
|
|
18
|
+
* 2026-09-06. The sessions-root guard did not cover it because this was a
|
|
19
|
+
* second, independent home resolution.
|
|
20
|
+
*
|
|
21
|
+
* Fails closed under a test runner instead. `bun test` sets NODE_ENV=test
|
|
22
|
+
* itself, so no test has to opt in, and a real host run is untouched.
|
|
23
|
+
*/
|
|
12
24
|
function userHome() {
|
|
13
|
-
|
|
25
|
+
const explicit = process.env.CODE_SHELL_HOME;
|
|
26
|
+
if (explicit)
|
|
27
|
+
return explicit;
|
|
28
|
+
if (process.env.NODE_ENV === "test" || process.env.BUN_TEST === "1") {
|
|
29
|
+
throw new ThemeInstallError("Refusing to use the real ~/.code-shell/themes from a test. Set CODE_SHELL_HOME " +
|
|
30
|
+
"to a temp dir (packages/core/test-setup.ts does this via the bunfig preload).");
|
|
31
|
+
}
|
|
32
|
+
return process.env.HOME ?? homedir();
|
|
14
33
|
}
|
|
15
34
|
/** A theme id must be a single safe path segment matching the manifest pattern. */
|
|
16
35
|
export function assertSafeThemeName(id) {
|
|
@@ -57,7 +57,7 @@ export interface BrowserSnapshot {
|
|
|
57
57
|
/** Which login identity this page is being viewed as (§8.3). */
|
|
58
58
|
identity?: BrowserIdentity;
|
|
59
59
|
}
|
|
60
|
-
export type BrowserResultCode = "OK" | "STALE_SNAPSHOT" | "STALE_CURSOR" | "NO_PROGRESS" | "NAVIGATION" | "BLOCKED" | "NEEDS_HUMAN" | "FAILED";
|
|
60
|
+
export type BrowserResultCode = "OK" | "STALE_SNAPSHOT" | "STALE_CURSOR" | "TARGET_CLOSED" | "NO_PROGRESS" | "NAVIGATION" | "BLOCKED" | "NEEDS_HUMAN" | "FAILED";
|
|
61
61
|
export interface BrowserScrollState {
|
|
62
62
|
x: number;
|
|
63
63
|
y: number;
|
|
@@ -210,6 +210,8 @@ export interface BrowserBridge {
|
|
|
210
210
|
}
|
|
211
211
|
/** One open browser tab, as the agent sees it. */
|
|
212
212
|
export interface BrowserTab {
|
|
213
|
+
/** Closed task-owned targets can be listed with a URL for explicit recovery. */
|
|
214
|
+
status?: "open" | "closed";
|
|
213
215
|
/** Stable id (the desktop webContents id, as a string). */
|
|
214
216
|
tabId: string;
|
|
215
217
|
url: string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RegisteredTool } from "../types.js";
|
|
2
|
+
export declare function isBuiltinBrowserTool(tool: Pick<RegisteredTool, "name" | "source">): boolean;
|
|
3
|
+
/** Prefer the native path for generic browser discovery, without redirecting
|
|
4
|
+
* exact selections, named providers, or DevTools-specific requests. Only the
|
|
5
|
+
* run's already-authorized tools are scored by the caller. */
|
|
6
|
+
export declare function browserDiscoveryScore(tool: RegisteredTool, query: string): number;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const BROWSER_TOOLS = ["browser_navigate", "browser_observe", "browser_act"];
|
|
2
|
+
export function isBuiltinBrowserTool(tool) {
|
|
3
|
+
return tool.source === "builtin" && BROWSER_TOOLS.some((name) => name === tool.name);
|
|
4
|
+
}
|
|
5
|
+
/** Prefer the native path for generic browser discovery, without redirecting
|
|
6
|
+
* exact selections, named providers, or DevTools-specific requests. Only the
|
|
7
|
+
* run's already-authorized tools are scored by the caller. */
|
|
8
|
+
export function browserDiscoveryScore(tool, query) {
|
|
9
|
+
if (!isBuiltinBrowserTool(tool))
|
|
10
|
+
return 0;
|
|
11
|
+
if (!/\b(browser|browse|webpage)\b|浏览器|网页/i.test(query))
|
|
12
|
+
return 0;
|
|
13
|
+
if (/\b(mcp|chrome|chromium|playwright|selenium|firefox|edge|devtools|network|console|performance|trace)\b|网络请求|控制台|性能/i.test(query)) {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
return 40 - BROWSER_TOOLS.findIndex((name) => name === tool.name);
|
|
17
|
+
}
|
|
@@ -222,7 +222,7 @@ export const browserActToolDef = {
|
|
|
222
222
|
"- select {ref, value}: choose an option in a NATIVE <select> (value = option " +
|
|
223
223
|
"value or visible text). Custom dropdowns: click to expand, then click the option.\n" +
|
|
224
224
|
"- press_key {key, ref?}: press a key/combo (Enter, Tab, Escape, ArrowDown, " +
|
|
225
|
-
"
|
|
225
|
+
"ControlOrMeta+a; resolves to Command on macOS, Control elsewhere). Focuses ref first if given.\n" +
|
|
226
226
|
"- hover {ref}: hover to reveal menus/tooltips.\n" +
|
|
227
227
|
"- scroll {direction: up|down, amount?}: scroll the page, then re-observe.\n" +
|
|
228
228
|
"- wait {timeout_ms?}: wait for the page to finish loading before observing.\n" +
|
|
@@ -255,7 +255,10 @@ export const browserActToolDef = {
|
|
|
255
255
|
ref: { type: "string", description: "Element ref (eN) — click/type/select/hover/press_key" },
|
|
256
256
|
text: { type: "string", description: "Text to type — type" },
|
|
257
257
|
value: { type: "string", description: "Option value or visible text — select" },
|
|
258
|
-
key: {
|
|
258
|
+
key: {
|
|
259
|
+
type: "string",
|
|
260
|
+
description: "Key or combo (Enter/Tab/ControlOrMeta+a). ControlOrMeta uses the browser host's platform; literal Control and Meta stay distinct — press_key",
|
|
261
|
+
},
|
|
259
262
|
direction: { type: "string", enum: ["up", "down"], description: "Scroll direction — scroll" },
|
|
260
263
|
amount: { type: "number", description: "Pixels to scroll (default one viewport) — scroll" },
|
|
261
264
|
timeout_ms: { type: "number", description: "Max wait in ms (default 10000) — wait" },
|
|
@@ -294,9 +297,9 @@ export async function browserActTool(args, ctx) {
|
|
|
294
297
|
const tabs = await b.listTabs();
|
|
295
298
|
if (tabs.length === 0)
|
|
296
299
|
return "(no open browser tabs)";
|
|
297
|
-
return ("
|
|
300
|
+
return ("Browser tabs:\n" +
|
|
298
301
|
tabs
|
|
299
|
-
.map((t) => `- [${t.tabId}]${t.active ? " (active)" : ""} ${t.title || "(untitled)"} — ${t.url || "(blank)"}`)
|
|
302
|
+
.map((t) => `- [${t.tabId}]${t.status === "closed" ? " (closed — navigate to reopen; old refs expired)" : t.active ? " (active)" : ""} ${t.title || "(untitled)"} — ${t.url || "(blank)"}`)
|
|
300
303
|
.join("\n"));
|
|
301
304
|
}
|
|
302
305
|
case "switch_tab": {
|
|
@@ -381,10 +384,11 @@ export async function browserActTool(args, ctx) {
|
|
|
381
384
|
// ════════════════════════════════════════════════════════════════════════════
|
|
382
385
|
export const browserNavigateToolDef = {
|
|
383
386
|
name: "browser_navigate",
|
|
384
|
-
description: "
|
|
385
|
-
"
|
|
386
|
-
"
|
|
387
|
-
"
|
|
387
|
+
description: "Open a URL in a task-owned tab of CodeShell's built-in browser. Default for web page " +
|
|
388
|
+
"tasks unless the user specifies another browser or a required capability is unavailable. " +
|
|
389
|
+
"Shares the in-app browser profile; existing user-opened tabs require an explicit grant. " +
|
|
390
|
+
"Starts in the background; browser_act(request_takeover) reveals this same tab when the user " +
|
|
391
|
+
"wants to see it or needs to sign in. Then call browser_act(wait) + " +
|
|
388
392
|
"browser_observe to inspect the page.",
|
|
389
393
|
inputSchema: {
|
|
390
394
|
type: "object",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Session working memory tools. The owning loop applies rollover after the tool batch. */
|
|
2
|
+
import type { ToolDefinition } from "../../types.js";
|
|
3
|
+
import type { ToolContext } from "../context.js";
|
|
4
|
+
export declare const SAVE_CONTEXT_NOTE_TOOL_NAME = "SaveContextNote";
|
|
5
|
+
export declare const NEW_CONTEXT_TOOL_NAME = "NewContext";
|
|
6
|
+
export declare const SEARCH_HISTORY_TOOL_NAME = "SearchHistory";
|
|
7
|
+
export declare const saveContextNoteToolDef: ToolDefinition;
|
|
8
|
+
export declare const newContextToolDef: ToolDefinition;
|
|
9
|
+
export declare const searchHistoryToolDef: ToolDefinition;
|
|
10
|
+
export declare function saveContextNoteTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
11
|
+
export declare function newContextTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
12
|
+
export declare function searchHistoryTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|