@code-yeongyu/senpi-codemode 2026.9.10-2 → 2026.9.11
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
CHANGED
|
@@ -12,6 +12,22 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.9.11] - 2026-09-11
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- The eval tool instructions now tell callers to emit large text in bounded chunks or through offset-based file reads, and to treat a truncation notice as incomplete data that must be recovered from the full-output path instead of being read as the whole result ([#1600](https://github.com/code-yeongyu/senpi/pull/1600)).
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Column-capped eval output now preserves a recoverable full-output artifact, so a cell whose output is clipped by a narrow terminal column cap still exposes the complete text through the artifact path ([#1600](https://github.com/code-yeongyu/senpi/pull/1600)).
|
|
28
|
+
|
|
29
|
+
### Removed
|
|
30
|
+
|
|
15
31
|
## [2026.9.10-2] - 2026-09-10
|
|
16
32
|
|
|
17
33
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -218,10 +218,11 @@ empty pipe (`true | ( … )`) while a cell is active. Output, exit codes, `cwd`,
|
|
|
218
218
|
## Output and artifacts
|
|
219
219
|
|
|
220
220
|
Cell output is streamed while the cell runs. Large streams spill to an absolute
|
|
221
|
-
file after the default 50 KiB threshold
|
|
222
|
-
`/path/session.jsonl`, artifacts live in
|
|
223
|
-
without a file use a unique temporary
|
|
224
|
-
plain-path notice such as
|
|
221
|
+
file after the default 50 KiB threshold or when the output column cap drops
|
|
222
|
+
bytes. With a session file such as `/path/session.jsonl`, artifacts live in
|
|
223
|
+
`/path/session-artifacts/`; sessions without a file use a unique temporary
|
|
224
|
+
directory. Truncated results include a plain-path notice such as
|
|
225
|
+
`[Full output: /absolute/path/eval-….log]`.
|
|
225
226
|
|
|
226
227
|
## Deliberate differences from oh-my-pi
|
|
227
228
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.11",
|
|
4
4
|
"description": "Source-only senpi extension package for codemode evaluation tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@babel/parser": "8.0.4",
|
|
33
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.
|
|
33
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.11",
|
|
34
34
|
"typebox": "1.3.18"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
37
|
+
"@code-yeongyu/senpi": "2026.9.11"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
40
|
+
"@code-yeongyu/senpi": "2026.9.11"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"senpi",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
interface ByteSlice {
|
|
2
|
+
readonly text: string;
|
|
3
|
+
readonly bytes: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function truncateHeadBytes(text: string, maxBytes: number): ByteSlice {
|
|
7
|
+
if (maxBytes <= 0) return { text: "", bytes: 0 };
|
|
8
|
+
const buffer = Buffer.from(text, "utf8");
|
|
9
|
+
if (buffer.length <= maxBytes) return { text, bytes: buffer.length };
|
|
10
|
+
let end = maxBytes;
|
|
11
|
+
while (end > 0 && (buffer[end] & 0xc0) === 0x80) end--;
|
|
12
|
+
const slice = buffer.subarray(0, end);
|
|
13
|
+
return { text: slice.toString("utf8"), bytes: slice.length };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function truncateTailBytes(text: string, maxBytes: number): ByteSlice {
|
|
17
|
+
if (maxBytes <= 0) return { text: "", bytes: 0 };
|
|
18
|
+
const buffer = Buffer.from(text, "utf8");
|
|
19
|
+
if (buffer.length <= maxBytes) return { text, bytes: buffer.length };
|
|
20
|
+
let start = buffer.length - maxBytes;
|
|
21
|
+
while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) start++;
|
|
22
|
+
const slice = buffer.subarray(start);
|
|
23
|
+
return { text: slice.toString("utf8"), bytes: slice.length };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class TailBuffer {
|
|
27
|
+
readonly #maxBytes: number;
|
|
28
|
+
#text = "";
|
|
29
|
+
#bytes = 0;
|
|
30
|
+
|
|
31
|
+
constructor(maxBytes: number) {
|
|
32
|
+
this.#maxBytes = Math.max(0, Math.floor(maxBytes));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
append(text: string): void {
|
|
36
|
+
if (text.length === 0) return;
|
|
37
|
+
if (this.#maxBytes === 0) {
|
|
38
|
+
this.#text = "";
|
|
39
|
+
this.#bytes = 0;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const incomingBytes = Buffer.byteLength(text, "utf8");
|
|
43
|
+
const next =
|
|
44
|
+
incomingBytes >= this.#maxBytes
|
|
45
|
+
? truncateTailBytes(text, this.#maxBytes)
|
|
46
|
+
: truncateTailBytes(this.#text + text, this.#maxBytes);
|
|
47
|
+
this.#text = next.text;
|
|
48
|
+
this.#bytes = next.bytes;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
text(): string {
|
|
52
|
+
return this.#text;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
bytes(): number {
|
|
56
|
+
return this.#bytes;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -2,9 +2,11 @@ import { createWriteStream, mkdirSync, type WriteStream } from "node:fs";
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, truncateTail } from "../host-sdk.ts";
|
|
4
4
|
import { formatMiddleElisionMarker } from "./output-meta.ts";
|
|
5
|
+
import { TailBuffer, truncateHeadBytes } from "./streaming-output-buffer.ts";
|
|
5
6
|
|
|
6
7
|
export { artifactNotice, formatMiddleElisionMarker, resolveSessionArtifactsDir } from "./output-meta.ts";
|
|
7
|
-
export {
|
|
8
|
+
export { truncateHeadBytes, truncateTailBytes } from "./streaming-output-buffer.ts";
|
|
9
|
+
export { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, TailBuffer, truncateTail };
|
|
8
10
|
|
|
9
11
|
export const ARTIFACT_DEFAULT_HEAD_BYTES = 3 * 1024 * 1024;
|
|
10
12
|
|
|
@@ -31,11 +33,6 @@ export interface OutputSinkOptions {
|
|
|
31
33
|
readonly chunkThrottleMs?: number;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
interface ByteSlice {
|
|
35
|
-
readonly text: string;
|
|
36
|
-
readonly bytes: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
36
|
function countNewlines(text: string): number {
|
|
40
37
|
let count = 0;
|
|
41
38
|
let cursor = text.indexOf("\n");
|
|
@@ -50,60 +47,6 @@ function lineCount(text: string): number {
|
|
|
50
47
|
return text.length === 0 ? 0 : countNewlines(text) + 1;
|
|
51
48
|
}
|
|
52
49
|
|
|
53
|
-
function truncateHeadBytes(text: string, maxBytes: number): ByteSlice {
|
|
54
|
-
if (maxBytes <= 0) return { text: "", bytes: 0 };
|
|
55
|
-
const buffer = Buffer.from(text, "utf8");
|
|
56
|
-
if (buffer.length <= maxBytes) return { text, bytes: buffer.length };
|
|
57
|
-
let end = maxBytes;
|
|
58
|
-
while (end > 0 && (buffer[end] & 0xc0) === 0x80) end--;
|
|
59
|
-
const slice = buffer.subarray(0, end);
|
|
60
|
-
return { text: slice.toString("utf8"), bytes: slice.length };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function truncateTailBytes(text: string, maxBytes: number): ByteSlice {
|
|
64
|
-
if (maxBytes <= 0) return { text: "", bytes: 0 };
|
|
65
|
-
const buffer = Buffer.from(text, "utf8");
|
|
66
|
-
if (buffer.length <= maxBytes) return { text, bytes: buffer.length };
|
|
67
|
-
let start = buffer.length - maxBytes;
|
|
68
|
-
while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) start++;
|
|
69
|
-
const slice = buffer.subarray(start);
|
|
70
|
-
return { text: slice.toString("utf8"), bytes: slice.length };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export class TailBuffer {
|
|
74
|
-
readonly #maxBytes: number;
|
|
75
|
-
#text = "";
|
|
76
|
-
#bytes = 0;
|
|
77
|
-
|
|
78
|
-
constructor(maxBytes: number) {
|
|
79
|
-
this.#maxBytes = Math.max(0, Math.floor(maxBytes));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
append(text: string): void {
|
|
83
|
-
if (text.length === 0) return;
|
|
84
|
-
if (this.#maxBytes === 0) {
|
|
85
|
-
this.#text = "";
|
|
86
|
-
this.#bytes = 0;
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
const incomingBytes = Buffer.byteLength(text, "utf8");
|
|
90
|
-
const next =
|
|
91
|
-
incomingBytes >= this.#maxBytes
|
|
92
|
-
? truncateTailBytes(text, this.#maxBytes)
|
|
93
|
-
: truncateTailBytes(this.#text + text, this.#maxBytes);
|
|
94
|
-
this.#text = next.text;
|
|
95
|
-
this.#bytes = next.bytes;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
text(): string {
|
|
99
|
-
return this.#text;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
bytes(): number {
|
|
103
|
-
return this.#bytes;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
50
|
export class OutputSink {
|
|
108
51
|
readonly #artifactPath: string | undefined;
|
|
109
52
|
readonly #spillThreshold: number;
|
|
@@ -146,8 +89,10 @@ export class OutputSink {
|
|
|
146
89
|
this.#totalBytes += rawBytes;
|
|
147
90
|
this.#totalNewlines += countNewlines(chunk);
|
|
148
91
|
this.#sawData = true;
|
|
149
|
-
this.#
|
|
150
|
-
this.#
|
|
92
|
+
const droppedBefore = this.#columnDroppedBytes;
|
|
93
|
+
const retained = this.#maxColumns > 0 ? this.#clampColumns(chunk) : chunk;
|
|
94
|
+
this.#mirrorRaw(chunk, this.#columnDroppedBytes > droppedBefore);
|
|
95
|
+
this.#retain(retained);
|
|
151
96
|
}
|
|
152
97
|
|
|
153
98
|
dump(notice?: string): Promise<OutputSummary> {
|
|
@@ -167,13 +112,13 @@ export class OutputSink {
|
|
|
167
112
|
this.#pendingChunk += chunk;
|
|
168
113
|
}
|
|
169
114
|
|
|
170
|
-
#mirrorRaw(chunk: string): void {
|
|
115
|
+
#mirrorRaw(chunk: string, columnCapDropped: boolean): void {
|
|
171
116
|
if (this.#artifactPath === undefined) return;
|
|
172
117
|
if (this.#file !== undefined) {
|
|
173
118
|
this.#file.write(chunk);
|
|
174
119
|
return;
|
|
175
120
|
}
|
|
176
|
-
if (this.#totalBytes <= this.#spillThreshold) {
|
|
121
|
+
if (!columnCapDropped && this.#totalBytes <= this.#spillThreshold) {
|
|
177
122
|
this.#beforeSpill += chunk;
|
|
178
123
|
return;
|
|
179
124
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const EVAL_PROMPT_TEMPLATE = `Run one step of code in a persistent kernel.
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
|
-
**One eval call = one cell = one logical step.** Top-level names persist per language across eval calls{{#if spawns}}, tool calls and \`task\` subagents{{else}} and tool calls{{/if}}: define helpers and clients once and reuse them instead of re-importing or re-reading. Rebuild state only after \`reset\`, a kernel restart, or a \`NameError\`/\`ReferenceError\`, and check a sentinel variable first so a re-run cannot duplicate side effects.
|
|
4
|
+
**One eval call = one cell = one logical step.** Top-level names persist per language across eval calls{{#if spawns}}, tool calls and \`task\` subagents{{else}} and tool calls{{/if}}: define helpers and clients once and reuse them instead of re-importing or re-reading. For large text, use bounded chunks or write it to a file and read it with offsets; treat truncation notices as incomplete data and follow the full-output path. Rebuild state only after \`reset\`, a kernel restart, or a \`NameError\`/\`ReferenceError\`, and check a sentinel variable first so a re-run cannot duplicate side effects.
|
|
5
5
|
|
|
6
6
|
{{#if styleClaude}}<eval_first_batching>
|
|
7
7
|
Batch a step's independent calls in one cell with \`parallel(thunks)\`; write real code around them - loops, branches, joins, a try/except per risky item - and keep every failed or missing item in the result verbatim; re-read truncated output before deciding.
|