@code-yeongyu/senpi-codemode 2026.9.29-5 → 2026.9.30

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,20 @@
12
12
 
13
13
  ### Removed
14
14
 
15
+ ## [2026.9.30] - 2026-09-30
16
+
17
+ ### Breaking Changes
18
+
19
+ ### Added
20
+
21
+ ### Changed
22
+
23
+ ### Fixed
24
+
25
+ - An `eval` cell's return value reaches the model whole up to the normal tool-output budget: a long single-line value is no longer cut after 768 bytes with a bare `…`. Any output that is still cut (a printed line past the column cap, or output past the byte or line budget) now tells the model so, with the kept and original sizes and a `[Full output: <path>]` pointer to the saved full output. Reported by @haamsuk-collab. ([#2402](https://github.com/code-yeongyu/senpi/issues/2402))
26
+
27
+ ### Removed
28
+
15
29
  ## [2026.9.29-5] - 2026-09-29
16
30
 
17
31
  ### Breaking Changes
package/README.md CHANGED
@@ -128,7 +128,7 @@ Configuration is loaded in this order:
128
128
  | `taskTools.task` | `"task"` | Registered tool name used by `agent()`. |
129
129
  | `taskTools.output` | `"task_output"` | Registered tool name used by `output()`. |
130
130
  | `outputSink.headBytes` | `20480` | Bytes retained from the beginning of a middle-truncated preview; `0` disables it. |
131
- | `outputSink.maxColumns` | `768` | Maximum rendered output columns; `0` disables column clamping. |
131
+ | `outputSink.maxColumns` | `768` | Maximum columns per printed output line; `0` disables column clamping. A cell's return value is never column-clamped (the byte and line budgets still apply). |
132
132
  | `statusEvents` | `true` | Enables kernel status-event forwarding and rendering. Each cell retains at most 100 status rows; after overflow, one omitted-count row precedes the latest 99 events. |
133
133
  | `memory.gcWatermarkMb` | `256` | JavaScript kernel: a finished cell whose heap reached this size and grew runs a full collection before its result; whenever at least this much stays live, an idle full collection runs about a second after the cell, so dropped globals return their memory without a reset. Python kernel: a finished cell whose process footprint reached this size and grew runs `gc.collect()` (plus glibc `malloc_trim(0)` on Linux) before its result, and while at least this much stays live the next cells collect too (at most 1/20 of the time), so `del rows` returns its memory. `0` disables. Env override: `SENPI_CODEMODE_MEMORY_GC_WATERMARK_MB`. |
134
134
  | `memory.noticeMb` | `1024` | JavaScript and Python kernels: when live memory after a collection (JS heap, Python process footprint) reaches this size (first time, or 25% more than at the last notice), the result gets one bracketed notice naming the largest globals and how to drop them (`rows = undefined`, `del rows`), plus `details.memory`. Ruby and Julia get no notice. `0` disables. Env override: `SENPI_CODEMODE_MEMORY_NOTICE_MB`. |
@@ -320,7 +320,8 @@ Cell output is streamed while the cell runs. Large streams spill to an absolute
320
320
  file after the default 50 KiB threshold or when the output column cap drops
321
321
  bytes. With a session file such as `/path/session.jsonl`, artifacts live in
322
322
  `/path/session-artifacts/`; sessions without a file use a unique temporary
323
- directory. Truncated results include a plain-path notice such as
323
+ directory. A truncated result tells the model so in its text: the kept and
324
+ original sizes, then a plain-path notice such as
324
325
  `[Full output: /absolute/path/eval-….log]`.
325
326
 
326
327
  ## Deliberate differences from oh-my-pi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-codemode",
3
- "version": "2026.9.29-5",
3
+ "version": "2026.9.30",
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.29-5",
33
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.30",
34
34
  "typebox": "1.3.34"
35
35
  },
36
36
  "peerDependencies": {
37
- "@code-yeongyu/senpi": "2026.9.29-5"
37
+ "@code-yeongyu/senpi": "2026.9.30"
38
38
  },
