@code-yeongyu/senpi 2026.10.5 → 2026.10.6
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 +29 -0
- package/dist/bundle/chunks/{anthropic-messages-G5236QNK.js → anthropic-messages-4MNFPQ66.js} +1 -1
- package/dist/bundle/chunks/{app-server-command-3Q5G4CAF.js → app-server-command-GNTX4MZT.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-NTA2HGG4.js → azure-openai-responses-A5OEHSJM.js} +1 -1
- package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
- package/dist/bundle/chunks/{chunk-EEOKVSNF.js → chunk-3EE6G4G7.js} +180 -176
- package/dist/bundle/chunks/{chunk-2WV426OJ.js → chunk-4MJKBXI3.js} +1 -1
- package/dist/bundle/chunks/{chunk-ECDLITEY.js → chunk-6EH7QXOG.js} +2 -2
- package/dist/bundle/chunks/chunk-6KQGOVQG.js +2 -0
- package/dist/bundle/chunks/{chunk-JVSOW4QU.js → chunk-7A3WRAKP.js} +1 -1
- package/dist/bundle/chunks/{chunk-WTJU5B2C.js → chunk-BE66QJ3Z.js} +1 -1
- package/dist/bundle/chunks/{chunk-UHRXXTFZ.js → chunk-D2W3MXRF.js} +2 -2
- package/dist/bundle/chunks/{chunk-U7CI2JGX.js → chunk-DDEAZX22.js} +1 -1
- package/dist/bundle/chunks/{chunk-UXIQK5NQ.js → chunk-ESSQUSRC.js} +1 -1
- package/dist/bundle/chunks/{chunk-KLBKUBDV.js → chunk-GQ3YLXVZ.js} +1 -1
- package/dist/bundle/chunks/{chunk-CTQU2XNX.js → chunk-MLNJNJFE.js} +1 -1
- package/dist/bundle/chunks/{chunk-BPX6QWPD.js → chunk-PELNNWGO.js} +1 -1
- package/dist/bundle/chunks/{chunk-UUAKEDVS.js → chunk-QLIW3YMI.js} +1 -1
- package/dist/bundle/chunks/{chunk-BBK6SSYT.js → chunk-TAV6QZFM.js} +1 -1
- package/dist/bundle/chunks/{chunk-QUROHJLP.js → chunk-TFO5JGLR.js} +1 -1
- package/dist/bundle/chunks/{cli-main-VZWSUHFB.js → cli-main-2C3YPHRE.js} +1 -1
- package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-5ZUHUKMU.js → cloudflare-workers-ai-system-one-TE3OEJEL.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-OTMG6ZXI.js → google-generative-ai-JBC53AMK.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-2LXS3ICC.js → google-vertex-46ODNPM2.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-SHZJ3QWQ.js → help-fast-path-FUMH5NSA.js} +1 -1
- package/dist/bundle/chunks/{host-command-7JIGGOWR.js → host-command-N6XS5O67.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-JA7AHYYL.js → interactive-mode-CM4C4ZFY.js} +1 -1
- package/dist/bundle/chunks/{llama-cpp-classify-U5XAMKZE.js → llama-cpp-classify-AD6TTKNL.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-34WOINP4.js → multi-session-host-QLLTPWIM.js} +5 -5
- package/dist/bundle/chunks/{openai-codex-responses-WNTYIPFZ.js → openai-codex-responses-Q4OWIK7Y.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-VSH22FCN.js → openai-completions-6HU7GIGD.js} +1 -1
- package/dist/bundle/chunks/{openai-images-XBTUJXPR.js → openai-images-M73YANTR.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-WLBHCFYL.js → openai-responses-S4QYZXF3.js} +1 -1
- package/dist/bundle/chunks/{openrouter-images-3LVJZY3G.js → openrouter-images-PCJFX2KO.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-2QLVGQSC.js → package-manager-cli-VRCAO7RV.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-JUCEXHWU.js → rotation-stream-7E37KOCX.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-QEBSN2OH.js → rpc-mode-2OPE6J7P.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-UGQIP6ZN.js → session-control-endpoint-SOG4XFF7.js} +1 -1
- package/dist/bundle/chunks/{session-picker-HX4LOJZ7.js → session-picker-VTTRFGE7.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +370 -366
- package/dist/bundle/chunks/{typesafe-system-one-FNI3UASE.js → typesafe-system-one-TF23RUVG.js} +1 -1
- 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 +12 -0
- package/dist/core/agent-session.d.ts +18 -0
- package/dist/core/agent-session.js +55 -19
- package/dist/core/dynamic-prompt/handoff.d.ts +4 -2
- package/dist/core/dynamic-prompt/handoff.js +1 -1
- package/dist/core/extensions/builtin/diff.js +14 -9
- package/dist/core/extensions/builtin/files.js +6 -2
- package/dist/core/extensions/builtin/gpt-apply-patch/parser.js +5 -1
- package/dist/core/extensions/builtin/gpt-apply-patch/streaming-parser.d.ts +8 -0
- package/dist/core/extensions/builtin/gpt-apply-patch/streaming-parser.js +26 -2
- package/dist/core/extensions/builtin/gpt-apply-patch/streaming-render.js +82 -11
- package/dist/core/extensions/builtin/gpt-apply-patch/types.d.ts +9 -1
- package/dist/core/extensions/builtin/permission-system/auto-credentials.d.ts +2 -0
- package/dist/core/extensions/builtin/permission-system/auto-credentials.js +17 -0
- package/dist/core/extensions/builtin/permission-system/auto-paths.d.ts +14 -0
- package/dist/core/extensions/builtin/permission-system/auto-paths.js +93 -0
- package/dist/core/extensions/builtin/permission-system/auto-policy.d.ts +20 -0
- package/dist/core/extensions/builtin/permission-system/auto-policy.js +126 -0
- package/dist/core/extensions/builtin/permission-system/auto-program-rules.d.ts +6 -0
- package/dist/core/extensions/builtin/permission-system/auto-program-rules.js +139 -0
- package/dist/core/extensions/builtin/permission-system/auto-shell-grammar.d.ts +29 -0
- package/dist/core/extensions/builtin/permission-system/auto-shell-grammar.js +74 -0
- package/dist/core/extensions/builtin/permission-system/auto-shell-segments.d.ts +13 -0
- package/dist/core/extensions/builtin/permission-system/auto-shell-segments.js +23 -0
- package/dist/core/extensions/builtin/permission-system/cli.d.ts +1 -0
- package/dist/core/extensions/builtin/permission-system/cli.js +9 -10
- package/dist/core/extensions/builtin/permission-system/config.d.ts +4 -1
- package/dist/core/extensions/builtin/permission-system/config.js +13 -1
- package/dist/core/extensions/builtin/permission-system/index.js +20 -12
- package/dist/core/extensions/builtin/permission-system/non-interactive.d.ts +11 -6
- package/dist/core/extensions/builtin/permission-system/non-interactive.js +8 -33
- package/dist/core/extensions/builtin/permission-system/service.d.ts +20 -1
- package/dist/core/extensions/builtin/permission-system/service.js +43 -7
- package/dist/core/extensions/builtin/permission-system/settings.d.ts +1 -0
- package/dist/core/extensions/builtin/permission-system/settings.js +4 -3
- package/dist/core/extensions/builtin/permission-system/types.d.ts +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js +16 -3
- package/dist/core/retry-fallback/usage-limit.js +5 -1
- package/dist/core/tools/diff-render.d.ts +4 -0
- package/dist/core/tools/diff-render.js +13 -0
- package/dist/core/tools/renderers/edit.js +6 -4
- package/dist/main.js +12 -0
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.js +55 -11
- package/dist/modes/interactive/provider-error-presentation.d.ts +25 -0
- package/dist/modes/interactive/provider-error-presentation.js +42 -0
- package/dist/modes/interactive/theme/dark.json +5 -5
- package/dist/modes/interactive/theme/light.json +5 -5
- package/dist/modes/rpc/custom-capability.d.ts +6 -0
- package/dist/modes/rpc/custom-capability.js +6 -0
- package/dist/modes/rpc/rpc-client.d.ts +3 -0
- package/dist/modes/rpc/rpc-input-validation.d.ts +7 -0
- package/dist/modes/rpc/rpc-input-validation.js +37 -0
- package/dist/modes/rpc/rpc-types.d.ts +9 -0
- package/dist/modes/rpc/session-command-activity.d.ts +2 -0
- package/dist/modes/rpc/session-command-activity.js +25 -0
- package/dist/modes/rpc/session-command-router.js +11 -4
- package/dist/modes/rpc/session-registry-attach.js +1 -0
- package/dist/modes/rpc/session-registry-types.d.ts +7 -0
- package/dist/modes/rpc/session-registry-types.js +11 -0
- package/dist/modes/rpc/session-registry.js +6 -3
- package/dist/modes/rpc/session-sweep.d.ts +12 -2
- package/dist/modes/rpc/session-sweep.js +36 -3
- package/dist/modes/rpc/session-teardown.d.ts +2 -0
- package/dist/modes/rpc/session-teardown.js +1 -0
- package/dist/modes/rpc/worker-session-registry.js +5 -1
- package/docs/rpc.md +39 -5
- package/docs/settings.md +3 -1
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-K3BXFPWZ.js +0 -2
|
@@ -50,6 +50,13 @@ export interface RpcSessionEntry {
|
|
|
50
50
|
retainOnDisconnect?: boolean;
|
|
51
51
|
/** Timestamp of the last routed command / observed activity; drives idle eviction. */
|
|
52
52
|
lastCommandAt: number;
|
|
53
|
+
/**
|
|
54
|
+
* When the last attachment left (a retained session now standing detached), on the registry's
|
|
55
|
+
* clock; `undefined` while anyone is attached. A detached observational read does not refresh
|
|
56
|
+
* `lastCommandAt`, and the early-retirement sweep honors a minimum detach age so a transient
|
|
57
|
+
* reconnect keeps its runtime instead of re-opening cold.
|
|
58
|
+
*/
|
|
59
|
+
detachedAt?: number;
|
|
53
60
|
lifecycleMutex: Promise<void>;
|
|
54
61
|
closeCompletion?: Promise<void>;
|
|
55
62
|
closeResolve?: () => void;
|
|
@@ -14,6 +14,17 @@ export function frozenProfile(profile) {
|
|
|
14
14
|
...profile,
|
|
15
15
|
...(profile.creationModel ? { creationModel: Object.freeze({ ...profile.creationModel }) } : {}),
|
|
16
16
|
...(profile.sessionContext ? { sessionContext: Object.freeze({ ...profile.sessionContext }) } : {}),
|
|
17
|
+
...(profile.retryFallback
|
|
18
|
+
? {
|
|
19
|
+
retryFallback: Object.freeze({
|
|
20
|
+
modelFallback: profile.retryFallback.modelFallback,
|
|
21
|
+
fallbackChains: Object.freeze(Object.fromEntries(Object.entries(profile.retryFallback.fallbackChains).map(([key, entries]) => [
|
|
22
|
+
key,
|
|
23
|
+
Object.freeze([...entries]),
|
|
24
|
+
]))),
|
|
25
|
+
}),
|
|
26
|
+
}
|
|
27
|
+
: {}),
|
|
17
28
|
});
|
|
18
29
|
}
|
|
19
30
|
/**
|
|
@@ -5,6 +5,7 @@ import { createAgentSessionRuntime } from "../../core/agent-session-runtime.js";
|
|
|
5
5
|
import { assertValidSessionId, SessionManager } from "../../core/session-manager.js";
|
|
6
6
|
import { SESSION_PATH_RETRY_AFTER_MS } from "./host-reservations.js";
|
|
7
7
|
import { createRegistryWarm } from "./host-warm.js";
|
|
8
|
+
import { refreshesSessionActivity } from "./session-command-activity.js";
|
|
8
9
|
import { canonicalSessionPath } from "./session-path-key.js";
|
|
9
10
|
import { attachToOpenSession } from "./session-registry-attach.js";
|
|
10
11
|
import { settleClosingReservation, syncRuntimeMetadata } from "./session-registry-claims.js";
|
|
@@ -45,6 +46,7 @@ export class RpcSessionRegistry {
|
|
|
45
46
|
this.options.pathReservations?.release(key);
|
|
46
47
|
},
|
|
47
48
|
markDetached: (key) => this.options.pathReservations?.setAttached(key, false),
|
|
49
|
+
now: () => this.now(),
|
|
48
50
|
sync: () => this.syncRuntimeMetadata(),
|
|
49
51
|
};
|
|
50
52
|
}
|
|
@@ -202,9 +204,10 @@ export class RpcSessionRegistry {
|
|
|
202
204
|
}
|
|
203
205
|
if (entry.state !== "open" && entry.state !== "closing")
|
|
204
206
|
throw new RpcSessionRegistryError("unknown_session");
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
entry.
|
|
207
|
+
// Polling a session nobody holds is observation, not work that needs its runtime;
|
|
208
|
+
// an attached client's polling keeps its session alive exactly as before.
|
|
209
|
+
if (entry.attachments > 0 || refreshesSessionActivity(command))
|
|
210
|
+
entry.lastCommandAt = this.now();
|
|
208
211
|
return entry;
|
|
209
212
|
}
|
|
210
213
|
/**
|
|
@@ -8,11 +8,21 @@ export interface SweepVerdicts {
|
|
|
8
8
|
*/
|
|
9
9
|
readonly orphaned: readonly string[];
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* How long a session must stand detached before the early-retirement branch may
|
|
13
|
+
* park it: a disconnect that overlaps a sweep is a client about to come back,
|
|
14
|
+
* and re-opening its runtime cold is the expensive path.
|
|
15
|
+
*/
|
|
16
|
+
export declare const DETACHED_RETIREMENT_GRACE_MS = 5000;
|
|
11
17
|
/**
|
|
12
18
|
* One occupancy sweep's decisions. "Idle" is the COMPLETE session-owned activity contract
|
|
13
19
|
* (`AgentSession.isSessionBusy`: agent run, bash, background terminal jobs and other published
|
|
14
20
|
* wake sources, compaction, barrier-held session work): a busy session restarts its idle clock
|
|
15
|
-
* instead, so work that outlives a turn is never killed. A
|
|
21
|
+
* instead, so work that outlives a turn is never killed. A quiet detached in-process worker
|
|
22
|
+
* with flushed history can reopen by path and need not keep its runtime for the idle window -
|
|
23
|
+
* once it has been detached for at least `DETACHED_RETIREMENT_GRACE_MS`, so a transient
|
|
24
|
+
* reconnect keeps the runtime it already owns.
|
|
25
|
+
* A non-finite window evicts nothing idle.
|
|
16
26
|
*/
|
|
17
|
-
export declare function selectSweepEvictions(registry: Pick<RpcSessionRegistry, "list" | "peek">, now: number, idleEvictionMs: number): SweepVerdicts;
|
|
27
|
+
export declare function selectSweepEvictions(registry: Pick<RpcSessionRegistry, "list" | "peek">, now: number, idleEvictionMs: number, hasPendingRequest?: (sessionId: string) => boolean): SweepVerdicts;
|
|
18
28
|
//# sourceMappingURL=session-sweep.d.ts.map
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { sessionDirectoryRemoved } from "./session-path-key.js";
|
|
2
|
+
/**
|
|
3
|
+
* How long a session must stand detached before the early-retirement branch may
|
|
4
|
+
* park it: a disconnect that overlaps a sweep is a client about to come back,
|
|
5
|
+
* and re-opening its runtime cold is the expensive path.
|
|
6
|
+
*/
|
|
7
|
+
export const DETACHED_RETIREMENT_GRACE_MS = 5_000;
|
|
2
8
|
/**
|
|
3
9
|
* One occupancy sweep's decisions. "Idle" is the COMPLETE session-owned activity contract
|
|
4
10
|
* (`AgentSession.isSessionBusy`: agent run, bash, background terminal jobs and other published
|
|
5
11
|
* wake sources, compaction, barrier-held session work): a busy session restarts its idle clock
|
|
6
|
-
* instead, so work that outlives a turn is never killed. A
|
|
12
|
+
* instead, so work that outlives a turn is never killed. A quiet detached in-process worker
|
|
13
|
+
* with flushed history can reopen by path and need not keep its runtime for the idle window -
|
|
14
|
+
* once it has been detached for at least `DETACHED_RETIREMENT_GRACE_MS`, so a transient
|
|
15
|
+
* reconnect keeps the runtime it already owns.
|
|
16
|
+
* A non-finite window evicts nothing idle.
|
|
7
17
|
*/
|
|
8
|
-
export function selectSweepEvictions(registry, now, idleEvictionMs) {
|
|
18
|
+
export function selectSweepEvictions(registry, now, idleEvictionMs, hasPendingRequest = () => false) {
|
|
9
19
|
const idle = [];
|
|
10
20
|
const orphaned = [];
|
|
11
21
|
for (const { sessionId, status, sessionPath } of registry.list()) {
|
|
@@ -20,10 +30,33 @@ export function selectSweepEvictions(registry, now, idleEvictionMs) {
|
|
|
20
30
|
}
|
|
21
31
|
if (!Number.isFinite(idleEvictionMs))
|
|
22
32
|
continue;
|
|
23
|
-
if (
|
|
33
|
+
if (hasPendingRequest(sessionId))
|
|
34
|
+
continue;
|
|
35
|
+
const session = entry.runtime?.session;
|
|
36
|
+
if (entry.worker?.busy || session?.isSessionBusy) {
|
|
24
37
|
entry.lastCommandAt = now;
|
|
25
38
|
continue;
|
|
26
39
|
}
|
|
40
|
+
if (entry.attachments > 0) {
|
|
41
|
+
// An attached client holds the live routing handle; only the ordinary deadline applies.
|
|
42
|
+
if (now - entry.lastCommandAt >= idleEvictionMs)
|
|
43
|
+
idle.push(sessionId);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (entry.kind === "worker" && entry.retainOnDisconnect && session?.sessionManager.isTranscriptFlushed()) {
|
|
47
|
+
// Delivery debt is still owed a turn, even after the ordinary idle deadline.
|
|
48
|
+
if (session.pendingMessageCount > 0 || session.externalAdmission.list().pending.length > 0)
|
|
49
|
+
continue;
|
|
50
|
+
// A just-detached worker is a client about to reconnect; park it only once the
|
|
51
|
+
// disconnect has stood longer than the grace age. An entry at zero attachments
|
|
52
|
+
// with no detach stamp was never disconnected, so it keeps its normal window.
|
|
53
|
+
if (entry.detachedAt !== undefined) {
|
|
54
|
+
if (now - entry.detachedAt < DETACHED_RETIREMENT_GRACE_MS)
|
|
55
|
+
continue;
|
|
56
|
+
idle.push(sessionId);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
27
60
|
if (now - entry.lastCommandAt >= idleEvictionMs)
|
|
28
61
|
idle.push(sessionId);
|
|
29
62
|
}
|
|
@@ -6,6 +6,8 @@ export interface SessionTeardownHost {
|
|
|
6
6
|
releaseReservation(key: string): 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
|
+
/** The registry's clock, so the detach stamp shares the sweep's timeline. */
|
|
10
|
+
now(): number;
|
|
9
11
|
sync(): void;
|
|
10
12
|
}
|
|
11
13
|
export declare function beginSessionClose(host: SessionTeardownHost, handle: string, onRole?: (finalizer: boolean) => void, options?: {
|
|
@@ -25,6 +25,7 @@ export function beginSessionClose(host, handle, onRole, options) {
|
|
|
25
25
|
// reservation, and is torn down only by an explicit close or the idle window.
|
|
26
26
|
if (options?.detach && entry.retainOnDisconnect) {
|
|
27
27
|
entry.attachments = 0;
|
|
28
|
+
entry.detachedAt ??= host.now();
|
|
28
29
|
// A retained session nobody is attached to is what another generation may reclaim the path
|
|
29
30
|
// from (#1893) - but only once nothing is still WRITING it. A session mid-turn keeps its claim
|
|
30
31
|
// until it parks, because reclaiming it would put two writers on one transcript.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isAbsolute } from "node:path";
|
|
2
2
|
import { ProviderScope } from "@earendil-works/pi-ai/node/provider-scope";
|
|
3
3
|
import { assertValidSessionId } from "../../core/session-manager.js";
|
|
4
|
+
import { refreshesSessionActivity } from "./session-command-activity.js";
|
|
4
5
|
import { RESERVATION_DENIAL_CODES, SessionPathReservations, } from "./session-path-reservations.js";
|
|
5
6
|
import { frozenProfile, RpcSessionRegistryError, sessionIdentity, } from "./session-registry.js";
|
|
6
7
|
import { SessionWorkerClient } from "./session-worker-client.js";
|
|
@@ -139,7 +140,10 @@ export class WorkerSessionRegistry {
|
|
|
139
140
|
throw new RpcSessionRegistryError("session_closing");
|
|
140
141
|
if (entry.state !== "open" && entry.state !== "closing")
|
|
141
142
|
throw new RpcSessionRegistryError("unknown_session");
|
|
142
|
-
|
|
143
|
+
// Polling a session nobody holds is observation, not work that needs its runtime;
|
|
144
|
+
// an attached client's polling keeps its session alive exactly as before.
|
|
145
|
+
if (entry.attachments > 0 || refreshesSessionActivity(command))
|
|
146
|
+
entry.lastCommandAt = this.now();
|
|
143
147
|
return entry;
|
|
144
148
|
}
|
|
145
149
|
beginClose(handle, onRole, options) {
|
package/docs/rpc.md
CHANGED
|
@@ -867,6 +867,22 @@ surface, and `new_session` / `switch_session` / `fork` inside the session keep i
|
|
|
867
867
|
`chat`: a host without it refuses `chat` with `invalid_launch_profile`, so a gateway falls back to `app`. Any value other
|
|
868
868
|
than `terminal`, `app` or `chat` is refused with `invalid_launch_profile`.
|
|
869
869
|
|
|
870
|
+
#### Fallback chain per session (`retry_fallback_profile`)
|
|
871
|
+
|
|
872
|
+
`open_session.retryFallback: { modelFallback: boolean, fallbackChains: Record<string, string[]> }` is the fallback
|
|
873
|
+
policy THIS session runs with, e.g. a task child's own chain. It overrides that session's `retry.modelFallback` and
|
|
874
|
+
`retry.fallbackChains` in memory only: it is never written to a settings file, and no other session on the host sees
|
|
875
|
+
it. A session opened without the field keeps the host's settings. It is applied when the open creates the session; an
|
|
876
|
+
attach to a live session keeps the policy that session was created with. Probe `retry_fallback_profile` in
|
|
877
|
+
`get_protocol_info` before sending the field. A malformed profile (a missing `modelFallback`, a chain that is not an
|
|
878
|
+
array of non-empty selectors, a selector longer than 512 characters, more than 32 chains or 32 entries per chain) is
|
|
879
|
+
refused with `invalid_launch_profile` rather than opening a session without the chain its caller asked for; an empty
|
|
880
|
+
chain (`[]`) is accepted and means no fallback for that key. If something inside that session later changes a
|
|
881
|
+
fallback setting through the settings setters, that write follows the setters' own rules (it persists to the global
|
|
882
|
+
settings file) and replaces the session's override for that one key only: a `setFallbackChain` drops the profile's
|
|
883
|
+
chains, while `modelFallback` keeps the profile's value. The policy is not stored with the session, so a client that
|
|
884
|
+
reopens a session after it was closed sends the profile again.
|
|
885
|
+
|
|
870
886
|
#### Browser engine per session (`browser_engine`)
|
|
871
887
|
|
|
872
888
|
`open_session.browserEngine: "connected" | "builtin" | "none"` names the browser THIS session's skills drive: the
|
|
@@ -1029,7 +1045,7 @@ extensions and contents; neither runtime provides process-fatal OOM containment.
|
|
|
1029
1045
|
|
|
1030
1046
|
What the host does enforce are lifecycle windows, and they only ever return memory from work nobody is doing:
|
|
1031
1047
|
|
|
1032
|
-
- **Idle eviction**: a session with no
|
|
1048
|
+
- **Idle eviction**: a session with no activity-refreshing command and no session-owned work for
|
|
1033
1049
|
`SENPI_RPC_SESSION_IDLE_EVICTION_MS` (default 30 minutes) is closed through the exact `close_session` sequence
|
|
1034
1050
|
(abort → waitForIdle → dispose, all attachments drained, path reservation released) and every attached connection
|
|
1035
1051
|
receives that handle's `session_closed { reason: "idle_evicted" }` broadcast plus a final `close_session` response
|
|
@@ -1048,6 +1064,21 @@ What the host does enforce are lifecycle windows, and they only ever return memo
|
|
|
1048
1064
|
background terminal jobs and any other published wake source (terminal monitors, loop-guard holds), compaction,
|
|
1049
1065
|
and barrier-held session work all defer eviction, and the idle clock restarts when that work settles. An evicted
|
|
1050
1066
|
session resumes like any other: the next `open_session` with the same `sessionPath` reopens it.
|
|
1067
|
+
- **Observational reads**: session state, history, model/auth inventory, loaded surfaces, and `memory_report`
|
|
1068
|
+
do not restart the idle clock of a **detached** session in either runtime. With any client still attached,
|
|
1069
|
+
these reads keep refreshing activity as before: an attached client polling `get_state` keeps the same live
|
|
1070
|
+
routing handle and runtime. An attached session receiving no commands can still idle-evict as before.
|
|
1071
|
+
Commands that change the session restart the clock even when detached. An in-flight request or prompt
|
|
1072
|
+
preflight prevents ordinary idle parking while the request is running; directory-removal teardown is separate.
|
|
1073
|
+
- **Completed detached workers**: on the in-process runtime, a retained `kind: "worker"` session with zero
|
|
1074
|
+
attachments is parked on the next occupancy sweep once its transcript is flushed and it has no session-owned
|
|
1075
|
+
work, queued input, admitted unwritten delivery, pending prompt, or in-flight request - provided the disconnect
|
|
1076
|
+
has already stood for at least five seconds (a short grace age, so a transient reconnect keeps the live runtime
|
|
1077
|
+
instead of re-opening cold). It does not wait out the normal idle window. This releases its runtime and eval
|
|
1078
|
+
kernels, not its durable history: reopen its
|
|
1079
|
+
`sessionPath` to continue under a new routing handle. Active monitors and other wake sources still prevent
|
|
1080
|
+
early parking. Unflushed workers, worker-isolate runtimes, and interactive sessions keep their normal window.
|
|
1081
|
+
A zero-attachment worker without a recorded disconnect time also keeps its normal idle deadline.
|
|
1051
1082
|
- **Worker capacity** (worker runtime ONLY - a stdio host, `--listen stdio://`, an embedder, or a socket host that
|
|
1052
1083
|
passed `--session-runtime worker` explicitly): at most 20 workers may be preparing, open, closing, or quarantined
|
|
1053
1084
|
together, because each one is an isolate the host must keep alive. Admission beyond this bound fails explicitly
|
|
@@ -1064,7 +1095,8 @@ What the host does enforce are lifecycle windows, and they only ever return memo
|
|
|
1064
1095
|
`open_session` with that `sessionPath` — from any connection — attaches to the same routing handle and returns
|
|
1065
1096
|
`attached: true`. Retention never outranks an explicit teardown: a `close_session` from an attached connection still
|
|
1066
1097
|
reaches zero attachments and closes the session, host shutdown closes it, and the idle-eviction window above still
|
|
1067
|
-
parks it
|
|
1098
|
+
parks it (or the completed-detached-worker rule above parks it sooner) — announced as
|
|
1099
|
+
`session_parked { sessionId, sessionPath }`, after which the file reopens by path like any
|
|
1068
1100
|
evicted session, and the parked session counts as gone for the empty-host exit below. It is also bounded by the empty-host exit
|
|
1069
1101
|
and the supervisor's idle-exit window below: retention survives a client, not the host. Any attach may turn retention
|
|
1070
1102
|
on for a live session; no attach turns it off for clients that already rely on it. Omitting the flag is byte-identical
|
|
@@ -1427,12 +1459,14 @@ Hosts that support `open_session.permissionPreset: "accept-edits"` advertise
|
|
|
1427
1459
|
`permission_preset_accept_edits`. This preset allows project read/list/grep/edit
|
|
1428
1460
|
and asks for bash, external_directory and other tools. Clients without that
|
|
1429
1461
|
capability must use `ask`, never `workspace`, for an edit-only approval promise.
|
|
1430
|
-
|
|
1462
|
+
Hosts that support `permissionPreset: "auto"` advertise `permission_preset_auto`;
|
|
1463
|
+
clients without it must not send `auto`.
|
|
1464
|
+
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.
|
|
1431
1465
|
|
|
1432
1466
|
| Command | Params | Success data | Notes |
|
|
1433
1467
|
| --- | --- | --- | --- |
|
|
1434
1468
|
| `get_protocol_info` | - | `{ protocolVersion: 1, serverVersion: string, capabilities: string[], mode: "classic"\|"multi", instanceId: string, generation: number, engineVersion: string, engineOrdinal: [y, m, d, n, epoch], launch_profile: { profile_id, core }, memory_pressure?: boolean }` | Answered in BOTH modes; side-effect-free; the capability probe. Multi-session hosts include `multi_session`, `retain_on_disconnect`, `session_kind`, `session_context`, `auto_title_per_session`, `durable_session_id`, `prompt_surface` and `prompt_surface_chat` plus the negotiated launch capabilities. Those are HOST capabilities (a client never sends them) and are advertised only in multi-session mode, where the host owns the attachment refcount and the per-session launch profile. The identity fields are described under "Host identity" above; compatibility is decided from `protocolVersion`, `capabilities` and `engineOrdinal`, NEVER from `serverVersion`. |
|
|
1435
|
-
| `open_session` | `sessionPath?`, `cwd?`, `provider?`, `modelId?`, `thinkingLevel?`, `permissionPreset?`, `retain_on_disconnect?`, `kind?`, `context?`, `auto_title?`, `durableSessionId?`, `promptSurface?`, `browserEngine?` (all optional; paths MUST be absolute) | `{ sessionId, state: RpcSessionState, attached?: true }` | `sessionPath` = today's `--session` semantics (open-if-exists else create persisting there, `session-manager.ts:926-940`); `provider`/`modelId` applied only on create (resume restores the session's model — mirrors `SenpiSessionRuntime.ts:198-200`); params form the immutable launch profile (D8). When the path is already held by a fully-open session, the open ATTACHES to it: same routing handle, `attached: true`, one more attachment counted; the runtime is torn down only when the last attachment closes. Idle sessions past the eviction window are closed by the host itself. `retain_on_disconnect: true` (default false) makes a dropped connection DETACH from this session instead of closing it — see "Retained sessions" below. `kind` (default `interactive`) and the opaque `context` map are described under "Session kind and context" above; both are stored frozen for the session's life and never influence auth, model or resource resolution. |
|
|
1469
|
+
| `open_session` | `sessionPath?`, `cwd?`, `provider?`, `modelId?`, `thinkingLevel?`, `permissionPreset?`, `retain_on_disconnect?`, `kind?`, `context?`, `auto_title?`, `durableSessionId?`, `promptSurface?`, `browserEngine?`, `retryFallback?` (all optional; paths MUST be absolute) | `{ sessionId, state: RpcSessionState, attached?: true }` | `sessionPath` = today's `--session` semantics (open-if-exists else create persisting there, `session-manager.ts:926-940`); `provider`/`modelId` applied only on create (resume restores the session's model — mirrors `SenpiSessionRuntime.ts:198-200`); params form the immutable launch profile (D8). When the path is already held by a fully-open session, the open ATTACHES to it: same routing handle, `attached: true`, one more attachment counted; the runtime is torn down only when the last attachment closes. Idle sessions past the eviction window are closed by the host itself. `retain_on_disconnect: true` (default false) makes a dropped connection DETACH from this session instead of closing it — see "Retained sessions" below. `kind` (default `interactive`) and the opaque `context` map are described under "Session kind and context" above; both are stored frozen for the session's life and never influence auth, model or resource resolution. |
|
|
1436
1470
|
| `close_session` | `sessionId` | `{}` | Refused with `unknown_session` when the requesting connection never attached to that handle (a close releases the CALLER's attachment, and `list_sessions` publishes every handle). Otherwise aborts active work, awaits agent idle + settled persistence for up to the host grace window (default 10s), then quarantines any worker that has not exited without releasing its path reservation; its response is the LAST record tagged with that handle for the first closer — no events after (test-pinned). An admitted concurrent close joins the same teardown and receives its own successful response; output saturation rejects admission with the bounded close-overflow/resync notice described above. |
|
|
1437
1471
|
| `list_sessions` | `include_workers?` (default false) | `{ sessions: [{ sessionId, durableSessionId, sessionPath, cwd, name, status, attachments, kind, context? }] }` | Includes `opening`/`closing` entries. Internally quarantined workers remain externally `closing` until exit. `attachments` is the session's live client attachment count; `0` on an `open` row is a retained session with no client attached. Every row carries `kind`. Rows with `kind: "worker"` are omitted unless `include_workers: true`, and `context` is published ONLY on that listing — a default listing carries no `context` at all. |
|
|
1438
1472
|
| `release_session` | `sessionId`, `reason: "takeover"`, `interrupt?`, `force?` | `{ released: true, session_path, attachments }` | Hands the session to a runtime outside this host. See "Handing a session over (`release_session`)" below. |
|
|
@@ -1465,7 +1499,7 @@ In the response `error` field, machine-matchable:
|
|
|
1465
1499
|
- `open_failed: <detail>`
|
|
1466
1500
|
- `invalid_session_context: <detail>` (`open_session.context` past a documented cap: more than 32 keys, a key that does not match `^[a-z][a-z0-9_]*$`, a non-string or >16 KiB value, or more than 32 KiB of JSON in total; the detail names the cap and its byte budget)
|
|
1467
1501
|
- `invalid_session_kind: <detail>` (`open_session.kind` other than `interactive` or `worker`)
|
|
1468
|
-
- `invalid_launch_profile: <detail>` (`open_session.auto_title` present but not a boolean, `open_session.promptSurface` other than `terminal`, `app` or `chat`,
|
|
1502
|
+
- `invalid_launch_profile: <detail>` (`open_session.auto_title` present but not a boolean, `open_session.promptSurface` other than `terminal`, `app` or `chat`, `open_session.browserEngine` other than `connected`, `builtin` or `none`, or a malformed `open_session.retryFallback`)
|
|
1469
1503
|
- `host_memory_pressure` (sent only by hosts released before #2207, which declined to CREATE a `kind: "worker"` session above `SENPI_RPC_HOST_RSS_REFUSE_MB`; `errorData { rssMb, retry_after_ms }` says when to ask again. Current hosts never refuse an open for memory; a client talking to an older generation waits and retries, it never starts a second host or a per-child process)
|
|
1470
1504
|
- `warm_failed: <detail>` (`warm` could not load its profile, for example a `cwd` that does not exist; not remembered, so a retry loads again)
|
|
1471
1505
|
- `media_not_found` (`get_media` for an unknown `toolCallId`, or a `contentIndex` that does not point at an image block)
|
package/docs/settings.md
CHANGED
|
@@ -27,7 +27,7 @@ Senpi includes a built-in permission system for tool calls. It evaluates a prese
|
|
|
27
27
|
|
|
28
28
|
| Setting | Type | Default | Description |
|
|
29
29
|
|---------|------|---------|-------------|
|
|
30
|
-
| `permissionPreset` | string | `"full-access"` | Permission preset: `"full-access"`, `"workspace"`, `"read-only"`, or `"ask"` |
|
|
30
|
+
| `permissionPreset` | string | `"full-access"` | Permission preset: `"full-access"`, `"workspace"`, `"accept-edits"`, `"auto"`, `"read-only"`, or `"ask"` |
|
|
31
31
|
| `permission` | object | - | Explicit permission rules that override the selected preset |
|
|
32
32
|
|
|
33
33
|
Presets:
|
|
@@ -36,6 +36,8 @@ Presets:
|
|
|
36
36
|
|--------|----------|
|
|
37
37
|
| `full-access` | Allow all permission checks without prompting |
|
|
38
38
|
| `workspace` | Allow `read`, `list`, `grep`, `edit`, and `bash`; ask for `external_directory` |
|
|
39
|
+
| `accept-edits` | Allow project `read`, `list`, `grep`, and `edit`; ask for `bash`, `external_directory`, and other tools |
|
|
40
|
+
| `auto` | Asks for everything except what it can prove stays inside the project, judged on the exact file the tool will open (the tool's own path resolution, symlinks followed): reads, listings and writes of project files through the file tools (`read`, `ls`, `find`, `write`, `edit`), `apply_patch` adding, changing or moving project files (a delete asks), a content search of one or more project files, and a small set of read-only shell commands (`ls cat head tail wc diff stat file cut grep rg echo pwd true which`, read-only git) with plain in-project arguments. Files count only when they land inside the project with no hidden component (except repository and formatter files such as `.gitignore`, `.github/`) and no credential-shaped name. Everything else asks: anything outside the project, `.env` and other dotfiles, `.git/`, `.vscode/`, keys, directory-wide content searches, any shell command with quotes, pipes, redirects, `cd` or a write (`cp`, `mv`, `rm`, `mkdir`), `git show`, text sent to a running process (`bash_input`), test runners, builds, installs and network tools. A session rooted at `/`, your home directory, any directory above it, or inside a hidden directory (such as `~/.config`) asks for everything. Each call is decided as the more restrictive of `auto`'s decision and your own rules' decision (deny over ask over allow), whatever order or layer (settings, CLI, RPC) your rules come from: your rules can narrow `auto`, never widen it. <br>`auto` prevents accidents; it is not a sandbox. Credential detection is by name, so a secret in a plainly named project file (for example `secrets.yml`, `terraform.tfvars`) can be read. A path the judge approved can be swapped for a symlink before the tool runs. An "Always" answer you gave (saved in `.senpi/permissions-approved.jsonl`, from this or an earlier session) still allows its pattern. |
|
|
39
41
|
| `read-only` | Allow `read`, `list`, and `grep`; ask for `edit`, `bash`, and `external_directory` |
|
|
40
42
|
| `ask` | Restore prompt-on-unknown behavior |
|
|
41
43
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.6",
|
|
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.
|
|
61
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.
|
|
62
|
-
"@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.
|
|
60
|
+
"@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.6",
|
|
61
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.6",
|
|
62
|
+
"@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.6",
|
|
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.288",
|
|
80
80
|
"@anthropic-ai/sdk": "0.127.0",
|
|
81
|
-
"@code-yeongyu/senpi-codemode": "2026.10.
|
|
82
|
-
"@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.
|
|
81
|
+
"@code-yeongyu/senpi-codemode": "2026.10.6",
|
|
82
|
+
"@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.6",
|
|
83
83
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
84
84
|
"@mozilla/readability": "0.6.0",
|
|
85
85
|
"linkedom": "0.18.13",
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import { createRequire as __piCreateRequire } from "node:module"; const require = __piCreateRequire(import.meta.url); { const __piWorkerThreads = require("node:worker_threads"); if (typeof __piWorkerThreads.markAsUncloneable !== "function") { __piWorkerThreads.markAsUncloneable = () => {}; } }
|
|
2
|
-
import{HOST_GENERATION_ENV,HOST_INSTANCE_ID_ENV}from"./chunk-CIRZTP7W.js";import{engineBuildIdentity}from"./chunk-XYDZ3LIE.js";import{parseArgs,resolveSessionRuntime}from"./chunk-MH2BFYGN.js";import*as crypto from"node:crypto";var CUSTOM_UNSUPPORTED_CAPABILITY="custom_unsupported",EXTENSION_EVENTS_CAPABILITY="extension_events",AUTO_TITLE_SESSIONS_CAPABILITY="auto_title_sessions",CONTINUE_FROM_LEAF_CAPABILITY="continue_from_leaf",MEDIA_PLACEHOLDERS_CAPABILITY="media_placeholders",QUESTION_CAPABILITY="question",RETAIN_ON_DISCONNECT_CAPABILITY="retain_on_disconnect",SESSION_CONTEXT_CAPABILITY="session_context",SESSION_KIND_CAPABILITY="session_kind",AUTO_TITLE_PER_SESSION_CAPABILITY="auto_title_per_session",WARM_CAPABILITY="warm",DURABLE_SESSION_ID_CAPABILITY="durable_session_id",PROMPT_SURFACE_CAPABILITY="prompt_surface",BROWSER_ENGINE_CAPABILITY="browser_engine",PROMPT_SURFACE_CHAT_CAPABILITY="prompt_surface_chat",RPC_CLIENT_CAPABILITIES_ENV="SENPI_RPC_CLIENT_CAPABILITIES";function parseClientCapabilities(value){return value?value.split(",").map(part=>part.trim()).filter(part=>part.length>0):[]}var DEFAULT_CUSTOM_EXTENSION_LABEL="custom UI component";function buildCustomUnsupportedRequest(capabilities,extensionName){if(!capabilities?.includes(CUSTOM_UNSUPPORTED_CAPABILITY))return;let name=extensionName.trim()||DEFAULT_CUSTOM_EXTENSION_LABEL;return{type:"extension_ui_request",id:crypto.randomUUID(),method:"custom_unsupported",extensionName:name}}import{createHash,randomUUID as randomUUID2}from"node:crypto";import{resolve}from"node:path";var INSTANCE_ID=resolveInstanceId(process.env[HOST_INSTANCE_ID_ENV]);function resolveInstanceId(value){return value!==void 0&&value.trim()!==""?value:randomUUID2()}function hostInstanceId(){return INSTANCE_ID}var cachedProfile;function hostGeneration(env){let value=env[HOST_GENERATION_ENV];return value!==void 0&&/^\d+$/.test(value)?Number(value):0}function hostLaunchProfile(argv,cwd){let parsed=parseArgs([...argv],{grokNeoEnabled:!1}),core={extensions:[...new Set((parsed.extensions??[]).map(path=>resolve(cwd,path)))].sort(),multi_session:parsed.multiSession===!0,session_runtime:resolveSessionRuntime(parsed)},canonical=JSON.stringify({extensions:core.extensions,multi_session:core.multi_session,session_runtime:core.session_runtime});return{profile_id:createHash("sha256").update(canonical).digest("hex"),core}}function socketHostLaunchProfile(hostArgs,cwd){return hostLaunchProfile(["--mode","rpc","--multi-session","--listen","unix://",...hostArgs],cwd)}function protocolIdentity(){cachedProfile??=hostLaunchProfile(process.argv.slice(2),process.cwd());let build=engineBuildIdentity();return{instanceId:INSTANCE_ID,generation:hostGeneration(process.env),engineVersion:build.text,engineOrdinal:build.ordinal,launch_profile:cachedProfile}}export{CUSTOM_UNSUPPORTED_CAPABILITY,EXTENSION_EVENTS_CAPABILITY,AUTO_TITLE_SESSIONS_CAPABILITY,CONTINUE_FROM_LEAF_CAPABILITY,MEDIA_PLACEHOLDERS_CAPABILITY,QUESTION_CAPABILITY,RETAIN_ON_DISCONNECT_CAPABILITY,SESSION_CONTEXT_CAPABILITY,SESSION_KIND_CAPABILITY,AUTO_TITLE_PER_SESSION_CAPABILITY,WARM_CAPABILITY,DURABLE_SESSION_ID_CAPABILITY,PROMPT_SURFACE_CAPABILITY,BROWSER_ENGINE_CAPABILITY,PROMPT_SURFACE_CHAT_CAPABILITY,RPC_CLIENT_CAPABILITIES_ENV,parseClientCapabilities,DEFAULT_CUSTOM_EXTENSION_LABEL,buildCustomUnsupportedRequest,hostInstanceId,hostGeneration,hostLaunchProfile,socketHostLaunchProfile,protocolIdentity};
|