@code-yeongyu/senpi 2026.10.7 → 2026.10.9

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 (65) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/bundle/chunks/{anthropic-messages-4MNFPQ66.js → anthropic-messages-CVXI6CLT.js} +1 -1
  3. package/dist/bundle/chunks/{app-server-command-SO5RJ2N7.js → app-server-command-2HNDZGTR.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-BE66QJ3Z.js → chunk-3YXB7Z7O.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-JJNIU2A2.js → chunk-CB77NQMN.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-EUU6W5B3.js → chunk-DERNLWDS.js} +43 -43
  7. package/dist/bundle/chunks/{chunk-K4AZKFRB.js → chunk-J5MUMVWB.js} +2 -2
  8. package/dist/bundle/chunks/{chunk-D2W3MXRF.js → chunk-LSE7KE2C.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-V2VQ4BHG.js → chunk-M2IHGL3M.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-7SMFF2QJ.js → chunk-M3BQ2PAQ.js} +4 -4
  11. package/dist/bundle/chunks/{chunk-HY4AOLON.js → chunk-MJGHZT4N.js} +3 -3
  12. package/dist/bundle/chunks/{chunk-7A3WRAKP.js → chunk-Q2ZTPMWD.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-ESSQUSRC.js → chunk-SB22X6ZR.js} +1 -1
  14. package/dist/bundle/chunks/{cli-main-XBQN73O2.js → cli-main-VMDAGZCA.js} +1 -1
  15. package/dist/bundle/chunks/{help-fast-path-FUMH5NSA.js → help-fast-path-JBQLYO7W.js} +1 -1
  16. package/dist/bundle/chunks/{host-command-YXQAXNKK.js → host-command-HTYYQBSG.js} +1 -1
  17. package/dist/bundle/chunks/{host-lifecycle-L4XQIATP.js → host-lifecycle-XZF6IKKO.js} +1 -1
  18. package/dist/bundle/chunks/host-lifecycle.js +3 -3
  19. package/dist/bundle/chunks/{interactive-mode-ROO5W56P.js → interactive-mode-U7IH4VMJ.js} +1 -1
  20. package/dist/bundle/chunks/{list-tips-JQDZPB6C.js → list-tips-IIRFXBTO.js} +1 -1
  21. package/dist/bundle/chunks/{multi-session-host-5WTKAQJM.js → multi-session-host-HHIC6N7C.js} +1 -1
  22. package/dist/bundle/chunks/{package-manager-cli-NCD5FQGV.js → package-manager-cli-2FXWPM2U.js} +1 -1
  23. package/dist/bundle/chunks/{rotation-stream-7E37KOCX.js → rotation-stream-ZJQ6I2RZ.js} +1 -1
  24. package/dist/bundle/chunks/{rpc-mode-MWURKCP2.js → rpc-mode-K5YGBGQO.js} +1 -1
  25. package/dist/bundle/chunks/{schedule-command-UTXB465F.js → schedule-command-CVAPK2WQ.js} +1 -1
  26. package/dist/bundle/chunks/{sdk-EO76I33H.js → sdk-U52J75K2.js} +50 -50
  27. package/dist/bundle/chunks/{session-control-endpoint-J6RHZXWB.js → session-control-endpoint-THEF3K7M.js} +1 -1
  28. package/dist/bundle/chunks/{session-picker-VTTRFGE7.js → session-picker-R5N2EKUS.js} +1 -1
  29. package/dist/bundle/chunks/session-worker.js +104 -104
  30. package/dist/bundle/cli.js +1 -1
  31. package/dist/bundle/index.js +1 -1
  32. package/dist/bundle/rpc-entry.js +1 -1
  33. package/dist/bundle/runtime-manifest.json +1 -1
  34. package/dist/core/agent-session.d.ts +7 -2
  35. package/dist/core/agent-session.js +31 -5
  36. package/dist/core/continue-from-leaf.d.ts +21 -0
  37. package/dist/core/continue-from-leaf.js +39 -0
  38. package/dist/core/dynamic-prompt/handoff.js +3 -3
  39. package/dist/core/dynamic-prompt/intent-gate.js +1 -1
  40. package/dist/core/dynamic-prompt/style.js +1 -1
  41. package/dist/core/extensions/builtin/permission-system/index.js +4 -0
  42. package/dist/core/extensions/builtin/permission-system/prompt.js +10 -2
  43. package/dist/core/extensions/builtin/permission-system/types.d.ts +3 -1
  44. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js +2 -2
  45. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js +2 -2
  46. package/dist/core/extensions/builtin/prompt-preset/claude-opus-5-5.js +2 -2
  47. package/dist/core/extensions/builtin/prompt-preset/claude-opus-5.js +2 -2
  48. package/dist/core/extensions/builtin/prompt-preset/claude-sonnet-5-5.js +2 -2
  49. package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js +3 -3
  50. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js +3 -3
  51. package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.d.ts +2 -2
  52. package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js +4 -4
  53. package/dist/core/extensions/builtin/prompt-preset/gpt-surface.js +2 -2
  54. package/dist/core/extensions/builtin/prompt-preset/grok-4.5.js +2 -2
  55. package/dist/core/extensions/builtin/prompt-preset/grok-4.6.js +1 -1
  56. package/dist/core/extensions/builtin/prompt-preset/grok-4.7.js +1 -1
  57. package/dist/core/extensions/builtin/prompt-preset/kimi-k3.js +2 -2
  58. package/dist/core/extensions/types.d.ts +4 -0
  59. package/dist/modes/rpc/connection-handler.js +11 -2
  60. package/dist/modes/rpc/host-lifecycle.js +5 -28
  61. package/dist/modes/rpc/host-run-activity.d.ts +13 -0
  62. package/dist/modes/rpc/host-run-activity.js +39 -0
  63. package/dist/modes/rpc/rpc-types.d.ts +5 -0
  64. package/docs/rpc.md +13 -0
  65. package/package.json +7 -7
