@danypops/papyrus 0.27.10 → 0.27.11

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.
@@ -63,19 +63,10 @@ export interface AskQuestionParams {
63
63
  displayMode?: AskDisplayMode;
64
64
  timeout?: number;
65
65
  /**
66
- * Joins a second concurrent call for the same key to the first's in-flight promise instead of
67
- * opening a second picker. Pass a stable id (the target Discussion's id) whenever the caller
68
- * cannot otherwise guarantee only one live ask is ever issued for that same question.
69
- */
70
- key?: string;
71
- /**
72
- * Streamed once before blocking on the human. A live ask can legitimately sit pending far
73
- * longer than a typical tool call (real human response time, not milliseconds) -- without any
74
- * progress signal, a tool call sitting silent that long looks indistinguishable from a dead
75
- * one to anything upstream watching for stalled calls. pi-ask-user's own original code (the
76
- * prior art this view is adapted from) sent exactly this same heartbeat before presenting its
77
- * UI; dropping it during the port was the regression that let two independent executions of
78
- * the same live ask run concurrently, each opening its own picker for the same question.
66
+ * Streamed once before blocking on the human, matching pi-ask-user's own original code (the
67
+ * prior art this view is adapted from) -- gives the tool call's progress UI something to show
68
+ * during a wait that legitimately runs far longer than a typical tool call (real human response
69
+ * time, not milliseconds).
79
70
  */
80
71
  onUpdate?: AgentToolUpdateCallback;
81
72
  /**
@@ -1099,9 +1090,8 @@ async function askViaDialogs(
1099
1090
  * driver (extension/src/index.ts's driveActiveTasks, on agent_settled) queue a "continue the
1100
1091
  * active task" nudge as a `deliverAs: "nextTurn"` message while this exact live ask is still
1101
1092
  * awaiting an answer -- starting a second, concurrent turn that reasons about the very Discussion
1102
- * this call is already resolving, independently of it. A live-observed bug (two pickers for the
1103
- * same question, one orphaned and later auto-resolving with fabricated "defer" text) traced back
1104
- * to exactly this race. driveActiveTasks checks isLiveAskPending() and skips queuing while true.
1093
+ * this call is already resolving, independently of it. driveActiveTasks checks isLiveAskPending()
1094
+ * and skips queuing while true.
1105
1095
  */
1106
1096
  let livePendingCount = 0;
1107
1097
 
@@ -1109,16 +1099,6 @@ export function isLiveAskPending(): boolean {
1109
1099
  return livePendingCount > 0;
1110
1100
  }
1111
1101
 
1112
- /**
1113
- * Keyed reentrancy join: whatever the exact external cause (an upstream retry, a duplicate turn,
1114
- * anything outside code this package controls -- verified Pi's own ctx.ui.custom() is a clean,
1115
- * single-shot, well-guarded call with no retry/timeout logic of its own), a second concurrent
1116
- * askQuestion() call for the SAME key must never open a second picker for the same question. It
1117
- * joins the already-in-flight promise instead. Keyed by the target Discussion's id (stable across
1118
- * a genuine duplicate call, unlike a fresh toolCallId each retry might mint).
1119
- */
1120
- const pendingByKey = new Map<string, Promise<AskAnswer | undefined>>();
1121
-
1122
1102
  /**
1123
1103
  * Discuss's live:true synchronous ask -- interactive AskComponent when a real TUI is available,
1124
1104
  * dialog fallback (ctx.ui.select/input) in RPC/headless mode, no-op undefined without any
@@ -1127,36 +1107,7 @@ const pendingByKey = new Map<string, Promise<AskAnswer | undefined>>();
1127
1107
  */
1128
1108
  export async function askQuestion(ctx: ExtensionContext, params: AskQuestionParams): Promise<AskAnswer | undefined> {
1129
1109
  if (!ctx.hasUI || !ctx.ui) return undefined;
1130
- if (params.key !== undefined) {
1131
- const existing = pendingByKey.get(params.key);
1132
- if (existing) return existing;
1133
- }
1134
- const promise = askQuestionUnguarded(ctx, params);
1135
- if (params.key !== undefined) {
1136
- const key = params.key;
1137
- pendingByKey.set(key, promise);
1138
- void promise.finally(() => {
1139
- if (pendingByKey.get(key) === promise) pendingByKey.delete(key);
1140
- });
1141
- }
1142
- return promise;
1143
- }
1144
-
1145
- /**
1146
- * Sent at least this often while blocked on the human -- well under any 8-second window a
1147
- * watchdog upstream might require to consider a tool call still alive. A single upfront ping
1148
- * (this view's original heartbeat) only covers the first ~8s; for genuinely long human response
1149
- * times (the whole point of a live ask) it goes silent again after that, indistinguishable from
1150
- * dead. A live-observed bug traced back to exactly this: cancellation always cited "idle timeout,
1151
- * no interaction within 8000ms" regardless of whether the real wait was 5 seconds or 18 minutes
1152
- * -- a fixed re-check window, not a measure of total elapsed time -- meaning periodic liveness,
1153
- * not a one-time ping, is what's required here.
1154
- */
1155
- let liveAskHeartbeatIntervalMs = 4_000;
1156
-
1157
- /** Test seam: real tiny interval, not a faked global timer -- exercises the periodic heartbeat deterministically and fast. */
1158
- export function setLiveAskHeartbeatIntervalMsForTests(ms: number): void {
1159
- liveAskHeartbeatIntervalMs = ms;
1110
+ return askQuestionUnguarded(ctx, params);
1160
1111
  }
1161
1112
 
1162
1113
  async function askQuestionUnguarded(ctx: ExtensionContext, params: AskQuestionParams): Promise<AskAnswer | undefined> {
@@ -1169,15 +1120,12 @@ async function askQuestionUnguarded(ctx: ExtensionContext, params: AskQuestionPa
1169
1120
  const displayMode: AskDisplayMode = params.displayMode ?? envDisplayMode ?? "overlay";
1170
1121
  const normalizedContext = params.context?.trim() || undefined;
1171
1122
 
1172
- const sendHeartbeat = () => params.onUpdate?.({ content: [{ type: "text", text: "Waiting for human input..." }], details: undefined });
1173
- sendHeartbeat();
1174
- const heartbeatTimer = params.onUpdate ? setInterval(sendHeartbeat, liveAskHeartbeatIntervalMs) : undefined;
1123
+ params.onUpdate?.({ content: [{ type: "text", text: "Waiting for human input..." }], details: undefined });
1175
1124
  livePendingCount += 1;
1176
1125
  try {
1177
1126
  return await askQuestionBlocking(ctx, params, options, allowMultiple, allowFreeform, allowComment, displayMode, normalizedContext);
1178
1127
  } finally {
1179
1128
  livePendingCount -= 1;
1180
- if (heartbeatTimer !== undefined) clearInterval(heartbeatTimer);
1181
1129
  }
1182
1130
  }
1183
1131
 
@@ -84,8 +84,8 @@ export async function showDiscussions(ctx: ExtensionCommandContext): Promise<voi
84
84
  const pending = (() => { try { return readDiscussionExtra(discussion.extra); } catch { return undefined; } })();
85
85
  const question = `Reply to "${discussion.title}":`;
86
86
  const answer = pending?.pendingOptions && pending.pendingOptions.length > 0 && pending.pendingOptionsMode
87
- ? await askQuestion(commandCtx, { question, options: pending.pendingOptions.map((title) => ({ title })), allowMultiple: pending.pendingOptionsMode === "multi", key: discussion.id })
88
- : await askQuestion(commandCtx, { question, key: discussion.id });
87
+ ? await askQuestion(commandCtx, { question, options: pending.pendingOptions.map((title) => ({ title })), allowMultiple: pending.pendingOptionsMode === "multi" })
88
+ : await askQuestion(commandCtx, { question });
89
89
  if (!answer) return; // canceled
90
90
  await callService("discuss.reply", { id: discussion.id, actor: ACTOR, content: answer.content, ...(answer.selected ? { selected: answer.selected } : {}), source: SOURCE });
91
91
  commandCtx.ui.notify(answer.selected ? `Selected: ${answer.selected.join(", ")}` : "Reply added.", "info");
@@ -50,10 +50,9 @@ async function liveAnswer(ctx: ExtensionContext, discussion: Artifact, onUpdate:
50
50
  allowMultiple: pending.pendingOptionsMode === "multi",
51
51
  onUpdate,
52
52
  signal,
53
- key: discussion.id,
54
53
  });
55
54
  }
56
- return askQuestion(ctx, { question, onUpdate, signal, key: discussion.id });
55
+ return askQuestion(ctx, { question, onUpdate, signal });
57
56
  }
58
57
 
59
58
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.27.10",
3
+ "version": "0.27.11",
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"],