@code-yeongyu/senpi-codemode 2026.9.28 → 2026.9.29-3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +123 -0
- package/README.md +13 -1
- package/package.json +4 -4
- package/src/bridge/memory-protocol.ts +39 -0
- package/src/bridge/protocol.ts +18 -0
- package/src/bridge/reserved.ts +2 -0
- package/src/config/memory-settings.ts +149 -0
- package/src/config/settings.ts +8 -2
- package/src/extension/session-manager-proxy.ts +6 -0
- package/src/extension/session-manager.ts +27 -25
- package/src/host-sdk.ts +1 -1
- package/src/index.ts +4 -0
- package/src/kernels/jl/kernel.ts +4 -1
- package/src/kernels/js/context-manager.ts +43 -5
- package/src/kernels/js/inline-worker.ts +2 -1
- package/src/kernels/js/interrupt-bounds.ts +8 -2
- package/src/kernels/js/kernel-contract.ts +13 -0
- package/src/kernels/js/local-module-loader.ts +2 -0
- package/src/kernels/js/webview-host.ts +59 -0
- package/src/kernels/js/worker-core.js +32 -3
- package/src/kernels/js/worker-global-sizes.js +123 -0
- package/src/kernels/js/worker-heap.js +59 -0
- package/src/kernels/js/worker-host.ts +2 -1
- package/src/kernels/js/worker-memory.js +94 -0
- package/src/kernels/js/worker-runtime.js +7 -2
- package/src/kernels/js/worker-slot.ts +16 -5
- package/src/kernels/js/worker-startup.ts +1 -0
- package/src/kernels/js/worker-webview-client.js +79 -0
- package/src/kernels/js/worker-webview.js +204 -0
- package/src/kernels/py/kernel-contract.ts +3 -0
- package/src/kernels/py/kernel.ts +15 -2
- package/src/kernels/py/prelude.py +327 -11
- package/src/kernels/py/transport.ts +4 -1
- package/src/kernels/rb/kernel.ts +4 -1
- package/src/kernels/shared/kernel-memory-host.ts +63 -0
- package/src/kernels/shared/kernel-memory.ts +136 -0
- package/src/kernels/shared/subprocess-contract.ts +10 -1
- package/src/kernels/shared/subprocess-kernel.ts +23 -2
- package/src/kernels/shared/subprocess-queue.ts +2 -0
- package/src/output/streaming-output-buffer.ts +229 -12
- package/src/output/streaming-output.ts +2 -2
- package/src/prompt/eval-prompt.ts +1 -1
- package/src/skill/bun-1-4/references/runtime-apis.md +1 -0
- package/src/tool/cell-runtime.ts +52 -10
- package/src/tool/detached-cell-contract.ts +4 -0
- package/src/tool/detached-cell-manager.ts +16 -1
- package/src/tool/detached-cell-notification.ts +10 -1
- package/src/tool/image.ts +6 -2
- package/src/tool/run-eval-cell.ts +19 -17
- package/src/tool/settled-image-spill.ts +108 -0
- package/src/tool/terminal-snapshot-store.ts +82 -10
- package/src/tool/types.ts +10 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { AgentToolResult } from "@code-yeongyu/senpi";
|
|
4
|
+
import type { EvalToolDetails } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export type SettledContentPart = AgentToolResult<EvalToolDetails>["content"][number];
|
|
7
|
+
|
|
8
|
+
export interface SpilledImageRef {
|
|
9
|
+
readonly type: "spilled-image";
|
|
10
|
+
readonly path: string;
|
|
11
|
+
readonly mimeType: string;
|
|
12
|
+
readonly length: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type RetainedContentPart = SettledContentPart | SpilledImageRef;
|
|
16
|
+
|
|
17
|
+
export interface SettledImageSpillOptions {
|
|
18
|
+
readonly dir: string;
|
|
19
|
+
/** Upper bound on the spilled base64 bytes on disk; 0 keeps only the snapshot count cap. */
|
|
20
|
+
readonly byteBudget: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Keeps settled-cell image payloads on disk instead of the session heap (#2259). Each image is one raw
|
|
25
|
+
* base64 file; the oldest files go first beyond the byte budget (never the newest cell's), a snapshot's
|
|
26
|
+
* files go with it, and the whole directory goes on dispose.
|
|
27
|
+
*/
|
|
28
|
+
export class SettledImageSpill {
|
|
29
|
+
readonly #dir: string;
|
|
30
|
+
readonly #byteBudget: number;
|
|
31
|
+
readonly #files = new Map<string, { readonly cellId: string; readonly bytes: number }>();
|
|
32
|
+
#bytes = 0;
|
|
33
|
+
#sequence = 0;
|
|
34
|
+
|
|
35
|
+
constructor(options: SettledImageSpillOptions) {
|
|
36
|
+
this.#dir = options.dir;
|
|
37
|
+
this.#byteBudget = options.byteBudget;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get bytes(): number {
|
|
41
|
+
return this.#bytes;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
spill(cellId: string, content: readonly SettledContentPart[]): readonly RetainedContentPart[] {
|
|
45
|
+
if (!content.some((part) => part.type === "image")) return content;
|
|
46
|
+
mkdirSync(this.#dir, { recursive: true });
|
|
47
|
+
const retained = content.map((part) => (part.type === "image" ? this.#write(cellId, part) : part));
|
|
48
|
+
this.#enforceBudget(cellId);
|
|
49
|
+
return retained;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
hydrate(parts: readonly RetainedContentPart[]): SettledContentPart[] {
|
|
53
|
+
return parts.map((part) => (part.type === "spilled-image" ? readSpilledImage(part) : part));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
release(parts: readonly RetainedContentPart[]): void {
|
|
57
|
+
for (const part of parts) if (part.type === "spilled-image") this.#remove(part.path);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
clear(): void {
|
|
61
|
+
rmSync(this.#dir, { recursive: true, force: true });
|
|
62
|
+
this.#files.clear();
|
|
63
|
+
this.#bytes = 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#write(cellId: string, image: Extract<SettledContentPart, { type: "image" }>): RetainedContentPart {
|
|
67
|
+
this.#sequence += 1;
|
|
68
|
+
const path = join(this.#dir, `${cellId.replace(/[^a-zA-Z0-9_-]/gu, "_")}-${this.#sequence}.b64`);
|
|
69
|
+
try {
|
|
70
|
+
writeFileSync(path, image.data, "latin1");
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// A full or read-only disk keeps this image inline, where the in-memory byte budget bounds it.
|
|
73
|
+
if (error instanceof Error && "code" in error) return image;
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
this.#files.set(path, { cellId, bytes: image.data.length });
|
|
77
|
+
this.#bytes += image.data.length;
|
|
78
|
+
return { type: "spilled-image", path, mimeType: image.mimeType, length: image.data.length };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#enforceBudget(newestCellId: string): void {
|
|
82
|
+
if (this.#byteBudget <= 0) return;
|
|
83
|
+
for (const [path, file] of this.#files) {
|
|
84
|
+
if (this.#bytes <= this.#byteBudget) return;
|
|
85
|
+
if (file.cellId !== newestCellId) this.#remove(path);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#remove(path: string): void {
|
|
90
|
+
const file = this.#files.get(path);
|
|
91
|
+
if (file === undefined) return;
|
|
92
|
+
this.#files.delete(path);
|
|
93
|
+
this.#bytes -= file.bytes;
|
|
94
|
+
rmSync(path, { force: true });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readSpilledImage(ref: SpilledImageRef): SettledContentPart {
|
|
99
|
+
try {
|
|
100
|
+
return { type: "image", mimeType: ref.mimeType, data: readFileSync(ref.path, "latin1") };
|
|
101
|
+
} catch (error) {
|
|
102
|
+
const reason = error instanceof Error && "code" in error ? String(error.code) : String(error);
|
|
103
|
+
return {
|
|
104
|
+
type: "text",
|
|
105
|
+
text: `[${ref.mimeType} image (${ref.length} base64 bytes) of this settled cell is no longer available: ${ref.path} (${reason})]`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -1,39 +1,111 @@
|
|
|
1
1
|
import type { EvalDetachedCellSnapshot } from "./detached-cell-contract.ts";
|
|
2
|
+
import type { RetainedContentPart, SettledContentPart, SettledImageSpill } from "./settled-image-spill.ts";
|
|
2
3
|
|
|
3
4
|
export const TERMINAL_SNAPSHOT_CAP = 32;
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
export interface TerminalSnapshotStoreOptions {
|
|
7
|
+
readonly cap?: number;
|
|
8
|
+
/** Upper bound on the estimated in-memory bytes of all retained snapshots; 0 keeps only the count cap. */
|
|
9
|
+
readonly byteBudget?: number;
|
|
10
|
+
/** Moves image payloads to disk; without it images stay in memory under the byte budget. */
|
|
11
|
+
readonly spill?: SettledImageSpill;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface StoredSnapshot {
|
|
15
|
+
/** Served by `list()`: every part except spilled images, so listing never reads the disk. */
|
|
16
|
+
readonly snapshot: EvalDetachedCellSnapshot;
|
|
17
|
+
readonly parts: readonly RetainedContentPart[];
|
|
18
|
+
readonly bytes: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Bounded LRU of settled-cell snapshots; without it every settled cell pins its result and closures for
|
|
23
|
+
* the session lifetime (#1695). Bounded by count and by estimated in-memory bytes, with image payloads
|
|
24
|
+
* spilled to disk and re-read on `get` (#2259); the newest snapshot is always kept.
|
|
25
|
+
*/
|
|
6
26
|
export class TerminalSnapshotStore {
|
|
7
|
-
readonly #snapshots = new Map<string,
|
|
27
|
+
readonly #snapshots = new Map<string, StoredSnapshot>();
|
|
8
28
|
readonly #cap: number;
|
|
29
|
+
readonly #byteBudget: number;
|
|
30
|
+
readonly #spill: SettledImageSpill | undefined;
|
|
31
|
+
#bytes = 0;
|
|
9
32
|
|
|
10
|
-
constructor(
|
|
11
|
-
this.#cap = cap;
|
|
33
|
+
constructor(options: TerminalSnapshotStoreOptions = {}) {
|
|
34
|
+
this.#cap = options.cap ?? TERMINAL_SNAPSHOT_CAP;
|
|
35
|
+
this.#byteBudget = options.byteBudget ?? 0;
|
|
36
|
+
this.#spill = options.spill;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get bytes(): number {
|
|
40
|
+
return this.#bytes;
|
|
12
41
|
}
|
|
13
42
|
|
|
14
43
|
remember(snapshot: EvalDetachedCellSnapshot): void {
|
|
15
|
-
this
|
|
16
|
-
|
|
17
|
-
|
|
44
|
+
this.delete(snapshot.cellId);
|
|
45
|
+
const content = snapshot.result.content;
|
|
46
|
+
const parts = this.#spill?.spill(snapshot.cellId, content) ?? content;
|
|
47
|
+
const inMemory = parts.filter((part): part is SettledContentPart => part.type !== "spilled-image");
|
|
48
|
+
const stored = inMemory.length === content.length ? snapshot : withContent(snapshot, inMemory);
|
|
49
|
+
const bytes = estimateSnapshotBytes(stored);
|
|
50
|
+
this.#snapshots.set(snapshot.cellId, { snapshot: stored, parts, bytes });
|
|
51
|
+
this.#bytes += bytes;
|
|
52
|
+
while (this.#snapshots.size > 1 && (this.#snapshots.size > this.#cap || this.#overBudget())) {
|
|
18
53
|
const oldest = this.#snapshots.keys().next();
|
|
19
54
|
if (oldest.done === true) break;
|
|
20
|
-
this
|
|
55
|
+
this.delete(oldest.value);
|
|
21
56
|
}
|
|
22
57
|
}
|
|
23
58
|
|
|
24
59
|
get(cellId: string): EvalDetachedCellSnapshot | undefined {
|
|
25
|
-
|
|
60
|
+
const entry = this.#snapshots.get(cellId);
|
|
61
|
+
if (
|
|
62
|
+
entry === undefined ||
|
|
63
|
+
this.#spill === undefined ||
|
|
64
|
+
entry.parts.length === entry.snapshot.result.content.length
|
|
65
|
+
)
|
|
66
|
+
return entry?.snapshot;
|
|
67
|
+
return withContent(entry.snapshot, this.#spill.hydrate(entry.parts));
|
|
26
68
|
}
|
|
27
69
|
|
|
28
70
|
delete(cellId: string): void {
|
|
71
|
+
const entry = this.#snapshots.get(cellId);
|
|
72
|
+
if (entry === undefined) return;
|
|
29
73
|
this.#snapshots.delete(cellId);
|
|
74
|
+
this.#bytes -= entry.bytes;
|
|
75
|
+
this.#spill?.release(entry.parts);
|
|
30
76
|
}
|
|
31
77
|
|
|
32
78
|
list(): readonly EvalDetachedCellSnapshot[] {
|
|
33
|
-
return [...this.#snapshots.values()];
|
|
79
|
+
return [...this.#snapshots.values()].map((entry) => entry.snapshot);
|
|
34
80
|
}
|
|
35
81
|
|
|
36
82
|
clear(): void {
|
|
37
83
|
this.#snapshots.clear();
|
|
84
|
+
this.#bytes = 0;
|
|
85
|
+
this.#spill?.clear();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#overBudget(): boolean {
|
|
89
|
+
return this.#byteBudget > 0 && this.#bytes > this.#byteBudget;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function withContent(
|
|
94
|
+
snapshot: EvalDetachedCellSnapshot,
|
|
95
|
+
content: readonly SettledContentPart[],
|
|
96
|
+
): EvalDetachedCellSnapshot {
|
|
97
|
+
return { ...snapshot, result: { ...snapshot.result, content: [...content] } };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function estimateSnapshotBytes(snapshot: EvalDetachedCellSnapshot): number {
|
|
101
|
+
let bytes = snapshot.outputTail.length * 2;
|
|
102
|
+
for (const part of snapshot.result.content) {
|
|
103
|
+
bytes += part.type === "text" ? part.text.length * 2 : part.data.length;
|
|
104
|
+
}
|
|
105
|
+
for (const cell of snapshot.result.details.cells ?? []) {
|
|
106
|
+
bytes += (cell.output.length + cell.code.length) * 2;
|
|
38
107
|
}
|
|
108
|
+
const jsonOutputs = snapshot.result.details.jsonOutputs;
|
|
109
|
+
if (jsonOutputs !== undefined && jsonOutputs.length > 0) bytes += JSON.stringify(jsonOutputs).length;
|
|
110
|
+
return bytes;
|
|
39
111
|
}
|
package/src/tool/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentToolResult, AgentToolUpdateCallback, KernelPreludeContribution } from "@code-yeongyu/senpi";
|
|
2
2
|
import { type TSchema, type TUnsafe, Type } from "typebox";
|
|
3
|
+
import type { KernelMemoryReport } from "../bridge/memory-protocol.ts";
|
|
3
4
|
import type { HostToKernelMessage, KernelToHostMessage } from "../bridge/protocol.ts";
|
|
4
5
|
import {
|
|
5
6
|
DEFAULT_FOREGROUND_WINDOW_SECONDS,
|
|
@@ -166,6 +167,11 @@ export interface EvalKernel {
|
|
|
166
167
|
|
|
167
168
|
export interface EvalKernelManager {
|
|
168
169
|
getKernel(language: EvalLanguage, onMessage: (message: KernelToHostMessage) => void): Promise<EvalKernel>;
|
|
170
|
+
/**
|
|
171
|
+
* Drops the per-cell listener `getKernel` registered for `language` once that cell settled.
|
|
172
|
+
* Identity-checked, so releasing a superseded listener never unbinds a newer cell's listener.
|
|
173
|
+
*/
|
|
174
|
+
releaseKernelListener?(language: EvalLanguage, onMessage: (message: KernelToHostMessage) => void): void;
|
|
169
175
|
}
|
|
170
176
|
|
|
171
177
|
export type ExecuteTool = (
|
|
@@ -257,4 +263,8 @@ export interface EvalToolDetails {
|
|
|
257
263
|
readonly jsonOutputs?: readonly unknown[];
|
|
258
264
|
readonly notice?: string;
|
|
259
265
|
readonly meta?: TruncationMeta;
|
|
266
|
+
/** Kernel memory after the cell; its notice text is delivered as its own content part. */
|
|
267
|
+
readonly memory?: EvalMemoryDetails;
|
|
260
268
|
}
|
|
269
|
+
|
|
270
|
+
export type EvalMemoryDetails = Omit<KernelMemoryReport, "notice">;
|