@evandu/super-pi 0.1.146 → 0.1.148
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.
- package/agents/super-pi-task/core.ts +223 -2
- package/agents/super-pi-task/index.ts +50 -0
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +8 -8
- package/dist/client/assets/{CodeMirrorEditor-CEwBCVM6.js → CodeMirrorEditor-DQCjEnxG.js} +2 -2
- package/dist/client/assets/{dist-DC1IocrS.js → dist-1Innu17E.js} +1 -1
- package/dist/client/assets/{dist-CSCUT1PQ.js → dist-1lynFOc5.js} +1 -1
- package/dist/client/assets/{dist-Do52MRwm.js → dist-B1qLZVMK.js} +1 -1
- package/dist/client/assets/{dist-CAOcjKAQ.js → dist-B4pz_xdd.js} +1 -1
- package/dist/client/assets/{dist-B0RXZeXQ.js → dist-B6KwDpeI.js} +1 -1
- package/dist/client/assets/{dist-DFMtP3wG.js → dist-BJVI1Unv.js} +1 -1
- package/dist/client/assets/{dist-oLq6HXLG.js → dist-BWvzD8Pe.js} +1 -1
- package/dist/client/assets/{dist-9F1wL-T5.js → dist-BayWssd0.js} +1 -1
- package/dist/client/assets/{dist-DBUqwiWG.js → dist-Bk_pkBVH.js} +1 -1
- package/dist/client/assets/{dist-CbVf2Cjn.js → dist-BpeClAv0.js} +1 -1
- package/dist/client/assets/{dist-DftD4ibj.js → dist-BsmJg5PX.js} +1 -1
- package/dist/client/assets/{dist-CUB_Chdh.js → dist-Bx7s4rni.js} +1 -1
- package/dist/client/assets/{dist-Bh8Q7Y_n.js → dist-CDBKM9tL.js} +1 -1
- package/dist/client/assets/{dist-BoyICDlL.js → dist-CGzXCzCG.js} +1 -1
- package/dist/client/assets/{dist-BCNaDgNW.js → dist-CRxCwn-Z.js} +1 -1
- package/dist/client/assets/{dist-76ArZ5M4.js → dist-CTDgUnBG.js} +1 -1
- package/dist/client/assets/{dist-BXBSIuTD.js → dist-D1VfMWmf.js} +1 -1
- package/dist/client/assets/{dist-DN0GlnnZ.js → dist-DTZLNEci.js} +1 -1
- package/dist/client/assets/{dist-JvQIIdqB.js → dist-DuUi29km.js} +1 -1
- package/dist/client/assets/{dist-CHt8PX8R.js → dist-F3S0t1oM.js} +1 -1
- package/dist/client/assets/{dist-BuR5Iagd.js → dist-QZ-FAVJV.js} +1 -1
- package/dist/client/assets/{dist-Bx6ZWYUW.js → dist-u0nBg0fZ.js} +1 -1
- package/dist/client/assets/index-BMpqviwU.css +1 -0
- package/dist/client/assets/{index-E7yaLa2C.js → index-Dzyp_7sf.js} +43 -43
- package/dist/client/index.html +2 -2
- package/dist/server/active-memory.d.ts +17 -0
- package/dist/server/active-memory.js +39 -0
- package/dist/server/agents.d.ts +1 -1
- package/dist/server/agents.js +1 -1
- package/dist/server/application/conversation-service.js +6 -0
- package/dist/server/application/run-service.d.ts +16 -0
- package/dist/server/application/run-service.js +175 -26
- package/dist/server/application/session-orchestrator-service.d.ts +103 -1
- package/dist/server/application/session-orchestrator-service.js +308 -8
- package/dist/server/application/session-watchdog.d.ts +159 -0
- package/dist/server/application/session-watchdog.js +244 -0
- package/dist/server/http-server.js +136 -76
- package/dist/server/index.js +3 -0
- package/dist/server/knowledge-vectors.d.ts +15 -0
- package/dist/server/knowledge-vectors.js +118 -37
- package/dist/server/knowledge.d.ts +7 -8
- package/dist/server/knowledge.js +133 -55
- package/dist/server/length-continuation.d.ts +50 -0
- package/dist/server/length-continuation.js +55 -0
- package/dist/server/pi-session-events.js +5 -1
- package/dist/server/pi-session-pool.d.ts +2 -0
- package/dist/server/pi-session-pool.js +16 -0
- package/dist/server/runtime.js +5 -4
- package/dist/server/session-tree-scope.d.ts +125 -0
- package/dist/server/session-tree-scope.js +337 -0
- package/dist/server/store.d.ts +13 -0
- package/dist/server/store.js +37 -4
- package/dist/server/task-tool.d.ts +4 -1
- package/dist/server/task-tool.js +25 -1
- package/dist/server/turn-failure.d.ts +26 -0
- package/dist/server/turn-failure.js +51 -0
- package/dist/server/types.d.ts +15 -1
- package/package.json +20 -3
- package/dist/client/assets/index-D-OxlUxN.css +0 -1
|
@@ -25,6 +25,7 @@ import { join } from "node:path";
|
|
|
25
25
|
import { createPiSession, sameFingerprint, superPiExtensionPaths, superPiSkillPaths, } from "./pi-session.js";
|
|
26
26
|
/** How long an idle session survives before it is evicted. */
|
|
27
27
|
const IDLE_EVICTION_MS = 10 * 60_000;
|
|
28
|
+
const MAX_LIVE_SESSIONS = Math.max(4, Number.parseInt(process.env.SUPER_PI_MAX_LIVE_SESSIONS ?? "32", 10) || 32);
|
|
28
29
|
/**
|
|
29
30
|
* The environment variable the session's credential travels in.
|
|
30
31
|
*
|
|
@@ -119,6 +120,7 @@ export class PiSessionPool {
|
|
|
119
120
|
* tool allowlist has changed.
|
|
120
121
|
*/
|
|
121
122
|
async acquire(request) {
|
|
123
|
+
await this.evictOverflow(request.conversationId);
|
|
122
124
|
const fingerprint = this.fingerprintOf(request);
|
|
123
125
|
const existing = this.sessions.get(request.conversationId);
|
|
124
126
|
if (existing) {
|
|
@@ -182,6 +184,20 @@ export class PiSessionPool {
|
|
|
182
184
|
throw error;
|
|
183
185
|
}
|
|
184
186
|
}
|
|
187
|
+
/** Keep an accidental session fan-out from retaining one full transcript context per conversation. */
|
|
188
|
+
async evictOverflow(exceptConversationId) {
|
|
189
|
+
if (this.sessions.size < MAX_LIVE_SESSIONS)
|
|
190
|
+
return;
|
|
191
|
+
const candidates = [...this.sessions.entries()]
|
|
192
|
+
.filter(([id, entry]) => id !== exceptConversationId && !entry.building)
|
|
193
|
+
.sort((a, b) => a[1].lastUsedAt - b[1].lastUsedAt);
|
|
194
|
+
while (this.sessions.size >= MAX_LIVE_SESSIONS && candidates.length) {
|
|
195
|
+
const [conversationId] = candidates.shift();
|
|
196
|
+
if (process.env.SUPER_PI_TRACE_LATENCY === "1")
|
|
197
|
+
console.warn(`[super-pi latency] conversation=${conversationId} phase=session.max-live-evict`);
|
|
198
|
+
await this.evict(conversationId);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
185
201
|
async build(request, fingerprint) {
|
|
186
202
|
const creation = {
|
|
187
203
|
cwd: request.cwd,
|
package/dist/server/runtime.js
CHANGED
|
@@ -18,12 +18,13 @@ import { TASK_CAPABILITY_ENV, mintCapability, releaseCapability } from "./capabi
|
|
|
18
18
|
import { resolvePiCommand, shellQuote } from "./pi-command.js";
|
|
19
19
|
import { PiSessionPool, repositoryRootFrom } from "./pi-session-pool.js";
|
|
20
20
|
import { describeDeliveredUserMessage, describePiQueue, handleSessionEvent, TurnQueue } from "./pi-session-events.js";
|
|
21
|
+
import { LengthExceededError } from "./turn-failure.js";
|
|
21
22
|
// The queue/steer recognisers live in pi-session-events.ts, where the SESSION path's bridge can use
|
|
22
23
|
// them; the one-shot drain loop below reads the same records through the same two functions, so both
|
|
23
24
|
// paths recognise a delivery identically. Re-exported because they were part of this module's surface.
|
|
24
25
|
export { describeDeliveredUserMessage, describePiQueue } from "./pi-session-events.js";
|
|
25
26
|
import { createPiSession, createSdkCatalog, piSdkAvailable, resolveTranscriptFile, superPiExtensionPaths } from "./pi-session.js";
|
|
26
|
-
import { TASK_CONVERSATION_ENV, PARENT_CONVERSATION_ENV, TASK_DEPTH_ENV, TASK_ENDPOINT_ENV, TASK_EXEC_ENDPOINT_ENV, TASK_ROSTER_ENV, TASK_TOOL_NAME, TODO_ENDPOINT_ENV, TODO_TOOL_NAME, SCHEDULED_TASK_ENDPOINT_ENV, SCHEDULED_TASK_TOOL_NAME, WORKFLOW_ENDPOINT_ENV, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, ORCHESTRATOR_ENDPOINT_ENV, ORCHESTRATOR_WORKSPACE_ENV, SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, rosterPayload, } from "./task-tool.js";
|
|
27
|
+
import { TASK_CONVERSATION_ENV, PARENT_CONVERSATION_ENV, TASK_DEPTH_ENV, TASK_ENDPOINT_ENV, TASK_EXEC_ENDPOINT_ENV, TASK_ROSTER_ENV, TASK_TOOL_NAME, TODO_ENDPOINT_ENV, TODO_TOOL_NAME, SCHEDULED_TASK_ENDPOINT_ENV, SCHEDULED_TASK_TOOL_NAME, WORKFLOW_ENDPOINT_ENV, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, ORCHESTRATOR_ENDPOINT_ENV, ORCHESTRATOR_WORKSPACE_ENV, SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, SESSION_REPORT_TOOL_NAME, rosterPayload, } from "./task-tool.js";
|
|
27
28
|
import { BLOCKING_UI_METHODS, EXTENSION_UI_METHODS, } from "./types.js";
|
|
28
29
|
import { loadConversationImages } from "./upload.js";
|
|
29
30
|
import { describeToolCall, describeToolCallArgs, describeToolCallInProgress, extractPartialResultText, extractToolResultText, readQuestionCall } from "./tool-calls.js";
|
|
@@ -1259,7 +1260,7 @@ export class PiRpcRuntime {
|
|
|
1259
1260
|
if (process.env.SUPER_PI_FEISHU_ENDPOINT)
|
|
1260
1261
|
names.push(FEISHU_READ_TOOL_NAME, FEISHU_WRITE_TOOL_NAME);
|
|
1261
1262
|
if (this.options.orchestratorEndpoint)
|
|
1262
|
-
names.push(SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME);
|
|
1263
|
+
names.push(SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, SESSION_REPORT_TOOL_NAME);
|
|
1263
1264
|
return names;
|
|
1264
1265
|
}
|
|
1265
1266
|
/**
|
|
@@ -1478,7 +1479,7 @@ export class PiRpcRuntime {
|
|
|
1478
1479
|
// allowlist and naming a tool Pi has not registered would change the spawn line for every other
|
|
1479
1480
|
// mode. The prompt asks for a bare JSON array and nothing else; if the model spends a turn on a
|
|
1480
1481
|
// tool call anyway the reranker times out and falls back to the lexical order.
|
|
1481
|
-
const agentArgs = memoryReview ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates", "--tools", "memory,skill_manage,session_search"] : knowledgeRerank ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates"] : await agentSpawnArgs(this.agents.promptDir, agent, { extraAllowedTools: [...(extensionArgs.length ? [TASK_TOOL_NAME, TODO_TOOL_NAME, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, SCHEDULED_TASK_TOOL_NAME, "question", "tool_register", ...(this.options.orchestratorEndpoint ? [SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME] : [])] : []), ...(memoryEnabled ? ["memory", "skill_manage", "session_search"] : []), ...(knowledgeEnabled ? [KNOWLEDGE_TOOL_NAME, KNOWLEDGE_IMPORT_TOOL_NAME] : []), ...(documentsEnabled ? [DOCUMENT_READ_TOOL_NAME, DOCUMENT_WRITE_TOOL_NAME] : []), ...(webEnabled ? [WEB_SEARCH_TOOL_NAME, WEB_FETCH_TOOL_NAME] : []), ...(feishuEnabled ? [FEISHU_READ_TOOL_NAME, FEISHU_WRITE_TOOL_NAME] : [])] });
|
|
1482
|
+
const agentArgs = memoryReview ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates", "--tools", "memory,skill_manage,session_search"] : knowledgeRerank ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates"] : await agentSpawnArgs(this.agents.promptDir, agent, { extraAllowedTools: [...(extensionArgs.length ? [TASK_TOOL_NAME, TODO_TOOL_NAME, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, SCHEDULED_TASK_TOOL_NAME, "question", "tool_register", ...(this.options.orchestratorEndpoint ? [SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, SESSION_REPORT_TOOL_NAME] : [])] : []), ...(memoryEnabled ? ["memory", "skill_manage", "session_search"] : []), ...(knowledgeEnabled ? [KNOWLEDGE_TOOL_NAME, KNOWLEDGE_IMPORT_TOOL_NAME] : []), ...(documentsEnabled ? [DOCUMENT_READ_TOOL_NAME, DOCUMENT_WRITE_TOOL_NAME] : []), ...(webEnabled ? [WEB_SEARCH_TOOL_NAME, WEB_FETCH_TOOL_NAME] : []), ...(feishuEnabled ? [FEISHU_READ_TOOL_NAME, FEISHU_WRITE_TOOL_NAME] : [])] });
|
|
1482
1483
|
// Refused here, with the reason, rather than handed to the spawn: an ENOENT from `spawn` reads as
|
|
1483
1484
|
// "the app is broken" and hides the one fact that matters, which is where it looked for Pi.
|
|
1484
1485
|
if (!this.pi.found)
|
|
@@ -1604,7 +1605,7 @@ export class PiRpcRuntime {
|
|
|
1604
1605
|
if (reason === "aborted")
|
|
1605
1606
|
throw new Error(outcome.errorMessage || "Pi 意外中止了本轮回答。");
|
|
1606
1607
|
if (reason === "length")
|
|
1607
|
-
throw new
|
|
1608
|
+
throw new LengthExceededError(outcome.errorMessage || "模型输出达到本轮长度上限,回答未完成。");
|
|
1608
1609
|
};
|
|
1609
1610
|
let firstPiRecord = true;
|
|
1610
1611
|
// Nothing may be dropped without a trace. Pi emits a wide, open-ended family of agent
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
/**
|
|
3
|
+
* THE SESSION TREE'S SCOPE RULE.
|
|
4
|
+
*
|
|
5
|
+
* A session may only manage its DIRECT children: the sessions whose `parentConversationId` is
|
|
6
|
+
* exactly its own id. Nothing else. A grandchild is a level away, a peer sits in somebody else's
|
|
7
|
+
* branch, an ancestor is above - none of them is this session's subordinate, so none of them may be
|
|
8
|
+
* dispatched to, read, stopped or resumed by it. A workspace is NOT an organisation chart: two
|
|
9
|
+
* supervisors can sit in the same workspace without being each other's subordinates.
|
|
10
|
+
*
|
|
11
|
+
* WHY THE RULE IS ONE LINE. The tree edge is written on the conversation itself at birth
|
|
12
|
+
* (`parentConversationId`), so "is this session my child?" is a comparison, not a walk. The walk up
|
|
13
|
+
* the chain exists for ONE purpose only: writing a refusal that says WHY, so the reader knows
|
|
14
|
+
* whether it asked for a peer, a grandchild or somebody else's department. The walk NEVER decides
|
|
15
|
+
* anything - a refusal is a refusal because the parent is not the caller, never because the walk
|
|
16
|
+
* came back empty.
|
|
17
|
+
*
|
|
18
|
+
* WHY THIS MATTERS AT ALL. The result callback of a dispatched session goes back to the session that
|
|
19
|
+
* OWNS it - the one whose id is the target's `parentConversationId`. If a session could dispatch to
|
|
20
|
+
* a session it does not own, that session's result would report to somebody else, and the tree the
|
|
21
|
+
* user sees in the sidebar would stop being the tree the work actually flows through. Making the
|
|
22
|
+
* target's parent be the caller is what keeps "who gave the order is who gets the report" true by
|
|
23
|
+
* construction.
|
|
24
|
+
*/
|
|
25
|
+
/** How the caller and the target relate in the session tree. Computed for the MESSAGE only. */
|
|
26
|
+
export type SessionRelation = "self" | "direct-child" | "descendant" | "ancestor" | "peer" | "other-root" | "unrelated";
|
|
27
|
+
/** The outcome of a scope check: allowed, or a refusal with the status code and the reason. */
|
|
28
|
+
export type ScopeVerdict = {
|
|
29
|
+
ok: true;
|
|
30
|
+
} | {
|
|
31
|
+
ok: false;
|
|
32
|
+
status: number;
|
|
33
|
+
error: string;
|
|
34
|
+
};
|
|
35
|
+
/** The verb a refusal is about, so the message reads like the action that was refused. */
|
|
36
|
+
export type ScopeVerb = "调度" | "读取" | "停止" | "恢复" | "列出" | "看护";
|
|
37
|
+
/** What the caller was trying to reach: a session to manage, a session's children, or a parent to claim. */
|
|
38
|
+
export type ScopeSubject = "manage" | "list-children" | "declare-parent";
|
|
39
|
+
/** The narrow slice of the store this file needs. */
|
|
40
|
+
type ConversationReader = Pick<Store, "conversation">;
|
|
41
|
+
/**
|
|
42
|
+
* The relation between two sessions, in the tree.
|
|
43
|
+
*
|
|
44
|
+
* READ ONLY. This classifies a pair for the sake of the refusal message; it never grants access.
|
|
45
|
+
*/
|
|
46
|
+
export declare const classifySessionRelation: (store: ConversationReader, callerConversationId: string, targetConversationId: string) => SessionRelation;
|
|
47
|
+
/**
|
|
48
|
+
* Writes the refusal for a pair that is not allowed to be reached.
|
|
49
|
+
*
|
|
50
|
+
* The three shapes, because the three failures are different failures:
|
|
51
|
+
* - managing a session -> 「不能调度/读取/停止/恢复会话 X」
|
|
52
|
+
* - listing a session's kids -> 「不能列出会话 X 的下属」
|
|
53
|
+
* - claiming a parent -> 「不能把会话 X 声明为自己的下属」
|
|
54
|
+
*/
|
|
55
|
+
export declare const describeScopeRefusal: (options: {
|
|
56
|
+
store: ConversationReader;
|
|
57
|
+
callerConversationId: string;
|
|
58
|
+
targetConversationId: string;
|
|
59
|
+
verb: ScopeVerb;
|
|
60
|
+
subject: ScopeSubject;
|
|
61
|
+
}) => string;
|
|
62
|
+
/**
|
|
63
|
+
* Checks that the caller may manage the target: the target must be the caller's DIRECT child.
|
|
64
|
+
*
|
|
65
|
+
* The workspace check is NOT part of this rule and is NOT removed by it - a conversation has to be
|
|
66
|
+
* in the caller's workspace AND in the caller's own subtree. The workspace refusal comes first
|
|
67
|
+
* because it is the older, coarser boundary, and its message names the two workspaces.
|
|
68
|
+
*/
|
|
69
|
+
export declare const verifyDirectChildScope: (options: {
|
|
70
|
+
store: ConversationReader;
|
|
71
|
+
callerConversationId: string;
|
|
72
|
+
targetConversationId: string;
|
|
73
|
+
verb?: ScopeVerb;
|
|
74
|
+
}) => ScopeVerdict;
|
|
75
|
+
/**
|
|
76
|
+
* Checks that the caller may list a parent's children.
|
|
77
|
+
*
|
|
78
|
+
* The rule is EQUALITY, not parenthood: the parent whose children are listed must be the caller
|
|
79
|
+
* itself, so the only sessions on the list are the caller's own direct children. Listing the
|
|
80
|
+
* children of the caller's own child would be listing grandchildren, which is the cross-level reach
|
|
81
|
+
* this whole rule exists to refuse.
|
|
82
|
+
*/
|
|
83
|
+
export declare const verifyListParentScope: (options: {
|
|
84
|
+
store: ConversationReader;
|
|
85
|
+
callerConversationId: string;
|
|
86
|
+
requestedParentId?: string;
|
|
87
|
+
}) => ScopeVerdict;
|
|
88
|
+
/**
|
|
89
|
+
* Checks that the caller may REPORT to the target: the target must be the caller's DIRECT PARENT.
|
|
90
|
+
*
|
|
91
|
+
* THE REPORTING LINE IS THE TREE EDGE, READ BACKWARDS. Every other rule in this file asks "may this
|
|
92
|
+
* session manage that one?" - and the answer is always about a session's subordinates. A report is the
|
|
93
|
+
* one message that travels UP, so the rule is inverted: a child reports to the session that owns it,
|
|
94
|
+
* the one whose id is the child's `parentConversationId`, and to nobody else.
|
|
95
|
+
*
|
|
96
|
+
* WHY THE INVERSION IS NOT A LOOPHOLE. A peer is somebody else's child, an ancestor is above the
|
|
97
|
+
* caller's own parent, a grandchild is below the caller, and a session in another workspace is another
|
|
98
|
+
* department. Every one of them would put one branch's news into another branch's inbox, and the inbox
|
|
99
|
+
* is where a parent's NEXT ACTION is decided - so a report that crossed a branch would be a stranger's
|
|
100
|
+
* news steering the wrong supervisor.
|
|
101
|
+
*
|
|
102
|
+
* THE RECIPIENT IS DERIVED, NOT CHOSEN. This check exists for the case where a caller NAMES a
|
|
103
|
+
* recipient at all: the server derives the parent from the tree and this refuses anything else, so a
|
|
104
|
+
* model cannot redirect its own report up or sideways.
|
|
105
|
+
*/
|
|
106
|
+
export declare const verifyReportTargetScope: (options: {
|
|
107
|
+
store: ConversationReader;
|
|
108
|
+
callerConversationId: string;
|
|
109
|
+
targetConversationId: string;
|
|
110
|
+
}) => ScopeVerdict;
|
|
111
|
+
/**
|
|
112
|
+
* Checks the parent a new session is created under.
|
|
113
|
+
*
|
|
114
|
+
* The parent edge is written onto the new conversation at birth, and the result callback follows
|
|
115
|
+
* that edge home. A caller that names somebody ELSE as the parent would therefore rewrite that
|
|
116
|
+
* session's reporting line - putting the victim's result into the caller's inbox - so the parent
|
|
117
|
+
* must be the caller. An empty parent means a top-level session, which is what the user's own "+"
|
|
118
|
+
* creates and what a session with no owner legitimately is.
|
|
119
|
+
*/
|
|
120
|
+
export declare const verifySpawnParentScope: (options: {
|
|
121
|
+
store: ConversationReader;
|
|
122
|
+
callerConversationId: string;
|
|
123
|
+
requestedParentId?: string;
|
|
124
|
+
}) => ScopeVerdict;
|
|
125
|
+
export {};
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/** The depth cap on an ancestor walk. A tree deeper than this is a bug, not a reason to hang. */
|
|
2
|
+
const MAX_ANCESTOR_DEPTH = 64;
|
|
3
|
+
/**
|
|
4
|
+
* The chain of ancestors of a conversation, starting with the conversation itself.
|
|
5
|
+
*
|
|
6
|
+
* A dangling parent (one whose conversation is gone) simply ends the chain, and a cycle is cut by
|
|
7
|
+
* the depth cap: the walk is for the message, so it must terminate whatever the data says.
|
|
8
|
+
*/
|
|
9
|
+
const ancestorChain = (store, conversationId) => {
|
|
10
|
+
const chain = [];
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
let current = conversationId;
|
|
13
|
+
while (current && !seen.has(current) && chain.length < MAX_ANCESTOR_DEPTH) {
|
|
14
|
+
seen.add(current);
|
|
15
|
+
chain.push(current);
|
|
16
|
+
current = store.conversation(current)?.parentConversationId;
|
|
17
|
+
}
|
|
18
|
+
return chain;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The relation between two sessions, in the tree.
|
|
22
|
+
*
|
|
23
|
+
* READ ONLY. This classifies a pair for the sake of the refusal message; it never grants access.
|
|
24
|
+
*/
|
|
25
|
+
export const classifySessionRelation = (store, callerConversationId, targetConversationId) => {
|
|
26
|
+
if (callerConversationId === targetConversationId)
|
|
27
|
+
return "self";
|
|
28
|
+
const callerChain = ancestorChain(store, callerConversationId);
|
|
29
|
+
const targetChain = ancestorChain(store, targetConversationId);
|
|
30
|
+
if (targetChain[1] === callerConversationId)
|
|
31
|
+
return "direct-child";
|
|
32
|
+
// The caller sits somewhere above the target: the target is a descendant, a level down or more.
|
|
33
|
+
if (targetChain.indexOf(callerConversationId) > 0)
|
|
34
|
+
return "descendant";
|
|
35
|
+
// The target sits above the caller.
|
|
36
|
+
if (callerChain.indexOf(targetConversationId) > 0)
|
|
37
|
+
return "ancestor";
|
|
38
|
+
// The target has no parent at all: it is the root of another tree.
|
|
39
|
+
if (targetChain.length === 1)
|
|
40
|
+
return "other-root";
|
|
41
|
+
// A shared ancestor means the same chart; without one, another department.
|
|
42
|
+
const shared = targetChain.slice(1).find((id) => callerChain.slice(1).includes(id));
|
|
43
|
+
return shared ? "peer" : "unrelated";
|
|
44
|
+
};
|
|
45
|
+
/** The session between the caller and the target, when the target is a descendant of it. */
|
|
46
|
+
const intermediateSession = (store, callerConversationId, targetConversationId) => {
|
|
47
|
+
const chain = ancestorChain(store, targetConversationId);
|
|
48
|
+
const index = chain.indexOf(callerConversationId);
|
|
49
|
+
return index > 1 ? chain[index - 1] : undefined;
|
|
50
|
+
};
|
|
51
|
+
/** The nearest session both the caller and the target descend from, when there is one. */
|
|
52
|
+
const commonAncestorSession = (store, callerConversationId, targetConversationId) => {
|
|
53
|
+
const callerChain = ancestorChain(store, callerConversationId);
|
|
54
|
+
const targetChain = ancestorChain(store, targetConversationId);
|
|
55
|
+
return targetChain.slice(1).find((id) => callerChain.slice(1).includes(id));
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The reason a pair was refused, in the words of the relation that was found.
|
|
59
|
+
*
|
|
60
|
+
* The reader is a model about to retry, or a person reading a log: the message says which relation
|
|
61
|
+
* was found (peer, grandchild, ancestor, another tree, another department) and what the rule is, so
|
|
62
|
+
* a retry with a different target is an informed one.
|
|
63
|
+
*
|
|
64
|
+
* The three subjects read differently on purpose. Refusing to DISPATCH a peer and refusing to
|
|
65
|
+
* LIST a peer's subordinates are the same boundary, but they are not the same mistake, and a
|
|
66
|
+
* reader who is told which one they made can fix the right thing.
|
|
67
|
+
*/
|
|
68
|
+
const relationReason = (relation, subject, callerConversationId, targetConversationId, verb, intermediateId, ancestorId) => {
|
|
69
|
+
if (subject === "list-children") {
|
|
70
|
+
switch (relation) {
|
|
71
|
+
case "direct-child":
|
|
72
|
+
return `列出 ${targetConversationId} 的下属属于跨级查询,跨级不被允许。`;
|
|
73
|
+
case "descendant":
|
|
74
|
+
return intermediateId
|
|
75
|
+
? `${targetConversationId} 是 ${callerConversationId} 的隔级下属(中间隔着 ${intermediateId}),跨级列出下属不被允许。`
|
|
76
|
+
: `${targetConversationId} 是 ${callerConversationId} 的隔级下属,跨级列出下属不被允许。`;
|
|
77
|
+
case "ancestor":
|
|
78
|
+
return `${callerConversationId} 不能列出自己上级的下属。`;
|
|
79
|
+
case "peer":
|
|
80
|
+
return ancestorId
|
|
81
|
+
? `${targetConversationId} 是 ${callerConversationId} 的平级会话(两者的共同上级是 ${ancestorId}),列出别人的下属属于越权。`
|
|
82
|
+
: `${targetConversationId} 是 ${callerConversationId} 的平级会话,列出别人的下属属于越权。`;
|
|
83
|
+
case "other-root":
|
|
84
|
+
return `${targetConversationId} 是一个独立的顶层会话,它的下属不属于 ${callerConversationId}。`;
|
|
85
|
+
case "unrelated":
|
|
86
|
+
return `${targetConversationId} 与 ${callerConversationId} 不在同一条会话树上(${targetConversationId} 不是 ${callerConversationId} 的直接下属),跨部门不被允许。`;
|
|
87
|
+
case "self":
|
|
88
|
+
return `会话不是自己的下属。`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (subject === "declare-parent") {
|
|
92
|
+
switch (relation) {
|
|
93
|
+
case "descendant":
|
|
94
|
+
return `${targetConversationId} 是 ${callerConversationId} 的隔级下属,不能被重新声明为 ${callerConversationId} 的直接下属。`;
|
|
95
|
+
case "ancestor":
|
|
96
|
+
return `${callerConversationId} 不能把会话 ${targetConversationId} 声明为自己的下属。`;
|
|
97
|
+
case "peer":
|
|
98
|
+
return `${targetConversationId} 是 ${callerConversationId} 的平级会话,不能被 ${callerConversationId} 收编为自己的下属。`;
|
|
99
|
+
case "other-root":
|
|
100
|
+
return `${targetConversationId} 是一个独立的顶层会话,不能被 ${callerConversationId} 声明为下属。`;
|
|
101
|
+
case "unrelated":
|
|
102
|
+
return `${targetConversationId} 与 ${callerConversationId} 不在同一条会话树上,不能被 ${callerConversationId} 声明为下属。`;
|
|
103
|
+
case "direct-child":
|
|
104
|
+
case "self":
|
|
105
|
+
return `会话不是自己的下属。`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
switch (relation) {
|
|
109
|
+
case "self":
|
|
110
|
+
return `会话不是自己的下属。`;
|
|
111
|
+
case "direct-child":
|
|
112
|
+
return `${targetConversationId} 是 ${callerConversationId} 的直接子会话。`;
|
|
113
|
+
case "descendant":
|
|
114
|
+
return intermediateId
|
|
115
|
+
? `${targetConversationId} 是 ${callerConversationId} 的隔级下属(中间隔着 ${intermediateId}),跨级${verb}不被允许。`
|
|
116
|
+
: `${targetConversationId} 是 ${callerConversationId} 的隔级下属,跨级${verb}不被允许。`;
|
|
117
|
+
case "ancestor":
|
|
118
|
+
return `${callerConversationId} 不能${verb}自己的上级。`;
|
|
119
|
+
case "peer":
|
|
120
|
+
return ancestorId
|
|
121
|
+
? `${targetConversationId} 是 ${callerConversationId} 的平级会话(两者的共同上级是 ${ancestorId}),不在 ${callerConversationId} 的直接下属中。`
|
|
122
|
+
: `${targetConversationId} 是 ${callerConversationId} 的平级会话,不在 ${callerConversationId} 的直接下属中。`;
|
|
123
|
+
case "other-root":
|
|
124
|
+
return `${targetConversationId} 是一个独立的顶层会话,不属于 ${callerConversationId}。`;
|
|
125
|
+
case "unrelated":
|
|
126
|
+
return `${targetConversationId} 与 ${callerConversationId} 不在同一条会话树上(${targetConversationId} 不是 ${callerConversationId} 的直接下属),跨部门${verb}不被允许。`;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Writes the refusal for a pair that is not allowed to be reached.
|
|
131
|
+
*
|
|
132
|
+
* The three shapes, because the three failures are different failures:
|
|
133
|
+
* - managing a session -> 「不能调度/读取/停止/恢复会话 X」
|
|
134
|
+
* - listing a session's kids -> 「不能列出会话 X 的下属」
|
|
135
|
+
* - claiming a parent -> 「不能把会话 X 声明为自己的下属」
|
|
136
|
+
*/
|
|
137
|
+
export const describeScopeRefusal = (options) => {
|
|
138
|
+
const { store, callerConversationId, targetConversationId, verb, subject } = options;
|
|
139
|
+
const relation = classifySessionRelation(store, callerConversationId, targetConversationId);
|
|
140
|
+
const intermediateId = intermediateSession(store, callerConversationId, targetConversationId);
|
|
141
|
+
const ancestorId = commonAncestorSession(store, callerConversationId, targetConversationId);
|
|
142
|
+
const reason = relationReason(relation, subject, callerConversationId, targetConversationId, verb, intermediateId, ancestorId);
|
|
143
|
+
if (subject === "list-children") {
|
|
144
|
+
return `调用方会话 ${callerConversationId} 不能列出会话 ${targetConversationId} 的下属:${reason}只能列出自己直接创建的子会话。`;
|
|
145
|
+
}
|
|
146
|
+
if (subject === "declare-parent") {
|
|
147
|
+
return `调用方会话 ${callerConversationId} 不能把会话 ${targetConversationId} 声明为自己的下属:${reason}创建会话的父会话必须是调用方自己。`;
|
|
148
|
+
}
|
|
149
|
+
return `调用方会话 ${callerConversationId} 不能${verb}会话 ${targetConversationId}:${reason}只能${verb}自己直接创建的子会话。`;
|
|
150
|
+
};
|
|
151
|
+
/** The refusal for a caller that cannot be identified at all. */
|
|
152
|
+
const missingCallerRefusal = (callerConversationId) => ({
|
|
153
|
+
ok: false,
|
|
154
|
+
status: 403,
|
|
155
|
+
error: callerConversationId
|
|
156
|
+
? `调用方会话 ${callerConversationId} 不存在或未关联有效工作区,拒绝编排操作。`
|
|
157
|
+
: "调用方会话为空,拒绝编排操作。",
|
|
158
|
+
});
|
|
159
|
+
/**
|
|
160
|
+
* Checks that the caller may manage the target: the target must be the caller's DIRECT child.
|
|
161
|
+
*
|
|
162
|
+
* The workspace check is NOT part of this rule and is NOT removed by it - a conversation has to be
|
|
163
|
+
* in the caller's workspace AND in the caller's own subtree. The workspace refusal comes first
|
|
164
|
+
* because it is the older, coarser boundary, and its message names the two workspaces.
|
|
165
|
+
*/
|
|
166
|
+
export const verifyDirectChildScope = (options) => {
|
|
167
|
+
const { store, callerConversationId, targetConversationId } = options;
|
|
168
|
+
const verb = options.verb ?? "调度";
|
|
169
|
+
const caller = callerConversationId?.trim();
|
|
170
|
+
if (!caller)
|
|
171
|
+
return missingCallerRefusal(callerConversationId);
|
|
172
|
+
const callerConversation = store.conversation(caller);
|
|
173
|
+
if (!callerConversation?.workspaceId)
|
|
174
|
+
return missingCallerRefusal(caller);
|
|
175
|
+
const target = store.conversation(targetConversationId);
|
|
176
|
+
if (!target) {
|
|
177
|
+
return { ok: false, status: 400, error: `找不到会话:${targetConversationId}` };
|
|
178
|
+
}
|
|
179
|
+
if (target.workspaceId !== callerConversation.workspaceId) {
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
status: 403,
|
|
183
|
+
error: `调用方会话 ${caller} 所在工作区为 ${callerConversation.workspaceId},目标会话 ${targetConversationId} 所在工作区为 ${target.workspaceId},拒绝跨工作区操作。目标会话必须属于调用方工作区。`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
// THE RULE, IN ONE LINE. The target's tree edge must point at the caller, exactly.
|
|
187
|
+
if (target.parentConversationId !== caller) {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
status: 403,
|
|
191
|
+
error: describeScopeRefusal({
|
|
192
|
+
store,
|
|
193
|
+
callerConversationId: caller,
|
|
194
|
+
targetConversationId,
|
|
195
|
+
verb,
|
|
196
|
+
subject: "manage",
|
|
197
|
+
}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return { ok: true };
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Checks that the caller may list a parent's children.
|
|
204
|
+
*
|
|
205
|
+
* The rule is EQUALITY, not parenthood: the parent whose children are listed must be the caller
|
|
206
|
+
* itself, so the only sessions on the list are the caller's own direct children. Listing the
|
|
207
|
+
* children of the caller's own child would be listing grandchildren, which is the cross-level reach
|
|
208
|
+
* this whole rule exists to refuse.
|
|
209
|
+
*/
|
|
210
|
+
export const verifyListParentScope = (options) => {
|
|
211
|
+
const { store, callerConversationId, requestedParentId } = options;
|
|
212
|
+
const caller = callerConversationId?.trim();
|
|
213
|
+
if (!caller)
|
|
214
|
+
return missingCallerRefusal(callerConversationId);
|
|
215
|
+
const callerConversation = store.conversation(caller);
|
|
216
|
+
if (!callerConversation?.workspaceId)
|
|
217
|
+
return missingCallerRefusal(caller);
|
|
218
|
+
const requested = requestedParentId?.trim();
|
|
219
|
+
// No parent named: the caller's own children are what gets listed.
|
|
220
|
+
if (!requested)
|
|
221
|
+
return { ok: true };
|
|
222
|
+
const parentConversation = store.conversation(requested);
|
|
223
|
+
if (parentConversation && parentConversation.workspaceId !== callerConversation.workspaceId) {
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
status: 403,
|
|
227
|
+
error: `调用方会话 ${caller} 所在工作区为 ${callerConversation.workspaceId},请求的父会话 ${requested} 所在工作区为 ${parentConversation.workspaceId},拒绝跨工作区查询。父会话必须属于调用方工作区。`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (requested === caller)
|
|
231
|
+
return { ok: true };
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
status: 403,
|
|
235
|
+
error: describeScopeRefusal({
|
|
236
|
+
store,
|
|
237
|
+
callerConversationId: caller,
|
|
238
|
+
targetConversationId: requested,
|
|
239
|
+
verb: "列出",
|
|
240
|
+
subject: "list-children",
|
|
241
|
+
}),
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
/**
|
|
245
|
+
* Checks that the caller may REPORT to the target: the target must be the caller's DIRECT PARENT.
|
|
246
|
+
*
|
|
247
|
+
* THE REPORTING LINE IS THE TREE EDGE, READ BACKWARDS. Every other rule in this file asks "may this
|
|
248
|
+
* session manage that one?" - and the answer is always about a session's subordinates. A report is the
|
|
249
|
+
* one message that travels UP, so the rule is inverted: a child reports to the session that owns it,
|
|
250
|
+
* the one whose id is the child's `parentConversationId`, and to nobody else.
|
|
251
|
+
*
|
|
252
|
+
* WHY THE INVERSION IS NOT A LOOPHOLE. A peer is somebody else's child, an ancestor is above the
|
|
253
|
+
* caller's own parent, a grandchild is below the caller, and a session in another workspace is another
|
|
254
|
+
* department. Every one of them would put one branch's news into another branch's inbox, and the inbox
|
|
255
|
+
* is where a parent's NEXT ACTION is decided - so a report that crossed a branch would be a stranger's
|
|
256
|
+
* news steering the wrong supervisor.
|
|
257
|
+
*
|
|
258
|
+
* THE RECIPIENT IS DERIVED, NOT CHOSEN. This check exists for the case where a caller NAMES a
|
|
259
|
+
* recipient at all: the server derives the parent from the tree and this refuses anything else, so a
|
|
260
|
+
* model cannot redirect its own report up or sideways.
|
|
261
|
+
*/
|
|
262
|
+
export const verifyReportTargetScope = (options) => {
|
|
263
|
+
const { store, callerConversationId, targetConversationId } = options;
|
|
264
|
+
const caller = callerConversationId?.trim();
|
|
265
|
+
if (!caller)
|
|
266
|
+
return missingCallerRefusal(callerConversationId);
|
|
267
|
+
const callerConversation = store.conversation(caller);
|
|
268
|
+
if (!callerConversation?.workspaceId)
|
|
269
|
+
return missingCallerRefusal(caller);
|
|
270
|
+
const target = store.conversation(targetConversationId);
|
|
271
|
+
if (!target) {
|
|
272
|
+
return { ok: false, status: 400, error: `找不到会话:${targetConversationId}` };
|
|
273
|
+
}
|
|
274
|
+
if (target.workspaceId !== callerConversation.workspaceId) {
|
|
275
|
+
return {
|
|
276
|
+
ok: false,
|
|
277
|
+
status: 403,
|
|
278
|
+
error: `调用方会话 ${caller} 所在工作区为 ${callerConversation.workspaceId},目标会话 ${targetConversationId} 所在工作区为 ${target.workspaceId},拒绝跨工作区汇报。汇报对象必须与调用方在同一工作区。`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// THE RULE, INVERTED. The caller's own parent edge must point at the target.
|
|
282
|
+
if (callerConversation.parentConversationId === targetConversationId) {
|
|
283
|
+
return { ok: true };
|
|
284
|
+
}
|
|
285
|
+
const relation = classifySessionRelation(store, caller, targetConversationId);
|
|
286
|
+
const reason = relation === "self"
|
|
287
|
+
? `会话 ${caller} 不能向自己汇报`
|
|
288
|
+
: relation === "ancestor"
|
|
289
|
+
? `${targetConversationId} 是 ${caller} 的上级会话,但不是它的直属父会话`
|
|
290
|
+
: relation === "descendant"
|
|
291
|
+
? `${targetConversationId} 是 ${caller} 的下属会话`
|
|
292
|
+
: relation === "peer"
|
|
293
|
+
? `${targetConversationId} 与 ${caller} 是平级会话`
|
|
294
|
+
: relation === "other-root"
|
|
295
|
+
? `${targetConversationId} 是另一个树的顶层会话`
|
|
296
|
+
: `${targetConversationId} 与 ${caller} 不在同一条会话树上`;
|
|
297
|
+
return {
|
|
298
|
+
ok: false,
|
|
299
|
+
status: 403,
|
|
300
|
+
error: `子会话 ${caller} 只能向自己的直属父会话汇报:${reason}。汇报对象由服务端按会话树决定,调用方不能指定。`,
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* Checks the parent a new session is created under.
|
|
305
|
+
*
|
|
306
|
+
* The parent edge is written onto the new conversation at birth, and the result callback follows
|
|
307
|
+
* that edge home. A caller that names somebody ELSE as the parent would therefore rewrite that
|
|
308
|
+
* session's reporting line - putting the victim's result into the caller's inbox - so the parent
|
|
309
|
+
* must be the caller. An empty parent means a top-level session, which is what the user's own "+"
|
|
310
|
+
* creates and what a session with no owner legitimately is.
|
|
311
|
+
*/
|
|
312
|
+
export const verifySpawnParentScope = (options) => {
|
|
313
|
+
const { store, callerConversationId, requestedParentId } = options;
|
|
314
|
+
const caller = callerConversationId?.trim();
|
|
315
|
+
if (!caller)
|
|
316
|
+
return missingCallerRefusal(callerConversationId);
|
|
317
|
+
const callerConversation = store.conversation(caller);
|
|
318
|
+
if (!callerConversation?.workspaceId)
|
|
319
|
+
return missingCallerRefusal(caller);
|
|
320
|
+
const requested = requestedParentId?.trim();
|
|
321
|
+
// No parent named: a top-level session, owned by nobody and reportable to nobody.
|
|
322
|
+
if (!requested)
|
|
323
|
+
return { ok: true };
|
|
324
|
+
if (requested === caller)
|
|
325
|
+
return { ok: true };
|
|
326
|
+
return {
|
|
327
|
+
ok: false,
|
|
328
|
+
status: 403,
|
|
329
|
+
error: describeScopeRefusal({
|
|
330
|
+
store,
|
|
331
|
+
callerConversationId: caller,
|
|
332
|
+
targetConversationId: requested,
|
|
333
|
+
verb: "调度",
|
|
334
|
+
subject: "declare-parent",
|
|
335
|
+
}),
|
|
336
|
+
};
|
|
337
|
+
};
|
package/dist/server/store.d.ts
CHANGED
|
@@ -120,6 +120,8 @@ export declare class Store {
|
|
|
120
120
|
*/
|
|
121
121
|
workspaceForConversation(conversationId: string): Workspace | undefined;
|
|
122
122
|
workspace(workspaceId: string): Workspace | undefined;
|
|
123
|
+
/** Read every workspace as detached shallow copies. */
|
|
124
|
+
workspaces(): Workspace[];
|
|
123
125
|
/** Read the app-wide default without cloning conversations or transcripts. */
|
|
124
126
|
defaultModel(): ModelSelection | undefined;
|
|
125
127
|
/** Read one detached conversation for lifecycle operations. */
|
|
@@ -616,6 +618,17 @@ export declare class Store {
|
|
|
616
618
|
* going to act on it without sending another prompt first).
|
|
617
619
|
*/
|
|
618
620
|
setFailureReason(conversationId: string, reason: string | undefined): Promise<void>;
|
|
621
|
+
/**
|
|
622
|
+
* Records WHY THIS CONVERSATION'S LAST ANSWER IS INCOMPLETE, in a way the reader can see.
|
|
623
|
+
*
|
|
624
|
+
* `Conversation.warning` is a different fact from `Conversation.error`: the run did not break, the
|
|
625
|
+
* answer was cut short by the model's output budget. The reader's next move is different too - a
|
|
626
|
+
* failed turn asks them to retry, a truncated one asks them to let the model continue - so the two
|
|
627
|
+
* are kept apart rather than sharing one field and one colour.
|
|
628
|
+
*
|
|
629
|
+
* Set to `undefined` to clear, which the next `addUserMessage` does anyway.
|
|
630
|
+
*/
|
|
631
|
+
setWarningReason(conversationId: string, reason: string | undefined): Promise<void>;
|
|
619
632
|
/** The plan a revert would carry out, without carrying it out. The server reads this to stop children first. */
|
|
620
633
|
planRevert(conversationId: string, messageId: string, withdraw?: boolean): RevertPlan;
|
|
621
634
|
/**
|