@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
|
@@ -102,6 +102,8 @@ export interface UpdateJobPatch {
|
|
|
102
102
|
projectId?: string | null;
|
|
103
103
|
rootId?: string | null;
|
|
104
104
|
permissionLevel?: CronPermissionLevel;
|
|
105
|
+
/** Existing Session to resume; null returns the job to standalone execution. */
|
|
106
|
+
resumeSessionId?: string | null;
|
|
105
107
|
}
|
|
106
108
|
export declare class CronScheduler {
|
|
107
109
|
private jobs;
|
|
@@ -212,6 +214,7 @@ export declare class CronScheduler {
|
|
|
212
214
|
* or null if the id is unknown.
|
|
213
215
|
*/
|
|
214
216
|
update(id: string, patch: UpdateJobPatch): CronJob | null;
|
|
217
|
+
private assertBindingEditable;
|
|
215
218
|
stopAll(): void;
|
|
216
219
|
/**
|
|
217
220
|
* Fire a job immediately, out of band of its schedule (the "Run now" button).
|
|
@@ -75,6 +75,7 @@ function validateJobFields(input) {
|
|
|
75
75
|
for (const [field, value] of [
|
|
76
76
|
["projectId", input.projectId],
|
|
77
77
|
["rootId", input.rootId],
|
|
78
|
+
["resumeSessionId", input.resumeSessionId],
|
|
78
79
|
]) {
|
|
79
80
|
if (value !== undefined &&
|
|
80
81
|
value !== null &&
|
|
@@ -535,6 +536,7 @@ export class CronScheduler {
|
|
|
535
536
|
if (j.id !== id)
|
|
536
537
|
return j;
|
|
537
538
|
const job = { ...j };
|
|
539
|
+
this.assertBindingEditable(job, patch);
|
|
538
540
|
const nextSchedule = patch.schedule ?? job.schedule;
|
|
539
541
|
const nextTimezone = patch.timezone ?? job.timezone;
|
|
540
542
|
if (patch.schedule !== undefined || patch.timezone !== undefined) {
|
|
@@ -560,6 +562,10 @@ export class CronScheduler {
|
|
|
560
562
|
delete job.rootId;
|
|
561
563
|
else if (patch.rootId !== undefined)
|
|
562
564
|
job.rootId = patch.rootId;
|
|
565
|
+
if (patch.resumeSessionId === null)
|
|
566
|
+
delete job.resumeSessionId;
|
|
567
|
+
else if (patch.resumeSessionId !== undefined)
|
|
568
|
+
job.resumeSessionId = patch.resumeSessionId;
|
|
563
569
|
if (patch.permissionLevel !== undefined)
|
|
564
570
|
job.permissionLevel = patch.permissionLevel;
|
|
565
571
|
if (scheduleChanged)
|
|
@@ -575,6 +581,7 @@ export class CronScheduler {
|
|
|
575
581
|
const job = this.jobs.get(id);
|
|
576
582
|
if (!job)
|
|
577
583
|
return null;
|
|
584
|
+
this.assertBindingEditable(job, patch);
|
|
578
585
|
// Validate a new schedule/timezone BEFORE mutating anything.
|
|
579
586
|
const nextSchedule = patch.schedule ?? job.schedule;
|
|
580
587
|
const nextTimezone = patch.timezone ?? job.timezone;
|
|
@@ -601,6 +608,10 @@ export class CronScheduler {
|
|
|
601
608
|
delete job.rootId;
|
|
602
609
|
else if (patch.rootId !== undefined)
|
|
603
610
|
job.rootId = patch.rootId;
|
|
611
|
+
if (patch.resumeSessionId === null)
|
|
612
|
+
delete job.resumeSessionId;
|
|
613
|
+
else if (patch.resumeSessionId !== undefined)
|
|
614
|
+
job.resumeSessionId = patch.resumeSessionId;
|
|
604
615
|
if (patch.permissionLevel !== undefined)
|
|
605
616
|
job.permissionLevel = patch.permissionLevel;
|
|
606
617
|
// Re-arm only when the schedule definition changed, or when an enabled job
|
|
@@ -617,6 +628,16 @@ export class CronScheduler {
|
|
|
617
628
|
this.persist();
|
|
618
629
|
return job;
|
|
619
630
|
}
|
|
631
|
+
assertBindingEditable(job, patch) {
|
|
632
|
+
const bindingChanged = (patch.resumeSessionId !== undefined &&
|
|
633
|
+
(patch.resumeSessionId ?? null) !== (job.resumeSessionId ?? null)) ||
|
|
634
|
+
(patch.cwd !== undefined && patch.cwd !== job.cwd) ||
|
|
635
|
+
(patch.projectId !== undefined && (patch.projectId ?? null) !== (job.projectId ?? null)) ||
|
|
636
|
+
(patch.rootId !== undefined && (patch.rootId ?? null) !== (job.rootId ?? null));
|
|
637
|
+
if (bindingChanged && this.running.has(job.id)) {
|
|
638
|
+
throw new Error("automation is running; wait for it to finish before changing its Session or workspace binding");
|
|
639
|
+
}
|
|
640
|
+
}
|
|
620
641
|
stopAll() {
|
|
621
642
|
for (const timer of this.timers.values()) {
|
|
622
643
|
clearTimeout(timer);
|
|
@@ -31,7 +31,7 @@ export interface ContextManagerConfig {
|
|
|
31
31
|
* Injected by the Engine so the ContextManager doesn't depend on LLM directly.
|
|
32
32
|
*/
|
|
33
33
|
export type SummarizeFn = (prompt: string, signal?: AbortSignal) => Promise<string>;
|
|
34
|
-
export type CompactStrategy = "micro" | "summary" | "window" | "snip" | "emergency" | "range";
|
|
34
|
+
export type CompactStrategy = "micro" | "summary" | "window" | "snip" | "emergency" | "range" | "notes";
|
|
35
35
|
export type OnCompactFn = (info: {
|
|
36
36
|
strategy: CompactStrategy;
|
|
37
37
|
before: number;
|
|
@@ -66,6 +66,10 @@ export declare class ContextManager {
|
|
|
66
66
|
private toolResultsDir;
|
|
67
67
|
constructor(config?: Partial<ContextManagerConfig>);
|
|
68
68
|
setOnCompact(fn: OnCompactFn): void;
|
|
69
|
+
/** Reserve one model step for a working note before the normal compact gate. */
|
|
70
|
+
shouldPrepareNote(messages: Message[]): boolean;
|
|
71
|
+
/** A persisted notes checkpoint supersedes any cached summary/usage anchor. */
|
|
72
|
+
recordNotesCompaction(beforeMessages: Message[], afterMessages: Message[]): void;
|
|
69
73
|
/**
|
|
70
74
|
* Record actual token usage from API response.
|
|
71
75
|
* Used for hybrid estimation: actual + estimate for new messages.
|
|
@@ -111,7 +115,9 @@ export declare class ContextManager {
|
|
|
111
115
|
* Async context management — attempts LLM summarization before falling back.
|
|
112
116
|
* Call this when you have access to the LLM (between turns).
|
|
113
117
|
*/
|
|
114
|
-
manageAsync(messages: Message[], signal?: AbortSignal
|
|
118
|
+
manageAsync(messages: Message[], signal?: AbortSignal, options?: {
|
|
119
|
+
preferNotes?: boolean;
|
|
120
|
+
}): Promise<Message[]>;
|
|
115
121
|
/**
|
|
116
122
|
* Force maximum compaction, ignoring the ratio gates. This is what a manual
|
|
117
123
|
* `/compact` invokes: the user explicitly asked to shrink NOW, so we don't
|
package/dist/context/manager.js
CHANGED
|
@@ -73,6 +73,23 @@ export class ContextManager {
|
|
|
73
73
|
setOnCompact(fn) {
|
|
74
74
|
this.onCompact = fn;
|
|
75
75
|
}
|
|
76
|
+
/** Reserve one model step for a working note before the normal compact gate. */
|
|
77
|
+
shouldPrepareNote(messages) {
|
|
78
|
+
return this.checkLimits(messages).ratio >= Math.max(0.05, this.config.compactAtRatio - 0.1);
|
|
79
|
+
}
|
|
80
|
+
/** A persisted notes checkpoint supersedes any cached summary/usage anchor. */
|
|
81
|
+
recordNotesCompaction(beforeMessages, afterMessages) {
|
|
82
|
+
const before = this.estimateTokensHybrid(beforeMessages);
|
|
83
|
+
this.lastSummary = undefined;
|
|
84
|
+
this.lastActualTokens = undefined;
|
|
85
|
+
this.lastActualAtMessageCount = undefined;
|
|
86
|
+
this.lastActualAnchorEstimate = undefined;
|
|
87
|
+
this.lastActualRecordedAt = undefined;
|
|
88
|
+
this.lastActualProvider = undefined;
|
|
89
|
+
this.lastActualModel = undefined;
|
|
90
|
+
this.suppressNoOpMicroSummaryUntilCompact = false;
|
|
91
|
+
this.onCompact?.({ strategy: "notes", before, after: estimateTokens(afterMessages) });
|
|
92
|
+
}
|
|
76
93
|
/**
|
|
77
94
|
* Record actual token usage from API response.
|
|
78
95
|
* Used for hybrid estimation: actual + estimate for new messages.
|
|
@@ -132,8 +149,7 @@ export class ContextManager {
|
|
|
132
149
|
*/
|
|
133
150
|
estimateTokensHybridInfo(messages) {
|
|
134
151
|
const currentEstimate = estimateTokens(messages);
|
|
135
|
-
if (this.lastActualTokens !== undefined &&
|
|
136
|
-
this.lastActualAtMessageCount !== undefined) {
|
|
152
|
+
if (this.lastActualTokens !== undefined && this.lastActualAtMessageCount !== undefined) {
|
|
137
153
|
if (this.lastActualAtMessageCount < messages.length) {
|
|
138
154
|
const newMessages = messages.slice(this.lastActualAtMessageCount);
|
|
139
155
|
const newTokens = estimateTokens(newMessages);
|
|
@@ -358,7 +374,7 @@ export class ContextManager {
|
|
|
358
374
|
* Async context management — attempts LLM summarization before falling back.
|
|
359
375
|
* Call this when you have access to the LLM (between turns).
|
|
360
376
|
*/
|
|
361
|
-
async manageAsync(messages, signal) {
|
|
377
|
+
async manageAsync(messages, signal, options) {
|
|
362
378
|
let result = messages;
|
|
363
379
|
// Tier 0a: Persist large tool_results to disk + replace with preview.
|
|
364
380
|
result = this.persistLargeToolResults(result);
|
|
@@ -421,7 +437,7 @@ export class ContextManager {
|
|
|
421
437
|
const noOpMicroSpinBand = microNoOpAtFloor &&
|
|
422
438
|
ratio >= this.config.microcompactFloorRatio &&
|
|
423
439
|
ratio < this.config.compactAtRatio;
|
|
424
|
-
const shouldEscalateNoOpMicro = noOpMicroSpinBand && !this.suppressNoOpMicroSummaryUntilCompact;
|
|
440
|
+
const shouldEscalateNoOpMicro = !options?.preferNotes && noOpMicroSpinBand && !this.suppressNoOpMicroSummaryUntilCompact;
|
|
425
441
|
const snipGate = this.config.maxTokens * this.config.compactAtRatio;
|
|
426
442
|
const windowGate = this.config.maxTokens * (this.config.compactAtRatio + 0.05);
|
|
427
443
|
const emergencyGate = this.config.maxTokens * this.config.summarizeAtRatio;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Message, TranscriptEventType } from "../types.js";
|
|
2
|
+
import { Transcript } from "../session/transcript.js";
|
|
3
|
+
export declare const MAX_CONTEXT_NOTE_CHARS = 12000;
|
|
4
|
+
export declare const MAX_CONTEXT_HISTORY_READ_CHARS = 12000;
|
|
5
|
+
export declare const MAX_CONTEXT_HISTORY_RESULTS = 20;
|
|
6
|
+
export interface ContextHistoryEntry {
|
|
7
|
+
eventId: string;
|
|
8
|
+
type: TranscriptEventType;
|
|
9
|
+
turnNumber: number;
|
|
10
|
+
text: string;
|
|
11
|
+
truncated: boolean;
|
|
12
|
+
untrusted: true;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One session's continuation notes. The model authors text; this class alone
|
|
16
|
+
* constructs replacement messages and commits them at a completed tool round.
|
|
17
|
+
*/
|
|
18
|
+
export declare class SessionContextNotes {
|
|
19
|
+
private readonly transcript;
|
|
20
|
+
private modelBoundaryEventId;
|
|
21
|
+
private pendingNoteId;
|
|
22
|
+
constructor(transcript: Transcript);
|
|
23
|
+
/** Keep the normal tool/turn boundary free of replay work when no switch was requested. */
|
|
24
|
+
hasPendingRollover(): boolean;
|
|
25
|
+
/** Call immediately before a model request, before its assistant/tool events. */
|
|
26
|
+
markModelBoundary(): void;
|
|
27
|
+
save(text: string): string;
|
|
28
|
+
/** Queues a switch; the tool call itself must not alter an in-flight batch. */
|
|
29
|
+
requestRollover(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Invoke after all results from the assistant's tool batch have been appended.
|
|
32
|
+
* Every failure consumes the request and leaves the prior replay unchanged.
|
|
33
|
+
*/
|
|
34
|
+
applyRollover(currentMessages?: readonly Message[], retainedMessages?: readonly Message[]): Message[] | undefined;
|
|
35
|
+
private commitRollover;
|
|
36
|
+
search(query: string, limit?: number, beforeEventId?: string): ContextHistoryEntry[];
|
|
37
|
+
read(eventId: string): ContextHistoryEntry | undefined;
|
|
38
|
+
private originalEvents;
|
|
39
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { Transcript, hasCompleteContextToolPairs } from "../session/transcript.js";
|
|
2
|
+
import { downgradeImagePayloadsInHistory, estimateTokens } from "./compaction.js";
|
|
3
|
+
import { logger } from "../logging/logger.js";
|
|
4
|
+
export const MAX_CONTEXT_NOTE_CHARS = 12_000;
|
|
5
|
+
export const MAX_CONTEXT_HISTORY_READ_CHARS = 12_000;
|
|
6
|
+
export const MAX_CONTEXT_HISTORY_RESULTS = 20;
|
|
7
|
+
const HISTORY_SNIPPET_CHARS = 600;
|
|
8
|
+
const MODEL_BOUNDARY_EVENT_TYPES = new Set([
|
|
9
|
+
"message",
|
|
10
|
+
"tool_use",
|
|
11
|
+
"tool_result",
|
|
12
|
+
"summary",
|
|
13
|
+
"context_transfer",
|
|
14
|
+
"range_archive",
|
|
15
|
+
"context_note",
|
|
16
|
+
"context_checkpoint",
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* One session's continuation notes. The model authors text; this class alone
|
|
20
|
+
* constructs replacement messages and commits them at a completed tool round.
|
|
21
|
+
*/
|
|
22
|
+
export class SessionContextNotes {
|
|
23
|
+
transcript;
|
|
24
|
+
modelBoundaryEventId;
|
|
25
|
+
pendingNoteId;
|
|
26
|
+
constructor(transcript) {
|
|
27
|
+
this.transcript = transcript;
|
|
28
|
+
}
|
|
29
|
+
/** Keep the normal tool/turn boundary free of replay work when no switch was requested. */
|
|
30
|
+
hasPendingRollover() {
|
|
31
|
+
return this.pendingNoteId !== undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Call immediately before a model request, before its assistant/tool events. */
|
|
34
|
+
markModelBoundary() {
|
|
35
|
+
const events = this.transcript.getEvents();
|
|
36
|
+
// Skip audit-only receipts/metadata: forks intentionally omit those, and
|
|
37
|
+
// they do not represent any additional text the model has consumed.
|
|
38
|
+
const index = findLastEventIndex(events, (event) => MODEL_BOUNDARY_EVENT_TYPES.has(event.type));
|
|
39
|
+
this.modelBoundaryEventId = events[index]?.id;
|
|
40
|
+
}
|
|
41
|
+
save(text) {
|
|
42
|
+
if (typeof text !== "string" || text.trim().length === 0) {
|
|
43
|
+
throw new Error("A continuation note must contain non-empty text.");
|
|
44
|
+
}
|
|
45
|
+
if (text.length > MAX_CONTEXT_NOTE_CHARS) {
|
|
46
|
+
throw new Error(`A continuation note must not exceed ${MAX_CONTEXT_NOTE_CHARS} characters.`);
|
|
47
|
+
}
|
|
48
|
+
const cursor = this.modelBoundaryEventId;
|
|
49
|
+
if (!cursor || !this.transcript.getEvents().some((event) => event.id === cursor)) {
|
|
50
|
+
throw new Error("No model context boundary is available for this note.");
|
|
51
|
+
}
|
|
52
|
+
const note = this.transcript.appendContextNote(text, cursor);
|
|
53
|
+
if (!note)
|
|
54
|
+
throw new Error("The continuation note could not be saved; context was preserved.");
|
|
55
|
+
return note.id;
|
|
56
|
+
}
|
|
57
|
+
/** Queues a switch; the tool call itself must not alter an in-flight batch. */
|
|
58
|
+
requestRollover() {
|
|
59
|
+
const events = this.transcript.getEvents();
|
|
60
|
+
const noteIndex = findLastEventIndex(events, (event) => event.type === "context_note");
|
|
61
|
+
const note = events[noteIndex];
|
|
62
|
+
if (!note || !validNote(note)) {
|
|
63
|
+
throw new Error("Save a continuation note before starting a new context.");
|
|
64
|
+
}
|
|
65
|
+
if (events.slice(noteIndex + 1).some((event) => event.type === "context_checkpoint")) {
|
|
66
|
+
throw new Error("This note has already been used. Save a fresh continuation note first.");
|
|
67
|
+
}
|
|
68
|
+
if (this.transcript.flushFailed()) {
|
|
69
|
+
throw new Error("Transcript persistence is unavailable; the current context was preserved.");
|
|
70
|
+
}
|
|
71
|
+
this.pendingNoteId = note.id;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Invoke after all results from the assistant's tool batch have been appended.
|
|
75
|
+
* Every failure consumes the request and leaves the prior replay unchanged.
|
|
76
|
+
*/
|
|
77
|
+
applyRollover(currentMessages, retainedMessages = []) {
|
|
78
|
+
const noteId = this.pendingNoteId;
|
|
79
|
+
this.pendingNoteId = undefined;
|
|
80
|
+
if (!noteId || this.transcript.flushFailed())
|
|
81
|
+
return undefined;
|
|
82
|
+
try {
|
|
83
|
+
return this.commitRollover(noteId, currentMessages, retainedMessages);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
logger.warn("context.notes.rollover_failed", {
|
|
87
|
+
noteId,
|
|
88
|
+
message: error instanceof Error ? error.message : String(error),
|
|
89
|
+
});
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
commitRollover(noteId, currentMessages, retainedMessages = []) {
|
|
94
|
+
const events = this.transcript.getEvents();
|
|
95
|
+
const noteIndex = events.findIndex((event) => event.id === noteId);
|
|
96
|
+
const note = events[noteIndex];
|
|
97
|
+
if (!note || !validNote(note))
|
|
98
|
+
return undefined;
|
|
99
|
+
if (events.slice(noteIndex + 1).some((event) => event.type === "context_checkpoint")) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
const cursor = note.data.coveredThroughEventId;
|
|
103
|
+
const boundaryIndex = events.findIndex((event) => event.id === cursor);
|
|
104
|
+
if (boundaryIndex < 0 || boundaryIndex >= noteIndex)
|
|
105
|
+
return undefined;
|
|
106
|
+
const tail = events.slice(boundaryIndex + 1);
|
|
107
|
+
// A misplaced boundary must not silently discard a late result whose
|
|
108
|
+
// opening call was before that boundary. Normal model boundaries never
|
|
109
|
+
// split a batch, but interrupted or corrupt transcripts can do so.
|
|
110
|
+
const tailToolIds = new Set();
|
|
111
|
+
for (const event of tail) {
|
|
112
|
+
if (event.type !== "message" || event.data.role !== "assistant")
|
|
113
|
+
continue;
|
|
114
|
+
if (!Array.isArray(event.data.content))
|
|
115
|
+
continue;
|
|
116
|
+
for (const block of event.data.content) {
|
|
117
|
+
if (block.type === "tool_use" && block.id)
|
|
118
|
+
tailToolIds.add(block.id);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (tail.some((event) => event.type === "tool_result" &&
|
|
122
|
+
(typeof event.data.toolCallId !== "string" || !tailToolIds.has(event.data.toolCallId))))
|
|
123
|
+
return undefined;
|
|
124
|
+
const latestUserIndex = findLastEventIndex(events, isRealUserMessage);
|
|
125
|
+
const latestUser = events[latestUserIndex];
|
|
126
|
+
const noteMessage = {
|
|
127
|
+
id: `continuation:${note.id}`,
|
|
128
|
+
type: "message",
|
|
129
|
+
turnNumber: note.turnNumber,
|
|
130
|
+
timestamp: note.timestamp,
|
|
131
|
+
data: {
|
|
132
|
+
role: "user",
|
|
133
|
+
injected: true,
|
|
134
|
+
authority: "agent",
|
|
135
|
+
content: "<context-note>\nThe runtime has started a new context in this same session using this note. " +
|
|
136
|
+
"Continue the existing task; do not request another NewContext for this already-used note.\n" +
|
|
137
|
+
"Continuation notes written earlier by the assistant. " +
|
|
138
|
+
"These notes may be incomplete; consult the original session history for details.\n\n" +
|
|
139
|
+
`${note.data.text}\n</context-note>`,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
const rebuilt = Transcript.fromMemoryEvents("context-note-candidate", [
|
|
143
|
+
noteMessage,
|
|
144
|
+
...(latestUser && latestUserIndex <= boundaryIndex ? [latestUser] : []),
|
|
145
|
+
...tail,
|
|
146
|
+
]).toMessagesWithIndex();
|
|
147
|
+
const current = currentMessages ?? this.transcript.toMessages();
|
|
148
|
+
// Carry forward already-budgeted tool outputs from the working context.
|
|
149
|
+
// Replaying raw events must not resurrect a large result or consumed image.
|
|
150
|
+
const liveResults = new Map();
|
|
151
|
+
for (const message of current) {
|
|
152
|
+
if (!Array.isArray(message.content))
|
|
153
|
+
continue;
|
|
154
|
+
for (const block of message.content) {
|
|
155
|
+
if (block.type === "tool_result" && block.tool_use_id) {
|
|
156
|
+
liveResults.set(block.tool_use_id, block);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const snapshotMessages = downgradeImagePayloadsInHistory(rebuilt.messages.map((message) => ({
|
|
161
|
+
...message,
|
|
162
|
+
content: Array.isArray(message.content)
|
|
163
|
+
? message.content.map((block) => block.type === "tool_result" &&
|
|
164
|
+
block.tool_use_id &&
|
|
165
|
+
liveResults.has(block.tool_use_id)
|
|
166
|
+
? structuredClone(liveResults.get(block.tool_use_id))
|
|
167
|
+
: block)
|
|
168
|
+
: message.content,
|
|
169
|
+
}))).messages;
|
|
170
|
+
if (!hasCompleteContextToolPairs(snapshotMessages))
|
|
171
|
+
return undefined;
|
|
172
|
+
// A stale note, or a prior emergency summary, can make this candidate
|
|
173
|
+
// larger than the live model context. Never replace it in that case.
|
|
174
|
+
if (estimateTokens([...retainedMessages, ...snapshotMessages]) >= estimateTokens([...current])) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
const checkpoint = this.transcript.appendContextCheckpoint({
|
|
178
|
+
version: 1,
|
|
179
|
+
noteId,
|
|
180
|
+
coveredThroughEventId: cursor,
|
|
181
|
+
messages: snapshotMessages,
|
|
182
|
+
clientMessageIds: [...rebuilt.liveIndexByClientMessageId],
|
|
183
|
+
});
|
|
184
|
+
return checkpoint ? snapshotMessages : undefined;
|
|
185
|
+
}
|
|
186
|
+
search(query, limit = 10, beforeEventId) {
|
|
187
|
+
if (typeof query !== "string" || !query.trim()) {
|
|
188
|
+
throw new Error("History search requires a non-empty query.");
|
|
189
|
+
}
|
|
190
|
+
if (query.length > MAX_CONTEXT_HISTORY_READ_CHARS) {
|
|
191
|
+
throw new Error("History search query is too long.");
|
|
192
|
+
}
|
|
193
|
+
const count = Math.min(MAX_CONTEXT_HISTORY_RESULTS, Math.max(1, Math.floor(limit)));
|
|
194
|
+
if (!Number.isFinite(count))
|
|
195
|
+
throw new Error("History search limit must be a finite number.");
|
|
196
|
+
const events = this.originalEvents();
|
|
197
|
+
const beforeIndex = beforeEventId
|
|
198
|
+
? events.findIndex((event) => matchesHistoryEventId(event, beforeEventId))
|
|
199
|
+
: events.length;
|
|
200
|
+
if (beforeIndex < 0)
|
|
201
|
+
throw new Error("History cursor does not belong to this session.");
|
|
202
|
+
const needle = query.trim().toLocaleLowerCase();
|
|
203
|
+
const entries = [];
|
|
204
|
+
for (let index = beforeIndex - 1; index >= 0 && entries.length < count; index -= 1) {
|
|
205
|
+
const event = events[index];
|
|
206
|
+
const text = historyText(event);
|
|
207
|
+
if (!text)
|
|
208
|
+
continue;
|
|
209
|
+
const match = text.toLocaleLowerCase().indexOf(needle);
|
|
210
|
+
if (match < 0)
|
|
211
|
+
continue;
|
|
212
|
+
const start = Math.max(0, match - Math.floor(HISTORY_SNIPPET_CHARS / 3));
|
|
213
|
+
entries.push(historyEntry(event, text, start, HISTORY_SNIPPET_CHARS));
|
|
214
|
+
}
|
|
215
|
+
return entries;
|
|
216
|
+
}
|
|
217
|
+
read(eventId) {
|
|
218
|
+
const event = this.originalEvents().find((candidate) => matchesHistoryEventId(candidate, eventId));
|
|
219
|
+
if (!event)
|
|
220
|
+
return undefined;
|
|
221
|
+
const text = historyText(event);
|
|
222
|
+
return text === undefined
|
|
223
|
+
? undefined
|
|
224
|
+
: historyEntry(event, text, 0, MAX_CONTEXT_HISTORY_READ_CHARS);
|
|
225
|
+
}
|
|
226
|
+
originalEvents() {
|
|
227
|
+
// Active transcripts can be loaded from a bounded tail. Retrieval uses
|
|
228
|
+
// only their own file and never accepts a model-supplied path/session id.
|
|
229
|
+
if (this.transcript.isPersistent()) {
|
|
230
|
+
const stored = Transcript.readEvents(this.transcript.getFilePath()).events;
|
|
231
|
+
const seen = new Set(stored.map((event) => event.id));
|
|
232
|
+
return [...stored, ...this.transcript.getEvents().filter((event) => !seen.has(event.id))];
|
|
233
|
+
}
|
|
234
|
+
return this.transcript.getEvents();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function findLastEventIndex(events, predicate) {
|
|
238
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
239
|
+
if (predicate(events[index]))
|
|
240
|
+
return index;
|
|
241
|
+
}
|
|
242
|
+
return -1;
|
|
243
|
+
}
|
|
244
|
+
function validNote(event) {
|
|
245
|
+
return (event.type === "context_note" &&
|
|
246
|
+
Boolean(event.data) &&
|
|
247
|
+
typeof event.data.text === "string" &&
|
|
248
|
+
event.data.text.trim().length > 0 &&
|
|
249
|
+
event.data.text.length <= MAX_CONTEXT_NOTE_CHARS &&
|
|
250
|
+
typeof event.data.coveredThroughEventId === "string");
|
|
251
|
+
}
|
|
252
|
+
function matchesHistoryEventId(event, eventId) {
|
|
253
|
+
return (event.id === eventId ||
|
|
254
|
+
(Array.isArray(event.data.contextHistorySourceIds) &&
|
|
255
|
+
event.data.contextHistorySourceIds.includes(eventId)));
|
|
256
|
+
}
|
|
257
|
+
function isRealUserMessage(event) {
|
|
258
|
+
return (event.type === "message" &&
|
|
259
|
+
event.data.role === "user" &&
|
|
260
|
+
event.data.injected !== true &&
|
|
261
|
+
(event.data.authority === undefined || event.data.authority === "user"));
|
|
262
|
+
}
|
|
263
|
+
function contentText(content) {
|
|
264
|
+
if (typeof content === "string")
|
|
265
|
+
return content;
|
|
266
|
+
if (!Array.isArray(content))
|
|
267
|
+
return "";
|
|
268
|
+
return content
|
|
269
|
+
.map((block) => {
|
|
270
|
+
if (!block || typeof block !== "object")
|
|
271
|
+
return "";
|
|
272
|
+
if (block.type === "text")
|
|
273
|
+
return block.text ?? "";
|
|
274
|
+
if (block.type === "image")
|
|
275
|
+
return "[image]";
|
|
276
|
+
if (block.type === "tool_use")
|
|
277
|
+
return `${block.name ?? "tool"}: ${JSON.stringify(block.input)}`;
|
|
278
|
+
if (block.type === "tool_result")
|
|
279
|
+
return contentText(block.content);
|
|
280
|
+
return "";
|
|
281
|
+
})
|
|
282
|
+
.filter(Boolean)
|
|
283
|
+
.join("\n");
|
|
284
|
+
}
|
|
285
|
+
function historyText(event) {
|
|
286
|
+
switch (event.type) {
|
|
287
|
+
case "message":
|
|
288
|
+
return contentText(event.data.content);
|
|
289
|
+
case "tool_use":
|
|
290
|
+
return `${event.data.toolName ?? "tool"}: ${JSON.stringify(event.data.args)}`;
|
|
291
|
+
case "tool_result":
|
|
292
|
+
return [event.data.error, event.data.result, contentText(event.data.contentBlocks)]
|
|
293
|
+
.filter((value) => typeof value === "string" && value.length > 0)
|
|
294
|
+
.join("\n");
|
|
295
|
+
case "summary":
|
|
296
|
+
case "context_transfer":
|
|
297
|
+
case "range_archive":
|
|
298
|
+
return typeof event.data.summary === "string" ? event.data.summary : undefined;
|
|
299
|
+
case "context_note":
|
|
300
|
+
return typeof event.data.text === "string" ? event.data.text : undefined;
|
|
301
|
+
default:
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function historyEntry(event, text, start, length) {
|
|
306
|
+
return {
|
|
307
|
+
eventId: event.id,
|
|
308
|
+
type: event.type,
|
|
309
|
+
turnNumber: event.turnNumber,
|
|
310
|
+
text: text.slice(start, start + length),
|
|
311
|
+
truncated: start > 0 || text.length > length,
|
|
312
|
+
untrusted: true,
|
|
313
|
+
};
|
|
314
|
+
}
|
package/dist/engine/engine.d.ts
CHANGED
|
@@ -281,6 +281,7 @@ export declare class Engine {
|
|
|
281
281
|
* clear "no browser panel" error.
|
|
282
282
|
*/
|
|
283
283
|
setBrowserBridge(bridge: import("../tool-system/browser-bridge.js").BrowserBridge | undefined): void;
|
|
284
|
+
setChildHostBindings(factory: EngineConfig["createChildHostBindings"]): void;
|
|
284
285
|
/** Inject the host-backed workspace bridge after construction. */
|
|
285
286
|
setWorkspaceBridge(bridge: import("../tool-system/workspace-bridge.js").WorkspaceBridge | undefined): void;
|
|
286
287
|
/** Inject the host-backed panel discovery/focus bridge after construction. */
|
|
@@ -366,6 +367,7 @@ export declare class Engine {
|
|
|
366
367
|
* permission-boundary bypass.
|
|
367
368
|
*/
|
|
368
369
|
private resolveBehaviorProfile;
|
|
370
|
+
private resolveContextStrategy;
|
|
369
371
|
private runExclusive;
|
|
370
372
|
/** A receipt write failure must not turn an already finalized model result
|
|
371
373
|
* into a second synthetic failure (or reject the Engine.run contract). */
|
|
@@ -614,10 +616,9 @@ export declare class Engine {
|
|
|
614
616
|
migrateSessionMainRoot(sessionId: string, project: import("../types.js").SessionProjectBinding, mainRoot: string): SessionWorkspace;
|
|
615
617
|
releaseSessionWorkspace(sessionId: string): SessionWorkspace | null;
|
|
616
618
|
injectContext(sessionId: string, content: string): void;
|
|
617
|
-
/**
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
*/
|
|
619
|
+
/** Attempt the durable note path without issuing a separate model request. */
|
|
620
|
+
private tryNotesRollover;
|
|
621
|
+
/** Force context compaction and return token stats before/after. */
|
|
621
622
|
forceCompact(sessionId?: string): Promise<{
|
|
622
623
|
before: number;
|
|
623
624
|
after: number;
|