@bastani/atomic 0.9.19-alpha.7 → 0.9.19-alpha.9

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/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.19-alpha.9] - 2026-09-12
6
+
7
+ ### Breaking Changes
8
+
9
+ - Explicit Intercom `action: "reply"` selectors reject stale, unknown, empty, or sender-mismatched threads instead of falling back. Use `action: "pending"` and `replyTo: "<pending-ask-id>"` for unresolved questions.
10
+
11
+ ### Changed
12
+
13
+ - Intercom agent lists put exact copyable session IDs and workflow paths first, with useful metadata and meaningful names secondary rather than redundant generated aliases.
14
+
15
+ ### Fixed
16
+
17
+ - Intercom targeted replies now complete the requested ask when an unrelated message is active, rather than delivering a response with the wrong thread ID or refusing it.
18
+
5
19
  ## [0.9.19-alpha.7] - 2026-09-12
6
20
 
7
21
  ### Fixed
@@ -4,6 +4,20 @@ All notable changes to the `pi-intercom` extension will be documented in this fi
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.19-alpha.9] - 2026-09-12
8
+
9
+ ### Breaking Changes
10
+
11
+ - Explicit `reply` selectors now fail closed: stale, unknown, empty, or sender-mismatched `replyTo` values no longer fall back to another active or pending thread. Use `pending` to select the exact unresolved question.
12
+
13
+ ### Changed
14
+
15
+ - Agent-visible Intercom lists lead with copyable full session IDs and canonical workflow paths, keeping status, working directory, meaningful names, and future queued counts without redundant generated aliases.
16
+
17
+ ### Fixed
18
+
19
+ - Targeted replies select the requested sender's pending ask even when an unrelated message triggered the current turn, so a delivered reply no longer leaves that ask waiting on the wrong thread.
20
+
7
21
  ## [0.9.19-alpha.6] - 2026-09-11
8
22
 
9
23
  ### Fixed
@@ -57,7 +57,7 @@ A session becomes intercom-connected when all of these are true:
57
57
 
58
58
  The session list, ALT+M picker, and group counts include connected agent sessions only. Internal workflow routing/control connections, model-less `ctx.ui` prompts, and `ctx.tool` nodes are hidden and cannot receive ordinary messages, even by a known ID or through a supervisor route. This includes run-level prompts and retained completed synthetic prompt stages. An agent executing a tool, including `tool:workflow`, or awaiting human input remains visible and messageable.
59
59
 
60
- If a session is unnamed, intercom exposes a runtime-only fallback alias like `subagent-chat-1a2b3c4d-1111-4222-8333-123456789abc` so other sessions can still target it. That alias is not persisted as the session title, so resume pickers can keep showing the transcript snippet instead of a generic `session-...` name.
60
+ Unnamed sessions retain a runtime-only alias such as `subagent-chat-1a2b3c4d-1111-4222-8333-123456789abc` for name lookup. Agent lists omit this redundant alias and lead with the copyable full session ID. Meaningful names remain secondary metadata; resume titles are unchanged.
61
61
 
62
62
  ## Quick Start
63
63
 
@@ -76,10 +76,10 @@ The agent can list sessions and send messages using the `intercom` tool. Tool ca
76
76
  ```typescript
77
77
  // List active sessions
78
78
  intercom({ action: "list" })
79
- // → **Current session:**
80
- // → executor (20d43841-1111-4222-8333-123456789abc) ~/projects/api (claude-sonnet-4) [self, idle]
81
- // → **Other sessions:**
82
- // → research (6332faab-1111-4222-8333-123456789abc) ~/projects/api (claude-sonnet-4) [same cwd, thinking]
79
+ // → **Current session** (groups: default):
80
+ // → - `20d43841-1111-4222-8333-123456789abc` [self, idle] ~/projects/api (claude-sonnet-4) name: executor
81
+ // → **Other visible sessions and workflow stages:**
82
+ // → - `6332faab-1111-4222-8333-123456789abc` [same cwd, thinking] ~/projects/api (claude-sonnet-4) name: research
83
83
 
84
84
  // Add a named membership (it is created if no session is there yet)
85
85
  intercom({ action: "join", group: "api-review" })
@@ -156,7 +156,7 @@ Verify they see each other from either session:
156
156
 
157
157
  ```typescript
