@cjhyy/code-shell-web 0.9.32 → 0.9.33
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/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/mobileOutputRecovery.js +15 -1
- package/dist/lib/outputCoverage.d.ts +26 -0
- package/dist/lib/outputCoverage.js +153 -0
- package/dist/lib/streamReducer.d.ts +4 -0
- package/dist/lib/streamReducer.js +19 -0
- package/dist-app/assets/index-hVyQ_FRP.js +57 -0
- package/dist-app/index.html +1 -1
- package/package.json +3 -3
- package/dist-app/assets/index-DLKEXorj.js +0 -57
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from "./hooks/remoteAppSync.js";
|
|
|
14
14
|
export * from "./lib/streamReducer.js";
|
|
15
15
|
export * from "./lib/transcriptReplay.js";
|
|
16
16
|
export * from "./lib/outputJournalRecovery.js";
|
|
17
|
+
export * from "./lib/outputCoverage.js";
|
|
17
18
|
export * from "./lib/messageMappers.js";
|
|
18
19
|
export * from "./lib/riskClassify.js";
|
|
19
20
|
export * from "./lib/format.js";
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export * from "./hooks/remoteAppSync.js";
|
|
|
16
16
|
export * from "./lib/streamReducer.js";
|
|
17
17
|
export * from "./lib/transcriptReplay.js";
|
|
18
18
|
export * from "./lib/outputJournalRecovery.js";
|
|
19
|
+
export * from "./lib/outputCoverage.js";
|
|
19
20
|
export * from "./lib/messageMappers.js";
|
|
20
21
|
// ── Pure helpers ─────────────────────────────────────────────────────────────
|
|
21
22
|
export * from "./lib/riskClassify.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { applyOutputJournalPage, compareOutputCursors, } from "./outputJournalRecovery.js";
|
|
2
|
+
import { assertOutputCoverage } from "./outputCoverage.js";
|
|
2
3
|
import { initialChatState, reduceStream } from "./streamReducer.js";
|
|
3
4
|
/** Valid identities may arrive before their new Core Run acquires a durable anchor. */
|
|
4
5
|
export class MobilePendingInput extends Error {
|
|
@@ -29,7 +30,18 @@ export async function recoverMobileOutput(args) {
|
|
|
29
30
|
let reply = await args.read({});
|
|
30
31
|
if (!args.canContinue())
|
|
31
32
|
throw new Error("Recovery cancelled");
|
|
32
|
-
if (reply.page.status === "unavailable"
|
|
33
|
+
if (reply.page.status === "unavailable") {
|
|
34
|
+
const snapshot = reply.snapshot;
|
|
35
|
+
assertOutputCoverage({ unpaired: snapshot?.unpaired, inputIds: snapshot?.inputIds }, {}, () => true);
|
|
36
|
+
if (snapshot?.journalIdentity && !snapshot.outputCursor && !args.latestCursor())
|
|
37
|
+
throw new Error("Required output journal is unavailable");
|
|
38
|
+
// Valid live input may precede the first canonical Run. Preserve its typed
|
|
39
|
+
// retry; an already observed cursor instead requires the real reread below.
|
|
40
|
+
if (snapshot?.inputIds?.length && !snapshot.outputCursor && !args.latestCursor())
|
|
41
|
+
throw new MobilePendingInput(snapshot.inputIds);
|
|
42
|
+
}
|
|
43
|
+
if (reply.page.status === "unavailable" &&
|
|
44
|
+
(args.requireJournal || reply.snapshot?.outputCursor || args.latestCursor())) {
|
|
33
45
|
const snapshot = reply.snapshot;
|
|
34
46
|
if (!snapshot ||
|
|
35
47
|
snapshot.unpaired ||
|
|
@@ -91,6 +103,7 @@ export async function recoverMobileOutput(args) {
|
|
|
91
103
|
!Number.isSafeInteger(snapshot.nextSeq) ||
|
|
92
104
|
snapshot.nextSeq < 1)
|
|
93
105
|
throw new Error("Recovery snapshot is unpaired");
|
|
106
|
+
assertOutputCoverage({ ...snapshot, inputIds: undefined }, reply.page, () => true);
|
|
94
107
|
let target = recovery.cursor;
|
|
95
108
|
for (const value of [snapshot.outputCursor, args.latestCursor()]) {
|
|
96
109
|
if (value === undefined)
|
|
@@ -112,6 +125,7 @@ export async function recoverMobileOutput(args) {
|
|
|
112
125
|
const missing = ids?.filter((id) => !chat.items.some((item) => item.kind === "user" && item.clientMessageId === id));
|
|
113
126
|
if (missing?.length)
|
|
114
127
|
throw new MobilePendingInput(missing);
|
|
128
|
+
assertOutputCoverage(snapshot, reply.page, (id) => chat.items.some((item) => item.kind === "user" && item.clientMessageId === id));
|
|
115
129
|
return { chat, outputCursor: recovery.cursor, snapshot };
|
|
116
130
|
}
|
|
117
131
|
recovery = { incomplete: false, cursor: recovery.appliedCursor };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare class OutputCoverage {
|
|
2
|
+
private lastCursor?;
|
|
3
|
+
private blocked;
|
|
4
|
+
private overflowed;
|
|
5
|
+
private readonly inputs;
|
|
6
|
+
/** Stage observation before a Host publication; failure can discard this copy. */
|
|
7
|
+
copy(): OutputCoverage;
|
|
8
|
+
observe(raw: unknown): void;
|
|
9
|
+
acknowledgeInputs(ids: readonly string[]): void;
|
|
10
|
+
get inputIds(): string[];
|
|
11
|
+
get incomplete(): boolean;
|
|
12
|
+
get memoryBytes(): number;
|
|
13
|
+
discardProof(): void;
|
|
14
|
+
}
|
|
15
|
+
/** Validate the authoritative final coverage join without exposing a partial candidate. */
|
|
16
|
+
export declare function assertOutputCoverage(proof: {
|
|
17
|
+
unpaired?: boolean;
|
|
18
|
+
inputIds?: string[];
|
|
19
|
+
incarnation?: string;
|
|
20
|
+
journalIdentity?: string;
|
|
21
|
+
}, page: {
|
|
22
|
+
coverageIdentity?: string;
|
|
23
|
+
coverageStart?: string;
|
|
24
|
+
}, hasInput: (id: string) => boolean, options?: {
|
|
25
|
+
requireIdentity?: boolean;
|
|
26
|
+
}): void;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { compareOutputCursors } from "./outputJournalRecovery.js";
|
|
2
|
+
/** Bounded display-coverage proof, independent of the evictable Main event suffix. */
|
|
3
|
+
const visible = new Set([
|
|
4
|
+
"session_started",
|
|
5
|
+
"stream_request_start",
|
|
6
|
+
"text_delta",
|
|
7
|
+
"thinking_delta",
|
|
8
|
+
"tool_use_start",
|
|
9
|
+
"tool_use_args_delta",
|
|
10
|
+
"tool_result",
|
|
11
|
+
"room_tool_result",
|
|
12
|
+
"tool_summary",
|
|
13
|
+
"tombstone",
|
|
14
|
+
"assistant_message",
|
|
15
|
+
"turn_complete",
|
|
16
|
+
"goal_set",
|
|
17
|
+
"goal_updated",
|
|
18
|
+
"goal_cleared",
|
|
19
|
+
"goal_progress",
|
|
20
|
+
"task_update",
|
|
21
|
+
"agent_start",
|
|
22
|
+
"agent_end",
|
|
23
|
+
"background_agent_completed",
|
|
24
|
+
"context_compact",
|
|
25
|
+
"error",
|
|
26
|
+
"session_user_message",
|
|
27
|
+
"steer_injected",
|
|
28
|
+
"user_message",
|
|
29
|
+
"assistant_text",
|
|
30
|
+
]);
|
|
31
|
+
function validCursor(value) {
|
|
32
|
+
if (typeof value !== "string" || value.length > 2048 || !/^[A-Za-z0-9_-]+$/.test(value))
|
|
33
|
+
return false;
|
|
34
|
+
try {
|
|
35
|
+
const cursor = JSON.parse(atob(value.replace(/-/g, "+").replace(/_/g, "/")));
|
|
36
|
+
return (typeof cursor.identity === "string" &&
|
|
37
|
+
typeof cursor.hash === "string" &&
|
|
38
|
+
/^[a-f0-9]{64}$/.test(cursor.identity) &&
|
|
39
|
+
/^[a-f0-9]{64}$/.test(cursor.hash) &&
|
|
40
|
+
Number.isSafeInteger(cursor.sequence) &&
|
|
41
|
+
cursor.sequence > 0 &&
|
|
42
|
+
Number.isSafeInteger(cursor.offset) &&
|
|
43
|
+
cursor.offset > 0);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export class OutputCoverage {
|
|
50
|
+
lastCursor;
|
|
51
|
+
blocked = false;
|
|
52
|
+
overflowed = false;
|
|
53
|
+
inputs = new Set();
|
|
54
|
+
/** Stage observation before a Host publication; failure can discard this copy. */
|
|
55
|
+
copy() {
|
|
56
|
+
const proof = new OutputCoverage();
|
|
57
|
+
proof.lastCursor = this.lastCursor;
|
|
58
|
+
proof.blocked = this.blocked;
|
|
59
|
+
proof.overflowed = this.overflowed;
|
|
60
|
+
for (const id of this.inputs)
|
|
61
|
+
proof.inputs.add(id);
|
|
62
|
+
return proof;
|
|
63
|
+
}
|
|
64
|
+
observe(raw) {
|
|
65
|
+
if (this.overflowed)
|
|
66
|
+
return;
|
|
67
|
+
const event = raw;
|
|
68
|
+
if (!event)
|
|
69
|
+
return;
|
|
70
|
+
if (event.outputCursor !== undefined) {
|
|
71
|
+
const order = validCursor(event.outputCursor)
|
|
72
|
+
? this.lastCursor
|
|
73
|
+
? compareOutputCursors(event.outputCursor, this.lastCursor)
|
|
74
|
+
: 1
|
|
75
|
+
: undefined;
|
|
76
|
+
if (order === undefined || order <= 0) {
|
|
77
|
+
this.discardProof();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.lastCursor = event.outputCursor;
|
|
81
|
+
// A new Run does not remove historical errors or the current Goal from
|
|
82
|
+
// the display reducer. Only explicit coverage can release their barrier.
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!event.type || !visible.has(event.type))
|
|
86
|
+
return;
|
|
87
|
+
if (["session_user_message", "steer_injected", "user_message"].includes(event.type) &&
|
|
88
|
+
(event.agentId ||
|
|
89
|
+
event.injected === true ||
|
|
90
|
+
["agent", "system", "policy"].includes(String(event.authority))))
|
|
91
|
+
return;
|
|
92
|
+
if (event.type === "session_user_message") {
|
|
93
|
+
const id = event.clientMessageId;
|
|
94
|
+
if (typeof id !== "string" || !id.trim() || id.length > 512)
|
|
95
|
+
this.blocked = true;
|
|
96
|
+
else
|
|
97
|
+
this.inputs.add(id);
|
|
98
|
+
if (this.inputIds.length > 128 || this.memoryBytes > 32 * 1024)
|
|
99
|
+
this.discardProof();
|
|
100
|
+
}
|
|
101
|
+
else
|
|
102
|
+
this.blocked = true;
|
|
103
|
+
}
|
|
104
|
+
acknowledgeInputs(ids) {
|
|
105
|
+
for (const id of ids)
|
|
106
|
+
this.inputs.delete(id);
|
|
107
|
+
}
|
|
108
|
+
get inputIds() {
|
|
109
|
+
return [...this.inputs];
|
|
110
|
+
}
|
|
111
|
+
get incomplete() {
|
|
112
|
+
return this.blocked || this.overflowed;
|
|
113
|
+
}
|
|
114
|
+
get memoryBytes() {
|
|
115
|
+
return this.inputIds.reduce((sum, id) => sum + new TextEncoder().encode(id).length, 0);
|
|
116
|
+
}
|
|
117
|
+
discardProof() {
|
|
118
|
+
// Overflow never silently forgets a required identity or becomes success.
|
|
119
|
+
this.overflowed = true;
|
|
120
|
+
this.inputs.clear();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Validate the authoritative final coverage join without exposing a partial candidate. */
|
|
124
|
+
export function assertOutputCoverage(proof, page, hasInput, options = {}) {
|
|
125
|
+
if (proof.unpaired)
|
|
126
|
+
throw new Error("Output recovery contains unpaired visible output");
|
|
127
|
+
if (page.coverageIdentity !== undefined &&
|
|
128
|
+
options.requireIdentity !== false &&
|
|
129
|
+
(proof.incarnation === undefined || proof.journalIdentity === undefined))
|
|
130
|
+
throw new Error("Output recovery Session incarnation proof is unavailable");
|
|
131
|
+
if (proof.incarnation !== undefined && proof.incarnation !== page.coverageIdentity)
|
|
132
|
+
throw new Error("Output recovery Session incarnation changed");
|
|
133
|
+
if (proof.journalIdentity !== undefined) {
|
|
134
|
+
let pin;
|
|
135
|
+
try {
|
|
136
|
+
pin = JSON.parse(atob((page.coverageStart ?? "").replace(/-/g, "+").replace(/_/g, "/"))).hash;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
throw new Error("Output recovery journal pin is unpaired");
|
|
140
|
+
}
|
|
141
|
+
if (pin !== proof.journalIdentity)
|
|
142
|
+
throw new Error("Output recovery journal pin changed");
|
|
143
|
+
}
|
|
144
|
+
const ids = proof.inputIds ?? [];
|
|
145
|
+
if (!Array.isArray(ids) ||
|
|
146
|
+
ids.length > 128 ||
|
|
147
|
+
new Set(ids).size !== ids.length ||
|
|
148
|
+
ids.some((id) => typeof id !== "string" || !id.trim() || id.length > 512) ||
|
|
149
|
+
ids.reduce((sum, id) => sum + new TextEncoder().encode(id).length, 0) > 32 * 1024)
|
|
150
|
+
throw new Error("Output recovery input identities are unpaired");
|
|
151
|
+
if (ids.some((id) => !hasInput(id)))
|
|
152
|
+
throw new Error("Output recovery input has no recorded anchor");
|
|
153
|
+
}
|
|
@@ -489,6 +489,25 @@ export function reduceStream(state, raw) {
|
|
|
489
489
|
: i),
|
|
490
490
|
};
|
|
491
491
|
}
|
|
492
|
+
case "background_agent_completed": {
|
|
493
|
+
const agentId = typeof event.agentId === "string" ? event.agentId : "";
|
|
494
|
+
const failed = event.status === "failed";
|
|
495
|
+
const status = failed ? "error" : event.status === "cancelled" ? "cancelled" : "completed";
|
|
496
|
+
const name = typeof event.name === "string" ? event.name : "后台任务";
|
|
497
|
+
const preview = failed
|
|
498
|
+
? (event.error ?? event.description)
|
|
499
|
+
: (event.finalText ?? event.description);
|
|
500
|
+
const text = `${name} · ${status}${typeof preview === "string" && preview.trim() ? `: ${preview.trim().slice(0, 240)}` : ""}`;
|
|
501
|
+
const [id, next] = freshId("bg-done");
|
|
502
|
+
return {
|
|
503
|
+
...next,
|
|
504
|
+
// A late background result cannot finish the concurrently active turn.
|
|
505
|
+
items: [
|
|
506
|
+
...s.items.map((item) => item.kind === "subagent" && item.agentId === agentId ? { ...item, status } : item),
|
|
507
|
+
{ kind: "system", id, text },
|
|
508
|
+
],
|
|
509
|
+
};
|
|
510
|
+
}
|
|
492
511
|
case "error": {
|
|
493
512
|
const [id, s2] = freshId("err");
|
|
494
513
|
return {
|