zigmini 0.8.0-beta.7 → 0.9.0-beta.2
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/README.ko.md +27 -5
- package/README.md +27 -5
- package/assets/agent/gateway-request-rules.md +3 -1
- package/assets/android/zigmini.apk +0 -0
- package/assets/android/zigmini.apk.json +3 -3
- package/assets/build.json +1 -1
- package/docs/manual.ko.md +57 -4
- package/docs/manual.md +55 -4
- package/host/omo-bridge/ascii-json.ts +5 -0
- package/host/omo-bridge/bridge.ts +50 -44
- package/host/omo-check/patches/omo-5.1.44-compaction-port.patch +2630 -0
- package/host/omo-check/patches/omo-5.1.44-gateway-port.manifest.json +329 -0
- package/host/omo-check/patches/omo-5.1.44-provider-port.patch +440 -0
- package/host/omo-check/patches/omo-5.1.44-rpc-port.patch +778 -0
- package/host/omo-check/patches/omo-5.1.48-compaction-port.patch +2629 -0
- package/host/omo-check/patches/omo-5.1.48-gateway-port.manifest.json +355 -0
- package/host/omo-check/patches/omo-5.1.48-provider-port.patch +440 -0
- package/host/omo-check/patches/omo-5.1.48-rpc-port.patch +799 -0
- package/host/omo-check/patches/omo-5.1.48-runtime-compat.patch +31 -0
- package/package.json +1 -1
- package/src/cli/setup.ts +6 -2
- package/src/decisions.ts +32 -13
- package/src/engine/compat.json +4 -4
- package/src/engine/install.ts +68 -20
- package/src/engine/rpc-socket.ts +36 -0
- package/src/engine/schema-guard.ts +34 -2
- package/src/hub.ts +57 -31
- package/src/main.ts +22 -7
- package/src/notification-history.ts +56 -0
- package/src/omo/client.ts +16 -3
- package/src/omo/frame-socket.ts +330 -0
- package/src/protocol.ts +2 -1
- package/src/security-allow.ts +126 -0
- package/src/security-check.ts +355 -37
- package/src/store.ts +165 -5
- package/src/update/handoff-system.ts +22 -0
- package/src/update/handoff.ts +10 -2
- package/src/update/helper.ts +11 -1
- package/src/update/request.ts +15 -2
- package/src/work.ts +21 -7
- package/web/DESIGN.md +39 -0
- package/web/app.js +198 -69
- package/web/decisions.d.ts +1 -1
- package/web/decisions.js +3 -0
- package/web/diagram.d.ts +28 -0
- package/web/diagram.js +228 -0
- package/web/index.html +13 -0
- package/web/security-inbox.d.ts +35 -0
- package/web/security-inbox.js +237 -0
- package/web/style.css +31 -1
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
--- a/@code-yeongyu/senpi/dist/core/client-message-identity.d.ts
|
|
2
|
+
+++ b/@code-yeongyu/senpi/dist/core/client-message-identity.d.ts
|
|
3
|
+
@@ -2,6 +2,7 @@
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import type { InputSource } from "./extensions/types.ts";
|
|
6
|
+
export declare const clientMessageIdentitySchema: z.ZodObject<{
|
|
7
|
+
+ clientInputId: z.ZodOptional<z.ZodString>;
|
|
8
|
+
clientMessageId: z.ZodOptional<z.ZodString>;
|
|
9
|
+
clientTurnId: z.ZodOptional<z.ZodString>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
@@ -17,4 +18,4 @@
|
|
12
|
+
/** Copy only correlation metadata, never the rest of a prompt's options. */
|
|
13
|
+
export declare function clientMessageIdentity(value?: ClientMessageIdentity): ClientMessageIdentity;
|
|
14
|
+
export declare function readClientMessageIdentity(value: unknown): ClientMessageIdentity;
|
|
15
|
+
-//# sourceMappingURL=client-message-identity.d.ts.map
|
|
16
|
+
|
|
17
|
+
+//# sourceMappingURL=client-message-identity.d.ts.map
|
|
18
|
+
--- a/@code-yeongyu/senpi/dist/core/client-message-identity.js
|
|
19
|
+
+++ b/@code-yeongyu/senpi/dist/core/client-message-identity.js
|
|
20
|
+
@@ -1,12 +1,15 @@
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
export const clientMessageIdentitySchema = z.object({
|
|
23
|
+
+ clientInputId: z.string().min(1).max(128).optional(),
|
|
24
|
+
clientMessageId: z.string().min(1).max(256).optional(),
|
|
25
|
+
clientTurnId: z.string().min(1).max(256).optional(),
|
|
26
|
+
});
|
|
27
|
+
/** Copy only correlation metadata, never the rest of a prompt's options. */
|
|
28
|
+
export function clientMessageIdentity(value = {}) {
|
|
29
|
+
+ const clientMessageId = value.clientMessageId ?? value.clientInputId;
|
|
30
|
+
return {
|
|
31
|
+
- ...(value.clientMessageId === undefined ? {} : { clientMessageId: value.clientMessageId }),
|
|
32
|
+
+ ...(value.clientInputId === undefined ? {} : { clientInputId: value.clientInputId }),
|
|
33
|
+
+ ...(clientMessageId === undefined ? {} : { clientMessageId }),
|
|
34
|
+
...(value.clientTurnId === undefined ? {} : { clientTurnId: value.clientTurnId }),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
@@ -14,4 +17,4 @@
|
|
38
|
+
const parsed = clientMessageIdentitySchema.safeParse(value);
|
|
39
|
+
return parsed.success ? clientMessageIdentity(parsed.data) : {};
|
|
40
|
+
}
|
|
41
|
+
-//# sourceMappingURL=client-message-identity.js.map
|
|
42
|
+
|
|
43
|
+
+//# sourceMappingURL=client-message-identity.js.map
|
|
44
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/ask-user/pending.js
|
|
45
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/ask-user/pending.js
|
|
46
|
+
@@ -39,7 +39,8 @@
|
|
47
|
+
}
|
|
48
|
+
export function createPendingQuestion(options) {
|
|
49
|
+
const createdAtMs = options.now();
|
|
50
|
+
- const hardCapMs = options.hardCapMs ?? DEFAULT_HARD_CAP_MS;
|
|
51
|
+
+ // zgw: an unbounded idle timeout (required question) also has no hard cap.
|
|
52
|
+
+ const hardCapMs = Number.isFinite(options.idleTimeoutMs) ? (options.hardCapMs ?? DEFAULT_HARD_CAP_MS) : Infinity;
|
|
53
|
+
const hardDeadlineAtMs = createdAtMs + hardCapMs;
|
|
54
|
+
const schedule = options.setTimeout ?? ((handler, delayMs) => setTimeout(handler, delayMs));
|
|
55
|
+
const unsched = options.clearTimeout ?? clearTimeout;
|
|
56
|
+
@@ -72,6 +73,8 @@
|
|
57
|
+
if (terminal !== undefined)
|
|
58
|
+
return;
|
|
59
|
+
const delayMs = deadlineAtMs - options.now();
|
|
60
|
+
+ if (!Number.isFinite(delayMs))
|
|
61
|
+
+ return;
|
|
62
|
+
if (delayMs <= 0) {
|
|
63
|
+
fireTimeout();
|
|
64
|
+
return;
|
|
65
|
+
@@ -131,4 +134,4 @@
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
-//# sourceMappingURL=pending.js.map
|
|
70
|
+
|
|
71
|
+
+//# sourceMappingURL=pending.js.map
|
|
72
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/ask-user/resume.js
|
|
73
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/ask-user/resume.js
|
|
74
|
+
@@ -5,6 +5,8 @@
|
|
75
|
+
import { DEFAULT_ASK_USER_TIMEOUT_MS, toCanonical, } from "./schema.js";
|
|
76
|
+
import { deliverAnswer, startQuestion } from "./tool.js";
|
|
77
|
+
export const ASK_USER_RESUMED_ENTRY = "ask-user:resumed";
|
|
78
|
+
+/** zgw bug 69: a required question that was live when the host stopped; its abort result does not end it. */
|
|
79
|
+
+export const ASK_USER_INTERRUPTED_ENTRY = "ask-user:interrupted";
|
|
80
|
+
function isRecord(value) {
|
|
81
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
82
|
+
}
|
|
83
|
+
@@ -19,6 +21,7 @@
|
|
84
|
+
const results = new Set();
|
|
85
|
+
const accepted = new Set();
|
|
86
|
+
const resumed = new Set();
|
|
87
|
+
+ const interrupted = new Set();
|
|
88
|
+
const settled = new Set();
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
if (entry.type === "message" && entry.message.role === "user") {
|
|
91
|
+
@@ -34,9 +37,14 @@
|
|
92
|
+
}
|
|
93
|
+
if (entry.type === "custom" && entry.customType === ASK_USER_RESUMED_ENTRY && isRecord(entry.data)) {
|
|
94
|
+
const toolCallId = entry.data.toolCallId;
|
|
95
|
+
- if (typeof toolCallId === "string")
|
|
96
|
+
+ // zgw bug 32: a resumed question now records its own settlement, so only a settlement or an answer
|
|
97
|
+
+ // frame ends it and it is resumed again after every restart. A marker without the flag predates that
|
|
98
|
+
+ // record (its cancellation left none), so it still ends the question.
|
|
99
|
+
+ if (typeof toolCallId === "string" && entry.data.settlesDurably !== true)
|
|
100
|
+
resumed.add(toolCallId);
|
|
101
|
+
}
|
|
102
|
+
+ if (entry.type === "custom" && entry.customType === ASK_USER_INTERRUPTED_ENTRY && isRecord(entry.data) && typeof entry.data.toolCallId === "string")
|
|
103
|
+
+ interrupted.add(entry.data.toolCallId);
|
|
104
|
+
if (entry.type === "message" && entry.message.role === "toolResult") {
|
|
105
|
+
results.add(entry.message.toolCallId);
|
|
106
|
+
const details = entry.message.details;
|
|
107
|
+
@@ -65,7 +73,7 @@
|
|
108
|
+
continue;
|
|
109
|
+
const call = { toolCallId: block.id, variant, args: block.arguments };
|
|
110
|
+
const request = requestFromCall(call, DEFAULT_ASK_USER_TIMEOUT_MS);
|
|
111
|
+
- if (results.has(block.id) && (request?.waitForAnswer === true || !accepted.has(block.id)))
|
|
112
|
+
+ if (results.has(block.id) && !interrupted.has(block.id) && (request?.waitForAnswer === true || !accepted.has(block.id)))
|
|
113
|
+
continue;
|
|
114
|
+
if (resumed.has(block.id))
|
|
115
|
+
continue;
|
|
116
|
+
@@ -105,6 +113,18 @@
|
|
117
|
+
emitAskUserClosed(pi, lost.requestId, response);
|
|
118
|
+
deliverAnswer(pi, ctx, lost, response, dangling.variant);
|
|
119
|
+
}
|
|
120
|
+
+/**
|
|
121
|
+
+ * zgw bug 69: called when the host stops (SIGTERM/SIGHUP) before the session is aborted. Each live required question
|
|
122
|
+
+ * is recorded as interrupted and detached, so the shutdown abort neither settles it nor hides it from the next start.
|
|
123
|
+
+ */
|
|
124
|
+
+export function interruptRequiredQuestionsForHostShutdown(sessionManager) {
|
|
125
|
+
+ for (const entry of getPendingQuestions(sessionManager.getSessionId())) {
|
|
126
|
+
+ if (!entry.request.waitForAnswer)
|
|
127
|
+
+ continue;
|
|
128
|
+
+ sessionManager.appendCustomEntry(ASK_USER_INTERRUPTED_ENTRY, { toolCallId: entry.request.requestId });
|
|
129
|
+
+ entry.detach();
|
|
130
|
+
+ }
|
|
131
|
+
+}
|
|
132
|
+
export async function resumeDanglingQuestion(pi, event, ctx) {
|
|
133
|
+
if (event.reason !== "resume" && event.reason !== "reload")
|
|
134
|
+
return;
|
|
135
|
+
@@ -114,7 +134,7 @@
|
|
136
|
+
if (pending.has(dangling.toolCallId))
|
|
137
|
+
continue;
|
|
138
|
+
pending.add(dangling.toolCallId);
|
|
139
|
+
- pi.appendEntry(ASK_USER_RESUMED_ENTRY, { toolCallId: dangling.toolCallId });
|
|
140
|
+
+ pi.appendEntry(ASK_USER_RESUMED_ENTRY, { toolCallId: dangling.toolCallId, settlesDurably: true });
|
|
141
|
+
const request = requestFromCall(dangling, timeoutMs);
|
|
142
|
+
if (!request) {
|
|
143
|
+
settleUnrestorable(pi, ctx, dangling, timeoutMs);
|
|
144
|
+
@@ -126,4 +146,4 @@
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
-//# sourceMappingURL=resume.js.map
|
|
149
|
+
|
|
150
|
+
+//# sourceMappingURL=resume.js.map
|
|
151
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/ask-user/schema.js
|
|
152
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/ask-user/schema.js
|
|
153
|
+
@@ -99,8 +99,10 @@
|
|
154
|
+
requestId: options?.requestId ?? crypto.randomUUID(),
|
|
155
|
+
questions: args.questions.map((question, index) => parseQuestion(variant, question, index)),
|
|
156
|
+
waitForAnswer,
|
|
157
|
+
- timeoutMs: options?.timeoutMs ?? DEFAULT_ASK_USER_TIMEOUT_MS,
|
|
158
|
+
+ // zgw: a waiting question is a required decision; it has no idle timeout or hard cap and
|
|
159
|
+
+ // stays open until the user answers or cancels. Non-waiting questions keep the setting.
|
|
160
|
+
+ timeoutMs: waitForAnswer ? Infinity : (options?.timeoutMs ?? DEFAULT_ASK_USER_TIMEOUT_MS),
|
|
161
|
+
...(args.required === true ? { required: true } : {}),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
-//# sourceMappingURL=schema.js.map
|
|
165
|
+
|
|
166
|
+
+//# sourceMappingURL=schema.js.map
|
|
167
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/ask-user/tool.js
|
|
168
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/ask-user/tool.js
|
|
169
|
+
@@ -78,7 +78,8 @@
|
|
170
|
+
let unregister = () => { };
|
|
171
|
+
const publish = (ownerPi, ownerCtx, response) => {
|
|
172
|
+
emitAskUserClosed(ownerPi, request.requestId, response);
|
|
173
|
+
- if (!request.waitForAnswer)
|
|
174
|
+
+ // zgw bug 32: a resumed question has no tool result, so record every outcome (a silent cancellation too).
|
|
175
|
+
+ if (!request.waitForAnswer || resuming)
|
|
176
|
+
ownerPi.appendEntry(ASK_USER_SETTLEMENT_ENTRY, {
|
|
177
|
+
requestId: request.requestId,
|
|
178
|
+
status: response.status,
|
|
179
|
+
@@ -175,7 +176,7 @@
|
|
180
|
+
timeout: request.timeoutMs,
|
|
181
|
+
signal: controller.signal,
|
|
182
|
+
deliver: request.waitForAnswer ? "tool-result" : "user-message",
|
|
183
|
+
- hardDeadlineAtMs: Date.now() + 7_200_000,
|
|
184
|
+
+ hardDeadlineAtMs: Number.isFinite(request.timeoutMs) ? Date.now() + 7_200_000 : Infinity,
|
|
185
|
+
getDeadlineAtMs: () => pending.deadlineAtMs,
|
|
186
|
+
onProgress: (progress) => {
|
|
187
|
+
draft = { ...draft, ...progress, answers: progress.answers ?? draft.answers };
|
|
188
|
+
@@ -268,7 +269,7 @@
|
|
189
|
+
allowLazyActivation: false,
|
|
190
|
+
parameters: variant === "codex" ? CODEX_PARAMS : CLAUDE_PARAMS,
|
|
191
|
+
promptSnippet: "Ask a material question, explicitly choosing whether to wait or receive the answer later.",
|
|
192
|
+
- description: `${opener} Set ${flag} true to pause here until the user answers (the answer returns as this tool's result, or a timeout result after 30 idle minutes); set ${flag} false to keep working while the question stays open (the answer arrives later as a user message). Use it only when the answer materially changes the work; if it returns no answers, continue with best judgment instead of asking again, unless you set required true because the answer decides whether you take an action: then no answer means do not take it. Never use it for permission requests; ask those directly in your message. Unavailable to subagents.${variant === "claude" ? " Users will always be able to type a free-text answer or one comment covering everything; do not add an Other option." : ""} Call this tool directly, never from inside an eval cell (a cell that waits on the user would hold the kernel).`,
|
|
193
|
+
+ description: `${opener} Set ${flag} true for a required confirmation: the work pauses here until the user answers or cancels, with no timeout (the answer returns as this tool's result); never proceed on your own judgment past a required question. Set ${flag} false when the work may continue without the answer (it arrives later as a user message, or a timeout after 30 idle minutes); if such a question returns no answers, continue with best judgment instead of asking again. Use it only when the answer materially changes the work. Never use it for permission requests; ask those directly in your message. Unavailable to subagents.${variant === "claude" ? " Users will always be able to type a free-text answer or one comment covering everything; do not add an Other option." : ""} Call this tool directly, never from inside an eval cell (a cell that waits on the user would hold the kernel).`,
|
|
194
|
+
prepareArguments(args) {
|
|
195
|
+
toCanonical(variant, args);
|
|
196
|
+
return args;
|
|
197
|
+
@@ -327,4 +328,4 @@
|
|
198
|
+
renderResult,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
-//# sourceMappingURL=tool.js.map
|
|
202
|
+
|
|
203
|
+
+//# sourceMappingURL=tool.js.map
|
|
204
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/permission-system/events.d.ts
|
|
205
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/permission-system/events.d.ts
|
|
206
|
+
@@ -13,6 +13,9 @@
|
|
207
|
+
requestID: string;
|
|
208
|
+
sessionID: string;
|
|
209
|
+
reply: PermissionDecision;
|
|
210
|
+
+ correlationId?: string;
|
|
211
|
+
+ source?: "user" | "system" | "policy" | "cascade";
|
|
212
|
+
+ choice?: string;
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Event emitter interface for permission events.
|
|
216
|
+
@@ -30,7 +33,7 @@
|
|
217
|
+
* @param sessionID - The session ID associated with the request
|
|
218
|
+
* @param reply - The user's reply (once, always, or reject)
|
|
219
|
+
*/
|
|
220
|
+
- emitReplied(requestID: string, sessionID: string, reply: PermissionDecision): void;
|
|
221
|
+
+ emitReplied(requestID: string, sessionID: string, reply: PermissionDecision, evidence?: Pick<PermissionRepliedEvent, "correlationId" | "source" | "choice">): void;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Creates an event emitter that wraps pi.events for inter-extension communication.
|
|
225
|
+
@@ -64,4 +67,4 @@
|
|
226
|
+
*/
|
|
227
|
+
clear(): void;
|
|
228
|
+
};
|
|
229
|
+
-//# sourceMappingURL=events.d.ts.map
|
|
230
|
+
|
|
231
|
+
+//# sourceMappingURL=events.d.ts.map
|
|
232
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/permission-system/events.js
|
|
233
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/permission-system/events.js
|
|
234
|
+
@@ -10,8 +10,8 @@
|
|
235
|
+
emitAsked: (request) => {
|
|
236
|
+
pi.events.emit("permission_asked", request);
|
|
237
|
+
},
|
|
238
|
+
- emitReplied: (requestID, sessionID, reply) => {
|
|
239
|
+
- pi.events.emit("permission_replied", { requestID, sessionID, reply });
|
|
240
|
+
+ emitReplied: (requestID, sessionID, reply, evidence = {}) => {
|
|
241
|
+
+ pi.events.emit("permission_replied", { requestID, sessionID, reply, ...evidence });
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
@@ -35,8 +35,8 @@
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
- emitReplied: (requestID, sessionID, reply) => {
|
|
250
|
+
- const event = { requestID, sessionID, reply };
|
|
251
|
+
+ emitReplied: (requestID, sessionID, reply, evidence = {}) => {
|
|
252
|
+
+ const event = { requestID, sessionID, reply, ...evidence };
|
|
253
|
+
for (const handler of repliedHandlers) {
|
|
254
|
+
try {
|
|
255
|
+
handler(event);
|
|
256
|
+
@@ -60,4 +60,4 @@
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
-//# sourceMappingURL=events.js.map
|
|
261
|
+
|
|
262
|
+
+//# sourceMappingURL=events.js.map
|
|
263
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/permission-system/index.js
|
|
264
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/permission-system/index.js
|
|
265
|
+
@@ -1,3 +1,4 @@
|
|
266
|
+
+import { randomUUID } from "node:crypto";
|
|
267
|
+
import { SettingsManager } from "../../../settings-manager.js";
|
|
268
|
+
import { extractPatchedPaths } from "../gpt-apply-patch/index.js";
|
|
269
|
+
import { decideAuto } from "./auto-policy.js";
|
|
270
|
+
@@ -258,6 +259,7 @@
|
|
271
|
+
const request = {
|
|
272
|
+
id: nextRequestID(),
|
|
273
|
+
sessionID,
|
|
274
|
+
+ correlationId: randomUUID(),
|
|
275
|
+
permission: permissionRequest.permission,
|
|
276
|
+
patterns: permissionRequest.patterns,
|
|
277
|
+
always: permissionRequest.always,
|
|
278
|
+
@@ -345,4 +347,4 @@
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
-//# sourceMappingURL=index.js.map
|
|
283
|
+
|
|
284
|
+
+//# sourceMappingURL=index.js.map
|
|
285
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/permission-system/internal-tools.js
|
|
286
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/permission-system/internal-tools.js
|
|
287
|
+
@@ -2,9 +2,6 @@
|
|
288
|
+
export const INTERNAL_PERMISSION_TOOLS = new Set([
|
|
289
|
+
"todo",
|
|
290
|
+
"tool_search",
|
|
291
|
+
- "ask_user",
|
|
292
|
+
- "request_user_input",
|
|
293
|
+
- "ask_user_question",
|
|
294
|
+
"memory",
|
|
295
|
+
"monitor",
|
|
296
|
+
"create_goal",
|
|
297
|
+
@@ -14,4 +11,4 @@
|
|
298
|
+
"bash_resize",
|
|
299
|
+
"kill_bash",
|
|
300
|
+
]);
|
|
301
|
+
-//# sourceMappingURL=internal-tools.js.map
|
|
302
|
+
|
|
303
|
+
+//# sourceMappingURL=internal-tools.js.map
|
|
304
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/permission-system/prompt.js
|
|
305
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/permission-system/prompt.js
|
|
306
|
+
@@ -14,17 +14,25 @@
|
|
307
|
+
: {};
|
|
308
|
+
if (signal !== undefined)
|
|
309
|
+
call.signal = signal;
|
|
310
|
+
- const choice = await ctx.ui.select(displayTitle, options, call);
|
|
311
|
+
+ const choice = await ctx.ui.select(displayTitle, options, {
|
|
312
|
+
+ ...call,
|
|
313
|
+
+ nativePermission: {
|
|
314
|
+
+ sessionID: request.sessionID,
|
|
315
|
+
+ requestID: request.id,
|
|
316
|
+
+ correlationId: request.correlationId,
|
|
317
|
+
+ },
|
|
318
|
+
+ });
|
|
319
|
+
if (choice === "Deny with feedback") {
|
|
320
|
+
- const feedback = await ctx.ui.input("Feedback", "Why are you denying this permission? (optional)", call);
|
|
321
|
+
+ const feedback = await ctx.ui.input("Feedback", "Why are you denying this permission? (optional)", { ...call, required: false });
|
|
322
|
+
return {
|
|
323
|
+
requestID: request.id,
|
|
324
|
+
reply: "reject",
|
|
325
|
+
message: feedback || undefined,
|
|
326
|
+
+ choice,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
if (wider !== undefined && choice === wider.label) {
|
|
330
|
+
- return { requestID: request.id, reply: "always", always: [...wider.patterns] };
|
|
331
|
+
+ return { requestID: request.id, reply: "always", always: [...wider.patterns], choice };
|
|
332
|
+
}
|
|
333
|
+
const replyMap = {
|
|
334
|
+
"Allow once": "once",
|
|
335
|
+
@@ -34,6 +42,7 @@
|
|
336
|
+
return {
|
|
337
|
+
requestID: request.id,
|
|
338
|
+
reply: choice ? replyMap[choice] : "reject",
|
|
339
|
+
+ choice,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function formatRequestForDisplay(request) {
|
|
343
|
+
@@ -80,4 +89,4 @@
|
|
344
|
+
}
|
|
345
|
+
return parts.join("\n");
|
|
346
|
+
}
|
|
347
|
+
-//# sourceMappingURL=prompt.js.map
|
|
348
|
+
|
|
349
|
+
+//# sourceMappingURL=prompt.js.map
|
|
350
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/permission-system/service.js
|
|
351
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/permission-system/service.js
|
|
352
|
+
@@ -165,7 +165,10 @@
|
|
353
|
+
throw new DeniedError(deniedPatterns);
|
|
354
|
+
}
|
|
355
|
+
if (!needsAsk) {
|
|
356
|
+
- this.emitter.emitReplied(info.id, info.sessionID, "allow");
|
|
357
|
+
+ this.emitter.emitReplied(info.id, info.sessionID, "allow", {
|
|
358
|
+
+ correlationId: info.correlationId,
|
|
359
|
+
+ source: "policy",
|
|
360
|
+
+ });
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const pendingPromise = new Promise((resolve, reject) => {
|
|
364
|
+
@@ -196,7 +199,11 @@
|
|
365
|
+
}
|
|
366
|
+
this.pending.delete(input.requestID);
|
|
367
|
+
this.pendingOptions.delete(input.requestID);
|
|
368
|
+
- this.emitter.emitReplied(existing.info.id, existing.info.sessionID, input.reply);
|
|
369
|
+
+ this.emitter.emitReplied(existing.info.id, existing.info.sessionID, input.reply, {
|
|
370
|
+
+ correlationId: existing.info.correlationId,
|
|
371
|
+
+ source: input.choice === undefined ? "system" : "user",
|
|
372
|
+
+ ...(input.choice === undefined ? {} : { choice: input.choice }),
|
|
373
|
+
+ });
|
|
374
|
+
if (input.reply === "reject") {
|
|
375
|
+
existing.reject(input.message ? new CorrectedError(input.message) : new RejectedError());
|
|
376
|
+
this.rejectPendingInSession(existing.info.sessionID);
|
|
377
|
+
@@ -247,7 +254,10 @@
|
|
378
|
+
}
|
|
379
|
+
this.pending.delete(requestID);
|
|
380
|
+
this.pendingOptions.delete(requestID);
|
|
381
|
+
- this.emitter.emitReplied(entry.info.id, entry.info.sessionID, "reject");
|
|
382
|
+
+ this.emitter.emitReplied(entry.info.id, entry.info.sessionID, "reject", {
|
|
383
|
+
+ correlationId: entry.info.correlationId,
|
|
384
|
+
+ source: "cascade",
|
|
385
|
+
+ });
|
|
386
|
+
entry.reject(new RejectedError());
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
@@ -263,9 +273,12 @@
|
|
390
|
+
}
|
|
391
|
+
this.pending.delete(requestID);
|
|
392
|
+
this.pendingOptions.delete(requestID);
|
|
393
|
+
- this.emitter.emitReplied(entry.info.id, entry.info.sessionID, "always");
|
|
394
|
+
+ this.emitter.emitReplied(entry.info.id, entry.info.sessionID, "always", {
|
|
395
|
+
+ correlationId: entry.info.correlationId,
|
|
396
|
+
+ source: "cascade",
|
|
397
|
+
+ });
|
|
398
|
+
entry.resolve();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
-//# sourceMappingURL=service.js.map
|
|
403
|
+
|
|
404
|
+
+//# sourceMappingURL=service.js.map
|
|
405
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/permission-system/types.d.ts
|
|
406
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/permission-system/types.d.ts
|
|
407
|
+
@@ -19,6 +19,7 @@
|
|
408
|
+
export type Request = {
|
|
409
|
+
id: string;
|
|
410
|
+
sessionID: string;
|
|
411
|
+
+ correlationId?: string;
|
|
412
|
+
permission: string;
|
|
413
|
+
patterns: string[];
|
|
414
|
+
always: string[];
|
|
415
|
+
@@ -42,6 +43,8 @@
|
|
416
|
+
message?: string;
|
|
417
|
+
/** With reply "always": the patterns to approve instead of the request's own (a wider scope). */
|
|
418
|
+
always?: string[];
|
|
419
|
+
+ /** Actual UI choice, absent for system/cancellation paths. Metadata only. */
|
|
420
|
+
+ choice?: string;
|
|
421
|
+
};
|
|
422
|
+
/** Error thrown when user rejects a permission request */
|
|
423
|
+
export declare class RejectedError extends Error {
|
|
424
|
+
@@ -68,4 +71,4 @@
|
|
425
|
+
resolve: () => void;
|
|
426
|
+
reject: (err: RejectedError | CorrectedError) => void;
|
|
427
|
+
};
|
|
428
|
+
-//# sourceMappingURL=types.d.ts.map
|
|
429
|
+
|
|
430
|
+
+//# sourceMappingURL=types.d.ts.map
|
|
431
|
+
--- a/@code-yeongyu/senpi/dist/modes/rpc/client-input-handler.js
|
|
432
|
+
+++ b/@code-yeongyu/senpi/dist/modes/rpc/client-input-handler.js
|
|
433
|
+
@@ -8,6 +8,7 @@
|
|
434
|
+
/** The same admission path serves classic stdio, shared sessions, and worker sessions. */
|
|
435
|
+
export async function handleClientInput(session, command, io) {
|
|
436
|
+
try {
|
|
437
|
+
+ command = { ...command, ...clientMessageIdentity(command) };
|
|
438
|
+
const admissions = ClientAdmissions.forSession(session);
|
|
439
|
+
let disposition;
|
|
440
|
+
let responded = false;
|
|
441
|
+
@@ -123,4 +124,4 @@
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
-//# sourceMappingURL=client-input-handler.js.map
|
|
446
|
+
|
|
447
|
+
+//# sourceMappingURL=client-input-handler.js.map
|
|
448
|
+
--- a/@code-yeongyu/senpi/dist/modes/rpc/connection-handler.js
|
|
449
|
+
+++ b/@code-yeongyu/senpi/dist/modes/rpc/connection-handler.js
|
|
450
|
+
@@ -310,7 +310,8 @@
|
|
451
|
+
resolve(defaultValue);
|
|
452
|
+
};
|
|
453
|
+
opts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
454
|
+
- if (opts?.timeout) {
|
|
455
|
+
+ // zgw: a required question degraded to dialogs has an infinite timeout; setTimeout(Infinity) fires at once.
|
|
456
|
+
+ if (opts?.timeout && Number.isFinite(opts.timeout)) {
|
|
457
|
+
timeoutId = setTimeout(() => {
|
|
458
|
+
cleanup();
|
|
459
|
+
resolve(defaultValue);
|
|
460
|
+
@@ -331,12 +332,27 @@
|
|
461
|
+
* Create an extension UI context that uses the RPC protocol.
|
|
462
|
+
*/
|
|
463
|
+
const createExtensionUIContext = () => ({
|
|
464
|
+
- question: (request, opts) => clientCapabilities?.includes(QUESTION_CAPABILITY)
|
|
465
|
+
+ // zgw (bug 151): a binding made without a connection (a handoff reopen) asks the session's live connections.
|
|
466
|
+
+ question: (request, opts) => clientCapabilities?.includes(QUESTION_CAPABILITY) || options.clientInfo?.sessionHasCapability?.(QUESTION_CAPABILITY)
|
|
467
|
+
? questions.ask(request, opts)
|
|
468
|
+
: degradeQuestion(createExtensionUIContext(), request, opts),
|
|
469
|
+
- select: (title, options, opts) => createDialogPromise(opts, undefined, { method: "select", title, options, timeout: opts?.timeout, ...dialogCall(opts) }, (r) => ("cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined)),
|
|
470
|
+
+ select: (title, options, opts) => createDialogPromise(opts, undefined, {
|
|
471
|
+
+ method: "select",
|
|
472
|
+
+ title,
|
|
473
|
+
+ options,
|
|
474
|
+
+ timeout: opts?.timeout,
|
|
475
|
+
+ ...dialogCall(opts),
|
|
476
|
+
+ nativePermission: opts?.nativePermission ?? null,
|
|
477
|
+
+ }, (r) => ("cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined)),
|
|
478
|
+
confirm: (title, message, opts) => createDialogPromise(opts, false, { method: "confirm", title, message, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? false : "confirmed" in r ? r.confirmed : false),
|
|
479
|
+
- input: (title, placeholder, opts) => createDialogPromise(opts, undefined, { method: "input", title, placeholder, timeout: opts?.timeout, ...dialogCall(opts) }, (r) => ("cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined)),
|
|
480
|
+
+ input: (title, placeholder, opts) => createDialogPromise(opts, undefined, {
|
|
481
|
+
+ method: "input",
|
|
482
|
+
+ title,
|
|
483
|
+
+ placeholder,
|
|
484
|
+
+ timeout: opts?.timeout,
|
|
485
|
+
+ ...dialogCall(opts),
|
|
486
|
+
+ required: opts?.required,
|
|
487
|
+
+ }, (r) => ("cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined)),
|
|
488
|
+
notify(message, type) {
|
|
489
|
+
// Fire and forget - no response needed
|
|
490
|
+
output({
|
|
491
|
+
@@ -870,6 +886,8 @@
|
|
492
|
+
await handleClientInput(session, command, { output, promptCalls });
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
495
|
+
+ case "resume_queued_input":
|
|
496
|
+
+ return success(id, "resume_queued_input", session.resumeQueuedInput(command.clientInputId, command.queueEpoch));
|
|
497
|
+
case "abort":
|
|
498
|
+
if (command.intent === "stop" && !hasRole(INTERACTIVE_USER_CAPABILITY))
|
|
499
|
+
return error(id, "abort", "stop_requires_interactive_client: declare interactive_user with set_client_info first");
|
|
500
|
+
@@ -1673,4 +1691,4 @@
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
-//# sourceMappingURL=connection-handler.js.map
|
|
505
|
+
|
|
506
|
+
+//# sourceMappingURL=connection-handler.js.map
|
|
507
|
+
--- a/@code-yeongyu/senpi/dist/modes/rpc/connection-question-bridge.js
|
|
508
|
+
+++ b/@code-yeongyu/senpi/dist/modes/rpc/connection-question-bridge.js
|
|
509
|
+
@@ -76,7 +76,9 @@
|
|
510
|
+
const arm = () => {
|
|
511
|
+
clearTimeout(timer);
|
|
512
|
+
frame.deadlineAtMs = Date.now() + timeout;
|
|
513
|
+
- timer = setTimeout(() => finish("timed_out"), timeout);
|
|
514
|
+
+ // zgw: a required question (infinite timeout) has no timer; setTimeout(Infinity) fires at once.
|
|
515
|
+
+ if (Number.isFinite(timeout))
|
|
516
|
+
+ timer = setTimeout(() => finish("timed_out"), timeout);
|
|
517
|
+
};
|
|
518
|
+
this.pending.set(id, {
|
|
519
|
+
frame,
|
|
520
|
+
@@ -174,4 +176,4 @@
|
|
521
|
+
unanswered,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
-//# sourceMappingURL=connection-question-bridge.js.map
|
|
525
|
+
|
|
526
|
+
+//# sourceMappingURL=connection-question-bridge.js.map
|
|
527
|
+
--- a/@code-yeongyu/senpi/dist/modes/rpc/multi-session-host.js
|
|
528
|
+
+++ b/@code-yeongyu/senpi/dist/modes/rpc/multi-session-host.js
|
|
529
|
+
@@ -26,7 +26,7 @@
|
|
530
|
+
import { hostGeneration, hostInstanceId, protocolIdentity } from "./protocol-identity.js";
|
|
531
|
+
import { rpcCommandShapeError } from "./rpc-input-validation.js";
|
|
532
|
+
import { computeRuntimeBuildId, RUNTIME_IDENTITY_HANDOVER_CAPABILITY } from "./runtime-build-id.js";
|
|
533
|
+
-import { SessionCommandRouter } from "./session-command-router.js";
|
|
534
|
+
+import { markHostStopping, SessionCommandRouter } from "./session-command-router.js";
|
|
535
|
+
import { SessionEventWriter } from "./session-event-writer.js";
|
|
536
|
+
import { canonicalSessionPath } from "./session-path-key.js";
|
|
537
|
+
import { RpcSessionRegistry, RpcSessionRegistryError } from "./session-registry.js";
|
|
538
|
+
@@ -585,9 +585,10 @@
|
|
539
|
+
function registerShutdownSignals(shutdown) {
|
|
540
|
+
for (const signal of process.platform === "win32" ? ["SIGTERM"] : ["SIGTERM", "SIGHUP"]) {
|
|
541
|
+
process.on(signal, () => {
|
|
542
|
+
+ markHostStopping();
|
|
543
|
+
killTrackedDetachedChildren();
|
|
544
|
+
void shutdown(signal === "SIGHUP" ? 129 : 143);
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
-//# sourceMappingURL=multi-session-host.js.map
|
|
549
|
+
|
|
550
|
+
+//# sourceMappingURL=multi-session-host.js.map
|
|
551
|
+
--- a/@code-yeongyu/senpi/dist/modes/rpc/rpc-input-validation.js
|
|
552
|
+
+++ b/@code-yeongyu/senpi/dist/modes/rpc/rpc-input-validation.js
|
|
553
|
+
@@ -75,8 +75,18 @@
|
|
554
|
+
const value = command;
|
|
555
|
+
if ((value.type === "prompt" || value.type === "steer" || value.type === "follow_up") &&
|
|
556
|
+
!clientMessageIdentitySchema.safeParse(value).success) {
|
|
557
|
+
- return "clientMessageId and clientTurnId must be non-empty strings of at most 256 characters.";
|
|
558
|
+
+ return "clientInputId must be a non-empty string of at most 128 characters; clientMessageId and clientTurnId must be non-empty strings of at most 256 characters.";
|
|
559
|
+
+ }
|
|
560
|
+
+ if (value.type === "resume_queued_input" &&
|
|
561
|
+
+ (typeof value.clientInputId !== "string" ||
|
|
562
|
+
+ value.clientInputId.length === 0 ||
|
|
563
|
+
+ value.clientInputId.length > 128)) {
|
|
564
|
+
+ return "clientInputId must be a non-empty string of at most 128 characters.";
|
|
565
|
+
}
|
|
566
|
+
+ if (value.type === "resume_queued_input" &&
|
|
567
|
+
+ (typeof value.queueEpoch !== "string" || value.queueEpoch.length === 0 || value.queueEpoch.length > 128)) {
|
|
568
|
+
+ return "resume_queued_input requires a valid queueEpoch.";
|
|
569
|
+
+ }
|
|
570
|
+
if ((value.type === "dequeue_message" || value.type === "edit_queued_message") &&
|
|
571
|
+
!(typeof value.queuedId === "string" && value.queuedId.length > 0 && value.queuedId.length <= 256)) {
|
|
572
|
+
return "queuedId must be a non-empty string of at most 256 characters; read it from get_state or queue_update.";
|
|
573
|
+
@@ -269,4 +279,4 @@
|
|
574
|
+
}
|
|
575
|
+
return `RPC ${input.type} message exceeds ${MAX_RPC_MESSAGE_CHARACTERS} characters.`;
|
|
576
|
+
}
|
|
577
|
+
-//# sourceMappingURL=rpc-input-validation.js.map
|
|
578
|
+
|
|
579
|
+
+//# sourceMappingURL=rpc-input-validation.js.map
|
|
580
|
+
--- a/@code-yeongyu/senpi/dist/modes/rpc/rpc-session-state.js
|
|
581
|
+
+++ b/@code-yeongyu/senpi/dist/modes/rpc/rpc-session-state.js
|
|
582
|
+
@@ -32,6 +32,9 @@
|
|
583
|
+
paused: session.paused ?? false,
|
|
584
|
+
pausedNoticeCount: session.pausedNoticeCount ?? 0,
|
|
585
|
+
holds: session.holds ?? [],
|
|
586
|
+
+ clientInputIdentity: true,
|
|
587
|
+
+ atomicQueuedResume: true,
|
|
588
|
+
+ queueEpoch: session.queueEpoch,
|
|
589
|
+
pendingQuestions: sessionQuestionBridges.get(session)?.pendingQuestions(),
|
|
590
|
+
pendingDialogs: sessionDialogRequests.get(session)?.snapshot() ?? [],
|
|
591
|
+
model: session.model,
|
|
592
|
+
@@ -82,4 +85,4 @@
|
|
593
|
+
scopedModels: session.scopedModels?.map((entry) => ({ ...entry })) ?? [],
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
-//# sourceMappingURL=rpc-session-state.js.map
|
|
597
|
+
|
|
598
|
+
+//# sourceMappingURL=rpc-session-state.js.map
|
|
599
|
+
--- a/@code-yeongyu/senpi/dist/modes/rpc/rpc-types.d.ts
|
|
600
|
+
+++ b/@code-yeongyu/senpi/dist/modes/rpc/rpc-types.d.ts
|
|
601
|
+
@@ -44,6 +44,7 @@
|
|
602
|
+
sessionTitlePrompt?: string | false;
|
|
603
|
+
expandPromptTemplates?: boolean;
|
|
604
|
+
unknownCommandAsText?: boolean;
|
|
605
|
+
+ clientInputId?: string;
|
|
606
|
+
clientMessageId?: string;
|
|
607
|
+
clientTurnId?: string;
|
|
608
|
+
} | {
|
|
609
|
+
@@ -97,6 +98,11 @@
|
|
610
|
+
* dispatched and makes a retry with the same id a replay; without it the abort behaves exactly as before.
|
|
611
|
+
*/
|
|
612
|
+
| {
|
|
613
|
+
+ id?: string;
|
|
614
|
+
+ type: "resume_queued_input";
|
|
615
|
+
+ clientInputId: string;
|
|
616
|
+
+ queueEpoch: string;
|
|
617
|
+
+} | {
|
|
618
|
+
id?: string;
|
|
619
|
+
type: "abort";
|
|
620
|
+
effectId?: string;
|
|
621
|
+
@@ -917,6 +923,9 @@
|
|
622
|
+
data?: unknown;
|
|
623
|
+
sourceCounts: Record<string, number>;
|
|
624
|
+
}[];
|
|
625
|
+
+ clientInputIdentity: boolean;
|
|
626
|
+
+ atomicQueuedResume: boolean;
|
|
627
|
+
+ queueEpoch: string;
|
|
628
|
+
model?: Model<any>;
|
|
629
|
+
/**
|
|
630
|
+
* Model switch held until the next compaction, or `null` when no switch is held.
|
|
631
|
+
@@ -1215,6 +1224,14 @@
|
|
632
|
+
} | {
|
|
633
|
+
id?: string;
|
|
634
|
+
type: "response";
|
|
635
|
+
+ command: "resume_queued_input";
|
|
636
|
+
+ success: true;
|
|
637
|
+
+ data: {
|
|
638
|
+
+ disposition: "stale_epoch" | "not_queued" | "busy" | "scheduled";
|
|
639
|
+
+ };
|
|
640
|
+
+} | {
|
|
641
|
+
+ id?: string;
|
|
642
|
+
+ type: "response";
|
|
643
|
+
command: "follow_up";
|
|
644
|
+
success: true;
|
|
645
|
+
data?: ClientMessageIdentity & {
|
|
646
|
+
@@ -1868,6 +1885,11 @@
|
|
647
|
+
type: "extension_ui_request";
|
|
648
|
+
id: string;
|
|
649
|
+
method: "select";
|
|
650
|
+
+ nativePermission?: {
|
|
651
|
+
+ sessionID: string;
|
|
652
|
+
+ requestID: string;
|
|
653
|
+
+ correlationId: string;
|
|
654
|
+
+ } | null;
|
|
655
|
+
title: string;
|
|
656
|
+
options: string[];
|
|
657
|
+
timeout?: number;
|
|
658
|
+
@@ -1885,6 +1907,7 @@
|
|
659
|
+
type: "extension_ui_request";
|
|
660
|
+
id: string;
|
|
661
|
+
method: "input";
|
|
662
|
+
+ required?: boolean;
|
|
663
|
+
title: string;
|
|
664
|
+
placeholder?: string;
|
|
665
|
+
timeout?: number;
|
|
666
|
+
@@ -2193,4 +2216,4 @@
|
|
667
|
+
to: string;
|
|
668
|
+
reason: string;
|
|
669
|
+
}
|
|
670
|
+
-//# sourceMappingURL=rpc-types.d.ts.map
|
|
671
|
+
|
|
672
|
+
+//# sourceMappingURL=rpc-types.d.ts.map
|
|
673
|
+
--- a/@code-yeongyu/senpi/dist/modes/rpc/session-command-router.js
|
|
674
|
+
+++ b/@code-yeongyu/senpi/dist/modes/rpc/session-command-router.js
|
|
675
|
+
@@ -1,6 +1,7 @@
|
|
676
|
+
import { VERSION } from "../../config.js";
|
|
677
|
+
import { subscriptionLimits } from "../../core/subscription-limits/limit-store.js";
|
|
678
|
+
import { detachConnection } from "./connection-detach.js";
|
|
679
|
+
+import { interruptRequiredQuestionsForHostShutdown } from "../../core/extensions/builtin/ask-user/resume.js";
|
|
680
|
+
import { buildRpcSessionState } from "./connection-handler.js";
|
|
681
|
+
import { CONNECTION_ROLE_FIXED, ConnectionRoles, roleConnection } from "./connection-roles.js";
|
|
682
|
+
import { answerEffectFromTranscript } from "./effect-commands.js";
|
|
683
|
+
@@ -26,6 +27,12 @@
|
|
684
|
+
import { selectSweepEvictions } from "./session-sweep.js";
|
|
685
|
+
import { unloadSession } from "./session-unload.js";
|
|
686
|
+
import { liveToolMediaScope, toolMediaPersister } from "./tool-media-store.js";
|
|
687
|
+
+// zgw bug 69: set once the host received its stop signal (SIGTERM/SIGHUP); a close after that is the host stopping,
|
|
688
|
+
+// not the user or the agent.
|
|
689
|
+
+let hostStopping = false;
|
|
690
|
+
+export function markHostStopping() {
|
|
691
|
+
+ hostStopping = true;
|
|
692
|
+
+}
|
|
693
|
+
/** How often a draining host re-checks whether the work it is waiting for has settled. */
|
|
694
|
+
const DRAIN_SWEEP_MS = 50;
|
|
695
|
+
function error(id, command, code, data) {
|
|
696
|
+
@@ -666,6 +673,27 @@
|
|
697
|
+
if (opens.size === 0)
|
|
698
|
+
this.opensByConnection.delete(owner);
|
|
699
|
+
});
|
|
700
|
+
+ }
|
|
701
|
+
+ /**
|
|
702
|
+
+ * zgw (bug 151): whether the session's clients advertised a capability, read when it is needed. A binding keeps the
|
|
703
|
+
+ * capabilities of the connection that opened it, and a handoff reopen has none. Attached live connections decide;
|
|
704
|
+
+ * with none attached, any live connection that advertised it does, since a question frame is replayed on attach.
|
|
705
|
+
+ */
|
|
706
|
+
+ sessionAdvertises(sessionId, capability) {
|
|
707
|
+
+ let attached = false;
|
|
708
|
+
+ for (const [connection, sessions] of this.sessionsByConnection) {
|
|
709
|
+
+ if (!sessions.has(sessionId) || this.releasedConnections.has(connection))
|
|
710
|
+
+ continue;
|
|
711
|
+
+ attached = true;
|
|
712
|
+
+ if (this.pendingCapabilities.get(connection)?.includes(capability))
|
|
713
|
+
+ return true;
|
|
714
|
+
+ }
|
|
715
|
+
+ if (attached)
|
|
716
|
+
+ return false;
|
|
717
|
+
+ for (const [connection, capabilities] of this.pendingCapabilities)
|
|
718
|
+
+ if (!this.releasedConnections.has(connection) && this.writer.hasRegisteredConnectionCapabilities(connection) && capabilities.includes(capability))
|
|
719
|
+
+ return true;
|
|
720
|
+
+ return false;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* The successor's reopen of a handoff-parked session: the same open as an `open_session`, with no
|
|
724
|
+
@@ -677,8 +705,19 @@
|
|
725
|
+
reply: false,
|
|
726
|
+
defaults: false,
|
|
727
|
+
contextOverlay: entry.contextOverlay,
|
|
728
|
+
+ releaseUnowned: true,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
+ /**
|
|
732
|
+
+ * zgw (rotation after a handoff): the registry counts one attachment for every open, and an owner-less reopen has no
|
|
733
|
+
+ * connection that could ever release it, so a later close by the real owner left the session open. Release it as a
|
|
734
|
+
+ * detach: a retained entry stays open at zero attachments, exactly as when its last client leaves.
|
|
735
|
+
+ */
|
|
736
|
+
+ releaseUnownedReopen(sessionId) {
|
|
737
|
+
+ if (this.draining || this.registry.peek(sessionId)?.retainOnDisconnect !== true)
|
|
738
|
+
+ return false;
|
|
739
|
+
+ return this.tryClaimClose(sessionId, { drainAttachments: false, detach: true }) !== undefined;
|
|
740
|
+
+ }
|
|
741
|
+
async open(command, owner, options) {
|
|
742
|
+
// A parked session's reopen replays what its park recorded; a field it left empty stays empty
|
|
743
|
+
// rather than taking THIS host's default (senpi#3376 review).
|
|
744
|
+
@@ -796,6 +835,7 @@
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
connectionId: () => this.writer.currentConnection(),
|
|
748
|
+
+ sessionHasCapability: (capability) => this.sessionAdvertises(openedSession.sessionId, capability),
|
|
749
|
+
},
|
|
750
|
+
}));
|
|
751
|
+
if (entry.worker)
|
|
752
|
+
@@ -842,6 +882,8 @@
|
|
753
|
+
this.releaseOwnerAttachment(owner, openedSession.sessionId);
|
|
754
|
+
await this.releaseOwnedSession(openedSession.sessionId);
|
|
755
|
+
}
|
|
756
|
+
+ if (owner === undefined && options?.releaseUnowned === true)
|
|
757
|
+
+ this.releaseUnownedReopen(openedSession.sessionId);
|
|
758
|
+
const replyState = entry.worker?.snapshot?.state ?? (entry.runtime ? buildRpcSessionState(entry.runtime.session) : undefined);
|
|
759
|
+
if (!replyState)
|
|
760
|
+
throw new Error("Session state was not created");
|
|
761
|
+
@@ -1107,6 +1149,10 @@
|
|
762
|
+
* second response or an escaped rejection.
|
|
763
|
+
*/
|
|
764
|
+
async finalizeClose(sessionId, binding, terminal) {
|
|
765
|
+
+ // zgw bug 69: before the binding drops its UI requests, keep live required questions resumable.
|
|
766
|
+
+ const runtime = hostStopping ? this.registry.peek(sessionId)?.runtime : undefined;
|
|
767
|
+
+ if (runtime)
|
|
768
|
+
+ interruptRequiredQuestionsForHostShutdown(runtime.session.sessionManager);
|
|
769
|
+
const finalization = this.finalizations.get(sessionId) ?? this.createFinalization();
|
|
770
|
+
this.finalizations.set(sessionId, finalization);
|
|
771
|
+
try {
|
|
772
|
+
@@ -1159,4 +1205,4 @@
|
|
773
|
+
: `${RPC_ERROR_OPEN_FAILED}: ${cause.message}`;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
-//# sourceMappingURL=session-command-router.js.map
|
|
777
|
+
|
|
778
|
+
+//# sourceMappingURL=session-command-router.js.map
|