@danypops/papyrus 0.27.7 → 0.27.8

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.
@@ -99,6 +99,18 @@ export interface AskQuestionParams {
99
99
  * the same live ask run concurrently, each opening its own picker for the same question.
100
100
  */
101
101
  onUpdate?: AgentToolUpdateCallback;
102
+ /**
103
+ * The tool call's OWN abort signal (execute()'s 3rd parameter) -- fires only if this specific
104
+ * tool call is genuinely interrupted (e.g. the human pressed Ctrl+C on the whole agent
105
+ * operation). Deliberately NOT `ExtensionContext.signal`: that one tracks "is the agent
106
+ * currently streaming a model response" and settles/aborts within a second or two of the
107
+ * assistant's tool_call message finishing generation -- which is normal, unrelated bookkeeping
108
+ * that happens long before a human actually answers a slow interactive prompt. A live-observed
109
+ * bug (the picker silently self-cancelling ~7-10s after opening, well before the human
110
+ * finished deciding, with their real answer then arriving disconnected as a stray follow-up)
111
+ * traced back to listening on the wrong signal here.
112
+ */
113
+ signal?: AbortSignal;
102
114
  }
103
115
 
104
116
  export interface AskAnswer {
@@ -1200,9 +1212,9 @@ async function askQuestionBlocking(
1200
1212
  let hasAnnouncedHide = false;
1201
1213
  let response: AskResponse | null;
1202
1214
  try {
1203
- diag("factory about to construct AskComponent", { hasSignal: ctx.signal !== undefined, signalAlreadyAborted: ctx.signal?.aborted ?? null, hasExplicitTimeout: params.timeout !== undefined });
1215
+ diag("factory about to construct AskComponent", { hasSignal: params.signal !== undefined, signalAlreadyAborted: params.signal?.aborted ?? null, hasExplicitTimeout: params.timeout !== undefined });
1204
1216
  const factory = (tui: TUI, theme: Theme, keybindings: KeybindingsManager, done: (result: AskResponse | null) => void) => {
1205
- if (ctx.signal) ctx.signal.addEventListener("abort", () => { diag("ctx.signal fired abort -- resolving null"); done(null); }, { once: true });
1217
+ if (params.signal) params.signal.addEventListener("abort", () => { diag("tool call signal fired abort -- resolving null"); done(null); }, { once: true });
1206
1218
  if (params.timeout && params.timeout > 0) setTimeout(() => { diag("explicit params.timeout expired -- resolving null", { timeout: params.timeout }); done(null); }, params.timeout);
1207
1219
  return new AskComponent(params.question, normalizedContext, options, allowMultiple, allowFreeform, allowComment, displayMode, tui, theme, keybindings, shortcuts, done);
1208
1220
  };
@@ -39,7 +39,7 @@ function text(message: string, details: unknown = {}) {
39
39
  * is available, never throws -- an unanswered live prompt still leaves the round it already
40
40
  * recorded intact.
41
41
  */
42
- async function liveAnswer(ctx: ExtensionContext, discussion: Artifact, onUpdate: AgentToolUpdateCallback | undefined): Promise<{ content: string; selected?: string[] } | undefined> {
42
+ async function liveAnswer(ctx: ExtensionContext, discussion: Artifact, onUpdate: AgentToolUpdateCallback | undefined, signal: AbortSignal | undefined): Promise<{ content: string; selected?: string[] } | undefined> {
43
43
  if (!ctx.hasUI) return undefined;
44
44
  const pending = (() => { try { return readDiscussionExtra(discussion.extra); } catch { return undefined; } })();
45
45
  const question = `Reply to "${discussion.title}":`;
@@ -49,10 +49,11 @@ async function liveAnswer(ctx: ExtensionContext, discussion: Artifact, onUpdate:
49
49
  options: pending.pendingOptions.map((title) => ({ title })),
50
50
  allowMultiple: pending.pendingOptionsMode === "multi",
51
51
  onUpdate,
52
+ signal,
52
53
  key: discussion.id,
53
54
  });
54
55
  }
55
- return askQuestion(ctx, { question, onUpdate, key: discussion.id });
56
+ return askQuestion(ctx, { question, onUpdate, signal, key: discussion.id });
56
57
  }
57
58
 
58
59
  /**
@@ -705,7 +706,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
705
706
  executionMode: "sequential",
706
707
  renderCall(args, theme) { return renderPapyrusToolCall("Discuss", args, theme); },
707
708
  renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
708
- async execute(_id, rawParams, _signal, onUpdate, ctx) {
709
+ async execute(_id, rawParams, signal, onUpdate, ctx) {
709
710
  try {
710
711
  const params: Record<string, unknown> = { ...rawParams };
711
712
  const action = params.action;
@@ -722,7 +723,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
722
723
  ? text(`Opened discussion ${artifactLine(result.discussion)}`, createArtifactDetails("discuss.open", result.discussion))
723
724
  : text(`Round ${result.rounds[0]?.roundNumber} added to "${result.discussion.title}"`, createArtifactDetails("discuss.reply", result.discussion));
724
725
  if (params.live !== true) return fallback;
725
- const answer = await liveAnswer(ctx, result.discussion, onUpdate);
726
+ const answer = await liveAnswer(ctx, result.discussion, onUpdate, signal);
726
727
  if (!answer) return fallback;
727
728
  const answered = await callService<Record<string, unknown>, DiscussionAndRounds>("discuss.reply", {
728
729
  id: result.discussion.id, actor: "human", content: answer.content, ...(answer.selected ? { selected: answer.selected } : {}), source: "discuss-live",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.27.7",
3
+ "version": "0.27.8",
4
4
  "description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],