@@ -121,11 +121,11 @@ export declare const GPT6_ASTRA_RULES: readonly [{
121
121
  }, {
122
122
  readonly id: "handoff-report";
123
123
  readonly concern: "reporting";
124
- readonly directive: "At a handoff - the todo list's creation (in the message that creates it, after the routing line, or the next one), a todo phase change, a blocker or plan change, the final message; the routing line is not one - first work out what the user asked for and what they need to know now, then open with one block:\n\n> [Outcome so far] toward [the user's original ask and the result they wanted]. You need: [ledger N/M done, findings, blockers]. Now: [todo task in progress]. Next: [next open task].\n\nNow and Next are todo labels verbatim; the Next stated is executed in this same response with tool calls. Between handoffs, no narration. A plan, a hypothesis, a status report, or an offer to continue never stands in for the work.";
124
+ readonly directive: "At a handoff - the todo list's creation (in the message that creates it, after the routing line, or the next one), a todo phase change, a blocker or plan change, the final message of a turn that did work (a reply that only answers a question is the answer itself); the routing line is not one - first work out what the user asked for and what they need to know now, then open with one block:\n\n[Outcome so far] toward [the user's original ask and the result they wanted]. You need: [ledger N/M done, findings, blockers]. Now: [todo task in progress]. Next: [next open task].\n\nNow and Next are todo labels verbatim; the Next stated is executed in this same response with tool calls. Between handoffs, no narration. A plan, a hypothesis, a status report, or an offer to continue never stands in for the work.";
125
125
  }, {
126
126
  readonly id: "final-message-shape";
127
127
  readonly concern: "reporting";
128
- readonly directive: "The final message is the handoff block and stands alone: the outcome first, then in its You need slot what a reader needs to trust it - the checks that ran, summarized rather than listed, anything left unverified, and any pre-existing problem you left in place - ordered so the conclusion is easiest to check rather than in the order you worked. Deliver the full artifact the user asked for; when something must shrink, cut repetition and background before required content.";
128
+ readonly directive: "The final message of work is the handoff block and stands alone: the outcome first, then in its You need slot what a reader needs to trust it - the checks that ran, summarized rather than listed, anything left unverified, and any pre-existing problem you left in place - ordered so the conclusion is easiest to check rather than in the order you worked. Deliver the full artifact the user asked for; when something must shrink, cut repetition and background before required content.";
129
129
  }];
