@oh-my-pi/pi-coding-agent 16.1.0 → 16.1.2

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.
Files changed (76) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/dist/cli.js +3134 -3158
  3. package/dist/types/cli/bench-cli.d.ts +2 -1
  4. package/dist/types/config/settings-schema.d.ts +28 -37
  5. package/dist/types/lsp/types.d.ts +5 -3
  6. package/dist/types/main.d.ts +2 -0
  7. package/dist/types/modes/components/assistant-message.d.ts +12 -0
  8. package/dist/types/modes/components/cache-invalidation-marker.d.ts +7 -2
  9. package/dist/types/modes/components/welcome.d.ts +1 -1
  10. package/dist/types/sdk.d.ts +19 -2
  11. package/dist/types/session/auth-broker-config.d.ts +33 -6
  12. package/dist/types/system-prompt.d.ts +5 -1
  13. package/dist/types/task/executor.d.ts +10 -0
  14. package/dist/types/tools/find.d.ts +0 -2
  15. package/dist/types/tools/search.d.ts +3 -3
  16. package/package.json +12 -12
  17. package/scripts/measure-prompt-tokens.ts +63 -0
  18. package/src/cli/bench-cli.ts +64 -3
  19. package/src/cli/startup-cwd.ts +3 -13
  20. package/src/config/settings-schema.ts +34 -37
  21. package/src/config/settings.ts +40 -0
  22. package/src/cursor.ts +1 -1
  23. package/src/debug/raw-sse-buffer.ts +31 -10
  24. package/src/eval/py/prelude.py +1 -1
  25. package/src/export/html/tool-views.generated.js +1 -1
  26. package/src/extensibility/extensions/runner.ts +8 -2
  27. package/src/internal-urls/docs-index.generated.txt +1 -1
  28. package/src/lsp/client.ts +9 -9
  29. package/src/lsp/types.ts +6 -3
  30. package/src/main.ts +29 -9
  31. package/src/modes/components/assistant-message.ts +86 -0
  32. package/src/modes/components/cache-invalidation-marker.ts +12 -2
  33. package/src/modes/components/settings-defs.ts +7 -0
  34. package/src/modes/components/tips.txt +2 -1
  35. package/src/modes/components/welcome.ts +86 -8
  36. package/src/modes/controllers/event-controller.ts +1 -1
  37. package/src/prompts/system/personalities/default.md +8 -16
  38. package/src/prompts/system/system-prompt.md +101 -115
  39. package/src/prompts/tools/ast-edit.md +10 -12
  40. package/src/prompts/tools/ast-grep.md +14 -18
  41. package/src/prompts/tools/bash.md +19 -21
  42. package/src/prompts/tools/browser.md +24 -24
  43. package/src/prompts/tools/checkpoint.md +0 -1
  44. package/src/prompts/tools/debug.md +11 -15
  45. package/src/prompts/tools/eval.md +27 -27
  46. package/src/prompts/tools/find.md +6 -10
  47. package/src/prompts/tools/github.md +11 -15
  48. package/src/prompts/tools/goal.md +0 -7
  49. package/src/prompts/tools/inspect-image.md +0 -1
  50. package/src/prompts/tools/irc.md +15 -24
  51. package/src/prompts/tools/job.md +5 -8
  52. package/src/prompts/tools/learn.md +2 -2
  53. package/src/prompts/tools/lsp.md +27 -30
  54. package/src/prompts/tools/manage-skill.md +4 -4
  55. package/src/prompts/tools/read.md +21 -23
  56. package/src/prompts/tools/replace.md +0 -1
  57. package/src/prompts/tools/resolve.md +4 -9
  58. package/src/prompts/tools/rewind.md +1 -1
  59. package/src/prompts/tools/search.md +8 -10
  60. package/src/prompts/tools/task.md +33 -38
  61. package/src/prompts/tools/todo.md +14 -18
  62. package/src/prompts/tools/web-search.md +0 -4
  63. package/src/prompts/tools/write.md +1 -1
  64. package/src/sdk.ts +49 -102
  65. package/src/session/agent-session.ts +23 -12
  66. package/src/session/auth-broker-config.ts +36 -76
  67. package/src/session/session-history-format.ts +1 -1
  68. package/src/session/session-manager.ts +33 -6
  69. package/src/system-prompt.ts +28 -8
  70. package/src/task/executor.ts +57 -0
  71. package/src/task/index.ts +15 -1
  72. package/src/tools/browser.ts +1 -1
  73. package/src/tools/eval.ts +1 -1
  74. package/src/tools/find.ts +4 -17
  75. package/src/tools/memory-edit.ts +1 -1
  76. package/src/tools/search.ts +5 -5
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * Resolve auth-broker connection configuration for the local omp client.
3
3
  *
4
+ * This is a thin coding-agent wrapper around the shared resolver in
5
+ * `@oh-my-pi/pi-ai/auth-broker/discover` that preserves the process-lifetime
6
+ * memoization expected by the CLI and injects the full `resolveConfigValue`
7
+ * (including `!command` config indirection) from coding-agent's config layer.
8
+ *
4
9
  * Precedence (highest first):