39
39
  "devDependencies": {
40
- "@code-yeongyu/senpi": "2026.9.29-5"
40
+ "@code-yeongyu/senpi": "2026.9.30"
41
41
  },
42
42
  "keywords": [
43
43
  "senpi",
@@ -63,9 +63,11 @@ export function formatTruncationWarning(meta: TruncationMeta | undefined): strin
63
63
  const elidedLines = meta.elidedLines ?? Math.max(0, meta.totalLines - meta.outputLines);
64
64
  const elidedBytes = meta.elidedBytes ?? Math.max(0, meta.totalBytes - meta.outputBytes);
65
65
  message =
66
- meta.headRange !== undefined && meta.tailRange !== undefined
67
- ? `Showing lines ${meta.headRange.start}-${meta.headRange.end} and ${meta.tailRange.start}-${meta.tailRange.end} of ${meta.totalLines}; ${elidedLines} middle line${elidedLines === 1 ? "" : "s"} (${formatBytes(elidedBytes)}) elided`
68
- : `Showing ${meta.outputLines} of ${meta.totalLines} lines; middle elided`;
66
+ elidedLines === 0
67
+ ? `${formatBytes(elidedBytes)} elided from the middle of a line`
68
+ : meta.headRange !== undefined && meta.tailRange !== undefined
69
+ ? `Showing lines ${meta.headRange.start}-${meta.headRange.end} and ${meta.tailRange.start}-${meta.tailRange.end} of ${meta.totalLines}; ${elidedLines} middle line${elidedLines === 1 ? "" : "s"} (${formatBytes(elidedBytes)}) elided`
70
+ : `Showing ${meta.outputLines} of ${meta.totalLines} lines; middle elided`;
69
71
  break;
70
72
  }
71
73
  case "head":
@@ -83,6 +85,14 @@ export function formatTruncationWarning(meta: TruncationMeta | undefined): strin
83
85
  return `[${message}]`;
84
86
  }
85
87
 
