@code-yeongyu/senpi-codemode 2026.8.6 → 2026.8.9
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 +36 -0
- package/package.json +4 -4
- package/src/extension/resumption-channel.ts +29 -0
- package/src/index.ts +10 -0
- package/src/tool/detached-cell-manager.ts +26 -1
- package/src/tool/render.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,42 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.8.9] - 2026-08-09
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Detached eval cells now publish their liveness as a `resumption_channel_state` event (source `eval-detached`) on the
|
|
22
|
+
host event bus: a full per-source snapshot with `activeCount` and per-cell `id`/`description`/`startedAtMs` entries is
|
|
23
|
+
emitted whenever a cell detaches, settles, is stopped, or is disposed, and once on `session_start`. The goal builtin
|
|
24
|
+
consumes this to hold its hidden continuation while detached cells are still computing instead of nagging immediately
|
|
25
|
+
at turn end. Hosts without an event bus are unaffected (emission is a no-op), and the footer/status rendering is
|
|
26
|
+
unchanged.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
### Removed
|
|
33
|
+
|
|
34
|
+
## [2026.8.7] - 2026-08-07
|
|
35
|
+
|
|
36
|
+
### Breaking Changes
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- Formatted completed eval durations in the simple-result transcript branch with the same compact human-readable units
|
|
45
|
+
used by detailed cell headers and nested tool widgets, so sub-second, seconds, minutes, and hours values render as
|
|
46
|
+
labels such as `<1s`, `12s`, `3m 5s`, or `1h 2m` instead of raw millisecond counts. Live footer, working-status, and
|
|
47
|
+
thinking-duration policies are unchanged ([#743](https://github.com/code-yeongyu/senpi/pull/743)).
|
|
48
|
+
|
|
49
|
+
### Removed
|
|
50
|
+
|
|
15
51
|
## [2026.8.6] - 2026-08-06
|
|
16
52
|
|
|
17
53
|
### Breaking Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.8.
|
|
3
|
+
"version": "2026.8.9",
|
|
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.8.
|
|
33
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.8.9",
|
|
34
34
|
"typebox": "1.3.8"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@code-yeongyu/senpi": "2026.8.
|
|
37
|
+
"@code-yeongyu/senpi": "2026.8.9"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@code-yeongyu/senpi": "2026.8.
|
|
40
|
+
"@code-yeongyu/senpi": "2026.8.9"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"senpi",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resumption-channel liveness contract shared with the goal builtin.
|
|
3
|
+
*
|
|
4
|
+
* The goal builtin delays its hidden continuation while any live resumption
|
|
5
|
+
* channel can still wake the session. Each emitter publishes a full per-source
|
|
6
|
+
* snapshot on every liveness transition and once on `session_start`.
|
|
7
|
+
*
|
|
8
|
+
* The event name is duplicated here on purpose: senpi-codemode is a separate
|
|
9
|
+
* package and must not import across the packages/coding-agent boundary, so
|
|
10
|
+
* the sentinel test pins this literal to the exact cross-package contract.
|
|
11
|
+
*/
|
|
12
|
+
export const RESUMPTION_CHANNEL_STATE_EVENT = "resumption_channel_state";
|
|
13
|
+
|
|
14
|
+
/** Source key identifying detached eval cells in the per-source snapshot. */
|
|
15
|
+
export const EVAL_DETACHED_CHANNEL_SOURCE = "eval-detached";
|
|
16
|
+
|
|
17
|
+
/** One live channel as broadcast on the resumption channel state event. */
|
|
18
|
+
export interface ResumptionChannelEntry {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly description: string;
|
|
21
|
+
/** Epoch milliseconds when the channel registered; lets consumers render their own elapsed labels. */
|
|
22
|
+
readonly startedAtMs: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ResumptionChannelState {
|
|
26
|
+
readonly source: string;
|
|
27
|
+
readonly activeCount: number;
|
|
28
|
+
readonly channels?: readonly ResumptionChannelEntry[];
|
|
29
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { defaultCodemodeSettings } from "./config/settings.ts";
|
|
|
7
7
|
import { EvalNotifier } from "./extension/eval-notifier.ts";
|
|
8
8
|
import { EVAL_CELLS_STATUS_KEY } from "./extension/eval-status.ts";
|
|
9
9
|
import { EvalStatusTicker } from "./extension/eval-status-ticker.ts";
|
|
10
|
+
import { RESUMPTION_CHANNEL_STATE_EVENT, type ResumptionChannelState } from "./extension/resumption-channel.ts";
|
|
10
11
|
import {
|
|
11
12
|
createExecuteTool,
|
|
12
13
|
createRuntime,
|
|
@@ -38,6 +39,8 @@ export interface CodemodeExtensionAPI {
|
|
|
38
39
|
getActiveTools(): string[];
|
|
39
40
|
getAllTools(): readonly EvalSchemaToolInfo[];
|
|
40
41
|
sendUserMessage(content: string, options?: { deliverAs?: "steer" | "followUp" }): void;
|
|
42
|
+
/** Optional host event bus; a host without one turns resumption-channel emission into a harmless no-op. */
|
|
43
|
+
events?: { emit(name: string, data: unknown): void };
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
export interface SenpiCodemodeOptions {
|
|
@@ -79,6 +82,9 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
79
82
|
const showDetachedCells = (entries: readonly EvalDetachedCellStatusEntry[]): void => {
|
|
80
83
|
statusTicker.sync(entries);
|
|
81
84
|
};
|
|
85
|
+
const emitChannelState = (state: ResumptionChannelState): void => {
|
|
86
|
+
pi.events?.emit(RESUMPTION_CHANNEL_STATE_EVENT, state);
|
|
87
|
+
};
|
|
82
88
|
const registerEvalForRuntime = (
|
|
83
89
|
runtime: SessionRuntime,
|
|
84
90
|
modelId: string | undefined,
|
|
@@ -126,6 +132,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
126
132
|
cellManager: new EvalDetachedCellManager({
|
|
127
133
|
notifier,
|
|
128
134
|
onStatusChange: showDetachedCells,
|
|
135
|
+
onChannelState: emitChannelState,
|
|
129
136
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
130
137
|
}),
|
|
131
138
|
executionTracker: manager,
|
|
@@ -156,9 +163,12 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
156
163
|
artifactsDir: runtime.artifactsDir,
|
|
157
164
|
notifier,
|
|
158
165
|
onStatusChange: showDetachedCells,
|
|
166
|
+
onChannelState: emitChannelState,
|
|
159
167
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
160
168
|
});
|
|
161
169
|
activeCells = cellManager;
|
|
170
|
+
// The goal builtin clears its per-session counts at session_start; re-publish our snapshot.
|
|
171
|
+
cellManager.publishChannelState();
|
|
162
172
|
activeRuntime = runtime;
|
|
163
173
|
activeModelId = ctx.model?.id;
|
|
164
174
|
registerEvalForRuntime(runtime, activeModelId, cellManager);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@code-yeongyu/senpi";
|
|
2
|
+
import { EVAL_DETACHED_CHANNEL_SOURCE, type ResumptionChannelState } from "../extension/resumption-channel.ts";
|
|
2
3
|
import { detachedNotificationSpillPath } from "./detached-cell-notification.ts";
|
|
3
4
|
import { currentDetachedResult, detachedErrorResult, snapshotDetachedCell } from "./detached-cell-snapshot.ts";
|
|
4
5
|
import {
|
|
@@ -58,12 +59,15 @@ export interface EvalDetachedCellManagerOptions {
|
|
|
58
59
|
readonly artifactsDir?: string;
|
|
59
60
|
readonly notifier?: EvalDetachedCellNotifier;
|
|
60
61
|
readonly onStatusChange?: (entries: readonly EvalDetachedCellStatusEntry[]) => void;
|
|
62
|
+
/** Receives a full per-source liveness snapshot on every detached-cell transition; used by the goal builtin. */
|
|
63
|
+
readonly onChannelState?: (state: ResumptionChannelState) => void;
|
|
61
64
|
readonly now?: () => number;
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
export class EvalDetachedCellManager {
|
|
65
68
|
readonly #artifactsDir: string | undefined;
|
|
66
69
|
readonly #onStatusChange: ((entries: readonly EvalDetachedCellStatusEntry[]) => void) | undefined;
|
|
70
|
+
readonly #onChannelState: ((state: ResumptionChannelState) => void) | undefined;
|
|
67
71
|
readonly #cells = new Map<string, ManagedCell>();
|
|
68
72
|
readonly #detachedByLanguage = new Map<EvalLanguage, ManagedCell>();
|
|
69
73
|
readonly #notificationQueue: DetachedNotificationQueue;
|
|
@@ -72,6 +76,7 @@ export class EvalDetachedCellManager {
|
|
|
72
76
|
constructor(options: EvalDetachedCellManagerOptions = {}) {
|
|
73
77
|
this.#artifactsDir = options.artifactsDir;
|
|
74
78
|
this.#onStatusChange = options.onStatusChange;
|
|
79
|
+
this.#onChannelState = options.onChannelState;
|
|
75
80
|
this.#notificationQueue = new DetachedNotificationQueue(options.notifier, options.artifactsDir);
|
|
76
81
|
this.#now = options.now ?? Date.now;
|
|
77
82
|
}
|
|
@@ -162,6 +167,11 @@ export class EvalDetachedCellManager {
|
|
|
162
167
|
await this.#notificationQueue.flush();
|
|
163
168
|
}
|
|
164
169
|
|
|
170
|
+
/** Re-publish the current snapshot; consumers reset their per-source counts at session_start. */
|
|
171
|
+
publishChannelState(): void {
|
|
172
|
+
this.#emitChannelState([...this.#detachedByLanguage.values()]);
|
|
173
|
+
}
|
|
174
|
+
|
|
165
175
|
#settle(
|
|
166
176
|
cell: ManagedCell,
|
|
167
177
|
state: "completed" | "failed" | "cancelled",
|
|
@@ -188,14 +198,29 @@ export class EvalDetachedCellManager {
|
|
|
188
198
|
}
|
|
189
199
|
|
|
190
200
|
#emitStatus(): void {
|
|
201
|
+
const liveCells = [...this.#detachedByLanguage.values()];
|
|
191
202
|
this.#onStatusChange?.(
|
|
192
|
-
|
|
203
|
+
liveCells.map((cell) => ({
|
|
193
204
|
cellId: cell.cellId,
|
|
194
205
|
language: cell.input.language,
|
|
195
206
|
startedAtMs: cell.startedAtMs,
|
|
196
207
|
...(cell.input.summary === undefined ? {} : { summary: cell.input.summary }),
|
|
197
208
|
})),
|
|
198
209
|
);
|
|
210
|
+
this.#emitChannelState(liveCells);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#emitChannelState(liveCells: readonly ManagedCell[]): void {
|
|
214
|
+
this.#onChannelState?.({
|
|
215
|
+
source: EVAL_DETACHED_CHANNEL_SOURCE,
|
|
216
|
+
activeCount: liveCells.length,
|
|
217
|
+
channels: liveCells.map((cell) => ({
|
|
218
|
+
id: cell.cellId,
|
|
219
|
+
description:
|
|
220
|
+
cell.input.summary === undefined || cell.input.summary.length === 0 ? cell.cellId : cell.input.summary,
|
|
221
|
+
startedAtMs: cell.startedAtMs,
|
|
222
|
+
})),
|
|
223
|
+
});
|
|
199
224
|
}
|
|
200
225
|
|
|
201
226
|
#snapshot(cell: ManagedCell): EvalDetachedCellSnapshot {
|
package/src/tool/render.ts
CHANGED
|
@@ -757,7 +757,8 @@ function resultMetadata(
|
|
|
757
757
|
): RenderBlock[] {
|
|
758
758
|
const metadata: string[] = [];
|
|
759
759
|
if (details?.phase) metadata.push(`phase ${details.phase}`);
|
|
760
|
-
if (!options.isPartial && typeof details?.durationMs === "number")
|
|
760
|
+
if (!options.isPartial && typeof details?.durationMs === "number")
|
|
761
|
+
metadata.push(`took ${formatDuration(details.durationMs)}`);
|
|
761
762
|
if (metadata.length === 0) return [];
|
|
762
763
|
return [{ kind: "text", text: style(theme, "muted", metadata.join(" | ")) }];
|
|
763
764
|
}
|