@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
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
else if (current?.status === "cancelled")
|
|
184
|
-
|
|
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
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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()}`;
|