5
10
  * 1. `OMP_AUTH_BROKER_URL` / `OMP_AUTH_BROKER_TOKEN` env vars.
6
11
  * 2. `auth.broker.url` / `auth.broker.token` in `~/.omp/agent/config.yml`
@@ -15,55 +20,18 @@
15
20
  * `runRootCommand`, and we want hand-edited config entries to be honoured at
16
21
  * boot without forcing a startup reorder.
17
22
  */
18
- import * as path from "node:path";
19
- import { getAgentDir, getConfigRootDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
20
- import { YAML } from "bun";
21
- import { resolveConfigValue } from "../config/resolve-config-value";
22
-
23
- export interface AuthBrokerClientConfig {
24
- url: string;
25
- token: string;
26
- }
27
-
28
- /** Path to the local bearer token file. Created on the broker host by `omp auth-broker token`. */
29
- export function getAuthBrokerTokenFilePath(): string {
30
- return path.join(getConfigRootDir(), "auth-broker.token");
31
- }
32
-
33
- async function readTokenFile(): Promise<string | null> {
34
- try {
35
- const raw = await Bun.file(getAuthBrokerTokenFilePath()).text();
36
- const trimmed = raw.trim();
37
- return trimmed.length > 0 ? trimmed : null;
38
- } catch (err) {
39
- if (isEnoent(err)) return null;
40
- logger.warn("auth-broker token file unreadable", { error: String(err) });
41
- return null;
42
- }
43
- }
44
23
 
45
- interface ConfigSnapshot {
46
- url?: string;
47
- token?: string;
48
- }
24
+ import {
25
+ type AuthBrokerClientConfig,
26
+ type DiscoverAuthStorageOptions,
27
+ discoverAuthStorage as discoverAuthStorageShared,
28
+ getAuthBrokerTokenFilePath,
29
+ resolveAuthBrokerConfig as resolveAuthBrokerConfigShared,
30
+ } from "@oh-my-pi/pi-ai/auth-broker/discover";
31
+ import { getAgentDir } from "@oh-my-pi/pi-utils";
32
+ import { resolveConfigValue } from "../config/resolve-config-value";
49
33
 
50
- async function readConfigYaml(): Promise<ConfigSnapshot> {
51
- const configPath = path.join(getAgentDir(), "config.yml");
52
- try {
53
- const raw = await Bun.file(configPath).text();
54
- const parsed = YAML.parse(raw);
55
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
56
- const record = parsed as Record<string, unknown>;
57
- const url = typeof record["auth.broker.url"] === "string" ? (record["auth.broker.url"] as string) : undefined;
58
- const token =
59
- typeof record["auth.broker.token"] === "string" ? (record["auth.broker.token"] as string) : undefined;
60
- return { url, token };
61
- } catch (err) {
62
- if (isEnoent(err)) return {};
63
- logger.warn("auth-broker config.yml unreadable", { error: String(err) });
64
- return {};
65
- }
66
- }
34
+ export { type AuthBrokerClientConfig, getAuthBrokerTokenFilePath };
67
35
 
68
36
  /**
69
37
  * Process-lifetime memo for {@link resolveAuthBrokerConfig}. Keyed on the env
@@ -88,7 +56,10 @@ let cachedConfigPromise: Promise<AuthBrokerClientConfig | null> | null = null;
88
56
  export function resolveAuthBrokerConfig(): Promise<AuthBrokerClientConfig | null> {
89
57
  const key = `${process.env.OMP_AUTH_BROKER_URL ?? ""}\u0000${process.env.OMP_AUTH_BROKER_TOKEN ?? ""}\u0000${getAgentDir()}`;
90
58
  if (cachedConfigPromise && cachedConfigKey === key) return cachedConfigPromise;
91
- const promise = resolveAuthBrokerConfigUncached();
59
+ const promise = resolveAuthBrokerConfigShared({
60
+ agentDir: getAgentDir(),
61
+ configValueResolver: resolveConfigValue,
62
+ });
92
63
  cachedConfigKey = key;
93
64
  cachedConfigPromise = promise;
94
65
  promise.catch(() => {
@@ -100,32 +71,21 @@ export function resolveAuthBrokerConfig(): Promise<AuthBrokerClientConfig | null
100
71
  return promise;
101
72
  }
102
73
 
103
- async function resolveAuthBrokerConfigUncached(): Promise<AuthBrokerClientConfig | null> {
104
- const envUrl = process.env.OMP_AUTH_BROKER_URL;
105
- const envToken = process.env.OMP_AUTH_BROKER_TOKEN;
106
-
107
- let url = envUrl && envUrl.length > 0 ? envUrl : undefined;
108
- let configToken: string | undefined;
109
- if (!url || !envToken) {
110
- const fromConfig = await readConfigYaml();
111
- if (!url && fromConfig.url) {
112
- const resolved = await resolveConfigValue(fromConfig.url);
113
- if (resolved && resolved.length > 0) url = resolved;
114
- }
115
- if (fromConfig.token) {
116
- const resolved = await resolveConfigValue(fromConfig.token);
117
- if (resolved && resolved.length > 0) configToken = resolved;
118
- }
119
- }
120
- if (!url) return null;
121
-
122
- const token =
123
- (envToken && envToken.length > 0 ? envToken : undefined) ?? configToken ?? (await readTokenFile()) ?? undefined;
124
- if (!token) {
125
- throw new Error(
126
- `OMP_AUTH_BROKER_URL is set (${url}) but no bearer token is available. ` +
127
- `Set OMP_AUTH_BROKER_TOKEN, the \`auth.broker.token\` config entry, or place one at ${getAuthBrokerTokenFilePath()}.`,
128
- );
129
- }
130
- return { url, token };
74
+ /**
75
+ * Create an AuthStorage instance, using the broker when configured and falling
76
+ * back to the local SQLite store otherwise. Delegates to the shared resolver in
77
+ * pi-ai so the CLI, subagents, and the catalog generator all see the same
78
+ * credentials.
79
+ *
80
+ * Default `agentDir` is the current configured agent directory.
81
+ */
82
+ export function discoverAuthStorage(
83
+ agentDir: string = getAgentDir(),
84
+ options?: Omit<DiscoverAuthStorageOptions, "agentDir" | "configValueResolver">,
85
+ ): ReturnType<typeof discoverAuthStorageShared> {
86
+ return discoverAuthStorageShared({
87
+ ...options,
88
+ agentDir,
89
+ configValueResolver: resolveConfigValue,
90
+ });
131
91
  }
@@ -102,7 +102,7 @@ function primaryArg(name: string, args: Record<string, unknown> | undefined): st
102
102
  rest[key] = value;
103
103
  restCount++;
104
104
  }
105
- if (restCount === 0) return "";
105
+ if (restCount === 0) return "{}";
106
106
  try {
107
107
  return oneLine(JSON.stringify(rest));
108
108
  } catch {
@@ -1,7 +1,15 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { ImageContent, Message, MessageAttribution, ServiceTier, TextContent, Usage } from "@oh-my-pi/pi-ai";
4
- import { getBlobsDir, getProjectDir, getSessionsDir, isEnoent, logger, toError } from "@oh-my-pi/pi-utils";
4
+ import {
5
+ directoryExists,
6
+ getBlobsDir,
7
+ getProjectDir,
8
+ getSessionsDir,
9
+ isEnoent,
10
+ logger,
11
+ toError,
12
+ } from "@oh-my-pi/pi-utils";
5
13
  import { ArtifactManager } from "./artifacts";
6
14
  import { type BlobPutOptions, type BlobPutResult, BlobStore } from "./blob-store";
7
15
  import {
@@ -743,9 +751,12 @@ export class SessionManager {
743
751
 
744
752
  // Adopt the loaded session's working directory. Sessions live in a dir
745
753
  // keyed by their cwd, so resuming a session from another project must
746
- // re-point cwd/sessionDir at that project.
754
+ // re-point cwd/sessionDir at that project — unless that project directory
755
+ // no longer exists on disk, in which case adopting it (and the process
756
+ // chdir interactive mode then performs) would fail with ENOENT. Keep the
757
+ // current cwd so the resumed session stays where the user already is.
747
758
  const headerCwd = header.cwd ? path.resolve(header.cwd) : undefined;
748
- if (headerCwd && headerCwd !== path.resolve(this.#cwd)) {
759
+ if (headerCwd && headerCwd !== path.resolve(this.#cwd) && (await directoryExists(headerCwd))) {
749
760
  this.#cwd = headerCwd;
750
761
  this.#sessionDir = path.dirname(resolvedSessionFile);
751
762
  this.#rememberBreadcrumb(this.#cwd, resolvedSessionFile);
@@ -1562,8 +1573,19 @@ export class SessionManager {
1562
1573
  ): Promise<SessionManager> {
1563
1574
  const loaded = await loadEntriesFromFile(filePath, storage);
1564
1575
  const header = loaded.find(entry => entry.type === "session") as SessionHeader | undefined;
1565
- const cwd = header?.cwd ?? options?.initialCwd ?? getProjectDir();
1566
- const dir = sessionDir ?? path.dirname(path.resolve(filePath));
1576
+ // Resume into the session's recorded cwd only when that directory still
1577
+ // exists. A deleted project dir would make the constructor's #cwd — and the
1578
+ // `setProjectDir` chdir interactive mode runs next — point at (and fail on)
1579
+ // a missing path, so fall back to the launch cwd and anchor /new and /branch
1580
+ // there too, keeping the resumed session where the user already is.
1581
+ const recordedCwd = header?.cwd;
1582
+ const recordedCwdUsable = !!recordedCwd && (await directoryExists(recordedCwd));
1583
+ const cwd = recordedCwdUsable ? recordedCwd : (options?.initialCwd ?? getProjectDir());
1584
+ const dir =
1585
+ sessionDir ??
1586
+ (recordedCwd && !recordedCwdUsable
1587
+ ? SessionManager.getDefaultSessionDir(cwd, undefined, storage)
1588
+ : path.dirname(path.resolve(filePath)));
1567
1589
  const manager = new SessionManager(cwd, dir, true, storage);
1568
1590
  manager.#suppressBreadcrumb = options?.suppressBreadcrumb === true;
1569
1591
  await manager.setSessionFile(filePath);
@@ -1674,7 +1696,12 @@ export class SessionManager {
1674
1696
  (newestInTargetDir === null || (newestIsBreadcrumb && !currentProjectAlreadyHasSession));
1675
1697
  if (looksLikeMovedProject) {
1676
1698
  logger.info("Re-rooting moved session", { from: breadcrumbCwd, to: resolvedCwd });
1677
- const manager = await SessionManager.open(breadcrumb.sessionFile, undefined, storage);
1699
+ // Anchor at the gone breadcrumb cwd so the moveTo below relocates the
1700
+ // session: open() now falls back to the launch cwd for a missing
1701
+ // recorded cwd, which would no-op moveTo when it equals `cwd`.
1702
+ const manager = await SessionManager.open(breadcrumb.sessionFile, undefined, storage, {
1703
+ initialCwd: breadcrumbCwd,
1704
+ });
1678
1705
  await manager.moveTo(cwd, sessionDir);
1679
1706
  return manager;
1680
1707
  }
@@ -367,13 +367,17 @@ export function buildSystemPromptToolMetadata(
367
367
  export interface BuildSystemPromptOptions {
368
368
  /** Custom system prompt (replaces default). */
369
369
  customPrompt?: string;
370
+ /** Already-loaded custom system prompt text; bypasses path resolution. */
371
+ resolvedCustomPrompt?: string;
370
372
  /** Tools to include in prompt. */
371
373
  tools?: Map<string, SystemPromptToolMetadata>;
372
374
  /** Tool names to include in prompt. */
373
375
  toolNames?: string[];
374
376
  /** Text to append to system prompt. */
375
377
  appendSystemPrompt?: string;
376
- /** Inline full tool descriptors in the system prompt. Default: true */
378
+ /** Already-loaded append prompt text; bypasses path resolution. */
379
+ resolvedAppendSystemPrompt?: string;
380
+ /** Inline full tool descriptors in the system prompt. Default: false */
377
381
  inlineToolDescriptors?: boolean;
378
382
  /**
379
383
  * Whether provider-native tool calling is active (no owned/in-band syntax).
@@ -431,9 +435,11 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
431
435
 
432
436
  const {
433
437
  customPrompt,
438
+ resolvedCustomPrompt: providedResolvedCustomPrompt,
434
439
  tools,
435
440
  appendSystemPrompt,
436
441
  inlineToolDescriptors: providedInlineToolDescriptors,
442
+ resolvedAppendSystemPrompt: providedResolvedAppendPrompt,
437
443
  nativeTools = true,
438
444
  skillsSettings,
439
445
  toolNames: providedToolNames,
@@ -454,7 +460,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
454
460
  model,
455
461
  personality = "default",
456
462
  } = options;
457
- const inlineToolDescriptors = providedInlineToolDescriptors ?? true;
463
+ const inlineToolDescriptors = providedInlineToolDescriptors ?? false;
458
464
  const resolvedCwd = cwd ?? getProjectDir();
459
465
 
460
466
  const prepDefaults = {
@@ -500,9 +506,15 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
500
506
  return result.value;
501
507
  }
502
508
 
503
- const systemPromptCustomizationPromise = logger.time("loadSystemPromptFiles", loadSystemPromptFiles, {
504
- cwd: resolvedCwd,
505
- });
509
+ // Caller-supplied `customPrompt` / `resolvedCustomPrompt` owns block 0; the
510
+ // secondary capability-path `SYSTEM.md` walk-up MUST NOT silently augment it,
511
+ // because that would defeat CLI precedence over project/user `SYSTEM.md`.
512
+ const callerControlsCustomPrompt =
513
+ (typeof providedResolvedCustomPrompt === "string" && providedResolvedCustomPrompt.length > 0) ||
514
+ (typeof customPrompt === "string" && customPrompt.length > 0);
515
+ const systemPromptCustomizationPromise: Promise<string | null> = callerControlsCustomPrompt
516
+ ? Promise.resolve(null)
517
+ : logger.time("loadSystemPromptFiles", loadSystemPromptFiles, { cwd: resolvedCwd });
506
518
  const contextFilesPromise = providedContextFiles
507
519
  ? Promise.resolve(providedContextFiles)
508
520
  : logger.time("loadProjectContextFiles", loadProjectContextFiles, { cwd: resolvedCwd });
@@ -523,12 +535,16 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
523
535
  await Promise.all([
524
536
  withDeadline(
525
537
  "customPrompt",
526
- resolvePromptInput(customPrompt, "system prompt"),
538
+ providedResolvedCustomPrompt !== undefined
539
+ ? Promise.resolve(providedResolvedCustomPrompt)
540
+ : resolvePromptInput(customPrompt, "system prompt"),
527
541
  prepDefaults.resolvedCustomPrompt,
528
542
  ),
529
543
  withDeadline(
530
544
  "appendSystemPrompt",
531
- resolvePromptInput(appendSystemPrompt, "append system prompt"),
545
+ providedResolvedAppendPrompt !== undefined
546
+ ? Promise.resolve(providedResolvedAppendPrompt)
547
+ : resolvePromptInput(appendSystemPrompt, "append system prompt"),
532
548
  prepDefaults.resolvedAppendPrompt,
533
549
  ),
534
550
  withDeadline(
@@ -662,7 +678,11 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
662
678
  };
663
679
  const rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
664
680
  const systemPrompt = [rendered];
665
- const projectPrompt = resolvedCustomPrompt ? "" : prompt.render(projectPromptTemplate, data).trim();
681
+ // Custom prompt templates already render context files and append text; the
682
+ // project footer still carries environment, cwd, workspace, and dir-context.
683
+ const projectPrompt = prompt
684
+ .render(projectPromptTemplate, resolvedCustomPrompt ? { ...data, contextFiles: [], appendPrompt: "" } : data)
685
+ .trim();
666
686
  if (projectPrompt) {
667
687
  systemPrompt.push(projectPrompt);
668
688
  }
@@ -302,6 +302,16 @@ export interface ExecutorOptions {
302
302
  enableLsp?: boolean;
303
303
  signal?: AbortSignal;
304
304
  onProgress?: (progress: AgentProgress) => void;
305
+ /**
306
+ * Epochs (ms, `Date.now()`) bracketing the concurrency-semaphore wait:
307
+ * `invokedAt` is stamped at the spawn boundary before `acquire()`,
308
+ * `acquiredAt` immediately after. {@link runSubprocess} reports true queue
309
+ * wait (`acquiredAt - invokedAt`) and pre-run setup (`startTime - acquiredAt`)
310
+ * separately in the launch-timing debug log. Undefined for callers that
311
+ * bypass the semaphore path.
312
+ */
313
+ invokedAt?: number;
314
+ acquiredAt?: number;
305
315
  sessionFile?: string | null;
306
316
  persistArtifacts?: boolean;
307
317
  artifactsDir?: string;
@@ -1698,6 +1708,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1698
1708
  onProgress,
1699
1709
  } = options;
1700
1710
  const startTime = Date.now();
1711
+ // Set by the session's onFirstChatDispatch hook the first time the agent
1712
+ // loop dispatches a chat request to the provider — the launch-complete boundary.
1713
+ let firstChatDispatchAt: number | undefined;
1701
1714
 
1702
1715
  // Check if already aborted
1703
1716
  if (signal?.aborted) {
@@ -1868,6 +1881,14 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1868
1881
  abortSignal.removeEventListener("abort", onAbort);
1869
1882
  }
1870
1883
  };
1884
+ // Launch-latency phase marks (performance.now()); read by the debug log
1885
+ // emitted before this closure returns. Left undefined when setup throws
1886
+ // before reaching the phase, which itself localizes the cost.
1887
+ const perfStart = performance.now();
1888
+ let resolvedAt: number | undefined;
1889
+ let sessionOpenedAt: number | undefined;
1890
+ let sessionCreatedAt: number | undefined;
1891
+ let readyAt: number | undefined;
1871
1892
 
1872
1893
  try {
1873
1894
  checkAbort();
@@ -1935,6 +1956,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1935
1956
  const effectiveThinkingLevel = explicitThinkingLevel
1936
1957
  ? resolvedThinkingLevel
1937
1958
  : (thinkingLevel ?? resolvedThinkingLevel);
1959
+ resolvedAt = performance.now();
1938
1960
 
1939
1961
  const effectiveCwd = worktree ?? cwd;
1940
1962
  const sessionManager = sessionFile
@@ -1948,6 +1970,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1948
1970
  if (options.parentArtifactManager) {
1949
1971
  sessionManager.adoptArtifactManager(options.parentArtifactManager);
1950
1972
  }
1973
+ sessionOpenedAt = performance.now();
1951
1974
 
1952
1975
  const mcpProxyTools = options.mcpManager ? createMCPProxyTools(options.mcpManager) : [];
1953
1976
  const enableMCP = !options.mcpManager;
@@ -2043,6 +2066,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2043
2066
  localProtocolOptions: options.localProtocolOptions,
2044
2067
  telemetry: subagentTelemetry,
2045
2068
  parentEvalSessionId: options.parentEvalSessionId,
2069
+ onFirstChatDispatch: () => {
2070
+ firstChatDispatchAt ??= performance.now();
2071
+ },
2046
2072
  });
2047
2073
 
2048
2074
  const sessionPromise = createAgentSession(buildSubagentSessionOptions(sessionManager));
@@ -2056,6 +2082,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2056
2082
  void sessionPromise.then(created => created.session.dispose()).catch(() => {});
2057
2083
  throw err;
2058
2084
  }
2085
+ sessionCreatedAt = performance.now();
2059
2086
 
2060
2087
  monitor.setActiveSession(session);
2061
2088
  installRegistryStatusSync(session);
@@ -2201,6 +2228,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2201
2228
  }
2202
2229
  }
2203
2230
 
2231
+ readyAt = performance.now();
2204
2232
  const outcome = await driveSessionToYield(session, monitor, task);
2205
2233
  exitCode = outcome.exitCode;
2206
2234
  error = outcome.error;
@@ -2265,6 +2293,35 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2265
2293
  }
2266
2294
  }
2267
2295
 
2296
+ // Launch-latency breakdown (subagent invocation → first chat dispatch).
2297
+ // Phase deltas are performance.now() spans; the semaphore brackets use the
2298
+ // Date.now epochs captured by the spawn site (invokedAt before acquire,
2299
+ // acquiredAt after) so queue wait and pre-run setup are reported apart.
2300
+ const span = (from: number | undefined, to: number | undefined): number | undefined =>
2301
+ from !== undefined && to !== undefined ? Math.round(to - from) : undefined;
2302
+ const queueMs =
2303
+ options.invokedAt !== undefined && options.acquiredAt !== undefined
2304
+ ? Math.round(options.acquiredAt - options.invokedAt)
2305
+ : undefined;
2306
+ const preRunMs = options.acquiredAt !== undefined ? Math.round(startTime - options.acquiredAt) : undefined;
2307
+ const setupToFirstChatMs = span(perfStart, firstChatDispatchAt);
2308
+ const invokeToFirstChatMs =
2309
+ options.invokedAt !== undefined && setupToFirstChatMs !== undefined
2310
+ ? Math.round(startTime - options.invokedAt) + setupToFirstChatMs
2311
+ : undefined;
2312
+ logger.debug("subagent launch timing", {
2313
+ id,
2314
+ agent: agent.name,
2315
+ queueMs,
2316
+ preRunMs,
2317
+ resolveMs: span(perfStart, resolvedAt),
2318
+ sessionOpenMs: span(resolvedAt, sessionOpenedAt),
2319
+ createSessionMs: span(sessionOpenedAt, sessionCreatedAt),
2320
+ readyMs: span(sessionCreatedAt, readyAt),
2321
+ promptToFirstChatMs: span(readyAt, firstChatDispatchAt),
2322
+ setupToFirstChatMs,
2323
+ invokeToFirstChatMs,
2324
+ });
2268
2325
  return {
2269
2326
  exitCode,
2270
2327
  error,
package/src/task/index.ts CHANGED
@@ -798,6 +798,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
798
798
  const startedAt = Date.now();
799
799
  const semaphore = this.#getSpawnSemaphore();
800
800
  await semaphore.acquire();
801
+ const acquiredAt = Date.now();
801
802
  if (runSignal.aborted) {
802
803
  semaphore.release();
803
804
  progress.status = "aborted";
@@ -819,6 +820,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
819
820
  agentId,
820
821
  progress.index,
821
822
  true,
823
+ { invokedAt: startedAt, acquiredAt },
822
824
  );
823
825
  const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
824
826
  const singleResult = result.details?.results[0];
@@ -900,7 +902,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
900
902
  ): Promise<AgentToolResult<TaskToolDetails>> {
901
903
  const semaphore = this.#getSpawnSemaphore();
902
904
  if (spawnItems.length === 1) {
905
+ const invokedAt = Date.now();
903
906
  await semaphore.acquire();
907
+ const acquiredAt = Date.now();
904
908
  try {
905
909
  return await this.#executeSync(
906
910
  toolCallId,
@@ -909,6 +913,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
909
913
  onUpdate,
910
914
  undefined,
911
915
  0,
916
+ false,
917
+ { invokedAt, acquiredAt },
912
918
  );
913
919
  } finally {
914
920
  semaphore.release();
@@ -935,7 +941,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
935
941
  spawnItems,
936
942
  spawnItems.length,
937
943
  async (item, index, workerSignal) => {
944
+ const invokedAt = Date.now();
938
945
  await semaphore.acquire();
946
+ const acquiredAt = Date.now();
939
947
  try {
940
948
  const itemOnUpdate: AgentToolUpdateCallback<TaskToolDetails> | undefined = onUpdate
941
949
  ? update => {
@@ -953,6 +961,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
953
961
  itemOnUpdate,
954
962
  undefined,
955
963
  index,
964
+ false,
965
+ { invokedAt, acquiredAt },
956
966
  );
957
967
  } finally {
958
968
  semaphore.release();
@@ -1012,8 +1022,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1012
1022
  preAllocatedId?: string,
1013
1023
  spawnIndex = 0,
1014
1024
  detached = false,
1025
+ launchTiming?: { invokedAt: number; acquiredAt: number },
1015
1026
  ): Promise<AgentToolResult<TaskToolDetails>> {
1016
- return this.#runSpawn(toolCallId, params, signal, onUpdate, preAllocatedId, spawnIndex, detached);
1027
+ return this.#runSpawn(toolCallId, params, signal, onUpdate, preAllocatedId, spawnIndex, detached, launchTiming);
1017
1028
  }
1018
1029
 
1019
1030
  /** Spawn a fresh subagent and run it to completion. */
@@ -1025,6 +1036,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1025
1036
  preAllocatedId?: string,
1026
1037
  spawnIndex = 0,
1027
1038
  detached = false,
1039
+ launchTiming?: { invokedAt: number; acquiredAt: number },
1028
1040
  ): Promise<AgentToolResult<TaskToolDetails>> {
1029
1041
  const startTime = Date.now();
1030
1042
  const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
@@ -1265,6 +1277,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1265
1277
  detached,
1266
1278
  id: agentId,
1267
1279
  taskDepth,
1280
+ invokedAt: launchTiming?.invokedAt,
1281
+ acquiredAt: launchTiming?.acquiredAt,
1268
1282
  modelOverride,
1269
1283
  parentActiveModelPattern,
1270
1284
  thinkingLevel: thinkingLevelOverride,
@@ -45,7 +45,7 @@ const browserSchema = type({
45
45
  ),
46
46
  "dialogs?": type("'accept' | 'dismiss'").describe("auto-handle dialogs"),
47
47
  "code?": type("string").describe("js body to run in tab"),
48
- "timeout?": type("number").describe("timeout in seconds (default 30, max 300)"),
48
+ "timeout?": type("number").describe("timeout in seconds"),
49
49
  "all?": type("boolean").describe("close every tab"),
50
50
  "kill?": type("boolean").describe("also kill spawned-app browsers"),
51
51
  });
package/src/tools/eval.ts CHANGED
@@ -31,7 +31,7 @@ const evalCellSchema = type({
31
31
  language: type("'py' | 'js'").describe('runtime: "py" for the IPython kernel, "js" for the persistent JS VM'),
32
32
  code: type("string").describe("cell body, verbatim. Use top-level await freely."),
33
33
  "title?": type("string").describe('short label shown in transcript (e.g. "imports", "load config")'),
34
- "timeout?": type("number").describe("per-cell timeout in seconds (1-3600, default 30)"),
34
+ "timeout?": type("number").describe("per-cell timeout in seconds"),
35
35
  "reset?": type("boolean").describe(
36
36
  "wipe this cell's language kernel before running. Other languages are untouched.",
37
37
  ),
package/src/tools/find.ts CHANGED
@@ -44,13 +44,7 @@ const findSchema = type({
44
44
  .describe("globs including search paths"),
45
45
  "hidden?": type("boolean").describe("include hidden files"),
46
46
  "gitignore?": type("boolean").describe("respect gitignore"),
47
- "limit?": type("number").describe("max results (clamped to 1-200)"),
48
- "timeout?": type("number").describe("timeout in seconds (0.5–60)"),
49
- }).narrow((o, ctx) => {
50
- if (o.timeout !== undefined && (o.timeout < 0.5 || o.timeout > 60)) {
51
- return ctx.mustBe("a timeout between 0.5 and 60 seconds");
52
- }
53
- return true;
47
+ "limit?": type("number").describe("max results"),
54
48
  });
55
49
 
56
50
  export type FindToolInput = typeof findSchema.infer;
@@ -58,8 +52,6 @@ export type FindToolInput = typeof findSchema.infer;
58
52
  const DEFAULT_LIMIT = 200;
59
53
  const MAX_LIMIT = 200;
60
54
  const DEFAULT_GLOB_TIMEOUT_MS = 5000;
61
- const MIN_GLOB_TIMEOUT_MS = 500;
62
- const MAX_GLOB_TIMEOUT_MS = 60_000;
63
55
 
64
56
  export interface FindToolDetails {
65
57
  truncation?: TruncationResult;
@@ -132,10 +124,6 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
132
124
  caption: "Find directories matching a name (returns both files and dirs; directories are suffixed with `/`)",
133
125
  call: { paths: ["**/tests"] },
134
126
  },
135
- {
136
- caption: "Long-running search on a slow volume",
137
- call: { paths: ["/Volumes/Storage/**/*.py"], timeout: 30 },
138
- },
139
127
  ];
140
128
  readonly strict = true;
141
129
 
@@ -156,7 +144,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
156
144
  onUpdate?: AgentToolUpdateCallback<FindToolDetails>,
157
145
  _context?: AgentToolContext,
158
146
  ): Promise<AgentToolResult<FindToolDetails>> {
159
- const { paths, limit, hidden, gitignore, timeout } = params;
147
+ const { paths, limit, hidden, gitignore } = params;
160
148
 
161
149
  return untilAborted(signal, async () => {
162
150
  const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
@@ -237,8 +225,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
237
225
  const effectiveLimit = Math.min(MAX_LIMIT, Math.max(1, Math.floor(requestedLimit)));
238
226
  const includeHidden = hidden ?? true;
239
227
  const useGitignore = gitignore ?? true;
240
- const requestedTimeoutMs = timeout != null ? Math.round(timeout * 1000) : DEFAULT_GLOB_TIMEOUT_MS;
241
- const timeoutMs = Math.min(MAX_GLOB_TIMEOUT_MS, Math.max(MIN_GLOB_TIMEOUT_MS, requestedTimeoutMs));
228
+ const timeoutMs = DEFAULT_GLOB_TIMEOUT_MS;
242
229
  const timeoutSignal = AbortSignal.timeout(timeoutMs);
243
230
  const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
244
231
  const formatMatchPath = (matchPath: string, base: string, fileType?: natives.FileType): string => {
@@ -448,7 +435,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
448
435
  partial.sort((a, b) => b.m - a.m);
449
436
  const sortedPaths = partial.map(entry => entry.p);
450
437
  const seconds = timeoutMs % 1000 === 0 ? `${timeoutMs / 1000}` : (timeoutMs / 1000).toFixed(1);
451
- const notice = `find timed out after ${seconds}s; returning ${sortedPaths.length} partial matches — increase timeout or narrow pattern`;
438
+ const notice = `find timed out after ${seconds}s; returning ${sortedPaths.length} partial matches — narrow the pattern instead of retrying blindly`;
452
439
  return buildResult(sortedPaths, { notice, forceTruncated: true });
453
440
  }
454
441
 
@@ -7,7 +7,7 @@ const memoryEditSchema = type({
7
7
  op: type("'update' | 'forget' | 'invalidate'").describe("memory edit operation"),
8
8
  id: type("string").describe("memory id from recall output"),
9
9
  "content?": type("string").describe("replacement content for update"),
10
- "importance?": type("number").describe("replacement importance for update, clamped to [0, 1]"),
10
+ "importance?": type("number").describe("replacement importance for update (01)"),
11
11
  "replacement_id?": type("string").describe("replacement memory id for invalidate"),
12
12
  });
13
13
 
@@ -70,7 +70,7 @@ const searchSchema = type({
70
70
  .describe(
71
71
  'file, directory, glob, internal URL, or array of those to search; append `:<lines>` to scope a file to specific line ranges. Omitted or empty -> searches the workspace root (".")',
72
72
  ),
73
- "i?": type("boolean").describe("case-insensitive search"),
73
+ "case?": type("boolean").describe("case-sensitive search"),
74
74
  "gitignore?": type("boolean").describe("respect gitignore"),
75
75
  "skip?": type("number")
76
76
  .or("null")
@@ -680,7 +680,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
680
680
  _onUpdate?: AgentToolUpdateCallback<SearchToolDetails>,
681
681
  _toolContext?: AgentToolContext,
682
682
  ): Promise<AgentToolResult<SearchToolDetails>> {
683
- const { pattern, paths: rawPaths, i, gitignore, skip } = params;
683
+ const { pattern, paths: rawPaths, case: caseSensitive, gitignore, skip } = params;
684
684
 
685
685
  return untilAborted(signal, async () => {
686
686
  // Preserve the pattern verbatim — leading/trailing whitespace is
@@ -763,7 +763,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
763
763
  }
764
764
  const normalizedContextBefore = this.session.settings.get("search.contextBefore");
765
765
  const normalizedContextAfter = this.session.settings.get("search.contextAfter");
766
- const ignoreCase = i ?? false;
766
+ const ignoreCase = !(caseSensitive ?? true);
767
767
  const useGitignore = gitignore ?? true;
768
768
  const patternHasNewline = normalizedPattern.includes("\n") || normalizedPattern.includes("\\n");
769
769
  const effectiveMultiline = patternHasNewline;
@@ -1272,7 +1272,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
1272
1272
  interface SearchRenderArgs {
1273
1273
  pattern: string;
1274
1274
  paths?: string | string[];
1275
- i?: boolean;
1275
+ case?: boolean;
1276
1276
  gitignore?: boolean;
1277
1277
  skip?: number;
1278
1278
  }
@@ -1443,7 +1443,7 @@ export const searchToolRenderer = {
1443
1443
  const paths = toPathList(args.paths);
1444
1444
  const meta: string[] = [];
1445
1445
  if (paths.length) meta.push(`in ${paths.join(", ")}`);
1446
- if (args.i) meta.push("case:insensitive");
1446
+ if (args.case === false) meta.push("case:insensitive");
1447
1447
  if (args.gitignore === false) meta.push("gitignore:false");
1448
1448
  if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
1449
1449