158
158
  intercom({ action: "list" })
159
- // → worker ~/projects/api (claude-sonnet-4) [idle]
159
+ // → - `6332faab-1111-4222-8333-123456789abc` [same cwd, idle] ~/projects/api (claude-sonnet-4) name: worker
160
160
  ```
161
161
 
162
162
  ### The Conversation
@@ -383,7 +383,7 @@ When multiple live agent stages share a name, `ask` refuses the ambiguous target
383
383
 
384
384
  **`ask`** — Sends a message and waits for a live recipient to reply (10-minute timeout). Invocation control supports a live ask into an owned isolated subgroup, and the exact broker-recorded reply resolves the waiting tool call at the asker without opening reverse or lateral group access. Ask to an uninitialized stage remains refused with `pending_stage_ask_unsupported`; use queued `send` instead, because holding a reply waiter until a stage eventually starts would be unbounded. A recipient disconnect after live delivery fails only that peer's exact wait promptly; the timeout remains the backstop while the recipient stays connected. Up to `maxPendingAsks` blocking asks (default: 6) may run concurrently, including same-target and mixed-target fan-out. Replies resolve by exact sender and message ID, so out-of-order replies cannot cross-settle another call. When capacity is full, new asks receive a structured refusal.
385
385
 
386
- **`reply`** Replies to the current intercom-triggered message if there is one. Otherwise it falls back to the single unresolved inbound ask. If multiple asks are pending, pass an exact name/full session ID in `to`, or the listed message ID in `replyTo`; use `pending` to inspect them first. `replyTo` also disambiguates multiple asks from the same sender. Under the hood this is still a normal `send` with the exact `replyTo` value.
386
+ **`reply`** without selectors replies to the current intercom-triggered message, or otherwise the single unresolved inbound ask. Explicit `to` selects a pending ask from that exact name/full session ID, even during another sender's turn. If that sender has multiple asks, use `pending` and pass the exact message ID as `replyTo`. Explicit `replyTo` selects only that pending ask or the exact active ordinary message; stale, unknown, or empty IDs fail instead of falling back. When both selectors are supplied, the sender must match the selected thread. A successful reply keeps that sender/thread pair across internal retries.
387
387
 
388
388
  **`pending`** — Lists unresolved inbound asks with sender, message ID, elapsed time, and a short preview. Useful when replying after the original triggered turn.
389
389
 
@@ -38,6 +38,8 @@ export interface SendOptions {
38
38
  logicalTarget?: string;
39
39
  /** Public reply must use the broker's exact pending reverse question route. */
40
40
  requirePendingReply?: true;
41
+ /** An explicit reply selector must resolve to this frozen recipient, never a namesake. */
42
+ expectedRecipientId?: string;
41
43
  /** Broker-authorized identity for a canonical-path ask, delivered before its recipient can reply. */
42
44
  onReplyTarget?: (sessionId: string) => void;
43
45
  }
@@ -1004,6 +1006,7 @@ export class IntercomClient extends EventEmitter {
1004
1006
  transportTarget: to,
1005
1007
  logicalSignature: buildSendSignature(options.logicalTarget ?? to, options),
1006
1008
  requirePendingReply: options.requirePendingReply ?? false,
1009
+ ...(options.expectedRecipientId === undefined ? {} : { expectedRecipientId: options.expectedRecipientId }),
1007
1010
  });
1008
1011
  acquired = this.pendingSends.acquire(messageId, pendingSignature, 10000);
1009
1012
  } catch (error) {
@@ -1028,6 +1031,7 @@ export class IntercomClient extends EventEmitter {
1028
1031
  to,
1029
1032
  ...(options.logicalTarget === undefined ? {} : { logicalTarget: options.logicalTarget }),
1030
1033
  ...(options.requirePendingReply === undefined ? {} : { requirePendingReply: options.requirePendingReply }),
1034
+ ...(options.expectedRecipientId === undefined ? {} : { expectedRecipientId: options.expectedRecipientId }),
1031
1035
  ...(options.onReplyTarget === undefined ? {} : { resolveReplyTarget: true as const }),
1032
1036
  message,
1033
1037
  attemptId: acquired.attempt.attemptId,
@@ -143,6 +143,12 @@ export function handleBrokerSend(
143
143
  return;
144
144
  }
145
145
  const requirePendingReply = clientMessage.requirePendingReply === true;
146
+ const hasExpectedRecipientId = Object.prototype.hasOwnProperty.call(clientMessage, "expectedRecipientId");
147
+ if (hasExpectedRecipientId && (typeof clientMessage.expectedRecipientId !== "string" || !clientMessage.expectedRecipientId.trim())) {
148
+ write(socket, { type: "delivery_failed", messageId, attemptId, reason: "Invalid expectedRecipientId format" });
149
+ return;
150
+ }
151
+ const expectedRecipientId = hasExpectedRecipientId ? clientMessage.expectedRecipientId as string : undefined;
146
152
  if (Object.prototype.hasOwnProperty.call(clientMessage, "channel")) {
147
153
  write(socket, { type: "delivery_failed", messageId: message.id, attemptId, reason: "Invalid channel" });
148
154
  return;
@@ -152,6 +158,10 @@ export function handleBrokerSend(
152
158
  write(socket, { type: "delivery_failed", messageId, attemptId, reason: "Invalid requirePendingReply message" });
153
159
  return;
154
160
  }
161
+ if (expectedRecipientId !== undefined && (supervisorSend || message.replyTo === undefined || message.expectsReply === true)) {
162
+ write(socket, { type: "delivery_failed", messageId, attemptId, reason: "Invalid expectedRecipientId message" });
163
+ return;
164
+ }
155
165
 
156
166
 
157
167
  const fromSession = currentId ? sessions.get(currentId) : undefined;
@@ -161,7 +171,8 @@ export function handleBrokerSend(
161
171
  }
162
172
  const senderIdentity = fromSession.registrationReturnAddress ?? fromSession.info.id;
163
173
  const logicalTarget = typeof clientMessage.logicalTarget === "string" ? clientMessage.logicalTarget : clientMessage.to;
164
- const baseSignature = buildMessageSendSignature(logicalTarget, message, senderIdentity);
174
+ const messageSignature = buildMessageSendSignature(logicalTarget, message, senderIdentity);
175
+ const baseSignature = expectedRecipientId === undefined ? messageSignature : JSON.stringify({ expectedRecipientId, messageSignature });
165
176
  const signature = requirePendingReply
166
177
  ? JSON.stringify({
167
178
  requirePendingReply: true,
@@ -221,6 +232,12 @@ export function handleBrokerSend(
221
232
  const resolution = exactIdTarget
222
233
  ? ({ kind: "resolved", session: exactIdTarget.info } as const)
223
234
  : resolveSessionTarget(candidates, trimmedTo);
235
+ if (expectedRecipientId !== undefined && (resolution.kind !== "resolved" || resolution.session.id !== expectedRecipientId)) {
236
+ write(socket, { type: "delivery_failed", messageId, attemptId,
237
+ reason: resolution.kind === "resolved" ? "Reply target does not match the expected recipient" : sessionTargetFailureReason(clientMessage.to, resolution),
238
+ });
239
+ return;
240
+ }
224
241
  if (resolution.kind === "resolved") {
225
242
  const target = sessions.get(resolution.session.id);
226
243
  if (!target) {
@@ -1247,7 +1247,8 @@ var init_client = __esm(() => {
1247
1247
  const pendingSignature = JSON.stringify({
1248
1248
  transportTarget: to,
1249
1249
  logicalSignature: buildSendSignature(options.logicalTarget ?? to, options),
1250
- requirePendingReply: options.requirePendingReply ?? false
1250
+ requirePendingReply: options.requirePendingReply ?? false,
1251
+ ...options.expectedRecipientId === undefined ? {} : { expectedRecipientId: options.expectedRecipientId }
1251
1252
  });
1252
1253
  acquired = this.pendingSends.acquire(messageId, pendingSignature, 1e4);
1253
1254
  } catch (error) {
@@ -1273,6 +1274,7 @@ var init_client = __esm(() => {
1273
1274
  to,
1274
1275
  ...options.logicalTarget === undefined ? {} : { logicalTarget: options.logicalTarget },
1275
1276
  ...options.requirePendingReply === undefined ? {} : { requirePendingReply: options.requirePendingReply },
1277
+ ...options.expectedRecipientId === undefined ? {} : { expectedRecipientId: options.expectedRecipientId },
1276
1278
  ...options.onReplyTarget === undefined ? {} : { resolveReplyTarget: true },
1277
1279
  message,
1278
1280
  attemptId: acquired.attempt.attemptId,
@@ -2008,22 +2010,22 @@ class ReplyTracker {
2008
2010
  }
2009
2011
  resolveReplyTarget(options, now = Date.now()) {
2010
2012
  this.pruneExpired(now);
2011
- if (options.replyTo) {
2012
- const exact = this.pendingAsks.get(options.replyTo);
2013
- if (exact) {
2014
- if (options.to) {
2015
- const resolution = resolveSessionTarget([exact.from], options.to);
2016
- if (resolution.kind !== "resolved")
2017
- throw new Error(`Pending ask "${options.replyTo}" is not from "${options.to}"`);
2018
- }
2019
- return exact;
2013
+ if (options.replyTo !== undefined) {
2014
+ const exact = this.pendingAsks.get(options.replyTo) ?? (this.currentTurnContext?.message.id === options.replyTo && !this.currentTurnContext.message.expectsReply ? this.currentTurnContext : undefined);
2015
+ if (!exact)
2016
+ throw new Error(`No reply context for "${options.replyTo}"`);
2017
+ if (options.to !== undefined) {
2018
+ const resolution = resolveSessionTarget([exact.from], options.to);
2019
+ if (resolution.kind !== "resolved")
2020
+ throw new Error(`Reply context "${options.replyTo}" is not from "${options.to}"`);
2020
2021
  }
2022
+ return exact;
2021
2023
  }
2022
- if (this.currentTurnContext) {
2024
+ if (options.to === undefined && this.currentTurnContext) {
2023
2025
  return this.currentTurnContext;
2024
2026
  }
2025
2027
  const pending = Array.from(this.pendingAsks.values());
2026
- if (options.to) {
2028
+ if (options.to !== undefined) {
2027
2029
  const senders = [...new Map(pending.map((context) => [context.from.id, context.from])).values()];
2028
2030
  const resolution = resolveSessionTarget(senders, options.to);
2029
2031
  if (resolution.kind !== "resolved") {
@@ -2446,13 +2448,13 @@ function formatSessionLabel(session, duplicates) {
2446
2448
  return duplicates.has(session.name.toLowerCase()) ? `${session.name} (${session.id})` : session.name;
2447
2449
  }
2448
2450
  function formatSessionListRow(session, currentCwd, isSelf) {
2449
- const name = session.name || "Unnamed session";
2451
+ const name = session.name && session.name !== session.id && session.name !== resolveIntercomPresenceName(undefined, session.id) ? ` name: ${session.name}` : "";
2450
2452
  const normalizedGroup = normalizeGroup(session.group);
2451
2453
  const groupTag = normalizedGroup !== DEFAULT_GROUP ? `group: ${normalizedGroup}` : undefined;
2452
2454
  const capabilityTag = session.replyCapability === undefined ? undefined : `replyCapability: ${session.replyCapability}`;
2453
2455
  const tags = [isSelf ? "self" : session.cwd === currentCwd ? "same cwd" : undefined, session.status, capabilityTag, groupTag].filter((tag) => Boolean(tag));
2454
2456
  const suffix = tags.length ? ` [${tags.join(", ")}]` : "";
2455
- return `• ${name} (${session.id}) ${session.cwd} (${session.model})${suffix}`;
2457
+ return `- \`${session.id}\`${suffix} ${session.cwd} (${session.model})${name}`;
2456
2458
  }
