@cjhyy/code-shell-web 0.9.31 → 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 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" && args.requireJournal) {
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
+ }
@@ -53,6 +53,10 @@ export type ChatItem = {
53
53
  agentId: string;
54
54
  label: string;
55
55
  status: string;
56
+ } | {
57
+ kind: "system";
58
+ id: string;
59
+ text: string;
56
60
  } | {
57
61
  kind: "system_error";
58
62
  id: string;
@@ -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 {