@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.
@@ -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(8).default([]),
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 and workspace.write require context.workspace",
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;
@@ -102,18 +102,24 @@ export class PromptComposer {
102
102
  });
103
103
  const skillsListing = buildSkillListing(skills);
104
104
  const declaredSkillGap = this.buildDeclaredSkillGap(skills);
105
- const capabilityContext = await this.buildSystemContext();
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, CodeShell reveals the exact same runtime-owned in-app target for takeover. Scheduled and explicitly isolated work may use a separate Dedicated Playwright profile instead.
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 log in manually in the revealed Browser Runtime window, then continue do not attempt to enter credentials yourself. Sensitive actions (payment, delete, entering card/password values) require user approval.
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.
@@ -2240,7 +2240,17 @@ export class AgentServer {
2240
2240
  if (!archiveEngine)
2241
2241
  return;
2242
2242
  try {
2243
- const result = await archiveEngine.archiveTurnRange(archiveSessionId, { start, end });
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 }),
@@ -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;
@@ -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
@@ -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
- const globalEntries = collect(global);
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) {
@@ -154,6 +154,7 @@ const FORK_COPY_EVENT_TYPES = new Set([
154
154
  "tool_result",
155
155
  "summary",
156
156
  "context_transfer",
157
+ "range_archive",
157
158
  "content_replace",
158
159
  "subagent",
159
160
  "external_file_changes",
@@ -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)