2457
2459
  function previewText(value, maxLength = 72) {
2458
2460
  if (typeof value !== "string") {
@@ -3192,10 +3194,10 @@ async function resolveReplySender(client, logicalTarget, sendTarget) {
3192
3194
  return { kind: "unresolved" };
3193
3195
  }
3194
3196
  function formatWorkflowStageRow(stage) {
3195
- return `- **${stage.stageName}** — workflow stage [${stage.lifecycle.toUpperCase()}] target: \`${stage.target}\`${stage.sessionId === undefined ? "" : ` — intercom session: ${stage.sessionId}`}`;
3197
+ return `- \`${stage.target}\` [${stage.lifecycle.toUpperCase()}] workflow stage: ${stage.stageName}${stage.sessionId === undefined ? "" : ` session: \`${stage.sessionId}\``}`;
3196
3198
  }
3197
3199
  function formatWorkflowFutureStageRow(stage) {
3198
- return `- future workflow stage \`${stage.target}\` ${stage.queuedCount} queued message${stage.queuedCount === 1 ? "" : "s"}`;
3200
+ return `- \`${stage.target}\` [future] ${stage.queuedCount} queued message${stage.queuedCount === 1 ? "" : "s"}`;
3199
3201
  }
3200
3202
  function registerIntercomTool(pi, deps) {
3201
3203
  const retryIdentities = new RetryIdentityReservations({ maxEntries: deps.retryIdentityMaxEntries });
@@ -3949,12 +3951,13 @@ ${replyText}${replyAttachments}` }],
3949
3951
  messageId: replyMessageId,
3950
3952
  logicalTarget: replyLogicalTarget,
3951
3953
  ...route.expectsReply ? { requirePendingReply: true } : {},
3954
+ ...to === undefined && replyTo === undefined ? {} : { expectedRecipientId: route.senderId },
3952
3955
  text: message,
3953
3956
  attachments,
3954
3957
  replyTo: route.messageId
3955
3958
  });
3956
3959
  let result = await sendReply(replySendTo);
3957
- if (to === undefined && result.delivered === false && result.reasonCode === "session_not_found" && route.senderName !== undefined) {
3960
+ if (to === undefined && replyTo === undefined && result.delivered === false && result.reasonCode === "session_not_found" && route.senderName !== undefined) {
3958
3961
  result = await sendReply(route.senderName);
3959
3962
  }
3960
3963
  if (result.queued === true) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.9.19-alpha.7",
3
+ "version": "0.9.19-alpha.9",
4
4
  "private": true,
5
5
  "description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
6
6
  "contributors": [
@@ -67,11 +67,11 @@ intercom({
67
67
 
68
68
  ### Pattern 2: Quick Status Check
69
69
 
70
- Before sending, verify who's connected. The full session ID printed by `list` is directly usable by `send`, `ask`, and targeted `reply`:
70
+ Before sending, verify who's connected. Each list row leads with a copyable full session ID or canonical workflow path. Names are secondary; redundant generated aliases are omitted from the list but remain valid targets.
71
71
 
72
72
  ```typescript
73
73
  intercom({ action: "list" })
74
- // → planner (6332faab-1111-4222-8333-123456789abc) /workspace (model) [idle]
74
+ // → - `6332faab-1111-4222-8333-123456789abc` [idle] /workspace (model) name: planner
75
75
  intercom({ action: "ask", to: "6332faab-1111-4222-8333-123456789abc", message: "Which option should I use?" })
76
76
  ```
77
77
 
@@ -128,7 +128,7 @@ intercom({ action: "pending" })
128
128
  intercom({ action: "reply", to: "planner", message: "Use exponential backoff starting at 100ms." })
129
129
  ```
130
130
 
131
- `reply` still preserves exact threading under the hood by sending the response with the original `replyTo` value.
131
+ Explicit `to` selects that sender's pending ask even if another message triggered the current turn. Use `pending` and an exact `replyTo` when the sender has several asks. Stale, unknown, empty, or sender-mismatched explicit selectors fail without replying to another thread. Omit both selectors only when you intend to reply to the active message, or otherwise the single pending ask.
132
132
 
133
133
  ### Pattern 4: Broadcast to Multiple Workers
134
134
 
@@ -124,7 +124,7 @@ export type ClientMessage =
124
124
  | { type: "join_group"; requestId: string; group: string }
125
125
  | { type: "leave_group"; requestId: string; group?: string }
126
126
  | { type: "authorize_supervisor"; requestId: string; childName: string; capability?: string }
127
- | { type: "send" | "supervisor_send"; to: string; logicalTarget?: string; requirePendingReply?: true; resolveReplyTarget?: true; message: Message; attemptId?: string }
127
+ | { type: "send" | "supervisor_send"; to: string; logicalTarget?: string; requirePendingReply?: true; expectedRecipientId?: string; resolveReplyTarget?: true; message: Message; attemptId?: string }
128
128
  | {
129
129
  type: "send_pending_stage_notification";
130
130
  runId: string;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.9.19-alpha.7",
3
+ "version": "0.9.19-alpha.9",
4
4
  "private": true,
5
5
  "description": "Atomic extension that adapts MCP (Model Context Protocol) servers into the coding agent. Fork of: https://github.com/nicobailon/pi-mcp-adapter",
6
6
  "contributors": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.9.19-alpha.7",
3
+ "version": "0.9.19-alpha.9",
4
4
  "private": true,
5
5
  "description": "Atomic extension for delegating tasks to subagents with parallel execution. Fork of: https://github.com/nicobailon/pi-subagents",
6
6
  "contributors": [
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.19-alpha.9] - 2026-09-12
8
+
9
+ ### Fixed
10
+
11
+ - `fetch_content` ignores video frame options for non-video inputs, so webpages and mixed batches no longer fail with video-only errors. Tool and parameter descriptions now distinguish ordinary page fetching, video analysis, and frame extraction.
12
+
7
13
  ## [0.9.19-alpha.7] - 2026-09-12
8
14
 
9
15
  ### Fixed
@@ -74476,13 +74476,15 @@ async function extractContent(url, signal, options) {
74476
74476
  if (signal?.aborted) {
74477
74477
  return { url, title: "", content: "", error: "Aborted" };
74478
74478
  }
74479
- const frameResult = await extractRequestedFrames(url, options, signal);
74480
- if (frameResult)
74481
- return frameResult;
74482
74479
  const localVideo = safeVideoInfo2(url);
74483
74480
  if (localVideo.error) {
74484
74481
  return { url, title: "", content: "", error: localVideo.error };
74485
74482
  }
74483
+ if (localVideo.info || isYouTubeURL(url).isYouTube) {
74484
+ const frameResult = await extractRequestedFrames(url, options, signal);
74485
+ if (frameResult)
74486
+ return frameResult;
74487
+ }
74486
74488
  if (localVideo.info) {
74487
74489
  try {
74488
74490
  const result = await extractVideo(localVideo.info, signal, options);
@@ -90949,29 +90951,29 @@ function registerContentTools(pi, deps) {
90949
90951
  pi.registerTool({
90950
90952
  name: "fetch_content",
90951
90953
  label: "Fetch Content",
90952
- description: "Fetch URL(s) and extract readable content as markdown. Supports YouTube video transcripts (with thumbnail), GitHub repository contents, and local video files (with frame thumbnail). Video frames can be extracted via timestamp/range or sampled across the entire video with frames alone. Falls back to Gemini for pages that block bots or fail Readability extraction. For YouTube and video files: ALWAYS pass the user's specific question via the prompt parameter this directs the AI to focus on that aspect of the video, producing much better results than a generic extraction. Content is always stored and can be retrieved with get_search_content.",
90953
- promptSnippet: "Use to extract readable content from URL(s), YouTube, GitHub repos, or local videos. For video questions, pass the user's exact question in prompt.",
90954
+ description: `Fetch webpages, PDFs, GitHub repositories, YouTube videos, or local video files. For ordinary pages, pass only {"urls":["https://example.com"]}; readable content is returned as markdown. For video analysis, include the user's question in prompt. Optional frames and timestamp extract images only from YouTube or local videos and are ignored for other inputs, including in mixed batches. Content is stored for get_search_content. Blocked or unreadable pages use extraction fallbacks.`,
90955
+ promptSnippet: "Fetch webpages with urls only. For YouTube or local video analysis, include the user's question in prompt; omit frames and timestamp unless images are wanted.",
90954
90956
  parameters: Type2.Object({
90955
90957
  urls: Type2.Array(Type2.String({ minLength: 1 }), {
90956
90958
  minItems: 1,
90957
90959
  description: 'URLs or local video paths to fetch. Always use an array, even for one URL: {"urls":["https://example.com"]}. Multiple URLs are fetched in parallel.'
90958
90960
  }),
90959
90961
  forceClone: Type2.Optional(Type2.Boolean({
90960
- description: "Force cloning large GitHub repositories that exceed the size threshold"
90962
+ description: "GitHub repositories only: allow cloning above the configured size threshold. Omit for webpages and videos."
90961
90963
  })),
90962
90964
  prompt: Type2.Optional(Type2.String({
90963
- description: "Question or instruction for video analysis (YouTube and video files). Pass the user's specific question here — e.g. 'describe the book shown at the advice for beginners section'. Without this, a generic transcript extraction is used which may miss what the user is asking about."
90965
+ description: "YouTube/local video analysis only: the user's specific question. Omit for webpages; this is not a webpage search or extraction filter."
90964
90966
  })),
90965
90967
  timestamp: Type2.Optional(Type2.String({
90966
- description: "Extract video frame(s) at a timestamp or time range. Single: '1:23:45', '23:45', or '85' (seconds). Range: '23:41-25:00' extracts evenly-spaced frames across that span (default 6). Use frames with ranges to control density; single+frames uses a fixed 5s interval. YouTube requires yt-dlp + ffmpeg; local videos require ffmpeg. Use a range when you know the approximate area but not the exact moment — you'll get a contact sheet to visually identify the right frame."
90968
+ description: "YouTube/local video images only: seconds ('85'), time ('1:25'), or range ('1:25-2:00'). Omit for text/transcripts. Ignored for non-video inputs. Ranges default to 6 frames; use frames to adjust."
90967
90969
  })),
90968
90970
  frames: Type2.Optional(Type2.Integer({
90969
90971
  minimum: 1,
90970
90972
  maximum: 12,
90971
- description: "Number of frames to extract. Use with timestamp range for custom density, with single timestamp to get N frames at 5s intervals, or alone to sample across the entire video. Requires yt-dlp + ffmpeg for YouTube, ffmpeg for local video."
90973
+ description: "YouTube/local video images only: 1-12 frames. Alone, samples the whole video; with a range, samples that span; with a single timestamp, uses 5s intervals. Ignored for non-video inputs. Omit for text/transcripts. Requires ffmpeg, plus yt-dlp for YouTube."
90972
90974
  })),
90973
90975
  model: Type2.Optional(Type2.String({
90974
- description: "Override the Gemini model for video/YouTube analysis (e.g. 'gemini-2.5-flash', 'gemini-3-flash-preview'). Defaults to config or gemini-3-flash-preview."
90976
+ description: "Gemini model override for YouTube/local video analysis only. Omit to use the configured default; does not select the webpage extraction model."
90975
90977
  }))
90976
90978
  }, { additionalProperties: false }),
90977
90979
  async execute(_toolCallId, params, signal, onUpdate) {
@@ -97598,18 +97600,18 @@ function webAccess(pi) {
97598
97600
  pi.registerTool({
97599
97601
  name: "fetch_content",
97600
97602
  label: "Fetch Content",
97601
- description: "Fetch URL(s) and extract readable content as markdown. Supports YouTube video transcripts (with thumbnail), GitHub repository contents, and local video files (with frame thumbnail). Video frames can be extracted via timestamp/range or sampled across the entire video with frames alone. Falls back to Gemini for pages that block bots or fail Readability extraction. For YouTube and video files: ALWAYS pass the user's specific question via the prompt parameter this directs the AI to focus on that aspect of the video, producing much better results than a generic extraction. Content is always stored and can be retrieved with get_search_content.",
97602
- promptSnippet: "Use to extract readable content from URL(s), YouTube, GitHub repos, or local videos. For video questions, pass the user's exact question in prompt.",
97603
+ description: `Fetch webpages, PDFs, GitHub repositories, YouTube videos, or local video files. For ordinary pages, pass only {"urls":["https://example.com"]}; readable content is returned as markdown. For video analysis, include the user's question in prompt. Optional frames and timestamp extract images only from YouTube or local videos and are ignored for other inputs, including in mixed batches. Content is stored for get_search_content. Blocked or unreadable pages use extraction fallbacks.`,
97604
+ promptSnippet: "Fetch webpages with urls only. For YouTube or local video analysis, include the user's question in prompt; omit frames and timestamp unless images are wanted.",
97603
97605
  parameters: Type4.Object({
97604
97606
  urls: Type4.Array(Type4.String({ minLength: 1 }), {
97605
97607
  minItems: 1,
97606
97608
  description: 'URLs or local video paths to fetch. Always use an array, even for one URL: {"urls":["https://example.com"]}. Multiple URLs are fetched in parallel.'
97607
97609
  }),
97608
- forceClone: Type4.Optional(Type4.Boolean({ description: "Force cloning large GitHub repositories that exceed the size threshold" })),
97609
- prompt: Type4.Optional(Type4.String({ description: "Question or instruction for video analysis (YouTube and video files)." })),
97610
- timestamp: Type4.Optional(Type4.String({ description: "Extract video frame(s) at a timestamp or time range." })),
97611
- frames: Type4.Optional(Type4.Integer({ minimum: 1, maximum: 12, description: "Number of frames to extract." })),
97612
- model: Type4.Optional(Type4.String({ description: "Override the Gemini model for video/YouTube analysis." }))
97610
+ forceClone: Type4.Optional(Type4.Boolean({ description: "GitHub repositories only: allow cloning above the configured size threshold. Omit for webpages and videos." })),
97611
+ prompt: Type4.Optional(Type4.String({ description: "YouTube/local video analysis only: the user's specific question. Omit for webpages; this is not a webpage search or extraction filter." })),
97612
+ timestamp: Type4.Optional(Type4.String({ description: "YouTube/local video images only: seconds ('85'), time ('1:25'), or range ('1:25-2:00'). Omit for text/transcripts. Ignored for non-video inputs. Ranges default to 6 frames; use frames to adjust." })),
97613
+ frames: Type4.Optional(Type4.Integer({ minimum: 1, maximum: 12, description: "YouTube/local video images only: 1-12 frames. Alone, samples the whole video; with a range, samples that span; with a single timestamp, uses 5s intervals. Ignored for non-video inputs. Omit for text/transcripts. Requires ffmpeg, plus yt-dlp for YouTube." })),
97614
+ model: Type4.Optional(Type4.String({ description: "Gemini model override for YouTube/local video analysis only. Omit to use the configured default; does not select the webpage extraction model." }))
97613
97615
  }, { additionalProperties: false }),
97614
97616
  execute: (...args) => executeHeavyTool(loadHeavy, "fetch_content", args),
97615
97617
  renderResult: (...args) => renderHeavyToolResult(loadedHeavy?.heavy ?? null, "fetch_content", args)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.19-alpha.7",
3
+ "version": "0.9.19-alpha.9",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [
@@ -6,6 +6,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.19-alpha.9] - 2026-09-12
10
+
11
+ ### Fixed
12
+
13
+ - Kept the background workflow summary within narrow terminal widths, including 27 columns. The shared workflow widget now stays within ten rows, shrinks on short terminals, and scrolls one row at a time with Alt+PageUp / Alt+PageDown without taking focus from the editor. All workflow rows remain reachable when a multiline draft reduces the visible widget area ([#3015](https://github.com/bastani-inc/atomic/issues/3015)).
14
+ - Kept the workflow being read in view when live runs are inserted or removed above it, including workflows with identical names. Resizing to a collapsed summary and back preserves the reading position; removing the viewed run selects its next surviving neighbour, or the previous one when no later run remains ([#3017](https://github.com/bastani-inc/atomic/pull/3017)).
15
+
16
+ ## [0.9.19-alpha.8] - 2026-09-12
17
+
18
+ ### Fixed
19
+
20
+ - Workflow status detail now shows started and ended times in the system local timezone instead of UTC, preserving the compact `HH:mm:ss` display and elapsed durations ([#3008](https://github.com/bastani-inc/atomic/issues/3008)).
21
+
22
+ ### Changed
23
+
24
+ - Removed generated `root`, `dep`, and `deps` labels from workflow graph nodes and left model text blank when no model is set. Cards omit the extra padding row. Queued-message counts appear on a separate `✉ N queued` row, reusing empty space or expanding occupied cards without hiding status or model details. Dependency edges are unchanged.
25
+
9
26
  ## [0.9.19-alpha.7] - 2026-09-12
10
27
 
11
28
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.9.19-alpha.7",
3
+ "version": "0.9.19-alpha.9",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [