@code-yeongyu/senpi 2026.10.10-5 → 2026.10.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 +22 -0
- package/dist/bundle/chunks/{anthropic-messages-OE3QBX4P.js → anthropic-messages-DUULMQBF.js} +1 -1
- package/dist/bundle/chunks/{app-server-command-6FTH3TK6.js → app-server-command-GJ5NXQSY.js} +1 -1
- package/dist/bundle/chunks/{chunk-2UUTN6QT.js → chunk-C7LEZMWS.js} +97 -97
- package/dist/bundle/chunks/{chunk-ZSYRWQWG.js → chunk-JMI5OCIJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-ODYQK5XJ.js → chunk-TALS4X7S.js} +1 -1
- package/dist/bundle/chunks/{chunk-2PUAD5TD.js → chunk-VGYIPYOK.js} +1 -1
- package/dist/bundle/chunks/{chunk-4TK7VCZZ.js → chunk-WKMUAG23.js} +1 -1
- package/dist/bundle/chunks/{chunk-TIMLYJMK.js → chunk-YV2XQFXG.js} +1 -1
- package/dist/bundle/chunks/{chunk-TZVDXVHB.js → chunk-ZYHIRFGT.js} +1 -1
- package/dist/bundle/chunks/{cli-main-UYTUBG64.js → cli-main-SZ54SQLA.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-EHRZEBRO.js → help-fast-path-W5W3FYYM.js} +1 -1
- package/dist/bundle/chunks/{host-command-TOVYSHYH.js → host-command-YG2FIDPN.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-KJIOCRS7.js → interactive-mode-SCNDQ4R6.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-TEFLJTTA.js → multi-session-host-ZDZMPNP5.js} +4 -3
- package/dist/bundle/chunks/{package-manager-cli-QSJSIHUR.js → package-manager-cli-4LMYHHKH.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-C5BM7TMM.js → rotation-stream-E3XKSST7.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-RUKK7VBF.js → rpc-mode-ZFNFREN5.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-GZDV2FOZ.js → session-control-endpoint-MVSYBGM3.js} +1 -1
- package/dist/bundle/chunks/{session-picker-6D4LC3KR.js → session-picker-TNXWY5QY.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +158 -157
- 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/extensions/builtin/anthropic-subscription/auth-lane.js +3 -2
- package/dist/core/extensions/builtin/anthropic-subscription/options.js +11 -2
- package/dist/core/extensions/builtin/anthropic-subscription/session-reattach.js +4 -1
- package/dist/core/extensions/builtin/anthropic-subscription/session-sync.js +3 -0
- package/dist/core/extensions/builtin/anthropic-subscription/settings.d.ts +5 -0
- package/dist/core/extensions/builtin/anthropic-subscription/settings.js +11 -0
- package/dist/core/extensions/builtin/anthropic-subscription/stream.d.ts +1 -0
- package/dist/core/extensions/builtin/anthropic-subscription/stream.js +17 -1
- package/dist/core/extensions/builtin/compaction/lane-policy.d.ts +20 -12
- package/dist/core/extensions/builtin/compaction/lane-policy.js +39 -34
- package/dist/core/extensions/builtin/mcp/auth/catalog-identity.d.ts +4 -0
- package/dist/core/extensions/builtin/mcp/auth/catalog-identity.js +41 -0
- package/dist/core/extensions/builtin/mcp/auth/legacy-lock.d.ts +3 -0
- package/dist/core/extensions/builtin/mcp/auth/legacy-lock.js +39 -0
- package/dist/core/extensions/builtin/mcp/auth/legacy-migration.d.ts +14 -0
- package/dist/core/extensions/builtin/mcp/auth/legacy-migration.js +91 -0
- package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.js +1 -1
- package/dist/core/extensions/builtin/mcp/auth/token-store.d.ts +1 -0
- package/dist/core/extensions/builtin/mcp/auth/token-store.js +19 -90
- package/dist/core/extensions/builtin/mcp/catalog-cache.d.ts +7 -3
- package/dist/core/extensions/builtin/mcp/catalog-cache.js +61 -12
- package/dist/core/extensions/builtin/mcp/catalog.d.ts +3 -1
- package/dist/core/extensions/builtin/mcp/catalog.js +2 -0
- package/dist/core/extensions/builtin/mcp/connection-types.d.ts +2 -0
- package/dist/core/extensions/builtin/mcp/expose/call.d.ts +12 -0
- package/dist/core/extensions/builtin/mcp/expose/call.js +60 -0
- package/dist/core/extensions/builtin/mcp/expose/proxy.js +16 -3
- package/dist/core/extensions/builtin/mcp/expose/register.d.ts +3 -1
- package/dist/core/extensions/builtin/mcp/expose/register.js +41 -50
- package/dist/core/extensions/builtin/mcp/expose/session.d.ts +4 -0
- package/dist/core/extensions/builtin/mcp/expose/session.js +5 -1
- package/dist/core/extensions/builtin/mcp/expose/tier-b.js +15 -4
- package/dist/core/extensions/builtin/mcp/index.js +5 -26
- package/dist/core/extensions/builtin/mcp/invocation.d.ts +42 -0
- package/dist/core/extensions/builtin/mcp/invocation.js +118 -0
- package/dist/core/extensions/builtin/mcp/service-connection.d.ts +3 -0
- package/dist/core/extensions/builtin/mcp/service-connection.js +6 -0
- package/dist/core/extensions/builtin/mcp/service-register.d.ts +4 -0
- package/dist/core/extensions/builtin/mcp/service-register.js +45 -1
- package/dist/core/extensions/builtin/mcp/service-tools-changed.js +26 -3
- package/dist/core/extensions/builtin/mcp/service-types.d.ts +6 -0
- package/dist/core/extensions/builtin/mcp/service.js +83 -49
- package/dist/core/extensions/builtin/mcp/shared-connection.js +17 -4
- package/dist/core/extensions/builtin/mcp/sharing-policy.js +1 -0
- package/dist/core/extensions/builtin/mcp/startup-race.js +47 -9
- package/dist/core/extensions/builtin/permission-system/dispatch-metadata.d.ts +8 -0
- package/dist/core/extensions/builtin/permission-system/dispatch-metadata.js +23 -0
- package/dist/core/extensions/builtin/permission-system/dispatch-policy.d.ts +14 -0
- package/dist/core/extensions/builtin/permission-system/dispatch-policy.js +53 -0
- package/dist/core/extensions/builtin/permission-system/dispatch.d.ts +33 -0
- package/dist/core/extensions/builtin/permission-system/dispatch.js +106 -0
- package/dist/core/extensions/builtin/permission-system/index.js +25 -4
- package/dist/core/extensions/builtin/permission-system/prompt.d.ts +1 -1
- package/dist/core/extensions/builtin/permission-system/prompt.js +3 -1
- package/dist/core/extensions/builtin/permission-system/service.d.ts +11 -1
- package/dist/core/extensions/builtin/permission-system/service.js +14 -4
- package/dist/core/extensions/builtin/ttsr/detectors/collapse-near-duplicates.d.ts +1 -0
- package/dist/core/extensions/builtin/ttsr/detectors/collapse-near-duplicates.js +24 -5
- package/dist/modes/rpc/rpc-client.js +14 -2
- package/dist/modes/rpc/rpc-request-deadline.d.ts +8 -0
- package/dist/modes/rpc/rpc-request-deadline.js +10 -0
- package/dist/modes/rpc/rpc-types.d.ts +7 -4
- package/dist/modes/rpc/session-binding.js +15 -1
- package/dist/modes/rpc/session-command-router.d.ts +1 -0
- package/dist/modes/rpc/session-command-router.js +16 -0
- package/docs/providers.md +3 -2
- package/docs/rpc.md +12 -6
- package/package.json +6 -6
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { DeniedError } from "./types.js";
|
|
2
|
+
const authorizers = new WeakMap();
|
|
3
|
+
// AgentSession passes this validated object unchanged from preflight to execution.
|
|
4
|
+
// Identical calls, including calls with duplicate provider IDs, get distinct objects.
|
|
5
|
+
const approvals = new WeakMap();
|
|
6
|
+
/** Retirement cannot remove the authorizer installed by a newer extension load. */
|
|
7
|
+
export function registerDispatchAuthorizer(session, authorizer) {
|
|
8
|
+
const previous = authorizers.get(session);
|
|
9
|
+
const registration = { authorizer, retired: new AbortController() };
|
|
10
|
+
authorizers.set(session, registration);
|
|
11
|
+
previous?.retired.abort(new Error("Permission authorizer was retired"));
|
|
12
|
+
return () => {
|
|
13
|
+
// Retain the retired registration so required authority cannot become
|
|
14
|
+
// indistinguishable from a session where enforcement was never loaded.
|
|
15
|
+
registration.retired.abort(new Error("Permission authorizer was retired"));
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function hasDispatchAuthorizer(session) {
|
|
19
|
+
return authorizers.has(session);
|
|
20
|
+
}
|
|
21
|
+
/** Keep the policy and operation that were actually presented, not their post-reply replacements. */
|
|
22
|
+
export function prepareDispatchApproval(session, request, policy) {
|
|
23
|
+
const registration = authorizers.get(session);
|
|
24
|
+
if (!registration)
|
|
25
|
+
throw new Error("Permission authorizer is unavailable");
|
|
26
|
+
registration.retired.signal.throwIfAborted();
|
|
27
|
+
const approval = {
|
|
28
|
+
registration,
|
|
29
|
+
toolCallId: request.toolCallId,
|
|
30
|
+
toolName: request.toolName,
|
|
31
|
+
input: JSON.stringify(request.input),
|
|
32
|
+
identity: { ...request.identity },
|
|
33
|
+
policy: { ...policy },
|
|
34
|
+
};
|
|
35
|
+
return () => {
|
|
36
|
+
registration.retired.signal.throwIfAborted();
|
|
37
|
+
if (authorizers.get(session) !== registration)
|
|
38
|
+
throw new Error("Permission authorizer was retired");
|
|
39
|
+
approvals.set(request.input, approval);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function forgetDispatchApproval(input) {
|
|
43
|
+
approvals.delete(input);
|
|
44
|
+
}
|
|
45
|
+
function matches(approval, registration, request, policy) {
|
|
46
|
+
return (approval?.registration === registration &&
|
|
47
|
+
approval.toolCallId === request.toolCallId &&
|
|
48
|
+
approval.toolName === request.toolName &&
|
|
49
|
+
approval.input === JSON.stringify(request.input) &&
|
|
50
|
+
approval.identity.owner === request.identity.owner &&
|
|
51
|
+
approval.identity.metadata === request.identity.metadata &&
|
|
52
|
+
approval.policy.action === policy.action &&
|
|
53
|
+
approval.policy.fingerprint === policy.fingerprint);
|
|
54
|
+
}
|
|
55
|
+
/** Await only this caller's approval; aborting it does not retire another caller's authority. */
|
|
56
|
+
async function awaitApproval(pending, signal) {
|
|
57
|
+
if (!signal)
|
|
58
|
+
return pending;
|
|
59
|
+
let onAbort = () => { };
|
|
60
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
61
|
+
onAbort = () => reject(signal.reason);
|
|
62
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
63
|
+
if (signal.aborted)
|
|
64
|
+
onAbort();
|
|
65
|
+
});
|
|
66
|
+
try {
|
|
67
|
+
await Promise.race([pending, aborted]);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
signal.removeEventListener("abort", onAbort);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve live permission after MCP readiness. The returned synchronous fence must
|
|
75
|
+
* pass immediately before dispatch; a false result requires preparation again.
|
|
76
|
+
*/
|
|
77
|
+
export async function authorizeToolDispatch(session, request, ctx, signal) {
|
|
78
|
+
signal?.throwIfAborted();
|
|
79
|
+
const registration = authorizers.get(session);
|
|
80
|
+
if (!registration)
|
|
81
|
+
throw new Error("Permission authorizer is unavailable");
|
|
82
|
+
registration.retired.signal.throwIfAborted();
|
|
83
|
+
const policy = registration.authorizer.policy(request, ctx);
|
|
84
|
+
if (policy.action === "deny")
|
|
85
|
+
throw new DeniedError(["*"]);
|
|
86
|
+
if (policy.action === "ask" && !matches(approvals.get(request.input), registration, request, policy)) {
|
|
87
|
+
const approve = prepareDispatchApproval(session, request, policy);
|
|
88
|
+
const waitSignal = signal === undefined ? registration.retired.signal : AbortSignal.any([signal, registration.retired.signal]);
|
|
89
|
+
waitSignal.throwIfAborted();
|
|
90
|
+
await awaitApproval(registration.authorizer.ask(request, ctx, waitSignal), waitSignal);
|
|
91
|
+
signal?.throwIfAborted();
|
|
92
|
+
approve();
|
|
93
|
+
}
|
|
94
|
+
const approvedInput = JSON.stringify(request.input);
|
|
95
|
+
return () => {
|
|
96
|
+
if (signal?.aborted || registration.retired.signal.aborted || authorizers.get(session) !== registration)
|
|
97
|
+
return false;
|
|
98
|
+
if (JSON.stringify(request.input) !== approvedInput)
|
|
99
|
+
return false;
|
|
100
|
+
const current = registration.authorizer.policy(request, ctx);
|
|
101
|
+
if (current.action === "deny")
|
|
102
|
+
return false;
|
|
103
|
+
return current.action === "allow" || matches(approvals.get(request.input), registration, request, current);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=dispatch.js.map
|
|
@@ -3,6 +3,9 @@ import { extractPatchedPaths } from "../gpt-apply-patch/index.js";
|
|
|
3
3
|
import { decideAuto } from "./auto-policy.js";
|
|
4
4
|
import { PERMISSION_PRESET_NAMES, parsePermissionFlag, parsePermissionPresetFlag } from "./cli.js";
|
|
5
5
|
import { disabled } from "./config.js";
|
|
6
|
+
import { forgetDispatchApproval, registerDispatchAuthorizer } from "./dispatch.js";
|
|
7
|
+
import { prepareMcpDispatchApproval } from "./dispatch-metadata.js";
|
|
8
|
+
import { createDispatchAuthorizer } from "./dispatch-policy.js";
|
|
6
9
|
import { createEventEmitter } from "./events.js";
|
|
7
10
|
import { INTERNAL_PERMISSION_TOOLS } from "./internal-tools.js";
|
|
8
11
|
import { handleNoUI } from "./non-interactive.js";
|
|
@@ -54,6 +57,8 @@ export default function permissionSystemExtension(pi) {
|
|
|
54
57
|
let setupError = null;
|
|
55
58
|
// The `permission-preset` flag value the rules were last loaded from; absent until session_start.
|
|
56
59
|
let loadedPresetFlag;
|
|
60
|
+
let loadedPermissionFlag;
|
|
61
|
+
const retireAuthorizers = new WeakMap();
|
|
57
62
|
const nextRequestID = createRequestIDFactory();
|
|
58
63
|
pi.registerFlag("permission", {
|
|
59
64
|
description: "Set permission rules (format: tool=action or tool:pattern=action)",
|
|
@@ -74,12 +79,15 @@ export default function permissionSystemExtension(pi) {
|
|
|
74
79
|
setupError = getReason(error);
|
|
75
80
|
throw error;
|
|
76
81
|
}
|
|
82
|
+
retireAuthorizers.get(ctx.sessionManager)?.();
|
|
83
|
+
retireAuthorizers.set(ctx.sessionManager, registerDispatchAuthorizer(ctx.sessionManager, dispatchAuthorizer));
|
|
77
84
|
applyToolDenials();
|
|
78
85
|
});
|
|
79
86
|
// `sessionApproved` carries this session's "Always" answers, not yet written to disk, across a reload.
|
|
80
87
|
const loadPermissionRules = (cwd, sessionApproved = []) => {
|
|
81
88
|
const settingsManager = SettingsManager.create(cwd);
|
|
82
89
|
const permissionFlag = pi.getFlag("permission");
|
|
90
|
+
loadedPermissionFlag = permissionFlag;
|
|
83
91
|
const permissionPresetFlag = pi.getFlag("permission-preset");
|
|
84
92
|
loadedPresetFlag = { value: permissionPresetFlag };
|
|
85
93
|
cliRuleset = typeof permissionFlag === "string" ? parsePermissionFlag(permissionFlag) : [];
|
|
@@ -107,8 +115,11 @@ export default function permissionSystemExtension(pi) {
|
|
|
107
115
|
// A host moves a live session to another preset (an attach naming one) by changing the flag;
|
|
108
116
|
// the rules follow from the next tool call on, and a failed reload refuses calls like session_start.
|
|
109
117
|
const followPresetFlag = (cwd) => {
|
|
110
|
-
if (loadedPresetFlag === undefined ||
|
|
118
|
+
if (loadedPresetFlag === undefined ||
|
|
119
|
+
(loadedPresetFlag.value === pi.getFlag("permission-preset") &&
|
|
120
|
+
loadedPermissionFlag === pi.getFlag("permission"))) {
|
|
111
121
|
return;
|
|
122
|
+
}
|
|
112
123
|
const sessionApproved = service ? service.getApproved().slice(initialApprovedCount) : [];
|
|
113
124
|
setupError = null;
|
|
114
125
|
try {
|
|
@@ -120,7 +131,11 @@ export default function permissionSystemExtension(pi) {
|
|
|
120
131
|
}
|
|
121
132
|
applyToolDenials(true);
|
|
122
133
|
};
|
|
123
|
-
|
|
134
|
+
const dispatchAuthorizer = createDispatchAuthorizer(pi, (ctx) => {
|
|
135
|
+
followPresetFlag(ctx.cwd);
|
|
136
|
+
return { service, parsers: parserRegistry, preset: activePreset, setupError };
|
|
137
|
+
}, (event, ctx, signal) => authorizeToolCall(event, ctx, signal));
|
|
138
|
+
const authorizeToolCall = async (event, ctx, signal) => {
|
|
124
139
|
followPresetFlag(ctx.cwd);
|
|
125
140
|
if (setupError !== null) {
|
|
126
141
|
return { block: true, reason: `Permission setup failed: ${setupError}` };
|
|
@@ -143,6 +158,7 @@ export default function permissionSystemExtension(pi) {
|
|
|
143
158
|
return undefined;
|
|
144
159
|
}
|
|
145
160
|
const sessionID = ctx.sessionManager.getSessionId();
|
|
161
|
+
const approve = prepareMcpDispatchApproval(pi, event, ctx, dispatchAuthorizer);
|
|
146
162
|
for (const permissionRequest of permissionRequests) {
|
|
147
163
|
const request = {
|
|
148
164
|
id: nextRequestID(),
|
|
@@ -176,7 +192,7 @@ export default function permissionSystemExtension(pi) {
|
|
|
176
192
|
continue;
|
|
177
193
|
}
|
|
178
194
|
if (ctx.hasUI) {
|
|
179
|
-
const reply = await showPermissionPrompt(ctx, request);
|
|
195
|
+
const reply = await showPermissionPrompt(ctx, request, signal);
|
|
180
196
|
activeService.reply(reply);
|
|
181
197
|
}
|
|
182
198
|
else {
|
|
@@ -194,10 +210,15 @@ export default function permissionSystemExtension(pi) {
|
|
|
194
210
|
return { block: true, reason: getReason(askResult.error) };
|
|
195
211
|
}
|
|
196
212
|
}
|
|
213
|
+
approve?.();
|
|
197
214
|
return undefined;
|
|
198
|
-
}
|
|
215
|
+
};
|
|
216
|
+
pi.on("tool_call", authorizeToolCall);
|
|
217
|
+
pi.on("tool_result", (event) => forgetDispatchApproval(event.input));
|
|
199
218
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
200
219
|
void event;
|
|
220
|
+
retireAuthorizers.get(ctx.sessionManager)?.();
|
|
221
|
+
retireAuthorizers.delete(ctx.sessionManager);
|
|
201
222
|
if (!service) {
|
|
202
223
|
return;
|
|
203
224
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { ExtensionContext } from "../../types.ts";
|
|
2
2
|
import type { ReplyInput, Request } from "./types.ts";
|
|
3
|
-
export declare function showPermissionPrompt(ctx: ExtensionContext, request: Request): Promise<ReplyInput>;
|
|
3
|
+
export declare function showPermissionPrompt(ctx: ExtensionContext, request: Request, signal?: AbortSignal): Promise<ReplyInput>;
|
|
4
4
|
//# sourceMappingURL=prompt.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export async function showPermissionPrompt(ctx, request) {
|
|
1
|
+
export async function showPermissionPrompt(ctx, request, signal) {
|
|
2
2
|
const title = `Permission required: ${request.permission}`;
|
|
3
3
|
const message = formatRequestForDisplay(request);
|
|
4
4
|
const displayTitle = `${title}\n\n${message}`;
|
|
@@ -11,6 +11,8 @@ export async function showPermissionPrompt(ctx, request) {
|
|
|
11
11
|
...(request.tool.parentCallID === undefined ? {} : { parentToolCallId: request.tool.parentCallID }),
|
|
12
12
|
}
|
|
13
13
|
: {};
|
|
14
|
+
if (signal !== undefined)
|
|
15
|
+
call.signal = signal;
|
|
14
16
|
const choice = await ctx.ui.select(displayTitle, options, call);
|
|
15
17
|
if (choice === "Deny with feedback") {
|
|
16
18
|
const feedback = await ctx.ui.input("Feedback", "Why are you denying this permission? (optional)", call);
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { type PermissionEventEmitter } from "./events.ts";
|
|
2
|
-
import { type ReplyInput, type Request, type Ruleset } from "./types.ts";
|
|
2
|
+
import { type ReplyInput, type Request, type Rule, type Ruleset } from "./types.ts";
|
|
3
3
|
type RequestInput = Omit<Request, "id"> & {
|
|
4
4
|
id?: string;
|
|
5
5
|
};
|
|
6
|
+
interface DecisionOptions {
|
|
7
|
+
readonly approveBlanketAsk?: boolean;
|
|
8
|
+
readonly presetBound?: boolean;
|
|
9
|
+
readonly ruleAliases?: readonly string[] | undefined;
|
|
10
|
+
}
|
|
6
11
|
/** Core service for managing permission requests and rule evaluation */
|
|
7
12
|
export declare class PermissionService {
|
|
8
13
|
private pending;
|
|
@@ -24,6 +29,11 @@ export declare class PermissionService {
|
|
|
24
29
|
* explicit consent for that pattern) can then turn a remaining ask into allow.
|
|
25
30
|
*/
|
|
26
31
|
private decide;
|
|
32
|
+
/** Share the decision and its relevant rules with the final dispatch fence. */
|
|
33
|
+
dispatchDecision(permission: string, target: string | readonly string[], options?: DecisionOptions): {
|
|
34
|
+
readonly action: Rule["action"];
|
|
35
|
+
readonly rules: readonly Rule[];
|
|
36
|
+
};
|
|
27
37
|
/** Request permission for a tool call. Resolves if allowed, throws on denial. */
|
|
28
38
|
ask(request: RequestInput, { autoApproveAsk, approveBlanketAsk, presetBound, ruleAliases, }?: {
|
|
29
39
|
readonly autoApproveAsk?: boolean;
|
|
@@ -24,8 +24,14 @@ export class PermissionService {
|
|
|
24
24
|
* explicit consent for that pattern) can then turn a remaining ask into allow.
|
|
25
25
|
*/
|
|
26
26
|
decide(permission, target, options) {
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
return this.dispatchDecision(permission, target, options).action;
|
|
28
|
+
}
|
|
29
|
+
/** Share the decision and its relevant rules with the final dispatch fence. */
|
|
30
|
+
dispatchDecision(permission, target, options = {}) {
|
|
31
|
+
if (!options.presetBound) {
|
|
32
|
+
const rule = evaluate(permission, target, this.staticRuleset, this.approved);
|
|
33
|
+
return { action: rule.action, rules: [{ ...rule }] };
|
|
34
|
+
}
|
|
29
35
|
const presetRules = this.staticRuleset.filter((rule) => isPresetRule(rule));
|
|
30
36
|
const userRules = this.staticRuleset.filter((rule) => !isPresetRule(rule));
|
|
31
37
|
const presetRule = evaluate(permission, target, presetRules);
|
|
@@ -35,10 +41,14 @@ export class PermissionService {
|
|
|
35
41
|
const combined = user === undefined || PermissionService.RESTRICTIVENESS[preset] >= PermissionService.RESTRICTIVENESS[user]
|
|
36
42
|
? preset
|
|
37
43
|
: user;
|
|
44
|
+
const rules = [presetRule, ...(user === undefined ? [] : [userRule])].map((rule) => ({ ...rule }));
|
|
38
45
|
if (combined !== "ask")
|
|
39
|
-
return combined;
|
|
46
|
+
return { action: combined, rules };
|
|
40
47
|
const remembered = evaluate(permission, target, this.approved);
|
|
41
|
-
|
|
48
|
+
if (this.approved.includes(remembered) && remembered.action === "allow") {
|
|
49
|
+
return { action: "allow", rules: [...rules, { ...remembered }] };
|
|
50
|
+
}
|
|
51
|
+
return { action: "ask", rules };
|
|
42
52
|
}
|
|
43
53
|
/** Request permission for a tool call. Resolves if allowed, throws on denial. */
|
|
44
54
|
async ask(request, { autoApproveAsk = false, approveBlanketAsk = false, presetBound = false, ruleAliases, } = {}) {
|
|
@@ -11,6 +11,10 @@ export const NEAR_DUPLICATE_TEXT_RETENTION_MAX = 512;
|
|
|
11
11
|
const SAMPLE_LENGTH = 80;
|
|
12
12
|
const WORD_PATTERN = /[\p{L}\p{N}#]+/gu;
|
|
13
13
|
const FENCE_PATTERN = /^(?:```|~~~)/;
|
|
14
|
+
// An indented line that is not a nested list item (two spaces or a tab, then not `- `, `* `, `+ `, `1. `, `1) `).
|
|
15
|
+
const INDENTED_CODE = /^(?: {2,}|\t)(?![ \t]*(?:[-*+]|\d+[.)])[ \t])/;
|
|
16
|
+
// A line ending in a block, statement or markup delimiter: `{`, `}`, `[`, `]`, `(`, `;`, `,`, `>` (oh-my-pi's set).
|
|
17
|
+
const CODE_LINE_ENDINGS = new Set(["{", "}", "[", "]", "(", ";", ",", ">"]);
|
|
14
18
|
export function createNearDuplicateState() {
|
|
15
19
|
return {
|
|
16
20
|
history: new FixedRing(NEAR_DUPLICATE_LOOKBACK),
|
|
@@ -20,6 +24,7 @@ export function createNearDuplicateState() {
|
|
|
20
24
|
lineHasContent: false,
|
|
21
25
|
lineStartOffset: 0,
|
|
22
26
|
lineText: "",
|
|
27
|
+
lineLastChar: "",
|
|
23
28
|
length: 0,
|
|
24
29
|
wordChars: 0,
|
|
25
30
|
startOffset: 0,
|
|
@@ -113,17 +118,28 @@ function completeParagraph(state) {
|
|
|
113
118
|
},
|
|
114
119
|
};
|
|
115
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Code and markup repeat one skeleton with different literals, so a run of same-shaped lines (SVG elements, JSON
|
|
123
|
+
* objects) is not a narration loop. Lines shaped like code are left out of the paragraph, fenced or not, and a
|
|
124
|
+
* paragraph made only of them is never scored (senpi#2865; oh-my-pi v18.8.0 drops the same line shapes before its loop heuristics).
|
|
125
|
+
*/
|
|
126
|
+
function isCodeShapedLine(state) {
|
|
127
|
+
return INDENTED_CODE.test(state.lineText) || CODE_LINE_ENDINGS.has(state.lineLastChar);
|
|
128
|
+
}
|
|
116
129
|
function foldLine(state) {
|
|
130
|
+
const fence = FENCE_PATTERN.test(state.lineText.trimStart());
|
|
131
|
+
if (state.insideFence || fence)
|
|
132
|
+
state.fenced = true;
|
|
133
|
+
if (fence)
|
|
134
|
+
state.insideFence = !state.insideFence;
|
|
135
|
+
if (!state.fenced && isCodeShapedLine(state))
|
|
136
|
+
return;
|
|
117
137
|
if (state.length === 0)
|
|
118
138
|
state.startOffset = state.lineStartOffset;
|
|
119
139
|
state.length += state.lineLength + 1;
|
|
120
140
|
state.wordChars += state.lineWordChars;
|
|
121
|
-
if (state.insideFence || FENCE_PATTERN.test(state.lineText.trimStart()))
|
|
122
|
-
state.fenced = true;
|
|
123
141
|
if (state.retained.length < NEAR_DUPLICATE_TEXT_RETENTION_MAX)
|
|
124
142
|
state.retained += `${state.lineText}\n`;
|
|
125
|
-
if (FENCE_PATTERN.test(state.lineText.trimStart()))
|
|
126
|
-
state.insideFence = !state.insideFence;
|
|
127
143
|
}
|
|
128
144
|
function resetLine(state, startOffset) {
|
|
129
145
|
state.lineLength = 0;
|
|
@@ -131,6 +147,7 @@ function resetLine(state, startOffset) {
|
|
|
131
147
|
state.lineHasContent = false;
|
|
132
148
|
state.lineStartOffset = startOffset;
|
|
133
149
|
state.lineText = "";
|
|
150
|
+
state.lineLastChar = "";
|
|
134
151
|
}
|
|
135
152
|
export function updateNearDuplicates(state, entry) {
|
|
136
153
|
if (entry.value.charCodeAt(0) === CharCode.LineFeed) {
|
|
@@ -143,8 +160,10 @@ export function updateNearDuplicates(state, entry) {
|
|
|
143
160
|
return result;
|
|
144
161
|
}
|
|
145
162
|
const codePoint = entry.value.codePointAt(0) ?? 0;
|
|
146
|
-
if (!isAsciiWhitespace(codePoint))
|
|
163
|
+
if (!isAsciiWhitespace(codePoint)) {
|
|
147
164
|
state.lineHasContent = true;
|
|
165
|
+
state.lineLastChar = entry.value;
|
|
166
|
+
}
|
|
148
167
|
if (isAsciiAlphanumeric(codePoint) || (codePoint > 0x7f && !isBoxDrawing(codePoint)))
|
|
149
168
|
state.lineWordChars += 1;
|
|
150
169
|
state.lineLength += entry.width;
|
|
@@ -18,7 +18,7 @@ export class RpcCommandError extends Error {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js";
|
|
21
|
-
import { armRequestDeadline, OPEN_AFTER_QUEUED_DEADLINE_MS, openStalledMessage, PROMPT_ACK_MAX_WAIT_MS, PROMPT_COMPACTION_DEADLINE_MS, REQUEST_DEADLINE_MS, } from "./rpc-request-deadline.js";
|
|
21
|
+
import { armRequestDeadline, OPEN_AFTER_QUEUED_DEADLINE_MS, openStalledMessage, PROMPT_ACK_MAX_WAIT_MS, PROMPT_AFTER_QUEUED_DEADLINE_MS, PROMPT_COMPACTION_DEADLINE_MS, promptStalledMessage, REQUEST_DEADLINE_MS, } from "./rpc-request-deadline.js";
|
|
22
22
|
import { RPC_ERROR_UNKNOWN_COMMAND } from "./rpc-types.js";
|
|
23
23
|
import { readSocketSecret, resolveSocketTransportAddress, sendSocketHandshake, socketSecretPath, } from "./socket-transport.js";
|
|
24
24
|
import { socketNeedsHandshake } from "./tui-socket.js";
|
|
@@ -960,6 +960,7 @@ export class RpcClient {
|
|
|
960
960
|
return new Promise((resolve, reject) => {
|
|
961
961
|
const timeoutMessage = () => `Timeout waiting for response to ${command.type}. Stderr: ${this.stderr}`;
|
|
962
962
|
let promptCap;
|
|
963
|
+
let promptReceived = false;
|
|
963
964
|
const expire = (timeoutError) => {
|
|
964
965
|
clearTimeout(promptCap);
|
|
965
966
|
const pending = this.pendingRequests.get(id);
|
|
@@ -991,7 +992,18 @@ export class RpcClient {
|
|
|
991
992
|
},
|
|
992
993
|
...(command.type === "prompt"
|
|
993
994
|
? {
|
|
994
|
-
onCompaction: (compacting) =>
|
|
995
|
+
onCompaction: (compacting) => compacting
|
|
996
|
+
? deadline.extend(PROMPT_COMPACTION_DEADLINE_MS, timeoutMessage)
|
|
997
|
+
: promptReceived
|
|
998
|
+
? deadline.extend(PROMPT_AFTER_QUEUED_DEADLINE_MS, promptStalledMessage)
|
|
999
|
+
: deadline.extend(REQUEST_DEADLINE_MS, timeoutMessage),
|
|
1000
|
+
// The host received the prompt: its preflight may outlive the plain request deadline (senpi#2871).
|
|
1001
|
+
// A compaction's own wait is longer: the acknowledgement must never shorten it.
|
|
1002
|
+
onQueued: () => {
|
|
1003
|
+
promptReceived = true;
|
|
1004
|
+
if (!this.isCompacting())
|
|
1005
|
+
deadline.extend(PROMPT_AFTER_QUEUED_DEADLINE_MS, promptStalledMessage);
|
|
1006
|
+
},
|
|
995
1007
|
}
|
|
996
1008
|
: {}),
|
|
997
1009
|
...(command.type === "open_session"
|
|
@@ -14,6 +14,14 @@ export declare const PROMPT_ACK_MAX_WAIT_MS: number;
|
|
|
14
14
|
* (senpi#2209).
|
|
15
15
|
*/
|
|
16
16
|
export declare const OPEN_AFTER_QUEUED_DEADLINE_MS: number;
|
|
17
|
+
/**
|
|
18
|
+
* How long a prompt the host acknowledged as received may take to be accepted. Its preflight runs on the
|
|
19
|
+
* session's loop, which on a starved shared host outlived the 30 s request deadline: the client gave up,
|
|
20
|
+
* discarded the session, and the host answered into its closed scope (senpi#2871). A lost transport still
|
|
21
|
+
* rejects at once; this only bounds a host that received the prompt and then went silent.
|
|
22
|
+
*/
|
|
23
|
+
export declare const PROMPT_AFTER_QUEUED_DEADLINE_MS: number;
|
|
24
|
+
export declare function promptStalledMessage(): string;
|
|
17
25
|
export declare function openStalledMessage(position: unknown): string;
|
|
18
26
|
export interface RequestDeadline {
|
|
19
27
|
/** Restarts the wait with a new budget and the message it fails with. */
|
|
@@ -15,6 +15,16 @@ export const PROMPT_ACK_MAX_WAIT_MS = PROMPT_COMPACTION_DEADLINE_MS + REQUEST_DE
|
|
|
15
15
|
* (senpi#2209).
|
|
16
16
|
*/
|
|
17
17
|
export const OPEN_AFTER_QUEUED_DEADLINE_MS = 10 * 60_000;
|
|
18
|
+
/**
|
|
19
|
+
* How long a prompt the host acknowledged as received may take to be accepted. Its preflight runs on the
|
|
20
|
+
* session's loop, which on a starved shared host outlived the 30 s request deadline: the client gave up,
|
|
21
|
+
* discarded the session, and the host answered into its closed scope (senpi#2871). A lost transport still
|
|
22
|
+
* rejects at once; this only bounds a host that received the prompt and then went silent.
|
|
23
|
+
*/
|
|
24
|
+
export const PROMPT_AFTER_QUEUED_DEADLINE_MS = 5 * 60_000;
|
|
25
|
+
export function promptStalledMessage() {
|
|
26
|
+
return `prompt was received by the host but not accepted within ${PROMPT_AFTER_QUEUED_DEADLINE_MS / 60_000} minutes`;
|
|
27
|
+
}
|
|
18
28
|
export function openStalledMessage(position) {
|
|
19
29
|
const queued = typeof position === "number" ? ` at queue position ${position}` : "";
|
|
20
30
|
const minutes = OPEN_AFTER_QUEUED_DEADLINE_MS / 60_000;
|
|
@@ -1595,7 +1595,10 @@ export interface RpcAuthAccountsChangedEvent {
|
|
|
1595
1595
|
/**
|
|
1596
1596
|
* Sent to ONE opener the moment its `open_session` is accepted, before the open enters the
|
|
1597
1597
|
* session loop. The in-process host serves opens one at a time, so a burst queues; without this
|
|
1598
|
-
* the client's only signal is a deadline it cannot explain (senpi#1844).
|
|
1598
|
+
* the client's only signal is a deadline it cannot explain (senpi#1844). A multi-session host sends
|
|
1599
|
+
* the same record for a session-routed `prompt` the moment it is received, before its preflight runs
|
|
1600
|
+
* on the session's loop, so a client waits out a slow preflight instead of discarding the session at
|
|
1601
|
+
* its plain request deadline (senpi#2871).
|
|
1599
1602
|
*
|
|
1600
1603
|
* `for_request` carries the opener's request id deliberately, NOT the response-id field: a client
|
|
1601
1604
|
* settles pending requests by response id, and a queued record wearing the open's id would be
|
|
@@ -1603,11 +1606,11 @@ export interface RpcAuthAccountsChangedEvent {
|
|
|
1603
1606
|
*/
|
|
1604
1607
|
export interface RpcOpenQueuedEvent {
|
|
1605
1608
|
type: "queued";
|
|
1606
|
-
/** The `open_session` request this position belongs to. */
|
|
1609
|
+
/** The `open_session` or `prompt` request this position belongs to. */
|
|
1607
1610
|
for_request: string;
|
|
1608
|
-
/** 1-based place in the open queue at
|
|
1611
|
+
/** 1-based place in the open queue at acceptance; for a `prompt`, its place among that session's requests in flight. */
|
|
1609
1612
|
position: number;
|
|
1610
|
-
/** Opens already in flight when this one arrived; `position` is this plus one. */
|
|
1613
|
+
/** Opens (for a `prompt`, that session's requests) already in flight when this one arrived; `position` is this plus one. */
|
|
1611
1614
|
in_flight: number;
|
|
1612
1615
|
}
|
|
1613
1616
|
/** Emitted when the SDK failover engine advances to a different account slot. */
|
|
@@ -42,9 +42,23 @@ export async function createRpcSessionBinding(sessionId, entry, writer, requestC
|
|
|
42
42
|
return typeof value === "function" ? value.bind(runtime) : value;
|
|
43
43
|
},
|
|
44
44
|
});
|
|
45
|
+
// A reply can be written after its session closed: a prompt's preflight that outlived the client's
|
|
46
|
+
// deadline answers once the client already discarded the session. That record has no reader left, so it
|
|
47
|
+
// is dropped with one line naming the session instead of throwing "Provider scope is closed" out of
|
|
48
|
+
// an unawaited prompt (senpi#2871).
|
|
49
|
+
let droppedAfterClose = false;
|
|
45
50
|
const handler = await runWithProviderScope(entry.scope, async () => {
|
|
51
|
+
const writeInScope = bindToProviderScope(enqueueRecords);
|
|
46
52
|
const taggedSink = {
|
|
47
|
-
writeRaw:
|
|
53
|
+
writeRaw: (chunk) => {
|
|
54
|
+
if (entry.scope.state === "closed") {
|
|
55
|
+
if (!droppedAfterClose)
|
|
56
|
+
process.stderr.write(`senpi rpc host: dropped a record for closed session ${sessionId} at ${new Date().toISOString()}\n`);
|
|
57
|
+
droppedAfterClose = true;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
writeInScope(chunk);
|
|
61
|
+
},
|
|
48
62
|
waitForBackpressure: bindToProviderScope(async () => { }),
|
|
49
63
|
};
|
|
50
64
|
return createRpcConnectionHandler(runtimeHost, taggedSink, {
|
|
@@ -140,6 +140,7 @@ export declare class SessionCommandRouter {
|
|
|
140
140
|
/** Drops per-connection records for a handle the host closed on its own. */
|
|
141
141
|
private forgetSessionOwnership;
|
|
142
142
|
private stopSweep;
|
|
143
|
+
private acknowledgePrompt;
|
|
143
144
|
private openWithBarrier;
|
|
144
145
|
private open;
|
|
145
146
|
/**
|
|
@@ -283,6 +283,10 @@ export class SessionCommandRouter {
|
|
|
283
283
|
const binding = this.bindings.get(command.sessionId);
|
|
284
284
|
if (!binding)
|
|
285
285
|
return error(command.id, command.type, RPC_ERROR_UNKNOWN_SESSION);
|
|
286
|
+
// A prompt's preflight runs on the session's loop and can outlive the client's request deadline on a
|
|
287
|
+
// starved host; acknowledging receipt first lets the client wait it out (senpi#2871).
|
|
288
|
+
if (command.type === "prompt")
|
|
289
|
+
this.acknowledgePrompt(command.id, command.sessionId);
|
|
286
290
|
await binding.handle(command);
|
|
287
291
|
return undefined;
|
|
288
292
|
}
|
|
@@ -438,6 +442,18 @@ export class SessionCommandRouter {
|
|
|
438
442
|
clearInterval(this.sweepTimer);
|
|
439
443
|
this.sweepTimer = undefined;
|
|
440
444
|
}
|
|
445
|
+
acknowledgePrompt(id, sessionId) {
|
|
446
|
+
const connection = this.writer.currentConnection();
|
|
447
|
+
if (id === undefined || connection === undefined)
|
|
448
|
+
return;
|
|
449
|
+
const inFlight = Math.max(0, (this.activeRequests.get(sessionId) ?? 1) - 1);
|
|
450
|
+
this.writer.sendOpenQueued(connection, {
|
|
451
|
+
type: "queued",
|
|
452
|
+
for_request: id,
|
|
453
|
+
position: inFlight + 1,
|
|
454
|
+
in_flight: inFlight,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
441
457
|
openWithBarrier(command) {
|
|
442
458
|
const owner = this.writer.currentConnection();
|
|
443
459
|
if (owner === undefined)
|
package/docs/providers.md
CHANGED
|
@@ -54,7 +54,7 @@ The `anthropic-subscription` provider routes LLM calls through the official [Cla
|
|
|
54
54
|
- Settings (`anthropicSubscriptionProvider`):
|
|
55
55
|
- `systemPromptMode` — controls how the system prompt is delivered. **`full`** (default) sends senpi's composed system prompt verbatim; the lane no longer rebuilds from the SDK `claude_code` preset, so all prompt regions (project rules, response-language instructions, etc.) reach the model. **`preset-append`** is the previous behaviour (deprecated, kept for one release; emits a one-time warning). **`override`** loads the system prompt from a file (`systemPromptFile`). The legacy `appendSystemPrompt` key still works: `false` → `preset-append`, `true`/unset → `full`; setting both keys makes `systemPromptMode` win and warns.
|
|
56
56
|
- In `full` and `override` modes, `settingSources` defaults to `[]` on every lane because senpi's prompt already carries project context — loading the SDK's own CLAUDE.md would double-inject it. The CLI always prepends its own `"You are a Claude agent, built on Anthropic's Claude Agent SDK."` block, which senpi cannot suppress; `full` means the prompt is delivered intact, not that it is the only system-prompt text.
|
|
57
|
-
- `settingSources` (filesystem settings load only in the ambient lane, so they cannot override your selected account), `strictMcpConfig`, `pinnedAccount`, `tokenInjection` (`oauth-slots` | `config-dir` | `ambient`), `resumeMode` (`auto` default | `off` restores per-turn sessions), `systemPromptFile`, `enabled` (default `false`; gates the ambient lane only).
|
|
57
|
+
- `settingSources` (filesystem settings load only in the ambient lane, so they cannot override your selected account), `strictMcpConfig`, `pinnedAccount`, `tokenInjection` (`oauth-slots` | `config-dir` | `ambient`), `resumeMode` (`auto` default | `off` restores per-turn sessions), `compactionOwner` (`senpi` default | `sdk` hands compaction to Claude Code's native auto-compact), `systemPromptFile`, `enabled` (default `false`; gates the ambient lane only).
|
|
58
58
|
- **Environment overrides** (precedence: `env > project settings > global settings > default`; no new CLI flags):
|
|
59
59
|
|
|
60
60
|
| Variable | Purpose |
|
|
@@ -66,9 +66,10 @@ The `anthropic-subscription` provider routes LLM calls through the official [Cla
|
|
|
66
66
|
| `SENPI_CLAUDE_SDK_OAUTH_TOKEN_INJECTION` | `oauth-slots` \| `config-dir` \| `ambient` |
|
|
67
67
|
| `SENPI_CLAUDE_SDK_OAUTH_SETTING_SOURCES` | Overrides `settingSources` |
|
|
68
68
|
| `SENPI_CLAUDE_SDK_OAUTH_PINNED_ACCOUNT` | Overrides `pinnedAccount` |
|
|
69
|
+
| `SENPI_CLAUDE_SDK_OAUTH_COMPACTION_OWNER` | Overrides `compactionOwner` (`senpi` \| `sdk`) |
|
|
69
70
|
|
|
70
71
|
Every `SENPI_*` variable is stripped from the Claude Code subprocess environment on all three lanes; other inherited variables are preserved.
|
|
71
|
-
- **
|
|
72
|
+
- **Compaction.** senpi owns compaction on this lane by default: speculative and idle compaction, restoration, degradation recovery and per-model thresholds apply, the summary is written by the session's own model unless `compaction.model` is set, and an accepted compaction cold-seeds a fresh resident session from the compacted branch. So that only one side compacts, the lane pins the SDK's session-scoped `autoCompactEnabled: false` (overriding the user's global Claude Code preference) and sets `CLAUDE_CODE_TOTAL_TOKENS_REMINDER=off` for the Claude Code subprocess, whose per-turn token reminder otherwise defeats prompt-cache reuse; at the hard limit Claude Code rejects the turn and senpi's overflow recovery compacts and retries it. If that compaction fails, the turn ends with the error instead of Claude Code compacting natively. Set `compactionOwner: "sdk"` (or `SENPI_CLAUDE_SDK_OAUTH_COMPACTION_OWNER=sdk`) to let the resident Claude Code session compact instead: the lane pins `autoCompactEnabled: true`, does not set `autoCompactWindow`, and senpi stands down its automatic compaction (manual `/compact` stays senpi's). The owner is fixed into the resident Claude Code process when it starts, so changing it mid-session restarts that process on the next turn. Under either owner senpi never rewrites a message the resident session already received, so its per-turn context reduction stays off on this lane. `resumeMode: "off"` restores per-turn sessions, where senpi always owns compaction.
|
|
72
73
|
- **Session reuse.** By default one long-lived SDK query spans the entire senpi session instead of a fresh one per turn, so conversations continue with only the new delta sent. An accepted senpi compaction rewrites the transcript and forks to a fresh resident session; a rejected compaction leaves continuity unchanged. Reuse also fails closed on branch/fork navigation, account failover, an aborted turn, or any configuration change. Idle sessions retire after 30 minutes (max 32 resident); a session with an in-flight turn is never evicted. After a senpi process restart the lane always starts fresh. Set `resumeMode: "off"` (or `SENPI_CLAUDE_SDK_OAUTH_RESUME=off`) to restore the old per-turn behaviour. Accepted values are `"auto"` (default) and `"off"`; any other value is silently ignored.
|
|
73
74
|
- Account state is exposed to desktop/automation clients: RPC `get_provider_accounts`, `account_pin`, `account_remove` and the `auth_accounts_changed` / `account_failover` events, mirrored through the app-server protocol. Token material is never included.
|
|
74
75
|
|
package/docs/rpc.md
CHANGED
|
@@ -1308,6 +1308,12 @@ REPORT: nothing here aborts a turn, kills a session, or refuses an `open_session
|
|
|
1308
1308
|
- **Event-loop stalls**: a 200 ms timer measures how late it is actually invoked; that lateness is the time the loop
|
|
1309
1309
|
could serve nobody. Drift above `SENPI_RPC_LOOP_LAG_WARN_MS` (default 500) writes one stderr line per 10 seconds,
|
|
1310
1310
|
`senpi rpc host stall: event loop blocked <drift>ms (sessionId=<handle> tool=<tool>)`. Drift above
|
|
1311
|
+
`SENPI_RPC_LOOP_LAG_ERROR_MS` (default 5000) additionally broadcasts a `host_stalled` record
|
|
1312
|
+
(`{ type, driftMs, sessionId?, tool?, processCpuMs?, heapDeltaMb? }`) to every connection, like the other content-free lifecycle records.
|
|
1313
|
+
`processCpuMs` is the process CPU time spent during the stalled window and `heapDeltaMb` the JS heap change across it, so a
|
|
1314
|
+
stall explains itself: CPU close to `driftMs` means the host was busy (a large heap drop in the same window points at a
|
|
1315
|
+
collection), and CPU close to zero means the process did not run at all (the machine starved it, or it sat in a blocking
|
|
1316
|
+
wait). The stderr line carries the same two numbers as `cpu=<ms> heap=<+/-MB>`.
|
|
1311
1317
|
- On accepting an `open_session`, the host sends that connection a `queued` record
|
|
1312
1318
|
(`{ type, for_request, position, in_flight }`) before the open enters the session loop.
|
|
1313
1319
|
`position` is 1-based and `in_flight` counts opens already accepted across the whole host,
|
|
@@ -1317,12 +1323,12 @@ REPORT: nothing here aborts a turn, kills a session, or refuses an `open_session
|
|
|
1317
1323
|
acknowledged open waits up to 10 minutes for its response instead of the 30 s request
|
|
1318
1324
|
deadline (a busy host was measured answering after 57 s), and a timeout after it names the
|
|
1319
1325
|
queue position instead of a bare deadline. A lost transport still rejects at once.
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
`
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
+
- A multi-session host sends the same `queued` record for a session-routed `prompt` the moment it receives it,
|
|
1327
|
+
before the prompt's preflight runs on the session's loop (`position` counts that session's requests in flight).
|
|
1328
|
+
The bundled `RpcClient` then waits up to 5 minutes for the prompt to be accepted instead of the 30 s request
|
|
1329
|
+
deadline (a compaction's longer wait is never shortened by it), and a timeout after it says the host received the prompt. A prompt the host never acknowledged still
|
|
1330
|
+
fails at the request deadline. A reply written for a session that already closed is dropped, with one stderr
|
|
1331
|
+
line naming the session.
|
|
1326
1332
|
- **Stall attribution**: each routed command is dispatched inside an `AsyncLocalStorage` scope carrying its routing
|
|
1327
1333
|
`sessionId`, and an in-process session's tool executions open a span carrying `{ sessionId, tool }` for as long as
|
|
1328
1334
|
the tool runs. A stall is blamed on the synchronous work that finished inside the measured window, or on the tool
|
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-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.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-6",
|
|
61
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.10-6",
|
|
62
|
+
"@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.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.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-6",
|
|
82
|
+
"@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.10-6",
|
|
83
83
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
84
84
|
"@mozilla/readability": "0.6.0",
|
|
85
85
|
"linkedom": "0.18.13",
|