@code-yeongyu/senpi-codemode 2026.9.10 → 2026.9.12-2

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,58 @@
12
12
 
13
13
  ### Removed
14
14
 
15
+ ## [2026.9.12-2] - 2026-09-12
16
+
17
+ ### Breaking Changes
18
+
19
+ ### Added
20
+
21
+ ### Changed
22
+
23
+ ### Fixed
24
+
25
+ ### Removed
26
+
27
+ ## [2026.9.12] - 2026-09-12
28
+
29
+ ### Breaking Changes
30
+
31
+ ### Added
32
+
33
+ ### Changed
34
+
35
+ ### Fixed
36
+
37
+ ### Removed
38
+
39
+ ## [2026.9.11] - 2026-09-11
40
+
41
+ ### Breaking Changes
42
+
43
+ ### Added
44
+
45
+ ### Changed
46
+
47
+ - 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)).
48
+
49
+ ### Fixed
50
+
51
+ - 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)).
52
+
53
+ ### Removed
54
+
55
+ ## [2026.9.10-2] - 2026-09-10
56
+
57
+ ### Breaking Changes
58
+
59
+ ### Added
60
+
61
+ ### Changed
62
+
63
+ ### Fixed
64
+
65
+ ### Removed
66
+
15
67
  ## [2026.9.10] - 2026-09-10
16
68
 
17
69
  ### 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. With a session file such as
222
- `/path/session.jsonl`, artifacts live in `/path/session-artifacts/`; sessions
223
- without a file use a unique temporary directory. Truncated results include a
224
- plain-path notice such as `[Full output: /absolute/path/eval-….log]`.
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.10",
3
+ "version": "2026.9.12-2",
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.10",
33
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.12-2",
34
34
  "typebox": "1.3.18"
35
35
  },
36
36
  "peerDependencies": {
37
- "@code-yeongyu/senpi": "2026.9.10"
37
+ "@code-yeongyu/senpi": "2026.9.12-2"
38
38
  },
39
39
  "devDependencies": {
40
- "@code-yeongyu/senpi": "2026.9.10"
40
+ "@code-yeongyu/senpi": "2026.9.12-2"
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 { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, truncateTail };
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.#mirrorRaw(chunk);
150
- this.#retain(this.#maxColumns > 0 ? this.#clampColumns(chunk) : chunk);
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.