@code-yeongyu/senpi 2026.10.10-3 → 2026.10.10-5
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 +44 -0
- package/dist/bundle/chunks/{app-server-command-DVWM37GF.js → app-server-command-6FTH3TK6.js} +1 -1
- package/dist/bundle/chunks/{chunk-H4UIUFU5.js → chunk-2PUAD5TD.js} +1 -1
- package/dist/bundle/chunks/{chunk-MT2C4I5R.js → chunk-2UUTN6QT.js} +105 -104
- package/dist/bundle/chunks/{chunk-3A3C5VVO.js → chunk-3OGEJMZ7.js} +1 -1
- package/dist/bundle/chunks/{chunk-J5MUMVWB.js → chunk-64ZVBCTL.js} +8 -7
- package/dist/bundle/chunks/chunk-TIMLYJMK.js +14 -0
- package/dist/bundle/chunks/{chunk-Z7MMKKIY.js → chunk-TZVDXVHB.js} +1 -1
- package/dist/bundle/chunks/{chunk-7GVD54RM.js → chunk-U3IL5N76.js} +1 -1
- package/dist/bundle/chunks/{chunk-H67EVDWG.js → chunk-URQSNRHZ.js} +1 -1
- package/dist/bundle/chunks/{chunk-M2IHGL3M.js → chunk-VW74BGIC.js} +1 -1
- package/dist/bundle/chunks/chunk-YG5UO3E4.js +7 -0
- package/dist/bundle/chunks/{chunk-CTERKXVZ.js → chunk-ZSYRWQWG.js} +2 -2
- package/dist/bundle/chunks/{cli-main-B4S4DGBX.js → cli-main-UYTUBG64.js} +1 -1
- package/dist/bundle/chunks/{host-command-VATE7HRG.js → host-command-TOVYSHYH.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-GC3GIR2N.js → host-lifecycle-5AV4JXDQ.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-QOFA64JL.js → interactive-mode-KJIOCRS7.js} +1 -1
- package/dist/bundle/chunks/{list-tips-IIRFXBTO.js → list-tips-BXAA4FG5.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-E5RDPWQN.js → multi-session-host-TEFLJTTA.js} +5 -5
- package/dist/bundle/chunks/{package-manager-cli-YGTWXRG5.js → package-manager-cli-QSJSIHUR.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-HDAGQ4X3.js → rpc-mode-RUKK7VBF.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-EQ4PQCRP.js → schedule-command-K6CTAB4T.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-GZDV2FOZ.js +3 -0
- package/dist/bundle/chunks/{session-picker-O7OESJWU.js → session-picker-6D4LC3KR.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +178 -176
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session-runtime.d.ts +6 -0
- package/dist/core/agent-session-runtime.js +18 -0
- package/dist/core/agent-session.js +5 -2
- package/dist/core/extensions/builtin/html-render/bootstrap.d.ts +15 -0
- package/dist/core/extensions/builtin/html-render/bootstrap.js +123 -0
- package/dist/core/extensions/builtin/html-render/images.d.ts +16 -0
- package/dist/core/extensions/builtin/html-render/images.js +155 -0
- package/dist/core/extensions/builtin/html-render/index.d.ts +3 -0
- package/dist/core/extensions/builtin/html-render/index.js +5 -0
- package/dist/core/extensions/builtin/html-render/tool.d.ts +19 -0
- package/dist/core/extensions/builtin/html-render/tool.js +74 -0
- package/dist/core/extensions/builtin/index.js +4 -0
- package/dist/core/extensions/builtin/permission-system/index.js +40 -14
- package/dist/core/extensions/session-control-types.d.ts +25 -0
- package/dist/core/extensions/session-control-types.js +16 -0
- package/dist/core/external-admission.js +7 -1
- package/dist/core/resource-loader.js +63 -2
- package/dist/core/session-rebind.js +16 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/modes/interactive/components/extension-editor.js +4 -0
- package/dist/modes/interactive/components/remote-delivery-message.d.ts +6 -3
- package/dist/modes/interactive/components/remote-delivery-message.js +32 -3
- package/dist/modes/interactive/interactive-mode.d.ts +17 -0
- package/dist/modes/interactive/interactive-mode.js +79 -33
- package/dist/modes/interactive/interactive-stderr-guard.d.ts +8 -0
- package/dist/modes/interactive/interactive-stderr-guard.js +70 -0
- package/dist/modes/interactive/session-control-commands.d.ts +2 -1
- package/dist/modes/interactive/session-control-commands.js +21 -7
- package/dist/modes/interactive/session-control-session-commands.d.ts +22 -0
- package/dist/modes/interactive/session-control-session-commands.js +47 -0
- package/dist/modes/interactive/stdout-fd-redirect.d.ts +12 -0
- package/dist/modes/interactive/stdout-fd-redirect.js +114 -0
- package/dist/modes/rpc/connection-handler.js +6 -15
- package/dist/modes/rpc/host-reservations.d.ts +1 -1
- package/dist/modes/rpc/host-reservations.js +9 -3
- package/dist/modes/rpc/rpc-types.d.ts +13 -0
- package/dist/modes/rpc/session-control-actions-data.d.ts +17 -0
- package/dist/modes/rpc/session-control-actions-data.js +38 -0
- package/dist/modes/rpc/session-registry-attach.js +8 -2
- package/dist/modes/rpc/session-registry.js +5 -7
- package/dist/modes/rpc/session-teardown.d.ts +6 -1
- package/dist/modes/rpc/session-teardown.js +26 -7
- package/dist/modes/rpc/session-worker-client.d.ts +2 -0
- package/dist/modes/rpc/session-worker-client.js +6 -0
- package/dist/modes/rpc/session-worker-protocol.d.ts +4 -0
- package/dist/modes/rpc/session-worker.js +6 -0
- package/dist/modes/rpc/worker-session-registry.js +6 -1
- package/docs/extensions.md +1 -1
- package/docs/rpc.md +52 -8
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-PDSG6VER.js +0 -14
- package/dist/bundle/chunks/chunk-RADHRQ4K.js +0 -7
- package/dist/bundle/chunks/session-control-endpoint-NABTZHCZ.js +0 -3
|
@@ -58,6 +58,7 @@ export function createSessionPathReservations(options) {
|
|
|
58
58
|
const pid = options.pid ?? process.pid;
|
|
59
59
|
const startTime = readProcessStartTime(pid).then((value) => value ?? null, () => null);
|
|
60
60
|
const held = new Set();
|
|
61
|
+
const updates = new Map();
|
|
61
62
|
const report = (message) => options.onFailure?.(message);
|
|
62
63
|
const publish = async (sessionPath, attached, stillHeld) => {
|
|
63
64
|
const owner = {
|
|
@@ -100,19 +101,24 @@ export function createSessionPathReservations(options) {
|
|
|
100
101
|
}
|
|
101
102
|
return undefined;
|
|
102
103
|
},
|
|
103
|
-
release(sessionPath) {
|
|
104
|
+
async release(sessionPath) {
|
|
104
105
|
if (!held.delete(sessionPath))
|
|
105
106
|
return;
|
|
106
|
-
|
|
107
|
+
await updates.get(sessionPath);
|
|
108
|
+
updates.delete(sessionPath);
|
|
109
|
+
await rm(reservationFile(dir, sessionPath), { force: true }).catch((cause) => {
|
|
107
110
|
report(`session path reservation for ${sessionPath} could not be removed (${errorMessage(cause)})`);
|
|
108
111
|
});
|
|
109
112
|
},
|
|
110
113
|
setAttached(sessionPath, attached) {
|
|
111
114
|
if (!held.has(sessionPath))
|
|
112
115
|
return;
|
|
113
|
-
|
|
116
|
+
const update = (updates.get(sessionPath) ?? Promise.resolve())
|
|
117
|
+
.then(() => publish(sessionPath, attached, () => held.has(sessionPath)))
|
|
118
|
+
.catch((cause) => {
|
|
114
119
|
report(`session path reservation for ${sessionPath} could not be updated (${errorMessage(cause)})`);
|
|
115
120
|
});
|
|
121
|
+
updates.set(sessionPath, update);
|
|
116
122
|
},
|
|
117
123
|
};
|
|
118
124
|
}
|
|
@@ -73,6 +73,10 @@ type RpcSessionCommand = {
|
|
|
73
73
|
} & ClientMessageIdentity) | {
|
|
74
74
|
id?: string;
|
|
75
75
|
type: "abort";
|
|
76
|
+
} | {
|
|
77
|
+
id?: string;
|
|
78
|
+
type: "interrupt";
|
|
79
|
+
turnId?: string;
|
|
76
80
|
} | {
|
|
77
81
|
id?: string;
|
|
78
82
|
type: "abort_compaction";
|
|
@@ -832,6 +836,15 @@ export type RpcResponse = {
|
|
|
832
836
|
type: "response";
|
|
833
837
|
command: "abort";
|
|
834
838
|
success: true;
|
|
839
|
+
} | {
|
|
840
|
+
id?: string;
|
|
841
|
+
type: "response";
|
|
842
|
+
command: "interrupt";
|
|
843
|
+
success: true;
|
|
844
|
+
data: {
|
|
845
|
+
interrupted: boolean;
|
|
846
|
+
turnId?: string;
|
|
847
|
+
};
|
|
835
848
|
} | {
|
|
836
849
|
id?: string;
|
|
837
850
|
type: "response";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AgentSession } from "../../core/agent-session.ts";
|
|
2
|
+
export declare function availableModelsData(session: AgentSession): Promise<{
|
|
3
|
+
models: readonly object[];
|
|
4
|
+
}>;
|
|
5
|
+
/** The refusal of a level the active model cannot run; callers match this text. */
|
|
6
|
+
export declare function unsupportedThinkingLevel(level: unknown): string;
|
|
7
|
+
export interface InterruptOutcome {
|
|
8
|
+
readonly interrupted: boolean;
|
|
9
|
+
readonly turnId?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `interrupt { turnId? }`: stops the running turn and only that. An idle session answers
|
|
13
|
+
* `interrupted: false`; so does a `turnId` naming a turn that is no longer the running one, which
|
|
14
|
+
* then answers the running turn's id. `stop` resolves once the turn has stopped.
|
|
15
|
+
*/
|
|
16
|
+
export declare function interruptRunningTurn(session: AgentSession, requestedTurnId: unknown, stop: () => Promise<void>): Promise<InterruptOutcome>;
|
|
17
|
+
//# sourceMappingURL=session-control-actions-data.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session-control answers a multi-session host and a terminal control endpoint share, so a caller
|
|
3
|
+
* reads the same shapes from either: the model catalog, the thinking-level refusal, and the
|
|
4
|
+
* `interrupt` outcome.
|
|
5
|
+
*/
|
|
6
|
+
import { modelSupportsAssistantPrefill } from "@earendil-works/pi-ai";
|
|
7
|
+
import { getSupportedThinkingLevels } from "../../core/thinking-levels.js";
|
|
8
|
+
export async function availableModelsData(session) {
|
|
9
|
+
const models = await session.modelRegistry.modelRuntime.getAvailable();
|
|
10
|
+
return {
|
|
11
|
+
models: models.map((model) => ({
|
|
12
|
+
...model,
|
|
13
|
+
supportedThinkingLevels: getSupportedThinkingLevels(model),
|
|
14
|
+
supportsAssistantPrefill: modelSupportsAssistantPrefill(model, {
|
|
15
|
+
thinkingEnabled: session.thinkingLevel !== "off",
|
|
16
|
+
}),
|
|
17
|
+
})),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** The refusal of a level the active model cannot run; callers match this text. */
|
|
21
|
+
export function unsupportedThinkingLevel(level) {
|
|
22
|
+
return `Thinking level ${String(level)} is not supported by the active model.`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* `interrupt { turnId? }`: stops the running turn and only that. An idle session answers
|
|
26
|
+
* `interrupted: false`; so does a `turnId` naming a turn that is no longer the running one, which
|
|
27
|
+
* then answers the running turn's id. `stop` resolves once the turn has stopped.
|
|
28
|
+
*/
|
|
29
|
+
export async function interruptRunningTurn(session, requestedTurnId, stop) {
|
|
30
|
+
if (!session.isStreaming && session.retryAttempt === 0)
|
|
31
|
+
return { interrupted: false };
|
|
32
|
+
const turnId = String(session.externalAdmission.turnEpoch);
|
|
33
|
+
if (typeof requestedTurnId === "string" && requestedTurnId !== turnId)
|
|
34
|
+
return { interrupted: false, turnId };
|
|
35
|
+
await stop();
|
|
36
|
+
return { interrupted: true, turnId };
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=session-control-actions-data.js.map
|
|
@@ -16,8 +16,8 @@ export async function attachToOpenSession(entries, sessionPath, profile, options
|
|
|
16
16
|
entry.retainOnDisconnect = true;
|
|
17
17
|
if (!entry.durableSessionId)
|
|
18
18
|
throw new RpcSessionRegistryError("session_path_in_use");
|
|
19
|
-
// The surface
|
|
20
|
-
// the live session to it; an attach without one keeps what the session has.
|
|
19
|
+
// The surface, browser engine and permission preset follow the client: an attach that names one
|
|
20
|
+
// moves the live session to it; an attach without one keeps what the session has.
|
|
21
21
|
if (profile.promptSurface !== undefined && profile.promptSurface !== entry.profile.promptSurface) {
|
|
22
22
|
entry.profile = frozenProfile({ ...entry.profile, promptSurface: profile.promptSurface });
|
|
23
23
|
entry.runtime?.setPromptSurface(profile.promptSurface);
|
|
@@ -26,6 +26,12 @@ export async function attachToOpenSession(entries, sessionPath, profile, options
|
|
|
26
26
|
entry.profile = frozenProfile({ ...entry.profile, browserEngine: profile.browserEngine });
|
|
27
27
|
entry.runtime?.setBrowserEngine(profile.browserEngine);
|
|
28
28
|
}
|
|
29
|
+
// The preset is applied even when the record already names it: the record is not the live session,
|
|
30
|
+
// so a resent attach also repairs a session that drifted from it (senpi#2842).
|
|
31
|
+
if (profile.permissionPreset !== undefined) {
|
|
32
|
+
entry.profile = frozenProfile({ ...entry.profile, permissionPreset: profile.permissionPreset });
|
|
33
|
+
entry.runtime?.setPermissionPreset(profile.permissionPreset);
|
|
34
|
+
}
|
|
29
35
|
entry.lastCommandAt = now;
|
|
30
36
|
if (wasParked) {
|
|
31
37
|
entry.lifecycleMutex = entry.lifecycleMutex.then(() => entry.runtime?.emitAttachmentEvent("session_resumed"));
|
|
@@ -11,7 +11,7 @@ import { attachToOpenSession } from "./session-registry-attach.js";
|
|
|
11
11
|
import { settleClosingReservation, syncRuntimeMetadata } from "./session-registry-claims.js";
|
|
12
12
|
import { createEntrySwitchSession } from "./session-registry-switch.js";
|
|
13
13
|
import { frozenProfile, RpcSessionRegistryError, sessionIdentity, } from "./session-registry-types.js";
|
|
14
|
-
import { beginSessionClose, closeMarkedSession, closeSession } from "./session-teardown.js";
|
|
14
|
+
import { beginSessionClose, closeMarkedSession, closeSession, releaseWithinGrace, } from "./session-teardown.js";
|
|
15
15
|
export { frozenProfile, RpcSessionRegistryError, sessionIdentity, } from "./session-registry-types.js";
|
|
16
16
|
/** Process-local lifecycle owner for multi-session RPC runtimes. */
|
|
17
17
|
export class RpcSessionRegistry {
|
|
@@ -41,9 +41,9 @@ export class RpcSessionRegistry {
|
|
|
41
41
|
this.options.onSizeChange?.(this.entries.size);
|
|
42
42
|
return deleted;
|
|
43
43
|
},
|
|
44
|
-
releaseReservation: (key) => {
|
|
44
|
+
releaseReservation: async (key) => {
|
|
45
|
+
await this.options.pathReservations?.release(key);
|
|
45
46
|
this.reservations.delete(key);
|
|
46
|
-
this.options.pathReservations?.release(key);
|
|
47
47
|
},
|
|
48
48
|
markDetached: (key) => this.options.pathReservations?.setAttached(key, false),
|
|
49
49
|
now: () => this.now(),
|
|
@@ -174,10 +174,8 @@ export class RpcSessionRegistry {
|
|
|
174
174
|
finally {
|
|
175
175
|
this.entries.delete(handle);
|
|
176
176
|
this.options.onSizeChange?.(this.entries.size);
|
|
177
|
-
if (sessionPath)
|
|
178
|
-
this.
|
|
179
|
-
this.options.pathReservations?.release(sessionPath);
|
|
180
|
-
}
|
|
177
|
+
if (sessionPath)
|
|
178
|
+
await releaseWithinGrace(this.teardownHost, handle, sessionPath);
|
|
181
179
|
}
|
|
182
180
|
}
|
|
183
181
|
if (error instanceof RpcSessionRegistryError)
|
|
@@ -3,7 +3,7 @@ export interface SessionTeardownHost {
|
|
|
3
3
|
readonly closeGraceMs: number;
|
|
4
4
|
get(handle: string): RpcSessionEntry | undefined;
|
|
5
5
|
delete(handle: string): void;
|
|
6
|
-
releaseReservation(key: string): void
|
|
6
|
+
releaseReservation(key: string): Promise<void>;
|
|
7
7
|
/** Publishes that this path is retained with no client attached, for the cross-generation claim. */
|
|
8
8
|
markDetached(key: string): void;
|
|
9
9
|
/** The registry's clock, so the detach stamp shares the sweep's timeline. */
|
|
@@ -13,6 +13,11 @@ export interface SessionTeardownHost {
|
|
|
13
13
|
export declare function beginSessionClose(host: SessionTeardownHost, handle: string, onRole?: (finalizer: boolean) => void, options?: {
|
|
14
14
|
detach?: boolean;
|
|
15
15
|
}): RpcSessionEntry;
|
|
16
|
+
/**
|
|
17
|
+
* Close waits for the claim file to be gone, but a removal that never settles (a wedged mount)
|
|
18
|
+
* must not hold the close past the grace window: the close completes and the failure is reported.
|
|
19
|
+
*/
|
|
20
|
+
export declare function releaseWithinGrace(host: Pick<SessionTeardownHost, "closeGraceMs" | "releaseReservation">, handle: string, key: string): Promise<void>;
|
|
16
21
|
export declare function closeSession(host: SessionTeardownHost, handle: string): Promise<void>;
|
|
17
22
|
export declare function closeMarkedSession(host: SessionTeardownHost, handle: string): Promise<void>;
|
|
18
23
|
//# sourceMappingURL=session-teardown.d.ts.map
|
|
@@ -40,6 +40,25 @@ export function beginSessionClose(host, handle, onRole, options) {
|
|
|
40
40
|
});
|
|
41
41
|
return entry;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Close waits for the claim file to be gone, but a removal that never settles (a wedged mount)
|
|
45
|
+
* must not hold the close past the grace window: the close completes and the failure is reported.
|
|
46
|
+
*/
|
|
47
|
+
export async function releaseWithinGrace(host, handle, key) {
|
|
48
|
+
let timer;
|
|
49
|
+
const late = new Promise((resolve) => {
|
|
50
|
+
timer = setTimeout(() => resolve("late"), host.closeGraceMs);
|
|
51
|
+
});
|
|
52
|
+
const removal = host.releaseReservation(key).then(() => "removed", (cause) => {
|
|
53
|
+
reportDetachedFailure(handle, cause);
|
|
54
|
+
return "failed";
|
|
55
|
+
});
|
|
56
|
+
const outcome = await Promise.race([removal, late]);
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
if (outcome === "late") {
|
|
59
|
+
reportDetachedFailure(handle, new Error(`session path reservation was not removed within ${host.closeGraceMs} ms; closing anyway`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
43
62
|
export function closeSession(host, handle) {
|
|
44
63
|
const entry = beginSessionClose(host, handle);
|
|
45
64
|
if (entry.state !== "closing")
|
|
@@ -80,16 +99,16 @@ export function closeMarkedSession(host, handle) {
|
|
|
80
99
|
await disposeOnce().catch(() => undefined);
|
|
81
100
|
await entry.scope.close?.();
|
|
82
101
|
};
|
|
83
|
-
const release = () => {
|
|
102
|
+
const release = async () => {
|
|
84
103
|
if (released)
|
|
85
104
|
return;
|
|
86
105
|
released = true;
|
|
87
106
|
if (releaseTimer)
|
|
88
107
|
clearTimeout(releaseTimer);
|
|
108
|
+
if (entry.reservationKey)
|
|
109
|
+
await releaseWithinGrace(host, handle, entry.reservationKey);
|
|
89
110
|
entry.state = "closed";
|
|
90
111
|
host.delete(handle);
|
|
91
|
-
if (entry.reservationKey)
|
|
92
|
-
host.releaseReservation(entry.reservationKey);
|
|
93
112
|
};
|
|
94
113
|
const graceful = (async () => {
|
|
95
114
|
await previousLifecycle;
|
|
@@ -119,22 +138,22 @@ export function closeMarkedSession(host, handle) {
|
|
|
119
138
|
}
|
|
120
139
|
})();
|
|
121
140
|
let settled = false;
|
|
122
|
-
const finish = () => {
|
|
141
|
+
const finish = async () => {
|
|
123
142
|
if (settled)
|
|
124
143
|
return;
|
|
125
144
|
settled = true;
|
|
126
|
-
release();
|
|
145
|
+
await release();
|
|
127
146
|
entry.closeResolve?.();
|
|
128
147
|
};
|
|
129
148
|
releaseTimer = setTimeout(() => {
|
|
130
149
|
void disposeOnce().catch((cause) => reportDetachedFailure(handle, cause));
|
|
131
150
|
void closeScopeOnce().catch((cause) => reportDetachedFailure(handle, cause));
|
|
132
151
|
void graceful.catch((cause) => reportDetachedFailure(handle, cause));
|
|
133
|
-
finish();
|
|
152
|
+
void finish();
|
|
134
153
|
}, host.closeGraceMs);
|
|
135
154
|
void graceful.then(finish, (cause) => {
|
|
136
155
|
reportDetachedFailure(handle, cause);
|
|
137
|
-
finish();
|
|
156
|
+
return finish();
|
|
138
157
|
});
|
|
139
158
|
return completion;
|
|
140
159
|
}
|
|
@@ -40,6 +40,8 @@ export declare class SessionWorkerClient {
|
|
|
40
40
|
setPromptSurface(surface: PromptSurface): Promise<void>;
|
|
41
41
|
/** Moves the worker's live session to another browser engine (a later `open_session.browserEngine`). */
|
|
42
42
|
setBrowserEngine(engine: BrowserEngine): Promise<void>;
|
|
43
|
+
/** Moves the worker's live session to another permission preset (a later `open_session.permissionPreset`). */
|
|
44
|
+
setPermissionPreset(preset: string): Promise<void>;
|
|
43
45
|
commit(): Promise<WorkerSnapshot>;
|
|
44
46
|
bind(sessionId: string, writer: SessionEventWriter, requestClose: () => void, options: Pick<RpcConnectionOptions, "capabilities" | "clientInfo">): Promise<RpcSessionBinding>;
|
|
45
47
|
get busy(): boolean;
|
|
@@ -60,6 +60,12 @@ export class SessionWorkerClient {
|
|
|
60
60
|
if (result.type !== "result")
|
|
61
61
|
throw new Error("Invalid worker browser_engine response");
|
|
62
62
|
}
|
|
63
|
+
/** Moves the worker's live session to another permission preset (a later `open_session.permissionPreset`). */
|
|
64
|
+
async setPermissionPreset(preset) {
|
|
65
|
+
const result = await this.request({ type: "permission_preset", preset });
|
|
66
|
+
if (result.type !== "result")
|
|
67
|
+
throw new Error("Invalid worker permission_preset response");
|
|
68
|
+
}
|
|
63
69
|
async commit() {
|
|
64
70
|
const result = await this.request({ type: "commit" });
|
|
65
71
|
if (result.type !== "ready")
|
|
@@ -183,6 +183,12 @@ async function handle(message) {
|
|
|
183
183
|
entry.runtime.setBrowserEngine(message.engine);
|
|
184
184
|
send({ type: "result", request: message.request });
|
|
185
185
|
return;
|
|
186
|
+
case "permission_preset":
|
|
187
|
+
if (!entry?.runtime)
|
|
188
|
+
throw new Error("session_closing");
|
|
189
|
+
entry.runtime.setPermissionPreset(message.preset);
|
|
190
|
+
send({ type: "result", request: message.request });
|
|
191
|
+
return;
|
|
186
192
|
case "cancel_ui":
|
|
187
193
|
binding?.cancelPendingExtensionUiRequests?.();
|
|
188
194
|
send({ type: "control_done", control: "cancel_ui" });
|
|
@@ -233,7 +233,7 @@ export class WorkerSessionRegistry {
|
|
|
233
233
|
return undefined;
|
|
234
234
|
}
|
|
235
235
|
async attach(owner, path, options, requested = { cwd: "" }) {
|
|
236
|
-
const { promptSurface, browserEngine } = requested;
|
|
236
|
+
const { promptSurface, browserEngine, permissionPreset } = requested;
|
|
237
237
|
const entry = this.entries.get(owner);
|
|
238
238
|
if (entry?.state !== "open" || !entry.worker?.bindingReady || entry.worker.snapshot?.sessionPath !== path)
|
|
239
239
|
throw new RpcSessionRegistryError("session_path_in_use");
|
|
@@ -246,6 +246,11 @@ export class WorkerSessionRegistry {
|
|
|
246
246
|
entry.profile = frozenProfile({ ...entry.profile, browserEngine });
|
|
247
247
|
await entry.worker.setBrowserEngine(browserEngine);
|
|
248
248
|
}
|
|
249
|
+
// Sent even when the record already names it: only the worker sees its live session (senpi#2842).
|
|
250
|
+
if (permissionPreset !== undefined) {
|
|
251
|
+
entry.profile = frozenProfile({ ...entry.profile, permissionPreset });
|
|
252
|
+
await entry.worker.setPermissionPreset(permissionPreset);
|
|
253
|
+
}
|
|
249
254
|
const result = this.openResult(owner, entry);
|
|
250
255
|
entry.attachments++;
|
|
251
256
|
// Retention is a property of the live session: any attach may ask for it, and
|
package/docs/extensions.md
CHANGED
|
@@ -1684,7 +1684,7 @@ runtime's steering or follow-up queue like the user's queued input. The protocol
|
|
|
1684
1684
|
|---|---|---|
|
|
1685
1685
|
| `registerControlEndpoint({ inboxDir, drain, isSessionReferenced? })` | `Promise<SessionControlRegistration>` | POSIX interactive session: binds the `tui` control endpoint and answers `{ status: "registered", socket, dispose }`. On a multi-session host with a public socket it binds nothing and `socket` is the host's. Otherwise `{ status: "unsupported", reason: "unsupported_platform" \| "unsupported_mode" }`, or `{ status: "failed", reason }` when a registration step failed. `drain(event)` runs on every [`session_control_wake`](#session_control_wake) edge and answers `{ admitted: [{ delivery_id, kind }] }`. `inboxDir` is created 0700 and its entry creations and deletions wake the drain. At clean exit a header-only session file is removed unless `isSessionReferenced()` answers `true`. |
|
|
1686
1686
|
| `admissionGate()` | `{ can_admit, hold_reason?, editor_revision, turn_epoch }` | Read-only pre-check; `hold_reason` is `draft` while the user composes or their submission is still on its way into the runtime. |
|
|
1687
|
-
| `admitExternalMessage({ delivery_id, text, deliverAs, expected_turn_id? })` | `{ kind, turn_epoch }` | Decides and acts in one synchronous call. `kind` is `started`, `queued`, `steered`, `turn_conflict` (stale epoch, or a steer without one), `held_draft` (nothing enqueued; retry on the next wake) or `already_admitted` (also for a delivery whose entry the file refused, until the run that refused it has settled and the file's last write succeeded). The delivery becomes a `custom` entry `session_control_delivery` whose `details.delivery_id` proves it was applied. Throws once a host started handing the session over (`release_session`). |
|
|
1687
|
+
| `admitExternalMessage({ delivery_id, text, deliverAs, expected_turn_id?, sender?, display_text? })` | `{ kind, turn_epoch }` | Decides and acts in one synchronous call. `kind` is `started`, `queued`, `steered`, `turn_conflict` (stale epoch, or a steer without one), `held_draft` (nothing enqueued; retry on the next wake) or `already_admitted` (also for a delivery whose entry the file refused, until the run that refused it has settled and the file's last write succeeded). The delivery becomes a `custom` entry `session_control_delivery` whose `details.delivery_id` proves it was applied. Throws once a host started handing the session over (`release_session`). `sender` (`{ kind: "agent", session_id, name? }`, `{ kind: "command_line", user? }` or `{ kind: "external", platform, author? }`) and `display_text` (the message as written) go to the entry's `details`: the terminal labels the message by its sender over `display_text`, while the model reads `text`. |
|
|
1688
1688
|
| `listAdmittedDeliveries()` | `{ pending, emitted, failed? }` | The process-lifetime ledger: `pending` is held by the runtime, `emitted` has its transcript entry written, `failed` (present only when non-empty, `[{ delivery_id, error }]`) had its entry refused by the session file: no longer held, still with its sender, admissible again once the run that refused it has settled and the file's last write succeeded. |
|
|
1689
1689
|
| `persistHeaderNow()` | `Promise<void>` | Writes the session header now, so the session id is durable before anything exposes it. |
|
|
1690
1690
|
|
package/docs/rpc.md
CHANGED
|
@@ -870,7 +870,8 @@ session's prompt for the named surface, so one host serves a terminal client and
|
|
|
870
870
|
uses the host process's `SENPI_PROMPT_SURFACE` (`app` or `chat` selects that surface; unset or any other value is `terminal`),
|
|
871
871
|
which remains the only switch for a single-process `--mode rpc` client. A later `open_session` that attaches to a live
|
|
872
872
|
session with another `promptSurface` rebuilds that session's prompt; an attach without the field keeps the current
|
|
873
|
-
surface, and `new_session` / `switch_session` / `fork` inside the session keep it too
|
|
873
|
+
surface, and `new_session` / `switch_session` / `fork` inside the session keep it too, also when the attach lands while
|
|
874
|
+
one of them is still building the replacement. Probe `prompt_surface` in
|
|
874
875
|
`get_protocol_info` before sending the field; an older host ignores it. Probe `prompt_surface_chat` before sending
|
|
875
876
|
`chat`: a host without it refuses `chat` with `invalid_launch_profile`, so a gateway falls back to `app`. Any value other
|
|
876
877
|
than `terminal`, `app` or `chat` is refused with `invalid_launch_profile`.
|
|
@@ -900,9 +901,10 @@ subprocesses (the bash tool and everything it spawns) and its eval kernels, incl
|
|
|
900
901
|
variable, even when the host process itself exports one (a daemon never inherits it from the process that ensured it).
|
|
901
902
|
`BSK_HOME` and `BSK_BIN` are per install, not per session: they pass from the host environment to every session
|
|
902
903
|
unchanged. A later `open_session` that attaches to a live session with another `browserEngine` moves that session to
|
|
903
|
-
it, and an attach without the field keeps the current engine; `new_session` / `switch_session` / `fork` keep it too
|
|
904
|
-
|
|
905
|
-
|
|
904
|
+
it, and an attach without the field keeps the current engine; `new_session` / `switch_session` / `fork` keep it too,
|
|
905
|
+
also when the attach lands while one of them is still building the replacement. Probe `browser_engine` in
|
|
906
|
+
`get_protocol_info` before sending the field; the capability is advertised only because the value reaches every
|
|
907
|
+
consumer above. Any other value is refused with `invalid_launch_profile`.
|
|
906
908
|
|
|
907
909
|
A skill that must tell the client what the browser is doing publishes it as that session's own event: an extension
|
|
908
910
|
registers a tool that calls `pi.rpc.emit(name, data)`, an eval cell reaches it as `tool.<name>(...)`, and the host adds
|
|
@@ -1147,10 +1149,11 @@ registrant opens no socket and writes no registry directory.
|
|
|
1147
1149
|
`endpoint_kind: "tui"`, `owner` and `alive`, probing it for at most 1.5 s. `senpi host gc` reaps a dead one on
|
|
1148
1150
|
the same evidence as a host. `ensure`, `handoff` and `stop` refuse it with `unsupported_endpoint_kind` (exit 3)
|
|
1149
1151
|
before connecting, because a terminal endpoint is owned by its terminal process.
|
|
1150
|
-
- **Commands.**
|
|
1151
|
-
|
|
1152
|
-
- `get_protocol_info`: `mode: "tui"`, `capabilities: ["tui_control"]`, `generation: 0
|
|
1153
|
-
`instanceId
|
|
1152
|
+
- **Commands.** Anything not listed below is answered `unsupported` as data. No command can prompt, steer, queue a
|
|
1153
|
+
follow-up, open a session or run a command.
|
|
1154
|
+
- `get_protocol_info`: `mode: "tui"`, `capabilities: ["tui_control"]`, `generation: 0`, the endpoint's
|
|
1155
|
+
`instanceId`, and `commands`: every command this endpoint accepts. A terminal from before the session
|
|
1156
|
+
controls below lists no `commands`.
|
|
1154
1157
|
- `list_sessions`: exactly one row, `kind: "interactive"`, `surface: "tui"`, `attachments: 1`.
|
|
1155
1158
|
- `get_state`: the RPC session state plus `turn_epoch`, `blocking_question`, `compacting`,
|
|
1156
1159
|
`editor_has_draft` and `state_version`.
|
|
@@ -1166,6 +1169,21 @@ registrant opens no socket and writes no registry directory.
|
|
|
1166
1169
|
question (a pending question id from the `question` feed); without it, `id` does. The answer settles by
|
|
1167
1170
|
the host's rule (see `question`) and the reply carries the frame's `id`, as on a host (see "Extension
|
|
1168
1171
|
UI Responses").
|
|
1172
|
+
- The session controls run the pane's own paths, so validation, the footer and persistence are the ones the
|
|
1173
|
+
user gets doing it in that terminal:
|
|
1174
|
+
- `get_available_models` and `get_available_thinking_levels`: the same answers as a host's.
|
|
1175
|
+
- `set_model { provider, modelId }`: the `/model` switch (the footer, the remembered default model, the
|
|
1176
|
+
pane's status line). An unknown model is refused `Model not found: <provider>/<modelId>`; a switch the
|
|
1177
|
+
session refuses answers its reason. Success answers the model, as on a host.
|
|
1178
|
+
- `set_thinking_level { level, scope? }`: the thinking-level selector. `scope: "turn"` sets this session's
|
|
1179
|
+
level; otherwise the level is also remembered for the model (the selector's Ctrl+S). A level the active
|
|
1180
|
+
model cannot run is refused `Thinking level <level> is not supported by the active model.` and nothing
|
|
1181
|
+
changes, for either scope. A host refuses it only for `scope: "turn"`; for the default scope a host clamps it
|
|
1182
|
+
to a level the model runs.
|
|
1183
|
+
- `interrupt { turnId? }`: Esc on a running turn - queued input returns to the editor, the turn aborts and
|
|
1184
|
+
settles before the answer. Answers `{ interrupted: true, turnId }` with the `turn_epoch` it stopped. An idle
|
|
1185
|
+
session answers `{ interrupted: false }`; a `turnId` that is not the running turn answers
|
|
1186
|
+
`{ interrupted: false, turnId }` with the running one and leaves it alone. The session stays open.
|
|
1169
1187
|
- `prompt`, `steer` and `follow_up` are `unsupported`.
|
|
1170
1188
|
- **Admission.** A message from another session enters only through the registrant's drain, which calls
|
|
1171
1189
|
`pi.session.admitExternalMessage({ delivery_id, text, deliverAs, expected_turn_id? })`. One synchronous call
|
|
@@ -1470,6 +1488,14 @@ capability must use `ask`, never `workspace`, for an edit-only approval promise.
|
|
|
1470
1488
|
Hosts that support `permissionPreset: "auto"` advertise `permission_preset_auto`;
|
|
1471
1489
|
clients without it must not send `auto`.
|
|
1472
1490
|
Explicit permission rules and remembered approvals retain their existing precedence, except under `auto`: there settings and CLI rules can only narrow the preset (the more restrictive decision wins), and an "Always" answer (saved in `.senpi/permissions-approved.jsonl`, from this or an earlier session) still allows its pattern.
|
|
1491
|
+
An `open_session` that attaches to a live session with another `permissionPreset` moves that session to it: the next
|
|
1492
|
+
tool call is decided under the new preset, in either direction, and `new_session` / `switch_session` / `fork` keep it,
|
|
1493
|
+
also when the attach lands while one of them or a `reload` (or two overlapping reloads) is still rebuilding the
|
|
1494
|
+
session. An attach that names a preset is applied even when the session was opened or last attached with the same one.
|
|
1495
|
+
An attach without the field keeps the current preset. An attach accepts exactly the values `open_session` accepts and
|
|
1496
|
+
treats them the same way: an unknown preset name is applied like one given to `open_session`, so the session's next
|
|
1497
|
+
tool call is refused with `Permission setup failed: Invalid --permission-preset "<name>". ...` until a later attach
|
|
1498
|
+
names a valid preset.
|
|
1473
1499
|
|
|
1474
1500
|
| Command | Params | Success data | Notes |
|
|
1475
1501
|
| --- | --- | --- | --- |
|
|
@@ -1726,6 +1752,24 @@ Response:
|
|
|
1726
1752
|
{"type": "response", "command": "abort", "success": true}
|
|
1727
1753
|
```
|
|
1728
1754
|
|
|
1755
|
+
|
|
1756
|
+
#### interrupt
|
|
1757
|
+
|
|
1758
|
+
Stop the running turn and only that one, with the terminal control endpoint's contract (see "Interactive sessions
|
|
1759
|
+
expose a control endpoint"). `turnId` (the `turn_epoch` of `get_state`) is optional.
|
|
1760
|
+
|
|
1761
|
+
```json
|
|
1762
|
+
{"type": "interrupt", "turnId": "3"}
|
|
1763
|
+
```
|
|
1764
|
+
|
|
1765
|
+
Response: `{ interrupted: true, turnId }` once the turn has stopped; `{ interrupted: false }` when the session is
|
|
1766
|
+
idle; `{ interrupted: false, turnId }` (the running turn) when `turnId` names another turn. Unlike `abort`, it
|
|
1767
|
+
answers after the turn settled.
|
|
1768
|
+
|
|
1769
|
+
```json
|
|
1770
|
+
{"type": "response", "command": "interrupt", "success": true, "data": {"interrupted": true, "turnId": "3"}}
|
|
1771
|
+
```
|
|
1772
|
+
|
|
1729
1773
|
#### clear_queue
|
|
1730
1774
|
|
|
1731
1775
|
Remove queued steering and follow-up messages and return their text.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi",
|
|
3
|
-
"version": "2026.10.10-
|
|
3
|
+
"version": "2026.10.10-5",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piConfig": {
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"@earendil-works/chord": "0.99.1",
|
|
60
|
-
"@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.10-
|
|
61
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.10-
|
|
62
|
-
"@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.10-
|
|
60
|
+
"@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.10-5",
|
|
61
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.10-5",
|
|
62
|
+
"@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.10-5",
|
|
63
63
|
"@silvia-odwyer/photon-node": "0.3.4",
|
|
64
64
|
"chalk": "6.0.0",
|
|
65
65
|
"cross-spawn": "7.0.6",
|
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
"yaml": "2.9.1",
|
|
79
79
|
"@anthropic-ai/claude-agent-sdk": "0.3.289",
|
|
80
80
|
"@anthropic-ai/sdk": "0.127.0",
|
|
81
|
-
"@code-yeongyu/senpi-codemode": "2026.10.10-
|
|
82
|
-
"@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.10-
|
|
81
|
+
"@code-yeongyu/senpi-codemode": "2026.10.10-5",
|
|
82
|
+
"@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.10-5",
|
|
83
83
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
84
84
|
"@mozilla/readability": "0.6.0",
|
|
85
85
|
"linkedom": "0.18.13",
|