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.
Files changed (50) hide show
  1. package/README.ko.md +27 -5
  2. package/README.md +27 -5
  3. package/assets/agent/gateway-request-rules.md +3 -1
  4. package/assets/android/zigmini.apk +0 -0
  5. package/assets/android/zigmini.apk.json +3 -3
  6. package/assets/build.json +1 -1
  7. package/docs/manual.ko.md +57 -4
  8. package/docs/manual.md +55 -4
  9. package/host/omo-bridge/ascii-json.ts +5 -0
  10. package/host/omo-bridge/bridge.ts +50 -44
  11. package/host/omo-check/patches/omo-5.1.44-compaction-port.patch +2630 -0
  12. package/host/omo-check/patches/omo-5.1.44-gateway-port.manifest.json +329 -0
  13. package/host/omo-check/patches/omo-5.1.44-provider-port.patch +440 -0
  14. package/host/omo-check/patches/omo-5.1.44-rpc-port.patch +778 -0
  15. package/host/omo-check/patches/omo-5.1.48-compaction-port.patch +2629 -0
  16. package/host/omo-check/patches/omo-5.1.48-gateway-port.manifest.json +355 -0
  17. package/host/omo-check/patches/omo-5.1.48-provider-port.patch +440 -0
  18. package/host/omo-check/patches/omo-5.1.48-rpc-port.patch +799 -0
  19. package/host/omo-check/patches/omo-5.1.48-runtime-compat.patch +31 -0
  20. package/package.json +1 -1
  21. package/src/cli/setup.ts +6 -2
  22. package/src/decisions.ts +32 -13
  23. package/src/engine/compat.json +4 -4
  24. package/src/engine/install.ts +68 -20
  25. package/src/engine/rpc-socket.ts +36 -0
  26. package/src/engine/schema-guard.ts +34 -2
  27. package/src/hub.ts +57 -31
  28. package/src/main.ts +22 -7
  29. package/src/notification-history.ts +56 -0
  30. package/src/omo/client.ts +16 -3
  31. package/src/omo/frame-socket.ts +330 -0
  32. package/src/protocol.ts +2 -1
  33. package/src/security-allow.ts +126 -0
  34. package/src/security-check.ts +355 -37
  35. package/src/store.ts +165 -5
  36. package/src/update/handoff-system.ts +22 -0
  37. package/src/update/handoff.ts +10 -2
  38. package/src/update/helper.ts +11 -1
  39. package/src/update/request.ts +15 -2
  40. package/src/work.ts +21 -7
  41. package/web/DESIGN.md +39 -0
  42. package/web/app.js +198 -69
  43. package/web/decisions.d.ts +1 -1
  44. package/web/decisions.js +3 -0
  45. package/web/diagram.d.ts +28 -0
  46. package/web/diagram.js +228 -0
  47. package/web/index.html +13 -0
  48. package/web/security-inbox.d.ts +35 -0
  49. package/web/security-inbox.js +237 -0
  50. package/web/style.css +31 -1
