@oh-my-pi/pi-coding-agent 15.11.7 → 15.12.0
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 +63 -1
- package/dist/cli.js +8106 -7708
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/collab/crypto.d.ts +7 -0
- package/dist/types/collab/guest.d.ts +23 -0
- package/dist/types/collab/host.d.ts +29 -0
- package/dist/types/collab/protocol.d.ts +113 -0
- package/dist/types/collab/relay-client.d.ts +22 -0
- package/dist/types/commands/join.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +60 -5
- package/dist/types/export/custom-share.d.ts +1 -2
- package/dist/types/export/html/index.d.ts +39 -1
- package/dist/types/export/share.d.ts +43 -0
- package/dist/types/extensibility/slash-commands.d.ts +1 -11
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/agent-hub.d.ts +32 -1
- package/dist/types/modes/components/collab-prompt-message.d.ts +10 -0
- package/dist/types/modes/components/hook-selector.d.ts +4 -6
- package/dist/types/modes/components/segment-track.d.ts +11 -6
- package/dist/types/modes/components/status-line/component.d.ts +10 -2
- package/dist/types/modes/components/status-line/types.d.ts +11 -0
- package/dist/types/modes/controllers/event-controller.d.ts +7 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -1
- package/dist/types/modes/controllers/session-focus-controller.d.ts +31 -0
- package/dist/types/modes/interactive-mode.d.ts +16 -0
- package/dist/types/modes/session-observer-registry.d.ts +7 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +20 -0
- package/dist/types/session/agent-session.d.ts +13 -0
- package/dist/types/session/codex-auto-reset.d.ts +8 -4
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/snapcompact-inline.d.ts +6 -3
- package/dist/types/slash-commands/builtin-registry.d.ts +9 -0
- package/dist/types/task/executor.d.ts +7 -0
- package/dist/types/task/types.d.ts +9 -0
- package/package.json +14 -13
- package/scripts/bench-guard.ts +71 -0
- package/scripts/build-binary.ts +4 -0
- package/scripts/bundle-dist.ts +4 -0
- package/scripts/generate-share-viewer.ts +34 -0
- package/src/cli/args.ts +2 -0
- package/src/cli-commands.ts +1 -0
- package/src/collab/crypto.ts +63 -0
- package/src/collab/guest.ts +450 -0
- package/src/collab/host.ts +556 -0
- package/src/collab/protocol.ts +232 -0
- package/src/collab/relay-client.ts +216 -0
- package/src/commands/join.ts +39 -0
- package/src/config/model-registry.ts +22 -14
- package/src/config/settings-schema.ts +67 -5
- package/src/config/settings.ts +12 -0
- package/src/export/custom-share.ts +1 -1
- package/src/export/html/index.ts +122 -17
- package/src/export/html/share-loader.js +102 -0
- package/src/export/html/template.css +745 -459
- package/src/export/html/template.html +6 -3
- package/src/export/html/template.js +240 -915
- package/src/export/html/tool-views.generated.js +38 -0
- package/src/export/share.ts +268 -0
- package/src/extensibility/slash-commands.ts +1 -97
- package/src/internal-urls/docs-index.generated.ts +74 -73
- package/src/main.ts +33 -11
- package/src/modes/components/agent-hub.ts +659 -431
- package/src/modes/components/assistant-message.ts +126 -6
- package/src/modes/components/collab-prompt-message.ts +30 -0
- package/src/modes/components/hook-selector.ts +4 -5
- package/src/modes/components/segment-track.ts +44 -7
- package/src/modes/components/status-line/component.ts +59 -6
- package/src/modes/components/status-line/presets.ts +1 -1
- package/src/modes/components/status-line/segments.ts +18 -1
- package/src/modes/components/status-line/types.ts +12 -0
- package/src/modes/components/tips.txt +4 -1
- package/src/modes/controllers/command-controller.ts +55 -96
- package/src/modes/controllers/event-controller.ts +45 -16
- package/src/modes/controllers/input-controller.ts +175 -9
- package/src/modes/controllers/selector-controller.ts +13 -15
- package/src/modes/controllers/session-focus-controller.ts +112 -0
- package/src/modes/controllers/streaming-reveal.ts +7 -0
- package/src/modes/interactive-mode.ts +56 -6
- package/src/modes/session-observer-registry.ts +11 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/modes/types.ts +20 -0
- package/src/modes/utils/ui-helpers.ts +23 -13
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk.ts +239 -36
- package/src/session/agent-session.ts +82 -7
- package/src/session/codex-auto-reset.ts +23 -11
- package/src/session/session-manager.ts +44 -0
- package/src/session/snapcompact-inline.ts +9 -3
- package/src/slash-commands/builtin-registry.ts +261 -24
- package/src/task/executor.ts +14 -0
- package/src/task/index.ts +5 -1
- package/src/task/render.ts +76 -5
- package/src/task/types.ts +9 -0
- package/src/tiny/worker.ts +17 -95
- package/src/tools/job.ts +6 -9
- package/src/tools/read.ts +38 -5
- package/src/tools/write.ts +13 -42
- package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
- package/dist/types/export/html/template.generated.d.ts +0 -1
- package/dist/types/export/html/template.macro.d.ts +0 -5
- package/dist/types/tiny/compiled-runtime.d.ts +0 -35
- package/scripts/generate-template.ts +0 -33
- package/src/bun-imports.d.ts +0 -28
- package/src/export/html/template.generated.ts +0 -2
- package/src/export/html/template.macro.ts +0 -25
- package/src/tiny/compiled-runtime.ts +0 -179
package/src/sdk.ts
CHANGED
|
@@ -89,7 +89,14 @@ import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal }
|
|
|
89
89
|
import type { HindsightSessionState } from "./hindsight/state";
|
|
90
90
|
import { LocalProtocolHandler, type LocalProtocolOptions } from "./internal-urls";
|
|
91
91
|
import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-events";
|
|
92
|
-
import {
|
|
92
|
+
import {
|
|
93
|
+
discoverAndLoadMCPTools,
|
|
94
|
+
type MCPLoadResult,
|
|
95
|
+
MCPManager,
|
|
96
|
+
MCPToolCache,
|
|
97
|
+
type MCPToolsLoadResult,
|
|
98
|
+
parseMCPToolName,
|
|
99
|
+
} from "./mcp";
|
|
93
100
|
import { createSessionMemoryRuntimeContext, resolveMemoryBackend } from "./memory-backend";
|
|
94
101
|
import type { MnemopiSessionState } from "./mnemopi/state";
|
|
95
102
|
import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
|
|
@@ -147,6 +154,7 @@ import {
|
|
|
147
154
|
type DiscoverableTool,
|
|
148
155
|
filterBySource,
|
|
149
156
|
formatDiscoverableToolServerSummary,
|
|
157
|
+
isMCPToolName,
|
|
150
158
|
selectDiscoverableToolNamesByServer,
|
|
151
159
|
summarizeDiscoverableTools,
|
|
152
160
|
} from "./tool-discovery/tool-index";
|
|
@@ -302,6 +310,76 @@ function buildMcpNotificationBatchMessage(entries: McpNotificationEntry[]): Agen
|
|
|
302
310
|
};
|
|
303
311
|
}
|
|
304
312
|
|
|
313
|
+
type DeferredMCPActivation = {
|
|
314
|
+
mcpDiscoveryEnabled: boolean;
|
|
315
|
+
explicitlyRequestedMCPToolNames: string[];
|
|
316
|
+
activateAllMCPTools: boolean;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
function formatMCPConnectingMessage(serverNames: string[]): string {
|
|
320
|
+
return `Connecting to MCP servers: ${serverNames.join(", ")}…`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function createPendingMCPTool(name: string): Tool {
|
|
324
|
+
const parsed = parseMCPToolName(name);
|
|
325
|
+
const serverName = parsed?.serverName;
|
|
326
|
+
const mcpToolName = parsed?.toolName ?? name;
|
|
327
|
+
const label = serverName ? `${serverName}/${mcpToolName}` : name;
|
|
328
|
+
const message = serverName
|
|
329
|
+
? `MCP server "${serverName}" is still connecting; tool "${name}" is not yet available. Retry after the MCP connection completes.`
|
|
330
|
+
: `MCP discovery is still in progress; tool "${name}" is not yet available. Retry after MCP connection completes.`;
|
|
331
|
+
const tool: Tool & { mcpServerName?: string; mcpToolName?: string } = {
|
|
332
|
+
name,
|
|
333
|
+
label,
|
|
334
|
+
description: `Pending MCP tool. ${message}`,
|
|
335
|
+
parameters: {
|
|
336
|
+
type: "object",
|
|
337
|
+
properties: {},
|
|
338
|
+
additionalProperties: true,
|
|
339
|
+
},
|
|
340
|
+
approval: "write",
|
|
341
|
+
intent: "omit",
|
|
342
|
+
mcpServerName: serverName,
|
|
343
|
+
mcpToolName,
|
|
344
|
+
async execute() {
|
|
345
|
+
return {
|
|
346
|
+
content: [{ type: "text", text: message }],
|
|
347
|
+
details: { serverName, mcpToolName, isError: true },
|
|
348
|
+
isError: true,
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
return tool;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function collectPendingMCPToolNames(
|
|
356
|
+
explicitToolNames: readonly string[] | undefined,
|
|
357
|
+
restoredSelectedToolNames: readonly string[],
|
|
358
|
+
): string[] {
|
|
359
|
+
const names = new Set<string>();
|
|
360
|
+
for (const name of explicitToolNames ?? []) {
|
|
361
|
+
const normalized = name.toLowerCase();
|
|
362
|
+
if (isMCPToolName(normalized)) names.add(normalized);
|
|
363
|
+
}
|
|
364
|
+
for (const name of restoredSelectedToolNames) {
|
|
365
|
+
const normalized = name.toLowerCase();
|
|
366
|
+
if (isMCPToolName(normalized)) names.add(normalized);
|
|
367
|
+
}
|
|
368
|
+
return [...names];
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function logMCPLoadErrors(errors: MCPLoadResult["errors"]): void {
|
|
372
|
+
for (const [serverName, error] of errors) {
|
|
373
|
+
logger.error("MCP tool load failed", { path: `mcp:${serverName}`, error });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function applyMCPEnvironment(result: { exaApiKeys: string[] }): void {
|
|
378
|
+
if (result.exaApiKeys.length > 0 && !$env.EXA_API_KEY) {
|
|
379
|
+
Bun.env.EXA_API_KEY = result.exaApiKeys[0];
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
305
383
|
// Types
|
|
306
384
|
export interface CreateAgentSessionOptions {
|
|
307
385
|
/** Working directory for project-local discovery. Default: getProjectDir() */
|
|
@@ -1518,46 +1596,131 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1518
1596
|
let mcpManager: MCPManager | undefined = options.mcpManager;
|
|
1519
1597
|
toolSession.mcpManager = mcpManager;
|
|
1520
1598
|
const enableMCP = options.enableMCP ?? true;
|
|
1599
|
+
const deferMCPDiscoveryForUI = enableMCP && !mcpManager && options.hasUI === true;
|
|
1521
1600
|
const customTools: CustomTool[] = [];
|
|
1601
|
+
let startDeferredMCPDiscovery:
|
|
1602
|
+
| ((liveSession: AgentSession, activation: DeferredMCPActivation) => void)
|
|
1603
|
+
| undefined;
|
|
1604
|
+
const onMCPConnecting = (serverNames: string[]) => {
|
|
1605
|
+
if (!options.hasUI || serverNames.length === 0) return;
|
|
1606
|
+
process.stderr.write(`${chalk.gray(formatMCPConnectingMessage(serverNames))}\n`);
|
|
1607
|
+
};
|
|
1608
|
+
const mcpDiscoverOptions = {
|
|
1609
|
+
onConnecting: onMCPConnecting,
|
|
1610
|
+
enableProjectConfig: settings.get("mcp.enableProjectConfig") ?? true,
|
|
1611
|
+
// Always filter Exa - we have native integration
|
|
1612
|
+
filterExa: true,
|
|
1613
|
+
// Filter browser MCP servers when builtin browser tool is active
|
|
1614
|
+
filterBrowser: settings.get("browser.enabled") ?? false,
|
|
1615
|
+
};
|
|
1522
1616
|
if (enableMCP && !mcpManager) {
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
// Filter browser MCP servers when builtin browser tool is active
|
|
1533
|
-
filterBrowser: settings.get("browser.enabled") ?? false,
|
|
1534
|
-
cacheStorage: settings.getStorage(),
|
|
1535
|
-
authStorage,
|
|
1536
|
-
});
|
|
1537
|
-
mcpManager = mcpResult.manager;
|
|
1538
|
-
toolSession.mcpManager = mcpManager;
|
|
1617
|
+
if (deferMCPDiscoveryForUI) {
|
|
1618
|
+
const cacheStorage = settings.getStorage();
|
|
1619
|
+
mcpManager = new MCPManager(cwd, cacheStorage ? new MCPToolCache(cacheStorage) : null);
|
|
1620
|
+
mcpManager.setAuthStorage(authStorage);
|
|
1621
|
+
toolSession.mcpManager = mcpManager;
|
|
1622
|
+
|
|
1623
|
+
if (settings.get("mcp.notifications")) {
|
|
1624
|
+
mcpManager.setNotificationsEnabled(true);
|
|
1625
|
+
}
|
|
1539
1626
|
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1627
|
+
const deferredMCPManager = mcpManager;
|
|
1628
|
+
startDeferredMCPDiscovery = (liveSession, activation) => {
|
|
1629
|
+
void (async () => {
|
|
1630
|
+
try {
|
|
1631
|
+
const mcpResult = await logger.time("discoverAndLoadMCPTools", () =>
|
|
1632
|
+
deferredMCPManager.discoverAndConnect(mcpDiscoverOptions),
|
|
1633
|
+
);
|
|
1634
|
+
// The session can be torn down while servers are still connecting.
|
|
1635
|
+
// Don't resurrect tools on a disposed session, and don't leak the
|
|
1636
|
+
// transports/subprocesses the connect just spawned.
|
|
1637
|
+
if (liveSession.isDisposed) {
|
|
1638
|
+
await deferredMCPManager.disconnectAll();
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
applyMCPEnvironment(mcpResult);
|
|
1642
|
+
logMCPLoadErrors(mcpResult.errors);
|
|
1643
|
+
// `tools.discoveryMode: "auto"` was resolved against a registry that
|
|
1644
|
+
// held only built-ins plus persisted placeholder names. Recompute with
|
|
1645
|
+
// the real MCP tool count: a large toolset must flip discovery on
|
|
1646
|
+
// BEFORE the refresh, or activateAll would dump every MCP tool into
|
|
1647
|
+
// the active set with no search_tool_bm25 registered.
|
|
1648
|
+
let discoveryEnabled = activation.mcpDiscoveryEnabled;
|
|
1649
|
+
let activateAll = activation.activateAllMCPTools;
|
|
1650
|
+
if (!discoveryEnabled) {
|
|
1651
|
+
const nonMCPToolNames = [...toolRegistry.keys()].filter(name => !isMCPToolName(name));
|
|
1652
|
+
const projectedMode = resolveEffectiveToolDiscoveryMode(
|
|
1653
|
+
settings,
|
|
1654
|
+
countToolsForAutoDiscovery([...nonMCPToolNames, ...mcpResult.tools.map(tool => tool.name)]),
|
|
1655
|
+
);
|
|
1656
|
+
if (projectedMode !== "off") {
|
|
1657
|
+
effectiveDiscoveryMode = projectedMode;
|
|
1658
|
+
mcpDiscoveryEnabled = true;
|
|
1659
|
+
discoveryEnabled = true;
|
|
1660
|
+
activateAll = false;
|
|
1661
|
+
liveSession.enableMCPDiscovery();
|
|
1662
|
+
if (!toolRegistry.has("search_tool_bm25")) {
|
|
1663
|
+
const searchTool: Tool = new SearchToolBm25Tool(toolSession);
|
|
1664
|
+
toolRegistry.set(
|
|
1665
|
+
searchTool.name,
|
|
1666
|
+
new ExtensionToolWrapper(wrapToolWithMetaNotice(searchTool), extensionRunner) as Tool,
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
await liveSession.setActiveToolsByName([
|
|
1670
|
+
...liveSession.getActiveToolNames(),
|
|
1671
|
+
"search_tool_bm25",
|
|
1672
|
+
]);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
await liveSession.refreshMCPTools(mcpResult.tools, { activateAll });
|
|
1676
|
+
if (activation.explicitlyRequestedMCPToolNames.length > 0) {
|
|
1677
|
+
if (discoveryEnabled && !activation.mcpDiscoveryEnabled) {
|
|
1678
|
+
// Discovery flipped on mid-flight: route the explicit request
|
|
1679
|
+
// through discovery-aware activation so selection persists.
|
|
1680
|
+
await liveSession.activateDiscoveredMCPTools(activation.explicitlyRequestedMCPToolNames);
|
|
1681
|
+
} else if (!discoveryEnabled) {
|
|
1682
|
+
await liveSession.setActiveToolsByName([
|
|
1683
|
+
...liveSession.getActiveToolNames(),
|
|
1684
|
+
...activation.explicitlyRequestedMCPToolNames,
|
|
1685
|
+
]);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
} catch (error) {
|
|
1689
|
+
logger.error("MCP tool load failed", {
|
|
1690
|
+
path: ".mcp.json",
|
|
1691
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
})();
|
|
1695
|
+
};
|
|
1696
|
+
} else {
|
|
1697
|
+
const mcpResult = await logger.time("discoverAndLoadMCPTools", discoverAndLoadMCPTools, cwd, {
|
|
1698
|
+
...mcpDiscoverOptions,
|
|
1699
|
+
cacheStorage: settings.getStorage(),
|
|
1700
|
+
authStorage,
|
|
1701
|
+
});
|
|
1702
|
+
mcpManager = mcpResult.manager;
|
|
1703
|
+
toolSession.mcpManager = mcpManager;
|
|
1547
1704
|
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1705
|
+
if (settings.get("mcp.notifications")) {
|
|
1706
|
+
mcpManager.setNotificationsEnabled(true);
|
|
1707
|
+
}
|
|
1708
|
+
applyMCPEnvironment(mcpResult);
|
|
1552
1709
|
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1710
|
+
// Log MCP errors
|
|
1711
|
+
for (const { path, error } of mcpResult.errors) {
|
|
1712
|
+
logger.error("MCP tool load failed", { path, error });
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
if (mcpResult.tools.length > 0) {
|
|
1716
|
+
// MCP tools are LoadedCustomTool, extract the tool property
|
|
1717
|
+
customTools.push(...mcpResult.tools.map(loaded => loaded.tool));
|
|
1718
|
+
}
|
|
1556
1719
|
}
|
|
1557
1720
|
}
|
|
1558
1721
|
// Only top-level sessions own the global MCPManager. Subagents already
|
|
1559
1722
|
// receive the parent's manager via `options.mcpManager`, and reassigning
|
|
1560
|
-
// the singleton to the same value is a no-op
|
|
1723
|
+
// the singleton to the same value is a no-op — keep the gate explicit
|
|
1561
1724
|
// to mirror the AsyncJobManager ownership rule.
|
|
1562
1725
|
if (mcpManager && !options.parentTaskPrefix) MCPManager.setInstance(mcpManager);
|
|
1563
1726
|
|
|
@@ -1852,6 +2015,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1852
2015
|
for (const tool of wrappedExtensionTools) {
|
|
1853
2016
|
toolRegistry.set(tool.name, tool);
|
|
1854
2017
|
}
|
|
2018
|
+
if (deferMCPDiscoveryForUI && mcpManager) {
|
|
2019
|
+
for (const name of collectPendingMCPToolNames(options.toolNames, existingSession.selectedMCPToolNames)) {
|
|
2020
|
+
if (!toolRegistry.has(name)) {
|
|
2021
|
+
toolRegistry.set(name, createPendingMCPTool(name));
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1855
2026
|
// Wrap every tool with `ExtensionToolWrapper` so the per-tool approval gate runs on every
|
|
1856
2027
|
// call site, regardless of whether any user extensions are loaded. See the runner-construction
|
|
1857
2028
|
// comment above for the safety invariant this enforces.
|
|
@@ -1879,7 +2050,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1879
2050
|
}
|
|
1880
2051
|
}
|
|
1881
2052
|
|
|
1882
|
-
|
|
2053
|
+
// `let`: the deferred MCP discovery closure upgrades these when the real
|
|
2054
|
+
// MCP tool count pushes `auto` past its threshold; `rebuildSystemPrompt`
|
|
2055
|
+
// below reads the live bindings.
|
|
2056
|
+
let effectiveDiscoveryMode = resolveEffectiveToolDiscoveryMode(
|
|
1883
2057
|
settings,
|
|
1884
2058
|
countToolsForAutoDiscovery(toolRegistry.keys()),
|
|
1885
2059
|
);
|
|
@@ -1890,7 +2064,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1890
2064
|
new ExtensionToolWrapper(wrapToolWithMetaNotice(searchTool), extensionRunner) as Tool,
|
|
1891
2065
|
);
|
|
1892
2066
|
}
|
|
1893
|
-
|
|
2067
|
+
let mcpDiscoveryEnabled = effectiveDiscoveryMode !== "off"; // back-compat: true when any discovery active
|
|
1894
2068
|
|
|
1895
2069
|
const reloadSshTool = async (): Promise<AgentTool | null> => {
|
|
1896
2070
|
if (!requestedToolNameSet.has("ssh")) return null;
|
|
@@ -1942,7 +2116,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1942
2116
|
const memoryBackend = await resolveMemoryBackend(settings);
|
|
1943
2117
|
const memoryInstructions = await memoryBackend.buildDeveloperInstructions(agentDir, settings, session);
|
|
1944
2118
|
|
|
1945
|
-
// Build combined append prompt: memory instructions + MCP server instructions
|
|
2119
|
+
// Build combined append prompt: memory instructions + MCP server instructions.
|
|
2120
|
+
// For UI sessions MCP discovery is deferred, so `getServerInstructions()` is
|
|
2121
|
+
// empty until the background connect completes; the rebuild that
|
|
2122
|
+
// `refreshMCPTools` triggers post-discovery then picks up the now-connected
|
|
2123
|
+
// servers' instructions, so they join the prompt for the rest of the session.
|
|
1946
2124
|
const serverInstructions = mcpManager?.getServerInstructions();
|
|
1947
2125
|
let appendPrompt: string | undefined = memoryInstructions ?? undefined;
|
|
1948
2126
|
if (serverInstructions && serverInstructions.size > 0) {
|
|
@@ -2494,7 +2672,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2494
2672
|
// Skip when reusing a parent's manager — the parent owns the callbacks.
|
|
2495
2673
|
if (mcpManager && !options.mcpManager) {
|
|
2496
2674
|
mcpManager.setOnToolsChanged(tools => {
|
|
2497
|
-
void
|
|
2675
|
+
void (async () => {
|
|
2676
|
+
try {
|
|
2677
|
+
await session.refreshMCPTools(
|
|
2678
|
+
tools,
|
|
2679
|
+
deferMCPDiscoveryForUI && !mcpDiscoveryEnabled && options.toolNames === undefined
|
|
2680
|
+
? { activateAll: true }
|
|
2681
|
+
: undefined,
|
|
2682
|
+
);
|
|
2683
|
+
if (deferMCPDiscoveryForUI && !mcpDiscoveryEnabled && explicitlyRequestedMCPToolNames.length > 0) {
|
|
2684
|
+
await session.setActiveToolsByName([
|
|
2685
|
+
...session.getActiveToolNames(),
|
|
2686
|
+
...explicitlyRequestedMCPToolNames,
|
|
2687
|
+
]);
|
|
2688
|
+
}
|
|
2689
|
+
} catch (error) {
|
|
2690
|
+
logger.warn("MCP tool refresh failed", {
|
|
2691
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2692
|
+
});
|
|
2693
|
+
}
|
|
2694
|
+
})();
|
|
2498
2695
|
});
|
|
2499
2696
|
// Wire prompt refresh → rebuild MCP prompt slash commands
|
|
2500
2697
|
mcpManager.setOnPromptsChanged(serverName => {
|
|
@@ -2527,6 +2724,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2527
2724
|
});
|
|
2528
2725
|
}
|
|
2529
2726
|
|
|
2727
|
+
startDeferredMCPDiscovery?.(session, {
|
|
2728
|
+
mcpDiscoveryEnabled,
|
|
2729
|
+
explicitlyRequestedMCPToolNames,
|
|
2730
|
+
activateAllMCPTools: !mcpDiscoveryEnabled && options.toolNames === undefined,
|
|
2731
|
+
});
|
|
2732
|
+
|
|
2530
2733
|
return {
|
|
2531
2734
|
session,
|
|
2532
2735
|
extensionsResult,
|
|
@@ -100,6 +100,7 @@ import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
|
100
100
|
import { countTokens, MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
|
|
101
101
|
import {
|
|
102
102
|
extractRetryHint,
|
|
103
|
+
formatDuration,
|
|
103
104
|
getAgentDbPath,
|
|
104
105
|
getInstallId,
|
|
105
106
|
isBunTestRuntime,
|
|
@@ -240,7 +241,13 @@ import { normalizeModelContextImages } from "../utils/image-loading";
|
|
|
240
241
|
import { buildNamedToolChoice } from "../utils/tool-choice";
|
|
241
242
|
import type { AuthStorage } from "./auth-storage";
|
|
242
243
|
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
243
|
-
import {
|
|
244
|
+
import {
|
|
245
|
+
type CodexAutoRedeemRedeemDecision,
|
|
246
|
+
defaultCodexAutoRedeemCoordinator,
|
|
247
|
+
evaluateCodexAutoRedeem,
|
|
248
|
+
shouldEvaluateCodexAutoRedeem,
|
|
249
|
+
shouldPromptCodexAutoRedeem,
|
|
250
|
+
} from "./codex-auto-reset";
|
|
244
251
|
import {
|
|
245
252
|
type BashExecutionMessage,
|
|
246
253
|
type CustomMessage,
|
|
@@ -1418,6 +1425,11 @@ export class AgentSession {
|
|
|
1418
1425
|
return this.#ttsrManager;
|
|
1419
1426
|
}
|
|
1420
1427
|
|
|
1428
|
+
/** Secret obfuscator, when secrets are configured; /share redaction reuses it. */
|
|
1429
|
+
get obfuscator(): SecretObfuscator | undefined {
|
|
1430
|
+
return this.#obfuscator;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1421
1433
|
/** Whether a TTSR abort is pending (stream was aborted to inject rules) */
|
|
1422
1434
|
get isTtsrAbortPending(): boolean {
|
|
1423
1435
|
return this.#ttsrAbortPending;
|
|
@@ -3140,6 +3152,12 @@ export class AgentSession {
|
|
|
3140
3152
|
state.resetConversationTracking();
|
|
3141
3153
|
}
|
|
3142
3154
|
|
|
3155
|
+
/** True once dispose() has begun; deferred background work (e.g. the deferred
|
|
3156
|
+
* MCP discovery task in sdk.ts) must not touch the session past this point. */
|
|
3157
|
+
get isDisposed(): boolean {
|
|
3158
|
+
return this.#isDisposed;
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3143
3161
|
/**
|
|
3144
3162
|
* Synchronously mark the session as disposing so new work is rejected
|
|
3145
3163
|
* immediately: Python/eval starts throw, queued asides are dropped, and the
|
|
@@ -3473,6 +3491,17 @@ export class AgentSession {
|
|
|
3473
3491
|
return this.#mcpDiscoveryEnabled;
|
|
3474
3492
|
}
|
|
3475
3493
|
|
|
3494
|
+
/**
|
|
3495
|
+
* Flip MCP discovery on after deferred discovery learns the real tool count.
|
|
3496
|
+
* UI sessions resolve `tools.discoveryMode: "auto"` before MCP servers
|
|
3497
|
+
* connect, so a large MCP toolset discovered later must be able to upgrade
|
|
3498
|
+
* the session from the force-activate path to the discovery path. One-way:
|
|
3499
|
+
* discovery is never downgraded mid-session.
|
|
3500
|
+
*/
|
|
3501
|
+
enableMCPDiscovery(): void {
|
|
3502
|
+
this.#mcpDiscoveryEnabled = true;
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3476
3505
|
getSelectedMCPToolNames(): string[] {
|
|
3477
3506
|
if (!this.#mcpDiscoveryEnabled) {
|
|
3478
3507
|
return this.getActiveToolNames().filter(name => isMCPToolName(name) && this.#toolRegistry.has(name));
|
|
@@ -10176,20 +10205,63 @@ export class AgentSession {
|
|
|
10176
10205
|
signal,
|
|
10177
10206
|
});
|
|
10178
10207
|
}
|
|
10208
|
+
async #confirmCodexAutoRedeem(decision: CodexAutoRedeemRedeemDecision): Promise<boolean> {
|
|
10209
|
+
const runner = this.#extensionRunner;
|
|
10210
|
+
if (!runner?.hasUI()) {
|
|
10211
|
+
this.emitNotice(
|
|
10212
|
+
"warning",
|
|
10213
|
+
"Codex saved reset is eligible, but auto-redeem is unset and no prompt UI is available. Run `/usage reset` or set codexResets.autoRedeem.",
|
|
10214
|
+
"codex-auto-reset",
|
|
10215
|
+
);
|
|
10216
|
+
return false;
|
|
10217
|
+
}
|
|
10218
|
+
|
|
10219
|
+
const who = decision.target.email ?? decision.target.accountId ?? "the active account";
|
|
10220
|
+
const resetLabel = decision.availableCount === 1 ? "reset" : "resets";
|
|
10221
|
+
try {
|
|
10222
|
+
const choice = await runner
|
|
10223
|
+
.getUIContext()
|
|
10224
|
+
.select(
|
|
10225
|
+
`Do you wanna redeem your reset?\n${who} is blocked by the weekly Codex limit for about ${formatDuration(decision.remainingMs)}. Spend 1 of ${decision.availableCount} saved ${resetLabel}?`,
|
|
10226
|
+
[
|
|
10227
|
+
{
|
|
10228
|
+
label: "Yes",
|
|
10229
|
+
description: "Redeem now and remember yes for future eligible Codex weekly blocks.",
|
|
10230
|
+
},
|
|
10231
|
+
{
|
|
10232
|
+
label: "No",
|
|
10233
|
+
description: "Do not auto-redeem saved Codex resets.",
|
|
10234
|
+
},
|
|
10235
|
+
],
|
|
10236
|
+
);
|
|
10237
|
+
if (choice === "Yes") {
|
|
10238
|
+
this.settings.set("codexResets.autoRedeem", "yes");
|
|
10239
|
+
return true;
|
|
10240
|
+
}
|
|
10241
|
+
if (choice === "No") {
|
|
10242
|
+
this.settings.set("codexResets.autoRedeem", "no");
|
|
10243
|
+
}
|
|
10244
|
+
} catch (error) {
|
|
10245
|
+
logger.warn("codex-auto-reset prompt failed", { error: String(error) });
|
|
10246
|
+
}
|
|
10247
|
+
return false;
|
|
10248
|
+
}
|
|
10179
10249
|
|
|
10180
10250
|
/**
|
|
10181
10251
|
* Auto-redeem hook for {@link AgentSession.#handleRetryableError}'s
|
|
10182
10252
|
* usage-limit branch. Returns `true` only when a saved Codex reset was
|
|
10183
|
-
* actually spent (so the caller retries immediately).
|
|
10184
|
-
*
|
|
10185
|
-
*
|
|
10186
|
-
*
|
|
10253
|
+
* actually spent (so the caller retries immediately). The "unset" mode is
|
|
10254
|
+
* reactive but asks before spending; "yes" skips that prompt, and "no" avoids
|
|
10255
|
+
* the eligibility IO entirely. The decision remains heavily gated — see
|
|
10256
|
+
* `./codex-auto-reset` and the design in `local://autoreset-spec.md`.
|
|
10257
|
+
* Per-account in-flight dedup lets concurrent sessions adopt one redeem
|
|
10258
|
+
* instead of double-spending.
|
|
10187
10259
|
*/
|
|
10188
10260
|
async #maybeAutoRedeemCodexReset(coordinator = defaultCodexAutoRedeemCoordinator): Promise<boolean> {
|
|
10189
10261
|
const cfg = this.settings.getGroup("codexResets");
|
|
10190
10262
|
const model = this.model;
|
|
10191
10263
|
// Cheap exits before any IO.
|
|
10192
|
-
if (!cfg.autoRedeem || !model || model.provider !== "openai-codex") return false;
|
|
10264
|
+
if (!shouldEvaluateCodexAutoRedeem(cfg.autoRedeem) || !model || model.provider !== "openai-codex") return false;
|
|
10193
10265
|
const authStorage = this.#modelRegistry.authStorage;
|
|
10194
10266
|
// Capture identity BEFORE awaits: markUsageLimitReached leaves the
|
|
10195
10267
|
// usage-limit session credential sticky, so this names the blocked account.
|
|
@@ -10206,7 +10278,7 @@ export class AgentSession {
|
|
|
10206
10278
|
provider: model.provider,
|
|
10207
10279
|
modelId: model.id,
|
|
10208
10280
|
settings: {
|
|
10209
|
-
autoRedeem:
|
|
10281
|
+
autoRedeem: true,
|
|
10210
10282
|
minBlockedMinutes: Math.max(0, cfg.minBlockedMinutes),
|
|
10211
10283
|
keepCredits: Math.max(0, Math.trunc(cfg.keepCredits)),
|
|
10212
10284
|
},
|
|
@@ -10219,6 +10291,9 @@ export class AgentSession {
|
|
|
10219
10291
|
logger.debug("codex-auto-reset: skipped", { reason: decision.reason });
|
|
10220
10292
|
return false;
|
|
10221
10293
|
}
|
|
10294
|
+
if (shouldPromptCodexAutoRedeem(cfg.autoRedeem) && !(await this.#confirmCodexAutoRedeem(decision))) {
|
|
10295
|
+
return false;
|
|
10296
|
+
}
|
|
10222
10297
|
// Commit the attempt BEFORE acting so this block can never re-enter.
|
|
10223
10298
|
coordinator.attemptedBlockKeys.add(decision.blockKey);
|
|
10224
10299
|
coordinator.lastAttemptAtByAccount.set(decision.accountKey, Date.now());
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
* eligibility off exact limit ids (`openai-codex:primary` /
|
|
25
25
|
* `openai-codex:secondary`) and `usedFraction`, never off `status`.
|
|
26
26
|
*
|
|
27
|
-
* ANTI-WASTE GATES (in evaluation order): the policy must be
|
|
28
|
-
*
|
|
27
|
+
* ANTI-WASTE GATES (in evaluation order): the policy must not be set to "no";
|
|
28
|
+
* the active model must be Codex (not Spark — a Spark block lives on a
|
|
29
29
|
* separate meter and it is unknown whether a credit even resets it); a fresh
|
|
30
30
|
* usage report for the active account must confirm `limitReached`; the WEEKLY
|
|
31
31
|
* (secondary) window must be genuinely exhausted — a 5h-only block self-heals
|
|
@@ -38,12 +38,14 @@
|
|
|
38
38
|
* read-only views are passed in so the predicate itself stays deterministic.
|
|
39
39
|
*/
|
|
40
40
|
import type { OAuthAccountIdentity, ResetCreditTarget, UsageReport } from "@oh-my-pi/pi-ai";
|
|
41
|
+
import type { CodexAutoRedeemMode } from "../config/settings-schema";
|
|
41
42
|
import { reportMatchesActiveAccount } from "../slash-commands/helpers/active-oauth-account";
|
|
42
43
|
|
|
43
44
|
/** Weekly window counts as exhausted at `usedFraction >= 0.999` (used_percent >= 99.9). */
|
|
44
45
|
export const WEEKLY_EXHAUSTED_MIN_FRACTION = 0.999;
|
|
45
46
|
/** A weekly reset can never be more than one window length (7d) away; +1h slack for skew. */
|
|
46
47
|
export const MAX_PLAUSIBLE_REMAINING_MS = 7 * 24 * 3_600_000 + 60 * 60_000;
|
|
48
|
+
|
|
47
49
|
/** Report must be no older than the 5-min usage cache TTL plus slack. */
|
|
48
50
|
export const REPORT_FRESHNESS_MS = 10 * 60_000;
|
|
49
51
|
/** Per-account cooldown that catches blockKey drift across a minute boundary. */
|
|
@@ -51,6 +53,14 @@ export const ATTEMPT_COOLDOWN_MS = 60_000;
|
|
|
51
53
|
/** Minute bucket for blockKey, absorbing `reset_after_seconds`-derived jitter. */
|
|
52
54
|
export const DEBOUNCE_BUCKET_MS = 60_000;
|
|
53
55
|
|
|
56
|
+
export function shouldEvaluateCodexAutoRedeem(mode: CodexAutoRedeemMode): boolean {
|
|
57
|
+
return mode !== "no";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function shouldPromptCodexAutoRedeem(mode: CodexAutoRedeemMode): boolean {
|
|
61
|
+
return mode === "unset";
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
export type CodexAutoRedeemSkipReason =
|
|
55
65
|
| "disabled"
|
|
56
66
|
| "wrong-provider"
|
|
@@ -83,16 +93,18 @@ export interface CodexAutoRedeemInput {
|
|
|
83
93
|
lastAttemptAtByAccount: ReadonlyMap<string, number>;
|
|
84
94
|
}
|
|
85
95
|
|
|
96
|
+
export interface CodexAutoRedeemRedeemDecision {
|
|
97
|
+
redeem: true;
|
|
98
|
+
target: ResetCreditTarget;
|
|
99
|
+
accountKey: string;
|
|
100
|
+
blockKey: string;
|
|
101
|
+
weeklyResetAtMs: number;
|
|
102
|
+
remainingMs: number;
|
|
103
|
+
availableCount: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
86
106
|
export type CodexAutoRedeemDecision =
|
|
87
|
-
|
|
|
88
|
-
redeem: true;
|
|
89
|
-
target: ResetCreditTarget;
|
|
90
|
-
accountKey: string;
|
|
91
|
-
blockKey: string;
|
|
92
|
-
weeklyResetAtMs: number;
|
|
93
|
-
remainingMs: number;
|
|
94
|
-
availableCount: number;
|
|
95
|
-
}
|
|
107
|
+
| CodexAutoRedeemRedeemDecision
|
|
96
108
|
| { redeem: false; reason: CodexAutoRedeemSkipReason };
|
|
97
109
|
|
|
98
110
|
/** Trimmed lowercase, or undefined when blank. Mirrors `normalizeIdentityValue` in active-oauth-account.ts. */
|
|
@@ -1994,6 +1994,12 @@ export class SessionManager {
|
|
|
1994
1994
|
#byId: Map<string, SessionEntry> = new Map();
|
|
1995
1995
|
#labelsById: Map<string, string> = new Map();
|
|
1996
1996
|
#leafId: string | null = null;
|
|
1997
|
+
/**
|
|
1998
|
+
* Collab replication tap: invoked for every appended entry with the
|
|
1999
|
+
* in-memory (pre-blob-externalization) entry, so inline images survive.
|
|
2000
|
+
* Failures are swallowed — a broadcast error must never break persistence.
|
|
2001
|
+
*/
|
|
2002
|
+
onEntryAppended?: (entry: SessionEntry) => void;
|
|
1997
2003
|
#usageStatistics = {
|
|
1998
2004
|
input: 0,
|
|
1999
2005
|
output: 0,
|
|
@@ -2927,6 +2933,44 @@ export class SessionManager {
|
|
|
2927
2933
|
this.#usageStatistics.cost += usage.cost.total;
|
|
2928
2934
|
}
|
|
2929
2935
|
}
|
|
2936
|
+
if (this.onEntryAppended) {
|
|
2937
|
+
try {
|
|
2938
|
+
this.onEntryAppended(entry);
|
|
2939
|
+
} catch (err) {
|
|
2940
|
+
logger.warn("collab entry hook failed", { error: String(err) });
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
/**
|
|
2946
|
+
* Append a foreign (host-authored) entry verbatim, preserving its
|
|
2947
|
+
* `id`/`parentId` — no id minting. Used by collab guests to mirror the
|
|
2948
|
+
* host session into the local replica file.
|
|
2949
|
+
*/
|
|
2950
|
+
ingestReplicatedEntry(entry: SessionEntry): void {
|
|
2951
|
+
this.#appendEntry(entry);
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
/**
|
|
2955
|
+
* Snapshot the session for collab replication: the live header plus a deep
|
|
2956
|
+
* copy of every entry (the host mutates entries in place on
|
|
2957
|
+
* truncation/rewrite paths, so guests must not share references).
|
|
2958
|
+
*/
|
|
2959
|
+
snapshotForReplication(): { header: SessionHeader; entries: SessionEntry[] } {
|
|
2960
|
+
const live = this.getHeader();
|
|
2961
|
+
const header: SessionHeader = live
|
|
2962
|
+
? structuredClone(live)
|
|
2963
|
+
: {
|
|
2964
|
+
type: "session",
|
|
2965
|
+
version: CURRENT_SESSION_VERSION,
|
|
2966
|
+
id: this.#sessionId,
|
|
2967
|
+
title: this.#sessionName,
|
|
2968
|
+
titleSource: this.#titleSource,
|
|
2969
|
+
timestamp: new Date().toISOString(),
|
|
2970
|
+
cwd: this.cwd,
|
|
2971
|
+
};
|
|
2972
|
+
const entries = structuredClone(this.#fileEntries.filter(e => e.type !== "session")) as SessionEntry[];
|
|
2973
|
+
return { header, entries };
|
|
2930
2974
|
}
|
|
2931
2975
|
|
|
2932
2976
|
/** Append a message as child of current leaf, then advance leaf. Returns entry id.
|
|
@@ -123,14 +123,16 @@ function selectSystemPromptImageTarget(
|
|
|
123
123
|
|
|
124
124
|
/** Tool-result swap candidate, in context order. */
|
|
125
125
|
export interface InlineToolResultCandidate {
|
|
126
|
-
/**
|
|
126
|
+
/** Stable identifier for rendering cache key and applying the swap. */
|
|
127
127
|
id: string;
|
|
128
|
-
/** Token count of the joined text blocks (0
|
|
128
|
+
/** Token count of the joined text blocks (0 for empty or image-carrying). */
|
|
129
129
|
textTokens: number;
|
|
130
|
-
/** Frames needed to render the text (0 = empty
|
|
130
|
+
/** Frames needed to render the text (0 = empty, below floor, image-carrying, or error). */
|
|
131
131
|
frames: number;
|
|
132
132
|
/** Already carries an image (screenshot etc.) — never re-imaged. */
|
|
133
133
|
hasImage: boolean;
|
|
134
|
+
/** Error tool results must stay text-only for provider API validation. */
|
|
135
|
+
isError?: boolean;
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
export interface InlineSystemPromptCandidate {
|
|
@@ -173,6 +175,7 @@ export function planInlineSwaps(input: InlinePlanInput): InlineSwapPlan {
|
|
|
173
175
|
for (let k = 0; k < input.toolResults.length - 1 && budget > 0; k++) {
|
|
174
176
|
const candidate = input.toolResults[k];
|
|
175
177
|
if (candidate.hasImage) continue;
|
|
178
|
+
if (candidate.isError) continue;
|
|
176
179
|
if (candidate.textTokens < MIN_TOOL_RESULT_TOKENS) continue;
|
|
177
180
|
if (candidate.frames === 0 || candidate.frames > budget) continue;
|
|
178
181
|
if (!passesSavingsGate(candidate.frames, input.shape, candidate.textTokens)) continue;
|
|
@@ -211,6 +214,7 @@ export interface InlineMessageView {
|
|
|
211
214
|
role: string;
|
|
212
215
|
toolCallId?: string;
|
|
213
216
|
content?: unknown;
|
|
217
|
+
isError?: boolean;
|
|
214
218
|
}
|
|
215
219
|
|
|
216
220
|
export interface SnapcompactSavingsEstimate {
|
|
@@ -280,6 +284,7 @@ export function estimateInlineSavings(input: {
|
|
|
280
284
|
if (options.renderToolResults) {
|
|
281
285
|
for (const message of input.messages) {
|
|
282
286
|
if (message.role !== "toolResult" || typeof message.toolCallId !== "string") continue;
|
|
287
|
+
if (message.isError) continue;
|
|
283
288
|
const blocks: BlockViews = Array.isArray(message.content) ? (message.content as BlockViews) : [];
|
|
284
289
|
const hasImage = blocks.some(block => block.type === "image");
|
|
285
290
|
const text = hasImage
|
|
@@ -418,6 +423,7 @@ export class SnapcompactInlineTransformer {
|
|
|
418
423
|
liveToolCallIds.add(message.toolCallId);
|
|
419
424
|
// Don't re-image results that already carry images (screenshots etc.).
|
|
420
425
|
const hasImage = message.content.some(block => block.type === "image");
|
|
426
|
+
if (message.isError) continue;
|
|
421
427
|
const text = hasImage
|
|
422
428
|
? ""
|
|
423
429
|
: message.content
|