@code-yeongyu/senpi 2026.10.10-4 → 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 +46 -0
- package/dist/bundle/chunks/{anthropic-messages-OE3QBX4P.js → anthropic-messages-DUULMQBF.js} +1 -1
- package/dist/bundle/chunks/{app-server-command-N3L2LNP7.js → app-server-command-GJ5NXQSY.js} +1 -1
- 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-MP2DMTI7.js → chunk-C7LEZMWS.js} +124 -124
- 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-7GVD54RM.js → chunk-U3IL5N76.js} +1 -1
- package/dist/bundle/chunks/{chunk-H67EVDWG.js → chunk-URQSNRHZ.js} +1 -1
- package/dist/bundle/chunks/{chunk-H4UIUFU5.js → chunk-VGYIPYOK.js} +1 -1
- package/dist/bundle/chunks/{chunk-M2IHGL3M.js → chunk-VW74BGIC.js} +1 -1
- package/dist/bundle/chunks/{chunk-4TK7VCZZ.js → chunk-WKMUAG23.js} +1 -1
- package/dist/bundle/chunks/chunk-YG5UO3E4.js +7 -0
- package/dist/bundle/chunks/chunk-YV2XQFXG.js +14 -0
- package/dist/bundle/chunks/{chunk-Z7MMKKIY.js → chunk-ZYHIRFGT.js} +1 -1
- package/dist/bundle/chunks/{cli-main-CHDR33FZ.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-VATE7HRG.js → host-command-YG2FIDPN.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-GC3GIR2N.js → host-lifecycle-5AV4JXDQ.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-RVEM62WX.js → interactive-mode-SCNDQ4R6.js} +1 -1
- package/dist/bundle/chunks/{list-tips-IIRFXBTO.js → list-tips-BXAA4FG5.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-CFND7CXH.js → multi-session-host-ZDZMPNP5.js} +7 -6
- package/dist/bundle/chunks/{package-manager-cli-NWNECBF7.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-JQFUDLNB.js → rpc-mode-ZFNFREN5.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-EQ4PQCRP.js → schedule-command-K6CTAB4T.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-HMB5T5VD.js → session-control-endpoint-MVSYBGM3.js} +1 -1
- package/dist/bundle/chunks/{session-picker-O7OESJWU.js → session-picker-TNXWY5QY.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +224 -222
- 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/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/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/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 +64 -17
- 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/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/modes/interactive/interactive-mode.js +21 -4
- package/dist/modes/rpc/host-reservations.d.ts +1 -1
- package/dist/modes/rpc/host-reservations.js +9 -3
- 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/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/providers.md +3 -2
- package/docs/rpc.md +26 -10
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-RADHRQ4K.js +0 -7
- package/dist/bundle/chunks/chunk-USRAHAW4.js +0 -14
|
@@ -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";
|
|
@@ -52,6 +55,10 @@ export default function permissionSystemExtension(pi) {
|
|
|
52
55
|
let activePreset = null;
|
|
53
56
|
let initialApprovedCount = 0;
|
|
54
57
|
let setupError = null;
|
|
58
|
+
// The `permission-preset` flag value the rules were last loaded from; absent until session_start.
|
|
59
|
+
let loadedPresetFlag;
|
|
60
|
+
let loadedPermissionFlag;
|
|
61
|
+
const retireAuthorizers = new WeakMap();
|
|
55
62
|
const nextRequestID = createRequestIDFactory();
|
|
56
63
|
pi.registerFlag("permission", {
|
|
57
64
|
description: "Set permission rules (format: tool=action or tool:pattern=action)",
|
|
@@ -72,12 +79,17 @@ export default function permissionSystemExtension(pi) {
|
|
|
72
79
|
setupError = getReason(error);
|
|
73
80
|
throw error;
|
|
74
81
|
}
|
|
82
|
+
retireAuthorizers.get(ctx.sessionManager)?.();
|
|
83
|
+
retireAuthorizers.set(ctx.sessionManager, registerDispatchAuthorizer(ctx.sessionManager, dispatchAuthorizer));
|
|
75
84
|
applyToolDenials();
|
|
76
85
|
});
|
|
77
|
-
|
|
86
|
+
// `sessionApproved` carries this session's "Always" answers, not yet written to disk, across a reload.
|
|
87
|
+
const loadPermissionRules = (cwd, sessionApproved = []) => {
|
|
78
88
|
const settingsManager = SettingsManager.create(cwd);
|
|
79
89
|
const permissionFlag = pi.getFlag("permission");
|
|
90
|
+
loadedPermissionFlag = permissionFlag;
|
|
80
91
|
const permissionPresetFlag = pi.getFlag("permission-preset");
|
|
92
|
+
loadedPresetFlag = { value: permissionPresetFlag };
|
|
81
93
|
cliRuleset = typeof permissionFlag === "string" ? parsePermissionFlag(permissionFlag) : [];
|
|
82
94
|
const cliPreset = typeof permissionPresetFlag === "string" ? parsePermissionPresetFlag(permissionPresetFlag) : undefined;
|
|
83
95
|
if (typeof permissionPresetFlag === "string" && !cliPreset) {
|
|
@@ -88,22 +100,51 @@ export default function permissionSystemExtension(pi) {
|
|
|
88
100
|
activePreset = loadedSettings.preset;
|
|
89
101
|
const approved = loadedSettings.approved;
|
|
90
102
|
parserRegistry = createBuiltinParserRegistry();
|
|
91
|
-
service = new PermissionService(staticRuleset, approved, createEventEmitter(pi));
|
|
103
|
+
service = new PermissionService(staticRuleset, [...approved, ...sessionApproved], createEventEmitter(pi));
|
|
92
104
|
initialApprovedCount = approved.length;
|
|
93
105
|
};
|
|
94
|
-
const applyToolDenials = () => {
|
|
106
|
+
const applyToolDenials = (onlyWhenNarrowed = false) => {
|
|
95
107
|
const allTools = pi.getAllTools().map((tool) => tool.name);
|
|
96
108
|
const disabledTools = disabled(allTools, staticRuleset);
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
109
|
+
const currentTools = pi.getActiveTools();
|
|
110
|
+
const activeTools = currentTools.filter((toolName) => INTERNAL_PERMISSION_TOOLS.has(toolName) || !disabledTools.has(toolName));
|
|
111
|
+
if (onlyWhenNarrowed && activeTools.length === currentTools.length)
|
|
112
|
+
return;
|
|
100
113
|
pi.setActiveTools(activeTools);
|
|
101
114
|
};
|
|
102
|
-
|
|
115
|
+
// A host moves a live session to another preset (an attach naming one) by changing the flag;
|
|
116
|
+
// the rules follow from the next tool call on, and a failed reload refuses calls like session_start.
|
|
117
|
+
const followPresetFlag = (cwd) => {
|
|
118
|
+
if (loadedPresetFlag === undefined ||
|
|
119
|
+
(loadedPresetFlag.value === pi.getFlag("permission-preset") &&
|
|
120
|
+
loadedPermissionFlag === pi.getFlag("permission"))) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const sessionApproved = service ? service.getApproved().slice(initialApprovedCount) : [];
|
|
124
|
+
setupError = null;
|
|
125
|
+
try {
|
|
126
|
+
loadPermissionRules(cwd, sessionApproved);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
setupError = getReason(error);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
applyToolDenials(true);
|
|
133
|
+
};
|
|
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) => {
|
|
139
|
+
followPresetFlag(ctx.cwd);
|
|
103
140
|
if (setupError !== null) {
|
|
104
141
|
return { block: true, reason: `Permission setup failed: ${setupError}` };
|
|
105
142
|
}
|
|
106
|
-
|
|
143
|
+
// Pinned for this call: a preset change while it waits on a prompt must not move its reply
|
|
144
|
+
// to another service or its decision to another preset.
|
|
145
|
+
const activeService = service;
|
|
146
|
+
const callPreset = activePreset;
|
|
147
|
+
if (!activeService || !parserRegistry) {
|
|
107
148
|
return undefined;
|
|
108
149
|
}
|
|
109
150
|
const toolOwnedRequests = parserRegistry.has(event.toolName)
|
|
@@ -117,6 +158,7 @@ export default function permissionSystemExtension(pi) {
|
|
|
117
158
|
return undefined;
|
|
118
159
|
}
|
|
119
160
|
const sessionID = ctx.sessionManager.getSessionId();
|
|
161
|
+
const approve = prepareMcpDispatchApproval(pi, event, ctx, dispatchAuthorizer);
|
|
120
162
|
for (const permissionRequest of permissionRequests) {
|
|
121
163
|
const request = {
|
|
122
164
|
id: nextRequestID(),
|
|
@@ -130,18 +172,18 @@ export default function permissionSystemExtension(pi) {
|
|
|
130
172
|
...(event.parentToolCallId === undefined ? {} : { parentCallID: event.parentToolCallId }),
|
|
131
173
|
},
|
|
132
174
|
};
|
|
133
|
-
const auto =
|
|
175
|
+
const auto = callPreset === "auto"
|
|
134
176
|
? await decideAuto(event.toolName, event.input, permissionRequest, ctx.cwd)
|
|
135
177
|
: undefined;
|
|
136
|
-
const askResultPromise =
|
|
178
|
+
const askResultPromise = activeService
|
|
137
179
|
.ask(request, {
|
|
138
180
|
autoApproveAsk: permissionRequest.autoApproveAsk ?? false,
|
|
139
181
|
approveBlanketAsk: auto?.approveBlanketAsk ?? false,
|
|
140
|
-
presetBound:
|
|
182
|
+
presetBound: callPreset === "auto",
|
|
141
183
|
...(permissionRequest.ruleAliases ? { ruleAliases: permissionRequest.ruleAliases } : {}),
|
|
142
184
|
})
|
|
143
185
|
.then(() => ({ ok: true }), (error) => ({ ok: false, error }));
|
|
144
|
-
const isPending =
|
|
186
|
+
const isPending = activeService.list().some((pendingRequest) => pendingRequest.id === request.id);
|
|
145
187
|
if (!isPending) {
|
|
146
188
|
const askResult = await askResultPromise;
|
|
147
189
|
if (!askResult.ok) {
|
|
@@ -150,17 +192,17 @@ export default function permissionSystemExtension(pi) {
|
|
|
150
192
|
continue;
|
|
151
193
|
}
|
|
152
194
|
if (ctx.hasUI) {
|
|
153
|
-
const reply = await showPermissionPrompt(ctx, request);
|
|
154
|
-
|
|
195
|
+
const reply = await showPermissionPrompt(ctx, request, signal);
|
|
196
|
+
activeService.reply(reply);
|
|
155
197
|
}
|
|
156
198
|
else {
|
|
157
|
-
|
|
199
|
+
activeService.reply(handleNoUI(request, {
|
|
158
200
|
emitEvent: (eventName, data) => {
|
|
159
201
|
if (eventName !== "permission_asked") {
|
|
160
202
|
pi.events.emit(eventName, data);
|
|
161
203
|
}
|
|
162
204
|
},
|
|
163
|
-
presetBound:
|
|
205
|
+
presetBound: callPreset === "auto",
|
|
164
206
|
}));
|
|
165
207
|
}
|
|
166
208
|
const askResult = await askResultPromise;
|
|
@@ -168,10 +210,15 @@ export default function permissionSystemExtension(pi) {
|
|
|
168
210
|
return { block: true, reason: getReason(askResult.error) };
|
|
169
211
|
}
|
|
170
212
|
}
|
|
213
|
+
approve?.();
|
|
171
214
|
return undefined;
|
|
172
|
-
}
|
|
215
|
+
};
|
|
216
|
+
pi.on("tool_call", authorizeToolCall);
|
|
217
|
+
pi.on("tool_result", (event) => forgetDispatchApproval(event.input));
|
|
173
218
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
174
219
|
void event;
|
|
220
|
+
retireAuthorizers.get(ctx.sessionManager)?.();
|
|
221
|
+
retireAuthorizers.delete(ctx.sessionManager);
|
|
175
222
|
if (!service) {
|
|
176
223
|
return;
|
|
177
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;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { sanitizeTerminalLabel } from "@earendil-works/pi-tui";
|
|
1
|
+
export { sanitizeTerminalLabel, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
export { type Args, parseArgs } from "./cli/args.ts";
|
|
3
3
|
export { runHostCommand } from "./cli/host-command.ts";
|
|
4
4
|
export { CONFIG_DIR_NAME, getAgentDir, getDocsPath, getExamplesPath, getPackageDir, getReadmePath, VERSION, } from "./config.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Core session management
|
|
2
|
-
export { sanitizeTerminalLabel } from "@earendil-works/pi-tui";
|
|
2
|
+
export { sanitizeTerminalLabel, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
export { parseArgs } from "./cli/args.js";
|
|
4
4
|
// The shared-daemon command surface: one JSON line, one exit code, for launchers without a shell
|
|
5
5
|
export { runHostCommand } from "./cli/host-command.js";
|
|
@@ -4123,6 +4123,11 @@ export class InteractiveMode {
|
|
|
4123
4123
|
switch (event.type) {
|
|
4124
4124
|
case "agent_start":
|
|
4125
4125
|
this.agentIdle = false;
|
|
4126
|
+
// Keep a scrolled-up reader in place for this turn: rows re-laid out above the viewport are not
|
|
4127
|
+
// replayed until the next key press (#2836). No catch-up here: a turn nobody typed (auto-retry,
|
|
4128
|
+
// an extension's triggerTurn) can start while the reader is still scrolled up, and a turn the user
|
|
4129
|
+
// started already caught up on their Enter key.
|
|
4130
|
+
this.ui.setScrollbackReplayHold(true);
|
|
4126
4131
|
this.transcriptWriteNoticeShown = false;
|
|
4127
4132
|
this.clearPendingTools();
|
|
4128
4133
|
this.clearActiveToolExecutionStatus();
|
|
@@ -4411,6 +4416,8 @@ export class InteractiveMode {
|
|
|
4411
4416
|
if (this.settingsManager.getShowTerminalProgress() && this.ui.terminal) {
|
|
4412
4417
|
this.ui.terminal.setProgress(false);
|
|
4413
4418
|
}
|
|
4419
|
+
// Keep holding until the next key press: a replay at turn end would snap a reader who is scrolled up.
|
|
4420
|
+
this.ui.setScrollbackReplayHold("until-input");
|
|
4414
4421
|
this.clearActiveToolExecutionStatus();
|
|
4415
4422
|
this.clearToolHookStatuses();
|
|
4416
4423
|
this.streamingReveal.stop();
|
|
@@ -4850,8 +4857,13 @@ export class InteractiveMode {
|
|
|
4850
4857
|
*/
|
|
4851
4858
|
showStatus(message) {
|
|
4852
4859
|
const children = this.chatContainer.children;
|
|
4853
|
-
|
|
4854
|
-
|
|
4860
|
+
// While a turn streams, a notice goes above the live message, like a custom entry does. Appended
|
|
4861
|
+
// after it, a notice taller than the screen pushed the live output above the viewport, and every
|
|
4862
|
+
// streamed delta then replayed the whole scrollback: the view kept jumping to the top (#2836).
|
|
4863
|
+
const streamingIndex = this.streamingComponent ? children.indexOf(this.streamingComponent) : -1;
|
|
4864
|
+
const end = streamingIndex >= 0 ? streamingIndex : children.length;
|
|
4865
|
+
const last = end > 0 ? children[end - 1] : undefined;
|
|
4866
|
+
const secondLast = end > 1 ? children[end - 2] : undefined;
|
|
4855
4867
|
if (last && secondLast && last === this.lastStatusText && secondLast === this.lastStatusSpacer) {
|
|
4856
4868
|
this.lastStatusMessage = message;
|
|
4857
4869
|
this.lastStatusText.invalidate();
|
|
@@ -4861,8 +4873,13 @@ export class InteractiveMode {
|
|
|
4861
4873
|
const spacer = new Spacer(1);
|
|
4862
4874
|
this.lastStatusMessage = message;
|
|
4863
4875
|
const text = new ThemedText(() => theme.fg("dim", this.lastStatusMessage), 1, 0);
|
|
4864
|
-
|
|
4865
|
-
|
|
4876
|
+
if (streamingIndex >= 0) {
|
|
4877
|
+
children.splice(streamingIndex, 0, spacer, text);
|
|
4878
|
+
}
|
|
4879
|
+
else {
|
|
4880
|
+
this.chatContainer.addChild(spacer);
|
|
4881
|
+
this.chatContainer.addChild(text);
|
|
4882
|
+
}
|
|
4866
4883
|
this.lastStatusSpacer = spacer;
|
|
4867
4884
|
this.lastStatusText = text;
|
|
4868
4885
|
this.ui.requestRender();
|
|
@@ -14,7 +14,7 @@ export interface SessionPathReservations {
|
|
|
14
14
|
/** Records this host as the holder of `sessionPath`, or reports the holder whose claim stands. */
|
|
15
15
|
claim(sessionPath: string, attached?: boolean): Promise<SessionPathOwner | undefined>;
|
|
16
16
|
/** Drops this host's claim. A claim made by another generation is never touched. */
|
|
17
|
-
release(sessionPath: string): void
|
|
17
|
+
release(sessionPath: string): Promise<void>;
|
|
18
18
|
/** Republishes this host's claim with the attachment state that session now has. */
|
|
19
19
|
setAttached(sessionPath: string, attached: boolean): void;
|
|
20
20
|
}
|
|
@@ -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
|
}
|
|
@@ -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)
|
|
@@ -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"));
|