@evandu/super-pi 0.1.63 → 0.1.65

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.
@@ -3,6 +3,14 @@ import type { ExtensionUiAnswer, ExtensionUiCloseReason } from "../types.js";
3
3
  import type { EventBroadcaster } from "../server-context.js";
4
4
  /**
5
5
  * Service managing extension UI interactive prompts between Pi / subagents and connected clients.
6
+ *
7
+ * NO SAFETY-NET TIMEOUT. A question that nobody answers stays in the map until the run it belongs to
8
+ * is stopped, the conversation is reset, or the server shuts down - the run-level teardown is the
9
+ * only path that ever settles an unanswered ask, and it is the right path: a model waiting on a
10
+ * human for ten minutes is not a model that has decided to give up, and resolving the ask with a
11
+ * fabricated "cancelled" answer was the source of 「未定义行为」 the user reported - the model
12
+ * accepted the cancel and continued from a state that was not the state the person was supposed to
13
+ * produce. A living, unanswered question is honest; a timed-out one is fiction.
6
14
  */
7
15
  export declare class ExtensionUiService {
8
16
  private readonly broadcaster;
@@ -1,16 +1,13 @@
1
- /**
2
- * Safety net for questions Pi declared NO timeout for. The runtime races every blocking ask against the
3
- * run's abort and exit, but the losing `ask` promise is simply abandoned there - and for a detached
4
- * sub-agent run on an idle conversation nothing later cleans the entry up, so without a net it would sit
5
- * in the map forever with a question card the browser never sees close. The net settles such a question
6
- * as CANCELLED - never a default answer, per the design rule that a missing answer must not look like a
7
- * human decision - and broadcasts `extension.ui.closed` so the card disappears with a stated reason.
8
- * Generous, because a live run's question may legitimately wait on a slow human; this only bounds the
9
- * case where nothing can ever answer it.
10
- */
11
- const ASK_SAFETY_TIMEOUT_MS = 10 * 60 * 1000;
12
1
  /**
13
2
  * Service managing extension UI interactive prompts between Pi / subagents and connected clients.
3
+ *
4
+ * NO SAFETY-NET TIMEOUT. A question that nobody answers stays in the map until the run it belongs to
5
+ * is stopped, the conversation is reset, or the server shuts down - the run-level teardown is the
6
+ * only path that ever settles an unanswered ask, and it is the right path: a model waiting on a
7
+ * human for ten minutes is not a model that has decided to give up, and resolving the ask with a
8
+ * fabricated "cancelled" answer was the source of 「未定义行为」 the user reported - the model
9
+ * accepted the cancel and continued from a state that was not the state the person was supposed to
10
+ * produce. A living, unanswered question is honest; a timed-out one is fiction.
14
11
  */
15
12
  export class ExtensionUiService {
16
13
  broadcaster;
@@ -70,17 +67,16 @@ export class ExtensionUiService {
70
67
  ask: (request) => new Promise((resolve) => {
71
68
  const shown = scoped(request);
72
69
  const key = this.uiKey(conversationId, shown.requestId);
70
+ // Only the Pi-declared `timeoutMs` is honoured. There is no safety net when Pi did not set
71
+ // one: a question Pi chose to leave open is a question the model is waiting on, and a
72
+ // fabricated cancel would be the model continuing on a false premise. The run-level
73
+ // teardown (stop / abort / shutdown) is the path that closes an unanswered ask.
73
74
  const pending = { conversationId, request: shown, resolve };
74
75
  if (shown.timeoutMs !== undefined) {
75
76
  pending.timer = setTimeout(() => {
76
77
  this.settleUi(key, { cancelled: true }, "timeout");
77
78
  }, shown.timeoutMs);
78
79
  }
79
- else {
80
- pending.timer = setTimeout(() => {
81
- this.settleUi(key, { cancelled: true }, "expired");
82
- }, ASK_SAFETY_TIMEOUT_MS);
83
- }
84
80
  this.pendingUi.set(key, pending);
85
81
  this.broadcaster.broadcast({ type: "extension.ui", conversationId, request: shown });
86
82
  }),
@@ -173,17 +173,22 @@ export class RunService {
173
173
  throw new Error("这个会话里没有正在等待回答的 question 工具调用");
174
174
  const { messageId, index } = found;
175
175
  return new Promise((resolve, reject) => {
176
- const finish = (settle) => { clearInterval(poll); clearTimeout(deadline); settle(); };
176
+ // No timeout. The promise resolves when the question is answered, rejects when it is cancelled
177
+ // (the only honest "I will not answer" state - resolving with a default would be the model
178
+ // continuing from a question that was never decided). The run-level teardown (stop / abort /
179
+ // shutdown) clears the underlying ask and the client settles it as cancelled, which lands here
180
+ // and rejects. A question that is simply unanswered waits indefinitely.
177
181
  const poll = setInterval(() => {
178
182
  const current = this.store.snapshot().conversations.find((item) => item.id === conversationId)?.messages.find((item) => item.id === messageId)?.toolCalls?.[index]?.question;
179
- if (current?.status === "answered")
180
- finish(() => resolve(answerPayload(current)));
181
- // CANCELLED IS NOT AN ANSWER. Rejecting is what makes the tool call fail, which is what stops the
182
- // run; resolving here would let the model carry on from a question the person refused to answer.
183
- else if (current?.status === "cancelled")
184
- finish(() => reject(new Error("用户没有回答这个问题")));
183
+ if (current?.status === "answered") {
184
+ clearInterval(poll);
185
+ resolve(answerPayload(current));
186
+ }
187
+ else if (current?.status === "cancelled") {
188
+ clearInterval(poll);
189
+ reject(new Error("用户没有回答这个问题"));
190
+ }
185
191
  }, 250);
186
- const deadline = setTimeout(() => finish(() => reject(new Error("等待回答超时"))), 30 * 60 * 1000);
187
192
  // WHAT GOES BACK TO THE MODEL IS WRITTEN FOR THE MODEL. The answers pair to their questions by id,
188
193
  // which is what stops a person answering the second of two being recorded against the first - but an
189
194
  // id is a UUID, and a model handed `{answers:[{id:"671b0f0b-…", selected:["发"]}]}` has to guess at
@@ -382,16 +382,14 @@ export function createHttpServer(options) {
382
382
  json(403, { error: result.message });
383
383
  return;
384
384
  }
385
- // Validate cwd is a known workspace. Path comparison is normalized so Windows case
386
- // differences or trailing separators do not produce a false "unknown workspace" failure
387
- // when the extension hands back the same directory the server handed to it at spawn.
388
- const normalized = (value) => value.replace(/[\\/]+/g, "/").replace(/\/+$/, "").toLowerCase();
389
- const normalizedCwd = normalized(cwd);
390
- const workspace = store.snapshot().workspaces.find((w) => normalized(w.path) === normalizedCwd);
391
- if (!workspace) {
392
- json(400, { error: `cwd is not a known workspace: ${cwd} (known: ${store.snapshot().workspaces.map((w) => w.path).join(" | ")})` });
393
- return;
394
- }
385
+ // The `cwd` here is the directory the Pi child was launched in - which is the conversation's
386
+ // workspace, already authenticated by the per-conversation capability check above. Validating it
387
+ // against the workspace list looked cheap, but Windows path normalization (8.3 short names, UNC
388
+ // prefixes, drive-letter case, separator styles) routinely produces the same path in two different
389
+ // strings and a strict equality check refused valid calls; a relaxed comparison is no longer
390
+ // providing a security boundary that the capability check is not already providing. The trust
391
+ // decision is the one above; this stays as a no-op.
392
+ void cwd;
395
393
  // Set up the background task record BEFORE spawning, so the UI shows it immediately
396
394
  const { randomUUID } = await import("node:crypto");
397
395
  const sessionId = `exec_${randomUUID()}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.63",
3
+ "version": "0.1.65",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",