@@ -0,0 +1,799 @@
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 @@ import type { ImageContent } from "@earendil-works/pi-ai";
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 @@ export type PreparedClientInput = ClientMessageIdentity & {
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 @@ export function readClientMessageIdentity(value) {
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 @@ function buildResponse(status, request, answers, comment, autoResolvedAfterMs, r
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 @@ export function createPendingQuestion(options) {
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 @@ export function createPendingQuestion(options) {
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 @@ import { getPendingQuestions } from "./registry.js";
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 @@ function findDanglingQuestions(entries) {
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 @@ function findDanglingQuestions(entries) {
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 @@ function findDanglingQuestions(entries) {
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 @@ function settleUnrestorable(pi, ctx, dangling, timeoutMs) {
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 @@ export async function resumeDanglingQuestion(pi, event, ctx) {
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 @@ export async function resumeDanglingQuestion(pi, event, ctx) {
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 @@ export function toCanonical(variant, args, options) {
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
+ @@ -81,7 +81,8 @@ export function startQuestion(initialPi, initialCtx, request, signal, initialSta
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
+ @@ -183,7 +184,7 @@ export function startQuestion(initialPi, initialCtx, request, signal, initialSta
180
+ signal: controller.signal,
181
+ // A resumed question delivers its answer as a user message (deliverAnswer), like an async one.
182
+ deliver: request.waitForAnswer && !resuming ? "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
+ @@ -288,7 +289,7 @@ export function createAskUserTool(variant, pi, state) {
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
+ @@ -349,4 +350,4 @@ export function createAskUserTool(variant, pi, state) {
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 @@ export type PermissionRepliedEvent = {
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 @@ export interface PermissionEventEmitter {
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 @@ export declare function createLocalEventEmitter(): PermissionEventEmitter & {
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 @@ export function createEventEmitter(pi) {
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 @@ export function createLocalEventEmitter() {
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 @@ export function createLocalEventEmitter() {
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 @@ export default function permissionSystemExtension(pi) {
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 @@ export default function permissionSystemExtension(pi) {
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 @@ export const INTERNAL_PERMISSION_TOOLS = new Set([
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 @@ export async function showPermissionPrompt(ctx, request, signal) {
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 @@ export async function showPermissionPrompt(ctx, request, signal) {
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 @@ function formatRequestForDisplay(request) {
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 @@ export class PermissionService {
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 @@ export class PermissionService {
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 @@ export class PermissionService {
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 @@ export class PermissionService {
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 @@ export type PermissionDecision = Reply | "allow";
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 @@ export type ReplyInput = {
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 @@ export type PendingEntry = {
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 @@ import { RPC_ERROR_UNKNOWN_COMMAND } from "./rpc-types.js";
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
+ @@ -128,4 +129,4 @@ export async function handleClientInput(session, command, io) {
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
+ @@ -322,7 +322,8 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
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
+ @@ -356,12 +357,27 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
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
+ @@ -956,6 +972,8 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
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
+ @@ -1803,4 +1821,4 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
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
+ @@ -79,7 +79,9 @@ export class ConnectionQuestionBridge {
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
+ @@ -190,4 +192,4 @@ export async function degradeQuestion(ui, request, opts) {
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
+ @@ -27,7 +27,7 @@ import { attachJsonlLineReader, MAX_RPC_LINE_CHARACTERS } from "./jsonl.js";
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
+ @@ -556,9 +556,10 @@ function hostLog(message) {
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
+ @@ -76,7 +76,17 @@ export function rpcCommandPayloadError(command) {
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 === "prompt" && value.toolBarrier !== undefined) {
571
+ const barrierError = toolBarrierSpecError(value.toolBarrier);
572
+ @@ -281,4 +291,4 @@ export function rpcMessageLengthError(command) {
573
+ }
574
+ return `RPC ${input.type} message exceeds ${MAX_RPC_MESSAGE_CHARACTERS} characters.`;
575
+ }
576
+ -//# sourceMappingURL=rpc-input-validation.js.map
577
+
578
+ +//# sourceMappingURL=rpc-input-validation.js.map
579
+ --- a/@code-yeongyu/senpi/dist/modes/rpc/rpc-session-state.js
580
+ +++ b/@code-yeongyu/senpi/dist/modes/rpc/rpc-session-state.js
581
+ @@ -33,6 +33,9 @@ export function buildRpcSessionState(session, lastAbortSource) {
582
+ pausedNoticeCount: session.pausedNoticeCount ?? 0,
583
+ holds: session.holds ?? [],
584
+ lifecycle: session.lifecycle,
585
+ + clientInputIdentity: true,
586
+ + atomicQueuedResume: true,
587
+ + queueEpoch: session.queueEpoch,
588
+ pendingQuestions: sessionQuestionBridges.get(session)?.pendingQuestions(),
589
+ pendingDialogs: sessionDialogRequests.get(session)?.snapshot() ?? [],
590
+ model: session.model,
591
+ @@ -87,4 +90,4 @@ export function buildRpcSessionState(session, lastAbortSource) {
592
+ scopedModels: session.scopedModels?.map((entry) => ({ ...entry })) ?? [],
593
+ };
594
+ }
595
+ -//# sourceMappingURL=rpc-session-state.js.map
596
+
597
+ +//# sourceMappingURL=rpc-session-state.js.map
598
+ --- a/@code-yeongyu/senpi/dist/modes/rpc/session-command-router.js
599
+ +++ b/@code-yeongyu/senpi/dist/modes/rpc/session-command-router.js
600
+ @@ -4,6 +4,7 @@ import { RouterContext } from "./session-router-context.js";
601
+ import { beginDrain, sweepIdleSessions } from "./session-router-occupancy.js";
602
+ import { reopenParked } from "./session-router-open.js";
603
+ import { handle } from "./session-router-routing.js";
604
+ +export { markHostStopping } from "./session-router-close.js";
605
+ export class SessionCommandRouter {
606
+ constructor(...args) {
607
+ this.context = new RouterContext(...args);
608
+ @@ -41,4 +42,4 @@ export class SessionCommandRouter {
609
+ this.context.hostMemoryView = view;
610
+ }
611
+ }
612
+ -//# sourceMappingURL=session-command-router.js.map
613
+
614
+ +//# sourceMappingURL=session-command-router.js.map
615
+ --- a/@code-yeongyu/senpi/dist/modes/rpc/session-router-close.js
616
+ +++ b/@code-yeongyu/senpi/dist/modes/rpc/session-router-close.js
617
+ @@ -1,3 +1,8 @@
618
+ +import { interruptRequiredQuestionsForHostShutdown } from "../../core/extensions/builtin/ask-user/resume.js";
619
+ +let hostStopping = false;
620
+ +export function markHostStopping() {
621
+ + hostStopping = true;
622
+ +}
623
+ import { clearHandoverParkedSession } from "./host-handover-parked.js";
624
+ import { RPC_ERROR_UNKNOWN_SESSION } from "./rpc-types.js";
625
+ import { forgetSessionOwnership, releaseOwnerAttachment } from "./session-router-attachments.js";
626
+ @@ -91,6 +96,9 @@ export async function close(host, command) {
627
+ * second response or an escaped rejection.
628
+ */
629
+ export async function finalizeClose(host, sessionId, binding, terminal) {
630
+ + const runtime = hostStopping ? host.registry.peek(sessionId)?.runtime : undefined;
631
+ + if (runtime)
632
+ + interruptRequiredQuestionsForHostShutdown(runtime.session.sessionManager);
633
+ const finalization = host.finalizations.get(sessionId) ?? createFinalization();
634
+ host.finalizations.set(sessionId, finalization);
635
+ try {
636
+ @@ -178,4 +186,4 @@ export async function dispose(host) {
637
+ host.uninstallEffectSink?.();
638
+ host.uninstallEffectSink = undefined;
639
+ }
640
+ -//# sourceMappingURL=session-router-close.js.map
641
+
642
+ +//# sourceMappingURL=session-router-close.js.map
643
+ --- a/@code-yeongyu/senpi/dist/modes/rpc/session-router-open.js
644
+ +++ b/@code-yeongyu/senpi/dist/modes/rpc/session-router-open.js
645
+ @@ -3,7 +3,24 @@ import { clearHandoverParkedSession } from "./host-handover-parked.js";
646
+ import { parkedOpenCommand } from "./host-handover-parked-open.js";
647
+ import { sessionHeldCheck } from "./session-held.js";
648
+ import { forgetSessionOwnership, releaseOwnedSession, releaseOwnerAttachment } from "./session-router-attachments.js";
649
+ -import { close } from "./session-router-close.js";
650
+ +import { close, tryClaimClose } from "./session-router-close.js";
651
+ +export function sessionAdvertises(host, sessionId, capability) {
652
+ + let attached = false;
653
+ + for (const [connection, sessions] of host.sessionsByConnection) {
654
+ + if (!sessions.has(sessionId) || host.releasedConnections.has(connection))
655
+ + continue;
656
+ + attached = true;
657
+ + if (host.pendingCapabilities.get(connection)?.includes(capability))
658
+ + return true;
659
+ + }
660
+ + if (attached)
661
+ + return false;
662
+ + for (const [connection, capabilities] of host.pendingCapabilities)
663
+ + if (!host.releasedConnections.has(connection) && host.writer.hasRegisteredConnectionCapabilities(connection) && capabilities.includes(capability))
664
+ + return true;
665
+ + return false;
666
+ + }
667
+ +
668
+ import { code, detail, error } from "./session-router-errors.js";
669
+ import { sweepDrain } from "./session-router-occupancy.js";
670
+ import { invalidSpawnedBy, validatedOpenProfile } from "./session-router-open-profile.js";
671
+ @@ -68,6 +85,7 @@ export function reopenParked(host, entry) {
672
+ reply: false,
673
+ defaults: false,
674
+ contextOverlay: entry.contextOverlay,
675
+ + releaseUnowned: true,
676
+ });
677
+ }
678
+ export async function open(host, command, owner, options) {
679
+ @@ -137,6 +155,7 @@ export async function open(host, command, owner, options) {
680
+ }
681
+ },
682
+ connectionId: () => host.writer.currentConnection(),
683
+ + sessionHasCapability: (capability) => sessionAdvertises(host, openedSession.sessionId, capability),
684
+ relocationConnection: () => relocationConnection(host, openedSession.sessionId),
685
+ deliverTo: (connection, deliver) => host.writer.withConnection(connection, deliver),
686
+ },
687
+ @@ -185,6 +204,8 @@ export async function open(host, command, owner, options) {
688
+ releaseOwnerAttachment(host, owner, openedSession.sessionId);
689
+ await releaseOwnedSession(host, openedSession.sessionId);
690
+ }
691
+ + if (owner === undefined && options?.releaseUnowned === true && !host.draining && entry.retainOnDisconnect === true)
692
+ + tryClaimClose(host, openedSession.sessionId, { drainAttachments: false, detach: true });
693
+ const replyState = entry.worker?.snapshot?.state ?? (entry.runtime ? buildRpcSessionState(entry.runtime.session) : undefined);
694
+ if (!replyState)
695
+ throw new Error("Session state was not created");
696
+ @@ -227,4 +248,4 @@ export async function open(host, command, owner, options) {
697
+ return error(command.id, "open_session", code(cause), detail(cause));
698
+ }
699
+ }
700
+ -//# sourceMappingURL=session-router-open.js.map
701
+
702
+ +//# sourceMappingURL=session-router-open.js.map
703
+ --- a/@code-yeongyu/senpi/dist/modes/rpc/rpc-session-command-types.d.ts
704
+ +++ b/@code-yeongyu/senpi/dist/modes/rpc/rpc-session-command-types.d.ts
705
+ @@ -16,6 +16,7 @@ export type RpcSessionCommand = {
706
+ sessionTitlePrompt?: string | false;
707
+ expandPromptTemplates?: boolean;
708
+ unknownCommandAsText?: boolean;
709
+ + clientInputId?: string;
710
+ clientMessageId?: string;
711
+ clientTurnId?: string;
712
+ /** Arm the tool barrier for the turn this prompt starts (capability `tool_barrier`, senpi#3717). */
713
+ @@ -77,6 +78,11 @@ export type RpcSessionCommand = {
714
+ * dispatched and makes a retry with the same id a replay; without it the abort behaves exactly as before.
715
+ */
716
+ | {
717
+ + id?: string;
718
+ + type: "resume_queued_input";
719
+ + clientInputId: string;
720
+ + queueEpoch: string;
721
+ +} | {
722
+ id?: string;
723
+ type: "abort";
724
+ effectId?: string;
725
+ @@ -508,4 +514,4 @@ export type RpcWhenIdle = "queue" | "start";
726
+ export declare const STEER_WHEN_IDLE_CAPABILITY = "steer_when_idle";
727
+ /** Capability: `record_user_activity` reports composer draft activity (senpi#3859). */
728
+ export declare const USER_ACTIVITY_CAPABILITY = "user_activity";
729
+ -//# sourceMappingURL=rpc-session-command-types.d.ts.map
730
+
731
+ +//# sourceMappingURL=rpc-session-command-types.d.ts.map
732
+ --- a/@code-yeongyu/senpi/dist/modes/rpc/rpc-session-state-types.d.ts
733
+ +++ b/@code-yeongyu/senpi/dist/modes/rpc/rpc-session-state-types.d.ts
734
+ @@ -71,6 +71,9 @@ export interface RpcSessionState {
735
+ sourceCounts: Record<string, number>;
736
+ }[];
737
+ lifecycle: import("../../core/session-lifecycle-types.ts").SessionLifecycle;
738
+ + clientInputIdentity: boolean;
739
+ + atomicQueuedResume: boolean;
740
+ + queueEpoch: string;
741
+ model?: Model<any>;
742
+ /**
743
+ * Model switch held until the next compaction, or `null` when no switch is held.
744
+ @@ -159,4 +162,4 @@ export interface RpcSessionState {
745
+ /** Live select, confirm, input and editor request frames, including across session replacements; [] when none. */
746
+ pendingDialogs?: RpcExtensionUIRequest[];
747
+ }
748
+ -//# sourceMappingURL=rpc-session-state-types.d.ts.map
749
+
750
+ +//# sourceMappingURL=rpc-session-state-types.d.ts.map
751
+ --- a/@code-yeongyu/senpi/dist/modes/rpc/rpc-mutation-response-types.d.ts
752
+ +++ b/@code-yeongyu/senpi/dist/modes/rpc/rpc-mutation-response-types.d.ts
753
+ @@ -55,6 +55,14 @@ export type RpcMutationResponse = {
754
+ admission?: ClientMessageAdmission;
755
+ heldAsNotice?: boolean;
756
+ };
757
+ +} | {
758
+ + id?: string;
759
+ + type: "response";
760
+ + command: "resume_queued_input";
761
+ + success: true;
762
+ + data: {
763
+ + disposition: "stale_epoch" | "not_queued" | "busy" | "scheduled";
764
+ + };
765
+ } | {
766
+ id?: string;
767
+ type: "response";
768
+ @@ -391,4 +399,4 @@ export type RpcMutationResponse = {
769
+ errorCode?: string;
770
+ errorData?: unknown;
771
+ };
772
+ -//# sourceMappingURL=rpc-mutation-response-types.d.ts.map
773
+
774
+ +//# sourceMappingURL=rpc-mutation-response-types.d.ts.map
775
+ --- a/@code-yeongyu/senpi/dist/modes/rpc/rpc-question-types.d.ts
776
+ +++ b/@code-yeongyu/senpi/dist/modes/rpc/rpc-question-types.d.ts
777
+ @@ -33,6 +33,7 @@ export type RpcExtensionUIRequest = {
778
+ type: "extension_ui_request";
779
+ id: string;
780
+ method: "select";
781
+ + nativePermission?: { sessionID: string; requestID: string; correlationId: string } | null;
782
+ /** Permanent MCP env grant; only a grant_select presentation may answer. */
783
+ grantSelect?: boolean;
784
+ title: string;
785
+ @@ -52,6 +53,7 @@ export type RpcExtensionUIRequest = {
786
+ type: "extension_ui_request";
787
+ id: string;
788
+ method: "input";
789
+ + required?: boolean;
790
+ title: string;
791
+ placeholder?: string;
792
+ timeout?: number;
793
+ @@ -155,4 +157,4 @@ export type RpcQuestionResolvedEvent = {
794
+ unanswered: string[];
795
+ deadlineAtMs?: number;
796
+ };
797
+ -//# sourceMappingURL=rpc-question-types.d.ts.map
798
+
799
+ +//# sourceMappingURL=rpc-question-types.d.ts.map