88
+ /** Model-facing notice for a cut result: kept and original sizes, then the full-output pointer. */
89
+ export function formatModelTruncationNotice(meta: TruncationMeta): string {
90
+ const { artifactId, ...shown } = meta;
91
+ const detail = formatTruncationWarning(shown)?.slice(1, -1);
92
+ const notice = `[Output truncated: kept ${meta.outputBytes} of ${meta.totalBytes} bytes.${detail === undefined ? "" : ` ${detail}`}]`;
93
+ return artifactId === undefined ? notice : `${notice}\n${artifactNotice(artifactId)}`;
94
+ }
95
+
86
96
  function formatByteLoss(meta: TruncationMeta): string {
87
97
  const dropped = formatBytes(Math.max(0, meta.totalBytes - meta.outputBytes));
88
98
  switch (meta.truncatedBy) {
@@ -47,6 +47,11 @@ function lineCount(text: string): number {
47
47
  return text.length === 0 ? 0 : countNewlines(text) + 1;
48
48
  }
49
49
 
50
+ export interface PushOptions {
51
+ /** `false` exempts the chunk from the per-line column clamp; the byte and line budgets still apply. */
52
+ readonly clampColumns?: boolean;
53
+ }
54
+
50
55
  export class OutputSink {
51
56
  readonly #artifactPath: string | undefined;
52
57
  readonly #spillThreshold: number;
@@ -82,7 +87,7 @@ export class OutputSink {
82
87
  this.#tail = new TailBuffer(this.#spillThreshold);
83
88
  }
84
89
 
85
- push(chunk: string): void {
90
+ push(chunk: string, options: PushOptions = {}): void {
86
91
  if (chunk.length === 0) return;
87
92
  this.#emitPreview(chunk);
88
93
  const rawBytes = Buffer.byteLength(chunk, "utf8");
@@ -90,7 +95,9 @@ export class OutputSink {
90
95
  this.#totalNewlines += countNewlines(chunk);
91
96
  this.#sawData = true;
92
97
  const droppedBefore = this.#columnDroppedBytes;
93
- const retained = this.#maxColumns > 0 ? this.#clampColumns(chunk) : chunk;
98
+ const clamp = this.#maxColumns > 0 && options.clampColumns !== false;
99
+ if (!clamp) this.#trackLineBytes(chunk);
100
+ const retained = clamp ? this.#clampColumns(chunk) : chunk;
94
101
  this.#mirrorRaw(chunk, this.#columnDroppedBytes > droppedBefore);
95
102
  this.#retain(retained);
96
103
  }
@@ -146,6 +153,16 @@ export class OutputSink {
146
153
  if (effectiveBytes > this.#headBytes + this.#tail.bytes()) this.#truncated = true;
147
154
  }
148
155
 
156
+ #trackLineBytes(chunk: string): void {
157
+ const newline = chunk.lastIndexOf("\n");
158
+ if (newline === -1) {
159
+ this.#currentLineBytes += Buffer.byteLength(chunk, "utf8");
160
+ return;
161
+ }
162
+ this.#currentLineBytes = Buffer.byteLength(chunk.substring(newline + 1), "utf8");
163
+ this.#columnCapped = false;
164
+ }
165
+
149
166
  #clampColumns(chunk: string): string {
150
167
  const output: string[] = [];
151
168
  let cursor = 0;
@@ -1,6 +1,7 @@
1
1
  import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext } from "@code-yeongyu/senpi";
2
2
  import type { KernelMemoryReport } from "../bridge/memory-protocol.ts";
3
3
  import type { KernelToHostMessage } from "../bridge/protocol.ts";
4
+ import { formatModelTruncationNotice } from "../output/output-meta.ts";
4
5
  import { DEFAULT_MAX_BYTES, TailLineRing } from "../output/streaming-output.ts";
5
6
  import type { EvalToolCallMetric } from "./call-capture.ts";
6
7
  import { type EvalImageResizer, EvalOutputCollector, type EvalOutputResult } from "./image.ts";
@@ -85,7 +86,7 @@ export class CellResultBuilder {
85
86
  this.#state.durationMs = result.durationMs;
86
87
  this.#memory = result.memory;
87
88
  if (result.ok) {
88
- if (result.valueRepr) this.#output.push(`${result.valueRepr}\n`);
89
+ if (result.valueRepr) this.#output.pushValue(`${result.valueRepr}\n`);
89
90
  this.#state.status = "complete";
90
91
  } else {
91
92
  this.#state.error = result.error.message;
@@ -153,11 +154,12 @@ export class CellResultBuilder {
153
154
  this.#state.output = output.output;
154
155
  const details = this.#details(output, isError);
155
156
  this.emitUpdate(isError);
156
- const text =
157
+ const shown =
157
158
  output.output ||
158
159
  (output.images.length > 0
159
160
  ? `(displayed ${output.images.length} image${output.images.length === 1 ? "" : "s"}; no text output)`
160
161
  : "(no output)");
162
+ const text = output.meta === undefined ? shown : `${shown}\n${formatModelTruncationNotice(output.meta)}`;
161
163
  const notice = this.#memory?.notice;
162
164
  const noticePart = notice === undefined ? [] : [{ type: "text" as const, text: notice }];
163
165
  return { content: [{ type: "text", text }, ...noticePart, ...output.images], details };
package/src/tool/image.ts CHANGED
@@ -91,6 +91,11 @@ export class EvalOutputCollector {
91
91
  this.#sink.push(text);
92
92
  }
93
93
 
94
+ /** The cell's return value: exempt from the column clamp, still bound by the byte and line budgets. */
95
+ pushValue(text: string): void {
96
+ this.#sink.push(text, { clampColumns: false });
97
+ }
98
+
94
99
  display(message: DisplayMessage): void {
95
100
  if (message.mimeType.startsWith("image/")) {
96
101
  if (