130
130
  export declare function buildGpt6AstraPrompt(options: BuildDynamicSystemPromptOptions): string;
131
131
  //# sourceMappingURL=gpt-6-astra.d.ts.map
@@ -206,8 +206,8 @@ const PLAIN_PROSE = "Write the way a careful engineer writes to a colleague: pla
206
206
  const SLOP_BAN = 'Leave out stock phrases and filler: "delve", "leverage", "foster", "it\'s worth noting", "importantly", "genuinely", "Bottom line:", "In short:", "The simplest mental model is:", "Question? Answer." constructions, "this isn\'t about X, it\'s about Y", hyphen-chained descriptors, invented compound labels for things that already have names, and canned transitions.';
207
207
  const DIRECT_STATEMENTS = "State the action or finding directly and connect it to its purpose or consequence. Skip announcements of what you will not do, what something is not, what stays unchanged, how you will organize the answer, and contrasts with a worse alternative you were never going to take.";
208
208
  const NO_REFLEXIVE_APOLOGY = "Apologize or fault yourself only for an avoidable mistake of your own, and then plainly: acknowledge it, correct it, move on. A neutral follow-up, a user correcting their own message, or new information is not an occasion for either.";
209
- const HANDOFF_REPORT = "At a handoff - the todo list's creation (in the message that creates it, after the routing line, or the next one), a todo phase change, a blocker or plan change, the final message; the routing line is not one - first work out what the user asked for and what they need to know now, then open with one block:\n\n> [Outcome so far] toward [the user's original ask and the result they wanted]. You need: [ledger N/M done, findings, blockers]. Now: [todo task in progress]. Next: [next open task].\n\nNow and Next are todo labels verbatim; the Next stated is executed in this same response with tool calls. Between handoffs, no narration. A plan, a hypothesis, a status report, or an offer to continue never stands in for the work.";
210
- const FINAL_MESSAGE_SHAPE = "The final message is the handoff block and stands alone: the outcome first, then in its You need slot what a reader needs to trust it - the checks that ran, summarized rather than listed, anything left unverified, and any pre-existing problem you left in place - ordered so the conclusion is easiest to check rather than in the order you worked. Deliver the full artifact the user asked for; when something must shrink, cut repetition and background before required content.";
209
+ const HANDOFF_REPORT = "At a handoff - the todo list's creation (in the message that creates it, after the routing line, or the next one), a todo phase change, a blocker or plan change, the final message of a turn that did work (a reply that only answers a question is the answer itself); the routing line is not one - first work out what the user asked for and what they need to know now, then open with one block:\n\n[Outcome so far] toward [the user's original ask and the result they wanted]. You need: [ledger N/M done, findings, blockers]. Now: [todo task in progress]. Next: [next open task].\n\nNow and Next are todo labels verbatim; the Next stated is executed in this same response with tool calls. Between handoffs, no narration. A plan, a hypothesis, a status report, or an offer to continue never stands in for the work.";
210
+ const FINAL_MESSAGE_SHAPE = "The final message of work is the handoff block and stands alone: the outcome first, then in its You need slot what a reader needs to trust it - the checks that ran, summarized rather than listed, anything left unverified, and any pre-existing problem you left in place - ordered so the conclusion is easiest to check rather than in the order you worked. Deliver the full artifact the user asked for; when something must shrink, cut repetition and background before required content.";
211
211
  export const GPT6_ASTRA_RULES = [
212
212
  { id: "initiative-bias", concern: "initiative", directive: INITIATIVE_BIAS },
213
213
  { id: "approval-last", concern: "initiative", directive: APPROVAL_LAST },
@@ -244,7 +244,7 @@ export const GPT6_ASTRA_RULES = [
244
244
  const INTENT_GATE_LEAD = {
245
245
  terminal: `Open a new request with one short routing line:
246
246
 
247
- > I read this as [intent] - [plan]. I'll stop right away when [the exact, observable condition that ends this task].
247
+ I read this as [intent] - [plan]. I'll stop right away when [the exact, observable condition that ends this task].
248
248
 
249
249
  The declared stop condition is binding: work until it holds, then stop (see Stop Goal).`,
250
250
  app: "Open a new request by settling the exact, observable condition that ends the task. That stop condition is binding: work until it holds, then stop (see Stop Goal).",
@@ -262,7 +262,7 @@ const SURFACE_DIRECTIVE = {
262
262
  chat: {
263
263
  steering: APP_STEERING,
264
264
  handoffReport: HANDOFF_REPORT.replace(/^[\s\S]*Between handoffs, no narration\. /, `${CHAT_REPLY_RULE} `),
265
- finalMessageShape: APP_FINAL_MESSAGE_SHAPE.replace("The final message is the handoff block and stands alone: the outcome first, then in its You need slot what a reader needs", `${CHAT_FINAL_MESSAGE} and stands alone: the outcome first, then what a reader needs`),
265
+ finalMessageShape: APP_FINAL_MESSAGE_SHAPE.replace("The final message of work is the handoff block and stands alone: the outcome first, then in its You need slot what a reader needs", `${CHAT_FINAL_MESSAGE} and stands alone: the outcome first, then what a reader needs`),
266
266
  },
267
267
  };
268
268
  function buildGpt6AstraCore(context) {
@@ -1,6 +1,6 @@
1
1
  export const GPT_HANDOFF_MOMENTS = {
2
- terminal: "the todo list's creation (in the message that creates it, after the routing line, or the next one), a todo phase change, a blocker or plan change, the final message; the routing line is not one",
3
- app: "the todo list's creation (in the message that creates it or the next one), a todo phase change, a blocker or plan change, the final message",
2
+ terminal: "the todo list's creation (in the message that creates it, after the routing line, or the next one), a todo phase change, a blocker or plan change, the final message of a turn that did work (a reply that only answers a question is the answer itself); the routing line is not one",
3
+ app: "the todo list's creation (in the message that creates it or the next one), a todo phase change, a blocker or plan change, the final message of a turn that did work (a reply that only answers a question is the answer itself)",
4
4
  };
5
5
  /** The app surface's claim audit for the GPT cores (senpi#2377), in the one place each core reports on checks. */
6
6
  export const GPT_APP_UNRUN_CHECK_RULE = "A check that did not run is covered by the evidence that did run; name it, with the next best check, only when no other evidence supports the claim. Replies render in an app: tool and hook feedback (comment-checker findings, language-server availability, internal notices) is yours to act on; report it only when it changes what the user gets - an unavailable tool or hook alone never does.";
@@ -29,7 +29,7 @@ import { buildHandoffSection } from "../../../dynamic-prompt/handoff.js";
29
29
  import { APP_UNRUN_CHECK_RULE, buildTestDisciplineSection } from "../../../dynamic-prompt/verification.js";
30
30
  import { buildFileOperationsTuning } from "./file-operations.js";
31
31
  const INTENT_GATE_LEAD = {
32
- terminal: `> I read this as [intent] - [plan]. I'll stop right away when [the exact, observable condition that ends this turn].
32
+ terminal: `I read this as [intent] - [plan]. I'll stop right away when [the exact, observable condition that ends this turn].
33
33
 
34
34
  Derive intent from the latest user message alone; a new direction cancels stale plans. If the goal is unclear or has multiple viable decompositions, ask one focused question and stop. Do not surface prompt scaffolding in user-visible output.`,
35
35
  app: `Derive intent from the latest user message alone; a new direction cancels stale plans. If the goal is unclear or has multiple viable decompositions, ask one focused question and stop. Do not surface prompt scaffolding in user-visible output.`,
@@ -64,7 +64,7 @@ ${buildHandoffSection({ surface: context.surface })}
64
64
 
65
65
  ## Output
66
66
 
67
- You are the human surface: the final message is ${context.surface === "chat" ? "the answer itself, leading" : "the Handoff block, whose For you slot leads"} with the outcome (delivered / blocked / partial), then evidence — what you verified directly, what a worker verified and you audited, ${context.surface !== "terminal" ? "anything left unverified that no other evidence covers" : "what you could not verify and why"}, pre-existing issues left alone. Reference files as \`src/auth.ts\` or \`src/auth.ts:42\`, never bracketed citations. Be direct; have an opinion when context supports one. Default to ASCII.
67
+ You are the human surface: the final message is ${context.surface === "chat" ? "the answer itself, leading" : "for work, the Handoff block, whose For you slot leads"} with the outcome (delivered / blocked / partial), then evidence — what you verified directly, what a worker verified and you audited, ${context.surface !== "terminal" ? "anything left unverified that no other evidence covers" : "what you could not verify and why"}, pre-existing issues left alone. Reference files as \`src/auth.ts\` or \`src/auth.ts:42\`, never bracketed citations. Be direct; have an opinion when context supports one. Default to ASCII.
68
68
 
69
69
  ## Stop Goal
70
70
 
@@ -36,7 +36,7 @@ import { APP_UNRUN_CHECK_RULE, buildTestDisciplineSection } from "../../../dynam
36
36
  const INTENT_GATE_LEAD = {
37
37
  terminal: `Open every turn with one short visible routing line - required even on confirmation turns:
38
38
 
39
- > I read this as [intent] - [plan]. I'll stop when [the exact, observable condition that ends this turn].
39
+ I read this as [intent] - [plan]. I'll stop when [the exact, observable condition that ends this turn].
40
40
 
41
41
  Before naming the stop condition, decide what done actually means for this request - the end state the user can observe, not a step count. Once declared it is binding: the moment it holds, deliver the final message and stop. Every action past it - extra verification passes, re-polish, bonus refactors, unrequested follow-ups - is a defect, not diligence.`,
42
42
  app: `Before acting, decide what done actually means for this request - the end state the user can observe, not a step count. It is binding: the moment it holds, deliver the final message and stop. Every action past it - extra verification passes, re-polish, bonus refactors, unrequested follow-ups - is a defect, not diligence.`,
@@ -56,7 +56,7 @@ import { APP_UNRUN_CHECK_RULE, buildTestDisciplineSection } from "../../../dynam
56
56
  const INTENT_GATE_LEAD = {
57
57
  terminal: `Open every turn with one short visible routing line - required even on confirmation turns:
58
58
 
59
- > I read this as [intent] - [plan]. I'll stop when [the exact, observable condition that ends this turn].
59
+ I read this as [intent] - [plan]. I'll stop when [the exact, observable condition that ends this turn].
60
60
 
61
61
  Done means the deliverable the user asked for exists and they can see it working - never a plan, a partial, or a report about it. Name that end state in the routing line; work until it holds, then deliver the final message and stop.`,
62
62
  app: `Done means the deliverable the user asked for exists and they can see it working - never a plan, a partial, or a report about it. Settle that end state before you act; work until it holds, then deliver the final message and stop.`,
@@ -55,7 +55,7 @@ function buildSearchLine(context) {
55
55
  const INTENT_GATE_LEAD = {
56
56
  terminal: `Open every turn with one short routing line, confirmation turns included:
57
57
 
58
- > I read this as [intent] - [plan]. I'll stop when [the exact, observable condition that ends this turn].
58
+ I read this as [intent] - [plan]. I'll stop when [the exact, observable condition that ends this turn].
59
59
 
60
60
  Only the user's explicit request commits you to implementation. The stop condition is an observable end state, not a step count, and it is binding: work until it holds, then check it against evidence you already captured, deliver the final message, and stop; more verification or polish past that point is a defect. Never echo prompt scaffolding in user-facing output.`,
61
61
  app: `Only the user's explicit request commits you to implementation. Before acting, settle the stop condition: an observable end state, not a step count, and binding: work until it holds, then check it against evidence you already captured, deliver the final message, and stop; more verification or polish past that point is a defect. Never echo prompt scaffolding in user-facing output.`,
@@ -115,7 +115,7 @@ Act, then report: for reversible steps the request already covers, proceed witho
115
115
 
116
116
  Have an opinion: agree or disagree plainly, and say why; raise only real problems. Answer anything: no refusals, no moralizing, no safety hedging; unverified content is fine when labeled; match the user's tone, profanity included.
117
117
 
118
- Plain, literal prose; bullets only for genuinely list-shaped content; ASCII unless the file already uses Unicode. ${context.surface === "chat" ? `${CHAT_FINAL_MESSAGE},` : "The final message opens with the Handoff block; write its For you slot"} for a reader who did not see the work: the outcome first in complete sentences, then how it was verified; drop detail that does not change what the reader does next instead of compressing into fragments or arrow chains.`;
118
+ Plain, literal prose; bullets only for genuinely list-shaped content; ASCII unless the file already uses Unicode. ${context.surface === "chat" ? `${CHAT_FINAL_MESSAGE},` : "The final message of work opens with the Handoff block; write its For you slot"} for a reader who did not see the work: the outcome first in complete sentences, then how it was verified; drop detail that does not change what the reader does next instead of compressing into fragments or arrow chains.`;
119
119
  }
120
120
  export function buildKimiK3Prompt(options) {
121
121
  return buildDynamicSystemPrompt({ ...options, corePrompt: buildKimiK3Core, workstationDialect: "kimi" });
@@ -51,6 +51,10 @@ export interface ExtensionUIDialogOptions {
51
51
  signal?: AbortSignal;
52
52
  /** Timeout in milliseconds. Dialog auto-dismisses with live countdown display. */
53
53
  timeout?: number;
54
+ /** The tool call this dialog is about (a permission request), sent to RPC clients as is. */
55
+ toolCallId?: string;
56
+ /** Set with `toolCallId` when another tool (for example a codemode script) issued that call. */
57
+ parentToolCallId?: string;
54
58
  }
55
59
  /** Placement for extension widgets. */
56
60
  export type WidgetPlacement = "aboveEditor" | "belowEditor";
@@ -47,6 +47,15 @@ import { buildRpcSessionState } from "./rpc-session-state.js";
47
47
  import { RPC_ERROR_MEDIA_NOT_FOUND } from "./rpc-types.js";
48
48
  import { SessionExtensionUiRequests } from "./session-extension-ui-requests.js";
49
49
  export { buildRpcSessionState } from "./rpc-session-state.js";
50
+ /** The tool call a dialog is about, as wire fields (absent when the dialog is not a permission request). */
51
+ function dialogCall(opts) {
52
+ if (opts?.toolCallId === undefined)
53
+ return {};
54
+ return {
55
+ toolCallId: opts.toolCallId,
56
+ ...(opts.parentToolCallId === undefined ? {} : { parentToolCallId: opts.parentToolCallId }),
57
+ };
58
+ }
50
59
  function loadedExtensionName(path) {
51
60
  const synthetic = /^<(?:builtin|inline):([^>]+)>$/.exec(path);
52
61
  if (synthetic)
@@ -292,9 +301,9 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
292
301
  question: (request, opts) => clientCapabilities?.includes(QUESTION_CAPABILITY)
293
302
  ? questions.ask(request, opts)
294
303
  : degradeQuestion(createExtensionUIContext(), request, opts),
295
- select: (title, options, opts) => createDialogPromise(opts, undefined, { method: "select", title, options, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined),
304
+ 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)),
296
305
  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),
297
- input: (title, placeholder, opts) => createDialogPromise(opts, undefined, { method: "input", title, placeholder, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined),
306
+ 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)),
298
307
  notify(message, type) {
299
308
  // Fire and forget - no response needed
300
309
  output({
@@ -63,6 +63,7 @@ import { hostCrashCleanupPaths } from "./host-cleanup-paths.js";
63
63
  import { createHostDaemonPaths, generationPaths, HOST_DAEMON_DIR_ENV } from "./host-daemon-paths.js";
64
64
  import { releaseGeneration } from "./host-daemon-registration.js";
65
65
  import { HOST_INSTANCE_ID_ENV } from "./host-identity-env.js";
66
+ import { SessionRunActivity } from "./host-run-activity.js";
66
67
  import { watchForSupersession } from "./host-supersession.js";
67
68
  import { HOST_CLEANUP_PATHS_ENV, HOST_PUBLIC_SOCKET_ENV, HOST_SCRATCH_DIR_ENV, HOST_WATCH_FD_ENV, HOST_WATCH_PPID_ENV, } from "./host-watchdog.js";
68
69
  import { attachJsonlLineReader, MAX_RPC_LINE_CHARACTERS } from "./jsonl.js";
@@ -319,7 +320,7 @@ export async function runHostSupervisor(launch) {
319
320
  const internalSecretPath = internal.secretPath ?? socketSecretPath(internalSocket);
320
321
  const internalSecret = process.platform === "win32" ? await createSocketSecret(internalSecretPath) : undefined;
321
322
  const clients = new ClientOccupancy(() => decider.update(currentActivity()));
322
- const busySessions = new Map();
323
+ const runs = new SessionRunActivity();
323
324
  // Declared before anything that can reach `currentActivity()`. A client accepted during startup
324
325
  // asks for the activity snapshot, and a `const` read before its initializer runs is a
325
326
  // ReferenceError that fails the connection - which is how a successor's first `open_session`
@@ -461,37 +462,13 @@ export async function runHostSupervisor(launch) {
461
462
  unhealthySince: observerLink.unhealthySince(),
462
463
  now: Date.now(),
463
464
  unknownGraceMs: decider.idleExitMs,
464
- observedBusy: countBusySessions(),
465
+ observedBusy: runs.busySessions,
465
466
  }),
466
467
  };
467
468
  }
468
- function countBusySessions() {
469
- let busy = 0;
470
- for (const count of busySessions.values())
471
- if (count > 0)
472
- busy++;
473
- return busy;
474
- }
475
469
  function observeHostEvent(line) {
476
- let event;
477
- try {
478
- event = JSON.parse(line);
479
- }
480
- catch {
481
- return;
482
- }
483
- if (typeof event !== "object" || event === null)
484
- return;
485
- const { type, sessionId } = event;
486
- if (typeof sessionId !== "string")
487
- return;
488
- if (type === "agent_start")
489
- busySessions.set(sessionId, (busySessions.get(sessionId) ?? 0) + 1);
490
- else if (type === "agent_settled")
491
- busySessions.set(sessionId, Math.max(0, (busySessions.get(sessionId) ?? 1) - 1));
492
- else
493
- return;
494
- decider.update(currentActivity());
470
+ if (runs.observe(line))
471
+ decider.update(currentActivity());
495
472
  }
496
473
  async function shutdown(reason, exitCode) {
497
474
  // Single-flight: concurrent triggers (listener error, child exit, signals)
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Which sessions have an agent run in progress, from the host child's events. A run emits
3
+ * `agent_start` on every loop iteration (a provider retry, a post-compaction continue, a follow-up)
4
+ * but `agent_settled` once, so this tracks runs, not starts: a repeated start for a running session
5
+ * changes nothing. A session that closes is dropped even if its settle never arrives (#2713).
6
+ */
7
+ export declare class SessionRunActivity {
8
+ #private;
9
+ /** Applies one host event line; true when it is a run or session event the idle decision must re-read. */
10
+ observe(line: string): boolean;
11
+ get busySessions(): number;
12
+ }
13
+ //# sourceMappingURL=host-run-activity.d.ts.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Which sessions have an agent run in progress, from the host child's events. A run emits
3
+ * `agent_start` on every loop iteration (a provider retry, a post-compaction continue, a follow-up)
4
+ * but `agent_settled` once, so this tracks runs, not starts: a repeated start for a running session
5
+ * changes nothing. A session that closes is dropped even if its settle never arrives (#2713).
6
+ */
7
+ export class SessionRunActivity {
8
+ #running = new Set();
9
+ /** Applies one host event line; true when it is a run or session event the idle decision must re-read. */
10
+ observe(line) {
11
+ const event = parseHostEvent(line);
12
+ if (event === undefined)
13
+ return false;
14
+ if (event.type === "agent_start")
15
+ this.#running.add(event.sessionId);
16
+ else if (event.type === "agent_settled" || event.type === "session_closed")
17
+ this.#running.delete(event.sessionId);
18
+ else
19
+ return false;
20
+ return true;
21
+ }
22
+ get busySessions() {
23
+ return this.#running.size;
24
+ }
25
+ }
26
+ function parseHostEvent(line) {
27
+ let event;
28
+ try {
29
+ event = JSON.parse(line);
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ if (typeof event !== "object" || event === null)
35
+ return undefined;
36
+ const { type, sessionId } = event;
37
+ return typeof sessionId === "string" ? { type, sessionId } : undefined;
38
+ }
39
+ //# sourceMappingURL=host-run-activity.js.map
@@ -1319,6 +1319,9 @@ export type RpcExtensionUIRequest = {
1319
1319
  title: string;
1320
1320
  options: string[];
1321
1321
  timeout?: number;
1322
+ /** The tool call this dialog approves, when it is a permission request. */
1323
+ toolCallId?: string;
1324
+ parentToolCallId?: string;
1322
1325
  } | {
1323
1326
  type: "extension_ui_request";
1324
1327
  id: string;
@@ -1333,6 +1336,8 @@ export type RpcExtensionUIRequest = {
1333
1336
  title: string;
1334
1337
  placeholder?: string;
1335
1338
  timeout?: number;
1339
+ toolCallId?: string;
1340
+ parentToolCallId?: string;
1336
1341
  } | {
1337
1342
  type: "extension_ui_request";
1338
1343
  id: string;
package/docs/rpc.md CHANGED
@@ -3562,6 +3562,19 @@ Prompt the user to choose from a list. Dialog methods with a `timeout` field inc
3562
3562
 
3563
3563
  Expected response: `extension_ui_response` with `value` (the selected option string) or `cancelled: true`.
3564
3564
 
3565
+ A permission prompt (title `Permission required: <permission>`) also carries `toolCallId`, the id of the tool call it approves, and `parentToolCallId` when another tool (for example a codemode script) issued that call. Clients bind the prompt to that call by id: the engine raises the prompts for every call of a message before any of them runs, so the order of `tool_execution_start` events does not say which call a prompt belongs to. The `input` that follows a "Deny with feedback" choice carries the same fields.
3566
+
3567
+ ```json
3568
+ {
3569
+ "type": "extension_ui_request",
3570
+ "id": "uuid-3",
3571
+ "method": "select",
3572
+ "title": "Permission required: read\n\nPath: /project/a.txt\n\nPatterns:\n - /project/a.txt",
3573
+ "options": ["Allow once", "Allow always", "Deny", "Deny with feedback"],
3574
+ "toolCallId": "call-a"
3575
+ }
3576
+ ```
3577
+
3565
3578
  #### confirm
3566
3579
 
3567
3580
  Prompt the user for yes/no confirmation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi",
3
- "version": "2026.10.7",
3
+ "version": "2026.10.9",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -57,9 +57,9 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "@earendil-works/chord": "0.99.1",
60
- "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.7",
61
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.7",
62
- "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.7",
60
+ "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.9",
61
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.9",
62
+ "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.9",
63
63
  "@silvia-odwyer/photon-node": "0.3.4",
64
64
  "chalk": "6.0.0",
65
65
  "cross-spawn": "7.0.6",
@@ -76,10 +76,10 @@
76
76
  "typebox": "1.3.34",
77
77
  "undici": "8.10.2",
78
78
  "yaml": "2.9.1",
79
- "@anthropic-ai/claude-agent-sdk": "0.3.288",
79
+ "@anthropic-ai/claude-agent-sdk": "0.3.289",
80
80
  "@anthropic-ai/sdk": "0.127.0",
81
- "@code-yeongyu/senpi-codemode": "2026.10.7",
82
- "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.7",
81
+ "@code-yeongyu/senpi-codemode": "2026.10.9",
82
+ "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.9",
83
83
  "@modelcontextprotocol/sdk": "1.30.0",
84
84
  "@mozilla/readability": "0.6.0",
85
85
  "linkedom": "0.18.13",