@cjhyy/code-shell-core 0.8.6 → 0.8.8
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/cli/agent-server-stdio.js +7 -5
- package/dist/context/manager.js +10 -2
- package/dist/engine/engine.d.ts +60 -8
- package/dist/engine/engine.js +153 -16
- package/dist/engine/run-setup.d.ts +1 -0
- package/dist/engine/run-setup.js +2 -1
- package/dist/engine/run-types.d.ts +7 -0
- package/dist/engine/turn-loop.d.ts +0 -3
- package/dist/engine/turn-loop.js +1 -80
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/panel-apps/manifest.d.ts +11 -11
- package/dist/panel-apps/manifest.js +16 -3
- package/dist/prompt/composer.d.ts +7 -0
- package/dist/prompt/composer.js +15 -8
- package/dist/prompt/sections/browser.md +2 -2
- package/dist/protocol/server.js +41 -1
- package/dist/protocol/types.d.ts +14 -0
- package/dist/session/memory.d.ts +10 -0
- package/dist/session/memory.js +7 -1
- package/dist/session/session-manager.js +1 -0
- package/dist/session/transcript.d.ts +36 -0
- package/dist/session/transcript.js +149 -4
- package/dist/tool-system/browser-bridge.d.ts +3 -0
- package/dist/tool-system/builtin/browser-tools.js +13 -0
- package/dist/tool-system/path-policy.js +84 -5
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const PANEL_APP_MANIFEST_FILE = ".codeshell-panel/panel.json";
|
|
3
|
-
export declare const PANEL_APP_PERMISSIONS: readonly ["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "workspace.info", "workspace.read", "workspace.write", "notifications.send"];
|
|
3
|
+
export declare const PANEL_APP_PERMISSIONS: readonly ["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "audio.transcribe", "credentials.cookies", "automations.manage"];
|
|
4
4
|
export declare const PANEL_APP_ICONS: readonly ["panel", "activity", "bar-chart-3", "chart", "file-text", "globe", "image", "layout-dashboard", "line-chart", "palette", "pie-chart", "table", "terminal"];
|
|
5
5
|
export declare const PanelAppAgentTool: z.ZodEffects<z.ZodObject<{
|
|
6
6
|
name: z.ZodString;
|
|
@@ -116,12 +116,12 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
116
116
|
icon: z.ZodDefault<z.ZodEnum<["panel", "activity", "bar-chart-3", "chart", "file-text", "globe", "image", "layout-dashboard", "line-chart", "palette", "pie-chart", "table", "terminal"]>>;
|
|
117
117
|
placement: z.ZodDefault<z.ZodLiteral<"right-dock">>;
|
|
118
118
|
singleton: z.ZodDefault<z.ZodBoolean>;
|
|
119
|
-
permissions: z.ZodDefault<z.ZodArray<z.ZodEnum<["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "workspace.info", "workspace.read", "workspace.write", "notifications.send"]>, "many">>;
|
|
119
|
+
permissions: z.ZodDefault<z.ZodArray<z.ZodEnum<["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "audio.transcribe", "credentials.cookies", "automations.manage"]>, "many">>;
|
|
120
120
|
schemaVersion: z.ZodLiteral<1>;
|
|
121
121
|
}, "strict", z.ZodTypeAny, {
|
|
122
122
|
id: string;
|
|
123
123
|
version: string;
|
|
124
|
-
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send")[];
|
|
124
|
+
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[];
|
|
125
125
|
entry: string;
|
|
126
126
|
title: {
|
|
127
127
|
default: string;
|
|
@@ -144,7 +144,7 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
144
144
|
};
|
|
145
145
|
schemaVersion: 1;
|
|
146
146
|
description?: string | undefined;
|
|
147
|
-
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send")[] | undefined;
|
|
147
|
+
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[] | undefined;
|
|
148
148
|
icon?: "terminal" | "image" | "panel" | "activity" | "bar-chart-3" | "chart" | "file-text" | "globe" | "layout-dashboard" | "line-chart" | "palette" | "pie-chart" | "table" | undefined;
|
|
149
149
|
placement?: "right-dock" | undefined;
|
|
150
150
|
singleton?: boolean | undefined;
|
|
@@ -230,12 +230,12 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
230
230
|
icon: z.ZodDefault<z.ZodEnum<["panel", "activity", "bar-chart-3", "chart", "file-text", "globe", "image", "layout-dashboard", "line-chart", "palette", "pie-chart", "table", "terminal"]>>;
|
|
231
231
|
placement: z.ZodDefault<z.ZodLiteral<"right-dock">>;
|
|
232
232
|
singleton: z.ZodDefault<z.ZodBoolean>;
|
|
233
|
-
permissions: z.ZodDefault<z.ZodArray<z.ZodEnum<["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "workspace.info", "workspace.read", "workspace.write", "notifications.send"]>, "many">>;
|
|
233
|
+
permissions: z.ZodDefault<z.ZodArray<z.ZodEnum<["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "audio.transcribe", "credentials.cookies", "automations.manage"]>, "many">>;
|
|
234
234
|
schemaVersion: z.ZodLiteral<2>;
|
|
235
235
|
}, "strict", z.ZodTypeAny, {
|
|
236
236
|
id: string;
|
|
237
237
|
version: string;
|
|
238
|
-
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send")[];
|
|
238
|
+
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[];
|
|
239
239
|
entry: string;
|
|
240
240
|
title: {
|
|
241
241
|
default: string;
|
|
@@ -276,14 +276,14 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
276
276
|
}[] | undefined;
|
|
277
277
|
} | undefined;
|
|
278
278
|
description?: string | undefined;
|
|
279
|
-
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send")[] | undefined;
|
|
279
|
+
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[] | undefined;
|
|
280
280
|
icon?: "terminal" | "image" | "panel" | "activity" | "bar-chart-3" | "chart" | "file-text" | "globe" | "layout-dashboard" | "line-chart" | "palette" | "pie-chart" | "table" | undefined;
|
|
281
281
|
placement?: "right-dock" | undefined;
|
|
282
282
|
singleton?: boolean | undefined;
|
|
283
283
|
}>]>, {
|
|
284
284
|
id: string;
|
|
285
285
|
version: string;
|
|
286
|
-
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send")[];
|
|
286
|
+
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[];
|
|
287
287
|
entry: string;
|
|
288
288
|
title: {
|
|
289
289
|
default: string;
|
|
@@ -298,7 +298,7 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
298
298
|
} | {
|
|
299
299
|
id: string;
|
|
300
300
|
version: string;
|
|
301
|
-
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send")[];
|
|
301
|
+
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[];
|
|
302
302
|
entry: string;
|
|
303
303
|
title: {
|
|
304
304
|
default: string;
|
|
@@ -330,7 +330,7 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
330
330
|
};
|
|
331
331
|
schemaVersion: 1;
|
|
332
332
|
description?: string | undefined;
|
|
333
|
-
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send")[] | undefined;
|
|
333
|
+
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[] | undefined;
|
|
334
334
|
icon?: "terminal" | "image" | "panel" | "activity" | "bar-chart-3" | "chart" | "file-text" | "globe" | "layout-dashboard" | "line-chart" | "palette" | "pie-chart" | "table" | undefined;
|
|
335
335
|
placement?: "right-dock" | undefined;
|
|
336
336
|
singleton?: boolean | undefined;
|
|
@@ -354,7 +354,7 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
354
354
|
}[] | undefined;
|
|
355
355
|
} | undefined;
|
|
356
356
|
description?: string | undefined;
|
|
357
|
-
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send")[] | undefined;
|
|
357
|
+
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[] | undefined;
|
|
358
358
|
icon?: "terminal" | "image" | "panel" | "activity" | "bar-chart-3" | "chart" | "file-text" | "globe" | "layout-dashboard" | "line-chart" | "palette" | "pie-chart" | "table" | undefined;
|
|
359
359
|
placement?: "right-dock" | undefined;
|
|
360
360
|
singleton?: boolean | undefined;
|
|
@@ -11,6 +11,9 @@ export const PANEL_APP_PERMISSIONS = [
|
|
|
11
11
|
"workspace.read",
|
|
12
12
|
"workspace.write",
|
|
13
13
|
"notifications.send",
|
|
14
|
+
"audio.transcribe",
|
|
15
|
+
"credentials.cookies",
|
|
16
|
+
"automations.manage",
|
|
14
17
|
];
|
|
15
18
|
export const PANEL_APP_ICONS = [
|
|
16
19
|
"panel",
|
|
@@ -136,7 +139,7 @@ const PanelAppManifestFields = {
|
|
|
136
139
|
icon: z.enum(PANEL_APP_ICONS).default("panel"),
|
|
137
140
|
placement: z.literal("right-dock").default("right-dock"),
|
|
138
141
|
singleton: z.boolean().default(true),
|
|
139
|
-
permissions: z.array(z.enum(PANEL_APP_PERMISSIONS)).max(
|
|
142
|
+
permissions: z.array(z.enum(PANEL_APP_PERMISSIONS)).max(12).default([]),
|
|
140
143
|
};
|
|
141
144
|
/**
|
|
142
145
|
* Schema v2 keeps one installable Panel App identity while allowing it to
|
|
@@ -178,12 +181,22 @@ export const PanelAppManifest = z
|
|
|
178
181
|
});
|
|
179
182
|
}
|
|
180
183
|
if ((value.permissions.includes("workspace.read") ||
|
|
181
|
-
value.permissions.includes("workspace.write")
|
|
184
|
+
value.permissions.includes("workspace.write") ||
|
|
185
|
+
value.permissions.includes("audio.transcribe")) &&
|
|
182
186
|
!value.permissions.includes("context.workspace")) {
|
|
183
187
|
ctx.addIssue({
|
|
184
188
|
code: z.ZodIssueCode.custom,
|
|
185
189
|
path: ["permissions"],
|
|
186
|
-
message: "workspace.read
|
|
190
|
+
message: "workspace.read, workspace.write, and audio.transcribe require context.workspace",
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (value.permissions.includes("automations.manage") &&
|
|
194
|
+
(!value.permissions.includes("context.session") ||
|
|
195
|
+
!value.permissions.includes("context.workspace"))) {
|
|
196
|
+
ctx.addIssue({
|
|
197
|
+
code: z.ZodIssueCode.custom,
|
|
198
|
+
path: ["permissions"],
|
|
199
|
+
message: "automations.manage requires context.session and context.workspace",
|
|
187
200
|
});
|
|
188
201
|
}
|
|
189
202
|
});
|
|
@@ -89,6 +89,13 @@ export interface ComposerOptions {
|
|
|
89
89
|
* Undefined/0 → inject all.
|
|
90
90
|
*/
|
|
91
91
|
memoriesMaxAgeDays?: number;
|
|
92
|
+
/**
|
|
93
|
+
* When true (set from the active behavior profile, e.g. the Pet manager),
|
|
94
|
+
* the injected memory index keeps only memories relevant to this cwd —
|
|
95
|
+
* global-layer project/dream records tied to other projects are dropped.
|
|
96
|
+
* See MemoryManager.buildInjectionIndex(currentProjectOnly).
|
|
97
|
+
*/
|
|
98
|
+
memoryCurrentProjectOnly?: boolean;
|
|
92
99
|
}
|
|
93
100
|
export declare class PromptComposer {
|
|
94
101
|
private readonly options;
|
package/dist/prompt/composer.js
CHANGED
|
@@ -102,18 +102,24 @@ export class PromptComposer {
|
|
|
102
102
|
});
|
|
103
103
|
const skillsListing = buildSkillListing(skills);
|
|
104
104
|
const declaredSkillGap = this.buildDeclaredSkillGap(skills);
|
|
105
|
-
|
|
105
|
+
// Capability and sources context are independent I/O — resolve them
|
|
106
|
+
// concurrently instead of serially.
|
|
107
|
+
const [capabilityContext, sourcesContext] = await Promise.all([
|
|
108
|
+
this.buildSystemContext(),
|
|
109
|
+
(async () => {
|
|
110
|
+
try {
|
|
111
|
+
return (await this.options.sourcesContextProvider?.()) ?? "";
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Optional metadata context must not make a turn fail.
|
|
115
|
+
return "";
|
|
116
|
+
}
|
|
117
|
+
})(),
|
|
118
|
+
]);
|
|
106
119
|
// Memory rides here (tail, past the cache breakpoint) — not the system
|
|
107
120
|
// prefix — so a memory change (extraction / recall usage++ / approve) never
|
|
108
121
|
// re-bills the cached prefix. See buildUserContextMessage for the rationale.
|
|
109
122
|
const memoryContext = this.getMemoryContext();
|
|
110
|
-
let sourcesContext = "";
|
|
111
|
-
try {
|
|
112
|
-
sourcesContext = (await this.options.sourcesContextProvider?.()) ?? "";
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
// Optional metadata context must not make a turn fail.
|
|
116
|
-
}
|
|
117
123
|
const goalToolContext = this.buildGoalToolContext();
|
|
118
124
|
const parts = [
|
|
119
125
|
skillsListing,
|
|
@@ -268,6 +274,7 @@ export class PromptComposer {
|
|
|
268
274
|
projectDir: this.options.cwd,
|
|
269
275
|
profileDir: this.options.profileMemoryDir,
|
|
270
276
|
maxAgeDays: this.options.memoriesMaxAgeDays,
|
|
277
|
+
currentProjectOnly: this.options.memoryCurrentProjectOnly,
|
|
271
278
|
});
|
|
272
279
|
}
|
|
273
280
|
catch {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
## Browser automation (browser_observe / browser_act / browser_navigate)
|
|
2
2
|
|
|
3
3
|
- When a task needs you to operate a real web page — search a site, open a result, read/extract an article, fill a form — use the three browser tools. They drive a task-owned background tab in CodeShell's in-app browser profile. The tab can share that profile's login state, but it never controls a user-opened tab unless the user explicitly grants that exact tab. The user's regular Chrome profile is available only through an explicit Chrome extension grant.
|
|
4
|
-
- The Browser Runtime starts lazily in the background. Call `browser_navigate{url}`, then `browser_act{action:"wait"}` and `browser_observe`. If login, 2FA, or a high-consequence action needs the user
|
|
4
|
+
- The Browser Runtime starts lazily in the background. Call `browser_navigate{url}`, then `browser_act{action:"wait"}` and `browser_observe`. If login, 2FA, CAPTCHA, or a high-consequence action needs the user—or the user explicitly asks to see the page—call `browser_act{action:"request_takeover"}`. CodeShell reveals the exact same runtime-owned in-app target; do not send a second link and claim it is the page you operated. Scheduled and explicitly isolated work may use a separate Dedicated Playwright profile instead.
|
|
5
5
|
- The loop is observe → act → re-observe: ALWAYS `browser_observe` first (default mode `snapshot`) to see the page's interactive elements (each tagged `[ref=eN]`), then `browser_act` on elements BY THAT ref: `{action:"click",ref}`, `{action:"type",ref,text}`, `{action:"select",ref,value}` (native `<select>`), `{action:"hover",ref}`. Refs are only valid for the most recent snapshot — after any navigation, content-loading click, or `{action:"scroll"}`, run `{action:"wait"}` then `browser_observe` again. If an action says a ref is stale, re-observe.
|
|
6
6
|
- To submit a search: `browser_act{action:"type",ref,text}` into the search box, then `browser_act{action:"press_key",key:"Enter"}`, then `{action:"wait"}` + `browser_observe`. `press_key` also does Tab / Escape / arrows / combos like `Control+a`.
|
|
7
7
|
- To extract/summarize page content, navigate/click to it, `browser_act{action:"wait"}`, then `browser_observe{mode:"read"}`. If it returns `nextCursor`, continue with `browser_observe{mode:"read",cursor:"..."}` until `Read: complete`. Do NOT scroll merely to obtain the next text chunk. Use scroll only to load lazy/infinite content; stop when it reports `NO_PROGRESS` or `(end)`. For real link/image/video URLs, use `browser_observe{mode:"extract"}` (media is tagged [ref=imgN/vidN]).
|
|
8
8
|
- To SEE an actual image's content (e.g. a 小红书 笔记配图 or product photo), `browser_observe{mode:"extract"}` to get image refs, then `browser_observe{mode:"image", refs:["img3"]}` — it loads the real pixels (works behind hotlink protection). A vidN ref grabs the video's current frame. To see the rendered layout/canvas/chart, `browser_observe{mode:"vision"}` (optionally `ref` for one region). Use images sparingly — they cost tokens; prefer snapshot/read. (Both need a vision-capable model; on a non-vision model they're skipped.) Do NOT repeat `vision` on the same page hoping for a clearer view — one screenshot is all you get. If snapshot/screenshot don't reveal what you need (infinite-scroll/canvas feeds), scroll + re-observe or extract the URLs and act on them — don't loop screenshots.
|
|
9
9
|
- Multiple tabs: `browser_act{action:"list_tabs"}` shows open tabs (tabId/url/title/active); `browser_act{action:"switch_tab", tabId}` makes another the active one (then re-observe). Any action also accepts `tabId` to target a specific tab (it switches first). Refs are per-tab — re-observe after switching.
|
|
10
|
-
- If `browser_observe` reports a sign-in is required (or you hit a login wall / 2FA), STOP and ask the user to
|
|
10
|
+
- If `browser_observe` reports a sign-in is required (or you hit a login wall / 2FA / CAPTCHA), call `browser_act{action:"request_takeover"}`, STOP, and ask the user to act in the revealed Browser Runtime window. Continue only after the user finishes; do not attempt to enter credentials yourself. Sensitive actions (payment, delete, entering card/password values) require user approval.
|
|
11
11
|
- If the tools report that no Browser Runtime is available, the current host does not provide browser automation — say so rather than retrying.
|
package/dist/protocol/server.js
CHANGED
|
@@ -2240,7 +2240,17 @@ export class AgentServer {
|
|
|
2240
2240
|
if (!archiveEngine)
|
|
2241
2241
|
return;
|
|
2242
2242
|
try {
|
|
2243
|
-
const
|
|
2243
|
+
const toClientMessageId = typeof params.toClientMessageId === "string" ? params.toClientMessageId : undefined;
|
|
2244
|
+
const anchors = toClientMessageId
|
|
2245
|
+
? {
|
|
2246
|
+
toClientMessageId,
|
|
2247
|
+
...(typeof params.fromClientMessageId === "string"
|
|
2248
|
+
? { fromClientMessageId: params.fromClientMessageId }
|
|
2249
|
+
: {}),
|
|
2250
|
+
...(typeof params.segmentId === "string" ? { segmentId: params.segmentId } : {}),
|
|
2251
|
+
}
|
|
2252
|
+
: undefined;
|
|
2253
|
+
const result = await archiveEngine.archiveTurnRange(archiveSessionId, { start, end }, anchors);
|
|
2244
2254
|
if (result.before > result.after) {
|
|
2245
2255
|
const event = {
|
|
2246
2256
|
type: "context_compact",
|
|
@@ -2265,6 +2275,35 @@ export class AgentServer {
|
|
|
2265
2275
|
}
|
|
2266
2276
|
break;
|
|
2267
2277
|
}
|
|
2278
|
+
case "archive_marker": {
|
|
2279
|
+
const markerSessionId = typeof params.sessionId === "string" && params.sessionId.length > 0
|
|
2280
|
+
? params.sessionId
|
|
2281
|
+
: undefined;
|
|
2282
|
+
const summary = typeof params.summary === "string" ? params.summary : undefined;
|
|
2283
|
+
const toClientMessageId = typeof params.toClientMessageId === "string" ? params.toClientMessageId : undefined;
|
|
2284
|
+
if (!markerSessionId || !summary || !toClientMessageId) {
|
|
2285
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "archive_marker requires sessionId, summary and toClientMessageId"));
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
const markerEngine = await this.resolveEngineForSessionQuery(req, markerSessionId, engine, "archive_marker");
|
|
2289
|
+
if (!markerEngine)
|
|
2290
|
+
return;
|
|
2291
|
+
try {
|
|
2292
|
+
const appended = await markerEngine.appendArchiveMarker(markerSessionId, {
|
|
2293
|
+
summary,
|
|
2294
|
+
toClientMessageId,
|
|
2295
|
+
...(typeof params.fromClientMessageId === "string"
|
|
2296
|
+
? { fromClientMessageId: params.fromClientMessageId }
|
|
2297
|
+
: {}),
|
|
2298
|
+
...(typeof params.segmentId === "string" ? { segmentId: params.segmentId } : {}),
|
|
2299
|
+
});
|
|
2300
|
+
this.transport.send(createResponse(req.id, { type: "archive_marker", data: { appended } }));
|
|
2301
|
+
}
|
|
2302
|
+
catch (err) {
|
|
2303
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
2304
|
+
}
|
|
2305
|
+
break;
|
|
2306
|
+
}
|
|
2268
2307
|
case "models": {
|
|
2269
2308
|
if (!engine) {
|
|
2270
2309
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, "No engine available for models query"));
|
|
@@ -2790,6 +2829,7 @@ export class AgentServer {
|
|
|
2790
2829
|
makeBrowserBridge(session, sessionId) {
|
|
2791
2830
|
const call = (action, payload) => this.requestBrowserActionForSession(session, sessionId, action, payload);
|
|
2792
2831
|
return {
|
|
2832
|
+
requestHumanTakeover: () => call("requestTakeover", {}),
|
|
2793
2833
|
snapshot: () => call("snapshot", {}),
|
|
2794
2834
|
click: (ref) => call("click", { ref }),
|
|
2795
2835
|
type: (ref, text) => call("type", { ref, text }),
|
package/dist/protocol/types.d.ts
CHANGED
|
@@ -342,6 +342,20 @@ export interface QueryParams {
|
|
|
342
342
|
start?: unknown;
|
|
343
343
|
/** Used by archive_range: half-open message-index window end (exclusive). */
|
|
344
344
|
end?: unknown;
|
|
345
|
+
/**
|
|
346
|
+
* Used by archive_range (optional) and archive_marker (required): the
|
|
347
|
+
* clientMessageId the archived/marked span ends at. archive_range only
|
|
348
|
+
* persists a boundary when this anchor is supplied and resolves to a live
|
|
349
|
+
* message; without it, archive_range behaves exactly as before (in-memory
|
|
350
|
+
* compaction only, no persisted range_archive event).
|
|
351
|
+
*/
|
|
352
|
+
toClientMessageId?: string;
|
|
353
|
+
/** Used by archive_range / archive_marker: optional start-of-span anchor. */
|
|
354
|
+
fromClientMessageId?: string;
|
|
355
|
+
/** Used by archive_range / archive_marker: optional idempotency key. */
|
|
356
|
+
segmentId?: string;
|
|
357
|
+
/** Used by archive_marker: the summary text to persist without a model call. */
|
|
358
|
+
summary?: string;
|
|
345
359
|
}
|
|
346
360
|
export interface QueryResult {
|
|
347
361
|
type: string;
|
package/dist/session/memory.d.ts
CHANGED
|
@@ -233,6 +233,16 @@ export declare class MemoryManager {
|
|
|
233
233
|
baseDir?: string;
|
|
234
234
|
maxAgeDays?: number;
|
|
235
235
|
now?: number;
|
|
236
|
+
/**
|
|
237
|
+
* Trim the global layer to what is relevant to this run's project:
|
|
238
|
+
* project-type and dream-scope records are per-project experience, so
|
|
239
|
+
* they are kept only when their origin metadata ties them to
|
|
240
|
+
* `projectDir`. General knowledge (user/feedback/reference in user
|
|
241
|
+
* scope) always stays; the project layer is untouched because it is
|
|
242
|
+
* already this project's own store. Set by behavior profiles whose runs
|
|
243
|
+
* never work inside other repos (e.g. a manager/dispatcher session).
|
|
244
|
+
*/
|
|
245
|
+
currentProjectOnly?: boolean;
|
|
236
246
|
}): string;
|
|
237
247
|
/**
|
|
238
248
|
* Load every entry belonging to the given scope, without changing the
|
package/dist/session/memory.js
CHANGED
|
@@ -651,7 +651,13 @@ export class MemoryManager {
|
|
|
651
651
|
const profile = opts.profileDir ? new MemoryManager({ baseDir: opts.profileDir }) : null;
|
|
652
652
|
const pinnedFirst = (a, b) => Number(b.pinned ?? false) - Number(a.pinned ?? false);
|
|
653
653
|
const collect = (mm) => filterByAge([...mm.loadScope("user"), ...mm.loadScope("dream")], opts.maxAgeDays, opts.now).sort(pinnedFirst);
|
|
654
|
-
|
|
654
|
+
let globalEntries = collect(global);
|
|
655
|
+
if (opts.currentProjectOnly) {
|
|
656
|
+
const tiedToProject = (e) => opts.projectDir !== undefined &&
|
|
657
|
+
(e.originProject === opts.projectDir ||
|
|
658
|
+
(e.originProjects?.includes(opts.projectDir) ?? false));
|
|
659
|
+
globalEntries = globalEntries.filter((e) => (e.type !== "project" && e.scope !== "dream") || tiedToProject(e));
|
|
660
|
+
}
|
|
655
661
|
const profileEntries = profile ? collect(profile) : [];
|
|
656
662
|
const projectEntries = project ? collect(project) : [];
|
|
657
663
|
if (globalEntries.length === 0 && profileEntries.length === 0 && projectEntries.length === 0) {
|
|
@@ -106,12 +106,48 @@ export declare class Transcript {
|
|
|
106
106
|
*/
|
|
107
107
|
appendTurnStopped(): TranscriptEvent | undefined;
|
|
108
108
|
appendSummary(summary: string, metadata: SummaryAppendMetadata): TranscriptEvent;
|
|
109
|
+
/**
|
|
110
|
+
* Persist a range-archival boundary. Span is [fromClientMessageId,
|
|
111
|
+
* toClientMessageId) over message events; an absent from means "from the
|
|
112
|
+
* beginning". Idempotent on segmentId so a crash-replayed closure cannot
|
|
113
|
+
* double-archive.
|
|
114
|
+
*/
|
|
115
|
+
appendRangeArchive(data: {
|
|
116
|
+
summary: string;
|
|
117
|
+
toClientMessageId: string;
|
|
118
|
+
fromClientMessageId?: string;
|
|
119
|
+
segmentId?: string;
|
|
120
|
+
}): TranscriptEvent | undefined;
|
|
109
121
|
appendError(error: string, details?: Record<string, unknown>): TranscriptEvent;
|
|
110
122
|
/**
|
|
111
123
|
* Derive Message[] from transcript events for sending to the LLM.
|
|
112
124
|
* This is the critical boundary: the LLM never sees the event log directly.
|
|
113
125
|
*/
|
|
114
126
|
toMessages(): Message[];
|
|
127
|
+
/**
|
|
128
|
+
* Marker-aware replay that ALSO reports, for every emitted message event
|
|
129
|
+
* carrying a clientMessageId, the LIVE index (its position in the returned
|
|
130
|
+
* messages array) of its FIRST emission. This is how the engine resolves an
|
|
131
|
+
* anchored archival window: raw transcript indices grow forever and go stale
|
|
132
|
+
* the moment a range_archive marker trims the replay, so any caller-held
|
|
133
|
+
* index range is meaningless — only client-message-id anchors resolved over
|
|
134
|
+
* THIS replay identify the right span.
|
|
135
|
+
*
|
|
136
|
+
* Contract details:
|
|
137
|
+
* - Messages dropped inside an archived span get NO index entry — they are
|
|
138
|
+
* not in the live list, so a window anchored on them cannot be built
|
|
139
|
+
* (the engine fails open in that case).
|
|
140
|
+
* - Only the FIRST emission of a duplicated clientMessageId is recorded
|
|
141
|
+
* (duplicates replay as plain messages per the one-shot span rule; the
|
|
142
|
+
* first index is the meaningful one).
|
|
143
|
+
* - The clientMessageId is deliberately NOT attached to the Message objects
|
|
144
|
+
* themselves: Message[] is exactly what gets serialized into LLM request
|
|
145
|
+
* payloads, and transport metadata must not leak into them.
|
|
146
|
+
*/
|
|
147
|
+
toMessagesWithIndex(): {
|
|
148
|
+
messages: Message[];
|
|
149
|
+
liveIndexByClientMessageId: Map<string, number>;
|
|
150
|
+
};
|
|
115
151
|
getEvents(type?: TranscriptEventType): TranscriptEvent[];
|
|
116
152
|
get turnNumber(): number;
|
|
117
153
|
get eventCount(): number;
|
|
@@ -12,6 +12,7 @@ const CONTEXT_EVENT_TYPES = new Set([
|
|
|
12
12
|
"tool_result",
|
|
13
13
|
"summary",
|
|
14
14
|
"context_transfer",
|
|
15
|
+
"range_archive",
|
|
15
16
|
]);
|
|
16
17
|
const INTERRUPTED_TOOL_RESULT_ERROR = "[Tool result missing due to interrupted session]";
|
|
17
18
|
function isSyntheticInterruptedToolResult(event) {
|
|
@@ -212,6 +213,19 @@ export class Transcript {
|
|
|
212
213
|
},
|
|
213
214
|
});
|
|
214
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Persist a range-archival boundary. Span is [fromClientMessageId,
|
|
218
|
+
* toClientMessageId) over message events; an absent from means "from the
|
|
219
|
+
* beginning". Idempotent on segmentId so a crash-replayed closure cannot
|
|
220
|
+
* double-archive.
|
|
221
|
+
*/
|
|
222
|
+
appendRangeArchive(data) {
|
|
223
|
+
if (data.segmentId &&
|
|
224
|
+
this.events.some((e) => e.type === "range_archive" && e.data.segmentId === data.segmentId)) {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
return this.append("range_archive", { ...data });
|
|
228
|
+
}
|
|
215
229
|
appendError(error, details) {
|
|
216
230
|
return this.append("error", { error, ...details });
|
|
217
231
|
}
|
|
@@ -220,12 +234,128 @@ export class Transcript {
|
|
|
220
234
|
* This is the critical boundary: the LLM never sees the event log directly.
|
|
221
235
|
*/
|
|
222
236
|
toMessages() {
|
|
237
|
+
return this.toMessagesWithIndex().messages;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Marker-aware replay that ALSO reports, for every emitted message event
|
|
241
|
+
* carrying a clientMessageId, the LIVE index (its position in the returned
|
|
242
|
+
* messages array) of its FIRST emission. This is how the engine resolves an
|
|
243
|
+
* anchored archival window: raw transcript indices grow forever and go stale
|
|
244
|
+
* the moment a range_archive marker trims the replay, so any caller-held
|
|
245
|
+
* index range is meaningless — only client-message-id anchors resolved over
|
|
246
|
+
* THIS replay identify the right span.
|
|
247
|
+
*
|
|
248
|
+
* Contract details:
|
|
249
|
+
* - Messages dropped inside an archived span get NO index entry — they are
|
|
250
|
+
* not in the live list, so a window anchored on them cannot be built
|
|
251
|
+
* (the engine fails open in that case).
|
|
252
|
+
* - Only the FIRST emission of a duplicated clientMessageId is recorded
|
|
253
|
+
* (duplicates replay as plain messages per the one-shot span rule; the
|
|
254
|
+
* first index is the meaningful one).
|
|
255
|
+
* - The clientMessageId is deliberately NOT attached to the Message objects
|
|
256
|
+
* themselves: Message[] is exactly what gets serialized into LLM request
|
|
257
|
+
* payloads, and transport metadata must not leak into them.
|
|
258
|
+
*/
|
|
259
|
+
toMessagesWithIndex() {
|
|
223
260
|
const messages = [];
|
|
261
|
+
const liveIndexByClientMessageId = new Map();
|
|
224
262
|
const selectedToolResults = preferredToolResults(this.events);
|
|
263
|
+
const hasRangeArchive = this.events.some((e) => e.type === "range_archive");
|
|
264
|
+
const spansByFromId = new Map();
|
|
265
|
+
let openingSpan;
|
|
266
|
+
if (hasRangeArchive) {
|
|
267
|
+
// First-occurrence event index per client message id, so a marker whose
|
|
268
|
+
// `to` does not come strictly after its `from` (out of order, or a
|
|
269
|
+
// degenerate from === to) can be rejected. Without this check the span
|
|
270
|
+
// opens at `from` but its close condition (`to`) was already passed
|
|
271
|
+
// while scanning forward, so it would never close — silently swallowing
|
|
272
|
+
// the rest of the conversation. Fail open instead: ignore the marker.
|
|
273
|
+
const firstIndexByClientId = new Map();
|
|
274
|
+
for (const [index, event] of this.events.entries()) {
|
|
275
|
+
if (event.type === "message" && typeof event.data.clientMessageId === "string") {
|
|
276
|
+
if (!firstIndexByClientId.has(event.data.clientMessageId)) {
|
|
277
|
+
firstIndexByClientId.set(event.data.clientMessageId, index);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const presentClientIds = new Set(firstIndexByClientId.keys());
|
|
282
|
+
for (const event of this.events) {
|
|
283
|
+
if (event.type !== "range_archive")
|
|
284
|
+
continue;
|
|
285
|
+
const { summary, toClientMessageId, fromClientMessageId } = event.data;
|
|
286
|
+
if (typeof summary !== "string" || !presentClientIds.has(toClientMessageId))
|
|
287
|
+
continue;
|
|
288
|
+
if (fromClientMessageId === undefined) {
|
|
289
|
+
// Multiple from-less markers compete for this single opening-span
|
|
290
|
+
// slot; the LAST one wins (matching spansByFromId's Map.set
|
|
291
|
+
// semantics below). Last-wins is CORRECT by construction, not
|
|
292
|
+
// merely a tiebreak: the engine resolves a from-less archival
|
|
293
|
+
// window as [0, to) over the LIVE replay, so the window that
|
|
294
|
+
// produced a LATER from-less marker began with the EARLIER
|
|
295
|
+
// marker's replayed summary message, and summarizeRange merge-fed
|
|
296
|
+
// that prior summary (extractAnchoredSummary) into the new one.
|
|
297
|
+
// The later summary therefore already contains the earlier one's
|
|
298
|
+
// content — dropping the earlier marker here loses nothing. (And
|
|
299
|
+
// in production from-less windows only advance: each new marker
|
|
300
|
+
// ends at a later boundary, so the surviving span is the widest.)
|
|
301
|
+
openingSpan = { summary, toClientMessageId };
|
|
302
|
+
}
|
|
303
|
+
else if (presentClientIds.has(fromClientMessageId)) {
|
|
304
|
+
const fromIndex = firstIndexByClientId.get(fromClientMessageId);
|
|
305
|
+
const toIndex = firstIndexByClientId.get(toClientMessageId);
|
|
306
|
+
if (toIndex <= fromIndex)
|
|
307
|
+
continue; // out of order or degenerate: fail open
|
|
308
|
+
spansByFromId.set(fromClientMessageId, { summary, toClientMessageId });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
let activeSpan = null;
|
|
313
|
+
if (openingSpan) {
|
|
314
|
+
activeSpan = openingSpan;
|
|
315
|
+
messages.push({ role: "user", content: openingSpan.summary });
|
|
316
|
+
}
|
|
317
|
+
// tool_use ids actually emitted into assistant message content blocks so
|
|
318
|
+
// far. A tool_result whose tool_use_id isn't in this set — e.g. because
|
|
319
|
+
// its opening tool_use fell inside an archived span while the (later,
|
|
320
|
+
// preferred) real result landed outside it — would be an orphaned block
|
|
321
|
+
// that breaks provider validation; skip it instead of emitting it.
|
|
322
|
+
const emittedToolUseIds = new Set();
|
|
225
323
|
for (const event of this.events) {
|
|
324
|
+
// Span bookkeeping runs on message events only: exit before entry so
|
|
325
|
+
// adjacent spans (A.to === B.from) hand over on the boundary message.
|
|
326
|
+
if (event.type === "message") {
|
|
327
|
+
const clientMessageId = typeof event.data.clientMessageId === "string" ? event.data.clientMessageId : undefined;
|
|
328
|
+
if (activeSpan && clientMessageId === activeSpan.toClientMessageId) {
|
|
329
|
+
activeSpan = null;
|
|
330
|
+
}
|
|
331
|
+
if (!activeSpan && clientMessageId && spansByFromId.has(clientMessageId)) {
|
|
332
|
+
activeSpan = spansByFromId.get(clientMessageId);
|
|
333
|
+
// One-shot: a duplicate `from` message (e.g. from a torn JSONL
|
|
334
|
+
// reload) must not reopen this span a second time — it would have
|
|
335
|
+
// no more `to` ahead of it and swallow the rest of the transcript.
|
|
336
|
+
spansByFromId.delete(clientMessageId);
|
|
337
|
+
messages.push({ role: "user", content: activeSpan.summary });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (activeSpan)
|
|
341
|
+
continue; // archived span: drop every context event inside
|
|
226
342
|
switch (event.type) {
|
|
227
343
|
case "message": {
|
|
228
|
-
const { role, content } = event.data;
|
|
344
|
+
const { role, content, clientMessageId } = event.data;
|
|
345
|
+
// Record the live index of this message's FIRST emission before
|
|
346
|
+
// pushing it (the index it is about to occupy). Dropped-in-span
|
|
347
|
+
// messages never reach this point, so they get no entry.
|
|
348
|
+
if (typeof clientMessageId === "string" &&
|
|
349
|
+
!liveIndexByClientMessageId.has(clientMessageId)) {
|
|
350
|
+
liveIndexByClientMessageId.set(clientMessageId, messages.length);
|
|
351
|
+
}
|
|
352
|
+
if (role === "assistant" && Array.isArray(content)) {
|
|
353
|
+
for (const block of content) {
|
|
354
|
+
if (block.type === "tool_use" && typeof block.id === "string") {
|
|
355
|
+
emittedToolUseIds.add(block.id);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
229
359
|
messages.push({ role: role, content });
|
|
230
360
|
break;
|
|
231
361
|
}
|
|
@@ -237,7 +367,8 @@ export class Transcript {
|
|
|
237
367
|
case "tool_result": {
|
|
238
368
|
const eventToolCallId = event.data.toolCallId;
|
|
239
369
|
if (typeof eventToolCallId !== "string" ||
|
|
240
|
-
selectedToolResults.get(eventToolCallId) !== event
|
|
370
|
+
selectedToolResults.get(eventToolCallId) !== event ||
|
|
371
|
+
!emittedToolUseIds.has(eventToolCallId)) {
|
|
241
372
|
break;
|
|
242
373
|
}
|
|
243
374
|
const { toolCallId, result, error, contentBlocks } = event.data;
|
|
@@ -280,10 +411,12 @@ export class Transcript {
|
|
|
280
411
|
break;
|
|
281
412
|
}
|
|
282
413
|
// turn_boundary, run_result, session_meta, file_history, plan_operation, error
|
|
283
|
-
// are not included in LLM messages
|
|
414
|
+
// are not included in LLM messages. range_archive is handled entirely
|
|
415
|
+
// by the pre-pass above (it emits the summary at span entry and drops
|
|
416
|
+
// events inside the span); it never falls through to this switch.
|
|
284
417
|
}
|
|
285
418
|
}
|
|
286
|
-
return messages;
|
|
419
|
+
return { messages, liveIndexByClientMessageId };
|
|
287
420
|
}
|
|
288
421
|
getEvents(type) {
|
|
289
422
|
if (!type)
|
|
@@ -448,6 +581,18 @@ export class Transcript {
|
|
|
448
581
|
});
|
|
449
582
|
break;
|
|
450
583
|
}
|
|
584
|
+
case "range_archive": {
|
|
585
|
+
// A hand-picked context range is an explicit user selection: inject
|
|
586
|
+
// the archive summary as context but do NOT replace/drop the
|
|
587
|
+
// messages inside its span the way toMessages() does — the caller
|
|
588
|
+
// asked for exactly this range and expects to see it in full.
|
|
589
|
+
const { summary } = event.data;
|
|
590
|
+
messages.push({
|
|
591
|
+
role: "user",
|
|
592
|
+
content: `<system-reminder>Archived summary for part of this range:\n${summary}</system-reminder>`,
|
|
593
|
+
});
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
451
596
|
}
|
|
452
597
|
}
|
|
453
598
|
return messages;
|
|
@@ -145,6 +145,9 @@ export interface BrowserImageData {
|
|
|
145
145
|
detail?: string;
|
|
146
146
|
}
|
|
147
147
|
export interface BrowserBridge {
|
|
148
|
+
/** Reveal the exact task-owned browser target so the user can complete a
|
|
149
|
+
* login, 2FA, CAPTCHA, or another interaction that requires human control. */
|
|
150
|
+
requestHumanTakeover?(): Promise<BrowserResult>;
|
|
148
151
|
snapshot(): Promise<BrowserSnapshot>;
|
|
149
152
|
click(ref: string): Promise<BrowserResult>;
|
|
150
153
|
type(ref: string, text: string): Promise<BrowserResult>;
|
|
@@ -209,6 +209,9 @@ export const browserActToolDef = {
|
|
|
209
209
|
"- hover {ref}: hover to reveal menus/tooltips.\n" +
|
|
210
210
|
"- scroll {direction: up|down, amount?}: scroll the page, then re-observe.\n" +
|
|
211
211
|
"- wait {timeout_ms?}: wait for the page to finish loading before observing.\n" +
|
|
212
|
+
"- request_takeover: reveal the exact task-owned Browser Runtime page so the " +
|
|
213
|
+
"user can see it and complete login, 2FA, CAPTCHA, or another required manual step. " +
|
|
214
|
+
"Use only when the user asks to see the page or human interaction is required.\n" +
|
|
212
215
|
"- list_tabs: list open browser tabs (tabId, url, title, which is active).\n" +
|
|
213
216
|
"- switch_tab {tabId}: make another tab the active one that actions drive.\n" +
|
|
214
217
|
"Pass tabId on any action to target a specific tab (switches to it first). " +
|
|
@@ -226,6 +229,7 @@ export const browserActToolDef = {
|
|
|
226
229
|
"hover",
|
|
227
230
|
"scroll",
|
|
228
231
|
"wait",
|
|
232
|
+
"request_takeover",
|
|
229
233
|
"list_tabs",
|
|
230
234
|
"switch_tab",
|
|
231
235
|
],
|
|
@@ -260,6 +264,15 @@ export async function browserActTool(args, ctx) {
|
|
|
260
264
|
return `Error: could not switch to tab ${tabId} — ${sw.detail ?? "not found"}`;
|
|
261
265
|
}
|
|
262
266
|
switch (action) {
|
|
267
|
+
case "request_takeover": {
|
|
268
|
+
if (!b.requestHumanTakeover) {
|
|
269
|
+
return "Error: this Browser Runtime cannot reveal its page for user takeover";
|
|
270
|
+
}
|
|
271
|
+
const r = await b.requestHumanTakeover();
|
|
272
|
+
return r.ok
|
|
273
|
+
? `Browser Runtime is visible for user takeover${r.detail ? ` — ${r.detail}` : ""}`
|
|
274
|
+
: `Error: ${r.detail ?? "could not reveal Browser Runtime"}`;
|
|
275
|
+
}
|
|
263
276
|
case "list_tabs": {
|
|
264
277
|
const tabs = await b.listTabs();
|
|
265
278
|
if (tabs.length === 0)
|