@bytetrue/byspace 0.17.0 → 0.18.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.
Files changed (54) hide show
  1. package/node_modules/@bytetrue/client/package.json +3 -3
  2. package/node_modules/@bytetrue/highlight/package.json +1 -1
  3. package/node_modules/@bytetrue/protocol/package.json +1 -1
  4. package/node_modules/@bytetrue/relay/package.json +1 -1
  5. package/node_modules/@bytetrue/server/dist/server/server/agent/agent-manager.d.ts +30 -0
  6. package/node_modules/@bytetrue/server/dist/server/server/agent/agent-manager.js +118 -8
  7. package/node_modules/@bytetrue/server/dist/server/server/agent/mcp-shared.d.ts +1 -1
  8. package/node_modules/@bytetrue/server/dist/server/server/agent/{providers/claude/subagents → provider-subagents}/observation.d.ts +2 -2
  9. package/node_modules/@bytetrue/server/dist/server/server/agent/providers/claude/agent.js +1 -1
  10. package/node_modules/@bytetrue/server/dist/server/server/agent/providers/claude/subagents/live-source.d.ts +1 -1
  11. package/node_modules/@bytetrue/server/dist/server/server/agent/providers/claude/subagents/replay-source.d.ts +1 -1
  12. package/node_modules/@bytetrue/server/dist/server/server/agent/providers/claude/subagents/workflow-replay-source.d.ts +1 -1
  13. package/node_modules/@bytetrue/server/dist/server/server/agent/providers/omp/rpc-types.d.ts +6 -6
  14. package/node_modules/@bytetrue/server/dist/server/server/agent/providers/pi/agent-subagents.d.ts +45 -0
  15. package/node_modules/@bytetrue/server/dist/server/server/agent/providers/pi/agent-subagents.js +159 -0
  16. package/node_modules/@bytetrue/server/dist/server/server/agent/providers/pi/agent.d.ts +2 -2
  17. package/node_modules/@bytetrue/server/dist/server/server/agent/providers/pi/agent.js +86 -151
  18. package/node_modules/@bytetrue/server/dist/server/server/agent/providers/pi/runtime.d.ts +0 -2
  19. package/node_modules/@bytetrue/server/dist/server/server/agent/providers/pi/runtime.js +0 -4
  20. package/node_modules/@bytetrue/server/dist/server/server/agent/providers/pi/session-descriptor.d.ts +2 -0
  21. package/node_modules/@bytetrue/server/dist/server/server/agent/providers/pi/session-descriptor.js +2 -2
  22. package/node_modules/@bytetrue/server/dist/server/server/agent/providers/pi/subagent-session-tailer.d.ts +47 -0
  23. package/node_modules/@bytetrue/server/dist/server/server/agent/providers/pi/subagent-session-tailer.js +212 -0
  24. package/node_modules/@bytetrue/server/dist/server/server/bootstrap.d.ts +6 -0
  25. package/node_modules/@bytetrue/server/dist/server/server/bootstrap.js +49 -0
  26. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/{index-8d553cf4f8b1937dcd7c1da125243e39.js → index-5762beef0f3c7a285c88f6cb383d0f16.js} +485 -484
  27. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/index-5762beef0f3c7a285c88f6cb383d0f16.js.br +0 -0
  28. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/index-5762beef0f3c7a285c88f6cb383d0f16.js.gz +0 -0
  29. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/{indexeddb-attachment-store-9f3696181e42915ed4f9cb01f7595007.js → indexeddb-attachment-store-433f8ff1c621845eba95cc633f47b72d.js} +1 -1
  30. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-433f8ff1c621845eba95cc633f47b72d.js.br +0 -0
  31. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-433f8ff1c621845eba95cc633f47b72d.js.gz +0 -0
  32. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/{native-file-attachment-store-f829a425d0a064f4a9b4b153d185a3fb.js → native-file-attachment-store-6ed537a199e954caec287e6d28945868.js} +3 -3
  33. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-6ed537a199e954caec287e6d28945868.js.br +0 -0
  34. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-6ed537a199e954caec287e6d28945868.js.gz +0 -0
  35. package/node_modules/@bytetrue/server/dist/server/web-ui/index.html +1 -1
  36. package/node_modules/@bytetrue/server/dist/server/web-ui/index.html.br +0 -0
  37. package/node_modules/@bytetrue/server/dist/server/web-ui/index.html.gz +0 -0
  38. package/node_modules/@bytetrue/server/dist/src/server/agent/agent-manager.js +118 -8
  39. package/node_modules/@bytetrue/server/dist/src/server/agent/providers/claude/agent.js +1 -1
  40. package/node_modules/@bytetrue/server/dist/src/server/agent/providers/pi/agent-subagents.js +159 -0
  41. package/node_modules/@bytetrue/server/dist/src/server/agent/providers/pi/agent.js +86 -151
  42. package/node_modules/@bytetrue/server/dist/src/server/agent/providers/pi/runtime.js +0 -4
  43. package/node_modules/@bytetrue/server/dist/src/server/agent/providers/pi/session-descriptor.js +2 -2
  44. package/node_modules/@bytetrue/server/dist/src/server/agent/providers/pi/subagent-session-tailer.js +212 -0
  45. package/node_modules/@bytetrue/server/package.json +5 -5
  46. package/package.json +7 -7
  47. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/index-8d553cf4f8b1937dcd7c1da125243e39.js.br +0 -0
  48. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/index-8d553cf4f8b1937dcd7c1da125243e39.js.gz +0 -0
  49. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-9f3696181e42915ed4f9cb01f7595007.js.br +0 -0
  50. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-9f3696181e42915ed4f9cb01f7595007.js.gz +0 -0
  51. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-f829a425d0a064f4a9b4b153d185a3fb.js.br +0 -0
  52. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-f829a425d0a064f4a9b4b153d185a3fb.js.gz +0 -0
  53. /package/node_modules/@bytetrue/server/dist/server/server/agent/{providers/claude/subagents → provider-subagents}/observation.js +0 -0
  54. /package/node_modules/@bytetrue/server/dist/src/server/agent/{providers/claude/subagents → provider-subagents}/observation.js +0 -0
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { homedir, tmpdir } from "node:os";
4
- import { join, resolve as resolvePath } from "node:path";
4
+ import { join } from "node:path";
5
5
  import stripAnsi from "strip-ansi";
6
6
  import { z } from "zod";
7
7
  import { importSessionFromPersistence } from "../../provider-session-import.js";
@@ -19,6 +19,8 @@ import { revertPiConversation } from "./rewind.js";
19
19
  import { listPiImportableSessions, readPiImportSessionConfig } from "./session-descriptor.js";
20
20
  import { PiUsagePoller } from "./usage-poller.js";
21
21
  import { buildPiNestedToolCallMetadata, mapToolDetail, parseToolArgs, parseToolResult, resolveToolCallName, } from "./tool-call-mapper.js";
22
+ import { foldSubagentObservations } from "../../provider-subagents/observation.js";
23
+ import { isPiSubagentToolName, observePiSubagentExit, observePiSubagentLaunch, observePiSubagentRecord, replayPiSubagentObservations, } from "./agent-subagents.js";
22
24
  const PI_PROVIDER = "pi";
23
25
  const DEFAULT_PI_THINKING_LEVEL = "medium";
24
26
  const PI_MODEL_PROBE_CACHE_MS = 30000;
@@ -79,7 +81,7 @@ const PI_CAPABILITIES = {
79
81
  supportsSessionPersistence: true,
80
82
  supportsSessionListing: true,
81
83
  supportsDynamicModes: true,
82
- supportsMcpServers: false,
84
+ supportsMcpServers: true,
83
85
  supportsReasoningStream: true,
84
86
  supportsToolInvocations: true,
85
87
  supportsRewindConversation: true,
@@ -95,12 +97,6 @@ const PI_THINKING_OPTIONS = [
95
97
  { id: "xhigh", label: "XHigh", description: "Very deep reasoning" },
96
98
  { id: "max", label: "Max", description: "Extreme reasoning" },
97
99
  ];
98
- function capabilitiesForClient() {
99
- return withPiCapabilities(false);
100
- }
101
- function capabilitiesForSession(hasMcpConfig) {
102
- return withPiCapabilities(hasMcpConfig);
103
- }
104
100
  // COMPAT(piSteerFallback): added in v0.5.0, remove after 2027-03-01 once the pi floor
105
101
  // supports the steer RPC. Older binaries answer with "Unknown command: steer".
106
102
  function isPiDefinitiveSteerRejection(error) {
@@ -312,7 +308,6 @@ function buildResumeStartInput(input) {
312
308
  session: input.sessionFile,
313
309
  model: input.resumeConfig.model,
314
310
  thinkingOptionId: normalizePiThinkingOption(input.resumeConfig.thinkingOptionId) ?? undefined,
315
- mcpConfigPath: input.mcpConfig?.path,
316
311
  extensionPaths: input.byspaceExtension ? [input.byspaceExtension.path] : undefined,
317
312
  };
318
313
  }
@@ -327,72 +322,10 @@ function toPiMcpConfig(config) {
327
322
  return {
328
323
  url: config.url,
329
324
  ...(config.headers ? { headers: config.headers } : {}),
330
- auth: false,
331
- oauth: false,
332
- };
333
- }
334
- function resolvePiAgentDir(env) {
335
- const configured = env?.PI_CODING_AGENT_DIR?.trim() || process.env.PI_CODING_AGENT_DIR?.trim();
336
- if (!configured) {
337
- return join(homedir(), ".pi", "agent");
338
- }
339
- if (configured === "~") {
340
- return homedir();
341
- }
342
- if (configured.startsWith("~/")) {
343
- return resolvePath(homedir(), configured.slice(2));
344
- }
345
- return resolvePath(configured);
346
- }
347
- function readPiGlobalMcpConfig(env) {
348
- const globalConfigPath = join(resolvePiAgentDir(env), "mcp.json");
349
- if (!existsSync(globalConfigPath)) {
350
- return {};
351
- }
352
- let globalConfig;
353
- try {
354
- globalConfig = JSON.parse(readFileSync(globalConfigPath, "utf8"));
355
- }
356
- catch (error) {
357
- if (error instanceof SyntaxError) {
358
- throw new Error(`Failed to parse Pi MCP config: ${globalConfigPath}`, { cause: error });
359
- }
360
- throw error;
361
- }
362
- if (!isRecord(globalConfig)) {
363
- throw new Error(`Pi MCP config must contain a JSON object: ${globalConfigPath}`);
364
- }
365
- return globalConfig;
366
- }
367
- function createPiMcpConfigFile(servers, options) {
368
- const globalConfig = options?.piGlobalConfigEnv
369
- ? readPiGlobalMcpConfig(options.piGlobalConfigEnv)
370
- : {};
371
- let configuredServers = {};
372
- if (isRecord(globalConfig.mcpServers)) {
373
- configuredServers = globalConfig.mcpServers;
374
- }
375
- else if (isRecord(globalConfig["mcp-servers"])) {
376
- configuredServers = globalConfig["mcp-servers"];
377
- }
378
- const mcpServers = { ...configuredServers };
379
- for (const [name, serverConfig] of Object.entries(servers)) {
380
- mcpServers[name] = toPiMcpConfig(serverConfig);
381
- }
382
- const dir = mkdtempSync(join(tmpdir(), "byspace-pi-mcp-"));
383
- const filePath = join(dir, "mcp.json");
384
- const mergedConfig = { ...globalConfig, mcpServers };
385
- delete mergedConfig["mcp-servers"];
386
- writeFileSync(filePath, `${JSON.stringify(mergedConfig, null, 2)}\n`, {
387
- encoding: "utf8",
388
- mode: 0o600,
389
- });
390
- return {
391
- path: filePath,
392
- cleanup: () => rmSync(dir, { recursive: true, force: true }),
393
325
  };
394
326
  }
395
- function createPiBySpaceExtensionFile(systemPrompt) {
327
+ function createPiBySpaceExtensionFile(systemPrompt, mcpServers) {
328
+ const byspaceMcpServers = Object.fromEntries(Object.entries(mcpServers ?? {}).map(([name, config]) => [name, toPiMcpConfig(config)]));
396
329
  const dir = mkdtempSync(join(tmpdir(), "byspace-pi-extension-"));
397
330
  const filePath = join(dir, "byspace-integration.mjs");
398
331
  writeFileSync(filePath, `
@@ -444,6 +377,50 @@ function createPiBySpaceExtensionFile(systemPrompt) {
444
377
  }
445
378
 
446
379
  export default function byspaceIntegration(pi) {
380
+ const byspaceMcpServers = ${JSON.stringify(byspaceMcpServers)};
381
+ const rejectedMcpServers = [];
382
+ if (typeof pi.registerMcpServer === "function") {
383
+ for (const [name, config] of Object.entries(byspaceMcpServers)) {
384
+ // A duplicate name or an invalid config throws; the rest of this extension must still load.
385
+ try {
386
+ pi.registerMcpServer(name, config);
387
+ } catch (error) {
388
+ rejectedMcpServers.push({ name, message: error && error.message ? error.message : String(error) });
389
+ }
390
+ }
391
+ }
392
+
393
+ // Pi only connects registered MCP servers while its built-in MCP support is loaded. Another
394
+ // extension that owns the "mcp" command replaces it, leaving these registrations stranded.
395
+ function reportMcpInjection(ctx) {
396
+ for (const rejected of rejectedMcpServers) {
397
+ ctx.ui.notify(
398
+ 'BySpace could not register MCP server "' + rejected.name + '": ' + rejected.message,
399
+ "warning",
400
+ );
401
+ }
402
+ const registeredCount = Object.keys(byspaceMcpServers).length;
403
+ if (registeredCount === 0 || rejectedMcpServers.length === registeredCount) {
404
+ return;
405
+ }
406
+ if (typeof pi.getCommands !== "function") {
407
+ return;
408
+ }
409
+ const builtinMcpLoaded = pi.getCommands().some(
410
+ (command) =>
411
+ command &&
412
+ command.name === "mcp" &&
413
+ command.sourceInfo &&
414
+ command.sourceInfo.path === "builtin:mcp",
415
+ );
416
+ if (!builtinMcpLoaded) {
417
+ ctx.ui.notify(
418
+ "Pi's built-in MCP support is not loaded, so the MCP servers BySpace registered for this agent are not connected. Another extension may have replaced it.",
419
+ "warning",
420
+ );
421
+ }
422
+ }
423
+
447
424
  const submittedUserMessages = [];
448
425
 
449
426
  function emitSubmittedUserEntries(ctx) {
@@ -476,6 +453,7 @@ function createPiBySpaceExtensionFile(systemPrompt) {
476
453
 
477
454
  pi.on("session_start", async (_event, ctx) => {
478
455
  emitEntryCapture(ctx, "session_start");
456
+ reportMcpInjection(ctx);
479
457
  });
480
458
 
481
459
  pi.on("message_end", async (event) => {
@@ -525,32 +503,6 @@ function createPiBySpaceExtensionFile(systemPrompt) {
525
503
  cleanup: () => rmSync(dir, { recursive: true, force: true }),
526
504
  };
527
505
  }
528
- function combineCleanup(cleanups) {
529
- const activeCleanups = cleanups.filter((cleanup) => Boolean(cleanup));
530
- if (activeCleanups.length === 0) {
531
- return undefined;
532
- }
533
- return () => {
534
- for (const cleanup of activeCleanups) {
535
- cleanup();
536
- }
537
- };
538
- }
539
- function isPiMcpAdapterCommand(command) {
540
- if (command.source !== "extension" || !/^mcp(?::\d+)?$/.test(command.name)) {
541
- return false;
542
- }
543
- if (!command.sourceInfo) {
544
- return true;
545
- }
546
- return JSON.stringify(command.sourceInfo).includes("pi-mcp-adapter");
547
- }
548
- function withPiCapabilities(supportsMcpServers) {
549
- return {
550
- ...PI_CAPABILITIES,
551
- supportsMcpServers,
552
- };
553
- }
554
506
  function isPiRequestAbortError(error) {
555
507
  if (error instanceof Error && error.name === "AbortError") {
556
508
  return true;
@@ -1117,6 +1069,11 @@ export class PiRpcAgentSession {
1117
1069
  async *streamHistory() {
1118
1070
  await this.requestEntryCapture("history");
1119
1071
  yield* streamPiHistory(this.provider, await this.runtimeSession.getMessages(), this.capturedUserEntries);
1072
+ // Background subagent lifecycle lives in the transcript as tool result details and
1073
+ // subagent-exit custom messages, not in a sidechain — derive it from the same messages.
1074
+ for (const subagentEvent of foldSubagentObservations(replayPiSubagentObservations(await this.runtimeSession.getMessages()))) {
1075
+ yield { type: "provider_subagent", provider: this.provider, event: subagentEvent };
1076
+ }
1120
1077
  }
1121
1078
  async getRuntimeInfo() {
1122
1079
  await this.refreshState();
@@ -1904,6 +1861,23 @@ export class PiRpcAgentSession {
1904
1861
  this.emitToolCallEvent(event.toolCallId, toolCall, status, result, error, {
1905
1862
  parentToolCallId: event.parentToolCallId,
1906
1863
  });
1864
+ this.emitPiSubagentToolObservations(event.toolCallId, toolCall, result);
1865
+ }
1866
+ /** Background subagents announce themselves through tool result details, not the stream. */
1867
+ emitPiSubagentToolObservations(toolCallId, toolCall, result) {
1868
+ if (!isPiSubagentToolName(toolCall.toolName)) {
1869
+ return;
1870
+ }
1871
+ const details = typeof result === "object" && result !== null ? result.details : undefined;
1872
+ const observation = toolCall.toolName === "subagent"
1873
+ ? observePiSubagentLaunch({ toolCallId, args: toolCall.args, details })
1874
+ : observePiSubagentRecord(details);
1875
+ if (!observation) {
1876
+ return;
1877
+ }
1878
+ for (const event of foldSubagentObservations([observation])) {
1879
+ this.emit({ type: "provider_subagent", provider: this.provider, event });
1880
+ }
1907
1881
  }
1908
1882
  emitCompactionTimeline(input) {
1909
1883
  const emitOutOfBand = this.outOfBandCompactionEmit;
@@ -1977,6 +1951,11 @@ export class PiRpcAgentSession {
1977
1951
  // Pi extension sendMessage() payloads surface as custom messages. display=false
1978
1952
  // means context-only (Pi's TUI hides them too). display is optional on older
1979
1953
  // runtimes; treat missing as true so exit notifications still surface.
1954
+ if (event.message.customType === "subagent-exit") {
1955
+ for (const subagentEvent of foldSubagentObservations(observePiSubagentExit(event.message.details))) {
1956
+ this.emit({ type: "provider_subagent", provider: this.provider, event: subagentEvent });
1957
+ }
1958
+ }
1980
1959
  if (event.message.display !== false) {
1981
1960
  const text = getUserMessageText(event.message.content);
1982
1961
  const details = limitTimelineDetails(event.message.details);
@@ -2077,7 +2056,7 @@ export class PiRpcAgentClient {
2077
2056
  constructor(options) {
2078
2057
  this.availableProvidersCache = null;
2079
2058
  this.provider = PI_PROVIDER;
2080
- this.capabilities = capabilitiesForClient();
2059
+ this.capabilities = PI_CAPABILITIES;
2081
2060
  this.logger = options.logger;
2082
2061
  this.runtimeSettings = options.runtimeSettings;
2083
2062
  this.providerParams = PiProviderParamsSchema.parse(options.providerParams ?? {});
@@ -2087,12 +2066,7 @@ export class PiRpcAgentClient {
2087
2066
  this.usagePollScheduler = options.usagePollScheduler;
2088
2067
  }
2089
2068
  async createSession(config, launchContext) {
2090
- const mcpEnv = {
2091
- ...this.runtimeSettings?.env,
2092
- ...launchContext?.env,
2093
- };
2094
- const mcpConfig = await this.prepareMcpConfig(config.cwd, config.mcpServers, mcpEnv);
2095
- const byspaceExtension = createPiBySpaceExtensionFile(composeSystemPromptParts(config.systemPrompt, config.daemonAppendSystemPrompt));
2069
+ const byspaceExtension = createPiBySpaceExtensionFile(composeSystemPromptParts(config.systemPrompt, config.daemonAppendSystemPrompt), config.mcpServers);
2096
2070
  const launchModel = await this.resolveLaunchModel(config.cwd, launchContext?.env, config.model, {
2097
2071
  skipProbe: config.internal === true,
2098
2072
  });
@@ -2104,12 +2078,10 @@ export class PiRpcAgentClient {
2104
2078
  thinkingOptionId: normalizePiThinkingOption(config.thinkingOptionId) ?? DEFAULT_PI_THINKING_LEVEL,
2105
2079
  noSession: config.internal === true,
2106
2080
  env: launchContext?.env,
2107
- mcpConfigPath: mcpConfig?.path,
2108
2081
  extensionPaths: byspaceExtension ? [byspaceExtension.path] : undefined,
2109
2082
  });
2110
2083
  }
2111
2084
  catch (error) {
2112
- mcpConfig?.cleanup();
2113
2085
  byspaceExtension?.cleanup();
2114
2086
  throw error;
2115
2087
  }
@@ -2118,8 +2090,8 @@ export class PiRpcAgentClient {
2118
2090
  runtimeSession,
2119
2091
  config: launchModel === config.model ? config : { ...config, model: launchModel },
2120
2092
  initialState: await runtimeSession.getState(),
2121
- capabilities: capabilitiesForSession(mcpConfig !== null),
2122
- cleanup: combineCleanup([mcpConfig?.cleanup, byspaceExtension?.cleanup]),
2093
+ capabilities: PI_CAPABILITIES,
2094
+ cleanup: byspaceExtension?.cleanup,
2123
2095
  extensionTimeoutMs: this.providerParams.extensionTimeoutMs,
2124
2096
  logger: this.logger,
2125
2097
  usagePollScheduler: this.usagePollScheduler,
@@ -2127,7 +2099,6 @@ export class PiRpcAgentClient {
2127
2099
  }
2128
2100
  catch (error) {
2129
2101
  await runtimeSession.close().catch(() => undefined);
2130
- mcpConfig?.cleanup();
2131
2102
  byspaceExtension?.cleanup();
2132
2103
  throw error;
2133
2104
  }
@@ -2139,12 +2110,7 @@ export class PiRpcAgentClient {
2139
2110
  }
2140
2111
  const persistenceMetadata = parsePersistenceMetadata(handle.metadata);
2141
2112
  const resumeConfig = buildResumeConfig(persistenceMetadata, overrides, this.provider);
2142
- const mcpEnv = {
2143
- ...this.runtimeSettings?.env,
2144
- ...launchContext?.env,
2145
- };
2146
- const mcpConfig = await this.prepareMcpConfig(resumeConfig.cwd, resumeConfig.config.mcpServers, mcpEnv);
2147
- const byspaceExtension = createPiBySpaceExtensionFile(composeSystemPromptParts(resumeConfig.config.systemPrompt, resumeConfig.config.daemonAppendSystemPrompt));
2113
+ const byspaceExtension = createPiBySpaceExtensionFile(composeSystemPromptParts(resumeConfig.config.systemPrompt, resumeConfig.config.daemonAppendSystemPrompt), resumeConfig.config.mcpServers);
2148
2114
  const launchModel = await this.resolveLaunchModel(resumeConfig.cwd, launchContext?.env, resumeConfig.model);
2149
2115
  const resumedConfig = launchModel === resumeConfig.model
2150
2116
  ? resumeConfig
@@ -2159,12 +2125,10 @@ export class PiRpcAgentClient {
2159
2125
  resumeConfig: resumedConfig,
2160
2126
  sessionFile,
2161
2127
  launchContext,
2162
- mcpConfig,
2163
2128
  byspaceExtension,
2164
2129
  }));
2165
2130
  }
2166
2131
  catch (error) {
2167
- mcpConfig?.cleanup();
2168
2132
  byspaceExtension?.cleanup();
2169
2133
  throw error;
2170
2134
  }
@@ -2173,8 +2137,8 @@ export class PiRpcAgentClient {
2173
2137
  runtimeSession,
2174
2138
  config: resumedConfig.config,
2175
2139
  initialState: await runtimeSession.getState(),
2176
- capabilities: capabilitiesForSession(mcpConfig !== null),
2177
- cleanup: combineCleanup([mcpConfig?.cleanup, byspaceExtension?.cleanup]),
2140
+ capabilities: PI_CAPABILITIES,
2141
+ cleanup: byspaceExtension?.cleanup,
2178
2142
  extensionTimeoutMs: this.providerParams.extensionTimeoutMs,
2179
2143
  logger: this.logger,
2180
2144
  usagePollScheduler: this.usagePollScheduler,
@@ -2182,7 +2146,6 @@ export class PiRpcAgentClient {
2182
2146
  }
2183
2147
  catch (error) {
2184
2148
  await runtimeSession.close().catch(() => undefined);
2185
- mcpConfig?.cleanup();
2186
2149
  byspaceExtension?.cleanup();
2187
2150
  throw error;
2188
2151
  }
@@ -2335,34 +2298,6 @@ export class PiRpcAgentClient {
2335
2298
  await probeSession.close().catch(() => undefined);
2336
2299
  }
2337
2300
  }
2338
- async prepareMcpConfig(cwd, servers, env) {
2339
- if (!servers || Object.keys(servers).length === 0) {
2340
- return null;
2341
- }
2342
- if (!(await this.detectMcpAdapter(cwd, env))) {
2343
- return null;
2344
- }
2345
- return createPiMcpConfigFile(servers, { piGlobalConfigEnv: env });
2346
- }
2347
- async detectMcpAdapter(cwd, env) {
2348
- const runtimeSession = await this.runtime.startSession({ cwd, env }).catch((error) => {
2349
- this.logger.debug({ err: error, cwd }, "Pi MCP adapter probe failed to start");
2350
- return null;
2351
- });
2352
- if (!runtimeSession) {
2353
- return false;
2354
- }
2355
- try {
2356
- return (await runtimeSession.getCommands()).some(isPiMcpAdapterCommand);
2357
- }
2358
- catch (error) {
2359
- this.logger.debug({ err: error, cwd }, "Pi MCP adapter probe failed");
2360
- return false;
2361
- }
2362
- finally {
2363
- await runtimeSession.close().catch(() => undefined);
2364
- }
2365
- }
2366
2301
  async resolvePiLaunch() {
2367
2302
  return resolveProviderLaunch({
2368
2303
  commandConfig: this.runtimeSettings?.command,
@@ -20,7 +20,6 @@ export function buildPiLaunch(input) {
20
20
  modeId: input.session.modeId,
21
21
  session: input.session.session,
22
22
  noSession: input.session.noSession,
23
- mcpConfigPath: input.session.mcpConfigPath,
24
23
  extensionPaths: input.session.extensionPaths,
25
24
  extraArgs: input.session.extraArgs,
26
25
  };
@@ -44,9 +43,6 @@ function appendPiLaunchArgs(argv, session, protocolMode) {
44
43
  else if (session.session) {
45
44
  argv.push("--session", session.session);
46
45
  }
47
- if (session.mcpConfigPath) {
48
- argv.push("--mcp-config", session.mcpConfigPath);
49
- }
50
46
  for (const extensionPath of session.extensionPaths ?? []) {
51
47
  argv.push("--extension", extensionPath);
52
48
  }
@@ -41,7 +41,7 @@ export async function readPiImportSessionConfig(filePath) {
41
41
  return {};
42
42
  return toPiImportSessionConfig(descriptor);
43
43
  }
44
- async function resolvePiSessionsDir(options) {
44
+ export async function resolvePiSessionsDir(options) {
45
45
  const env = options.env ?? process.env;
46
46
  const homeDir = options.homeDir ?? homedir();
47
47
  const baseDir = options.cwd ?? process.cwd();
@@ -100,7 +100,7 @@ function resolveConfigPath(value, options) {
100
100
  }
101
101
  return path.isAbsolute(value) ? value : path.resolve(options.baseDir, value);
102
102
  }
103
- async function walkJsonlFiles(root) {
103
+ export async function walkJsonlFiles(root) {
104
104
  let entries;
105
105
  try {
106
106
  entries = await readdir(root, { withFileTypes: true });
@@ -0,0 +1,212 @@
1
+ import { open, stat } from "node:fs/promises";
2
+ import { mapPiSubagentStatus } from "./agent-subagents.js";
3
+ import { PiHistoryMapper } from "./history-mapper.js";
4
+ import { resolvePiSessionsDir, walkJsonlFiles } from "./session-descriptor.js";
5
+ export const PI_SUBAGENT_TERMINAL_STATUSES = ["completed", "failed", "canceled"];
6
+ const DEFAULT_POLL_MS = 1000;
7
+ const LOCATE_RETRY_MS = 5000;
8
+ const TERMINAL_DRAIN_GRACE_MS = 3000;
9
+ /**
10
+ * Extract the tailer-relevant fields from a pi-subagent report record. The subagent id is
11
+ * required (it keys the tailer); session references start the tail, status tracks the end.
12
+ * Status-only snapshots matter too: they flip the entry to terminal so the tail stops.
13
+ */
14
+ export function parsePiSubagentTailerSnapshot(record) {
15
+ if (typeof record !== "object" || record === null)
16
+ return null;
17
+ const source = record;
18
+ const subagentId = readString(source.id);
19
+ if (!subagentId)
20
+ return null;
21
+ const snapshot = { subagentId };
22
+ for (const key of ["sessionId", "sessionFile", "cwd", "status"]) {
23
+ const value = source[key];
24
+ if (typeof value === "string" && value.trim())
25
+ snapshot[key] = value;
26
+ }
27
+ return snapshot.sessionId || snapshot.sessionFile || snapshot.cwd || snapshot.status
28
+ ? snapshot
29
+ : null;
30
+ }
31
+ function readString(value) {
32
+ return typeof value === "string" && value.trim() ? value : null;
33
+ }
34
+ async function drainPiSessionTailer(tailer, onTimeline) {
35
+ const info = await stat(tailer.file).catch(() => null);
36
+ if (!info?.isFile())
37
+ return;
38
+ if (info.size < tailer.offset) {
39
+ // File shrank (recreated): restart from the beginning with fresh mapper state.
40
+ tailer.offset = 0;
41
+ tailer.mapper = new PiHistoryMapper("pi");
42
+ }
43
+ if (info.size === tailer.offset)
44
+ return;
45
+ const handle = await open(tailer.file, "r");
46
+ try {
47
+ const length = info.size - tailer.offset;
48
+ const buffer = Buffer.alloc(length);
49
+ // A short read means the file was truncated between stat and read; parse only
50
+ // the bytes that actually arrived.
51
+ const { bytesRead } = await handle.read(buffer, 0, length, tailer.offset);
52
+ const text = buffer.toString("utf8", 0, bytesRead);
53
+ const lastNewline = text.lastIndexOf("\n");
54
+ if (lastNewline === -1)
55
+ return;
56
+ const chunk = text.slice(0, lastNewline + 1);
57
+ tailer.offset += Buffer.byteLength(chunk);
58
+ for (const line of chunk.split("\n")) {
59
+ const trimmed = line.trim();
60
+ if (!trimmed)
61
+ continue;
62
+ let entry;
63
+ try {
64
+ entry = JSON.parse(trimmed);
65
+ }
66
+ catch {
67
+ continue;
68
+ }
69
+ if (typeof entry !== "object" || entry === null)
70
+ continue;
71
+ const message = entry.message;
72
+ if (typeof message !== "object" || message === null)
73
+ continue;
74
+ const timestamp = readString(entry.timestamp) ?? undefined;
75
+ const events = tailer.mapper.mapMessages([message]);
76
+ for (const event of events) {
77
+ if (event.type === "timeline") {
78
+ onTimeline(event.item, timestamp);
79
+ }
80
+ }
81
+ }
82
+ }
83
+ finally {
84
+ await handle.close();
85
+ }
86
+ }
87
+ export async function locatePiSubagentSessionFile(sessionId, options = {}) {
88
+ const sessionsDir = await resolvePiSessionsDir({
89
+ cwd: options.cwd ?? undefined,
90
+ env: options.env ?? process.env,
91
+ homeDir: options.homeDir,
92
+ });
93
+ const suffix = `_${sessionId}.jsonl`;
94
+ const files = await walkJsonlFiles(sessionsDir);
95
+ return files.find((file) => file.endsWith(suffix)) ?? null;
96
+ }
97
+ export class PiSubagentSessionTailers {
98
+ constructor(options) {
99
+ this.entries = new Map();
100
+ this.timer = null;
101
+ this.queue = Promise.resolve();
102
+ this.options = options;
103
+ }
104
+ start() {
105
+ if (this.timer)
106
+ return;
107
+ this.timer = setInterval(() => {
108
+ void this.tick();
109
+ }, DEFAULT_POLL_MS);
110
+ this.timer.unref?.();
111
+ }
112
+ stop() {
113
+ if (this.timer) {
114
+ clearInterval(this.timer);
115
+ this.timer = null;
116
+ }
117
+ }
118
+ update(agentId, snapshot) {
119
+ const subagentId = snapshot.subagentId;
120
+ const key = `${agentId}\u0000${subagentId}`;
121
+ let entry = this.entries.get(key);
122
+ if (!entry) {
123
+ entry = {
124
+ agentId,
125
+ subagentId,
126
+ sessionId: null,
127
+ sessionFile: null,
128
+ cwd: null,
129
+ tailer: null,
130
+ lastLocateAttempt: 0,
131
+ terminalAt: null,
132
+ };
133
+ this.entries.set(key, entry);
134
+ this.start();
135
+ }
136
+ if (snapshot.sessionId)
137
+ entry.sessionId = snapshot.sessionId;
138
+ if (snapshot.sessionFile)
139
+ entry.sessionFile = snapshot.sessionFile;
140
+ if (snapshot.cwd)
141
+ entry.cwd = snapshot.cwd;
142
+ const status = mapPiSubagentStatus(snapshot.status ?? "");
143
+ if (status &&
144
+ PI_SUBAGENT_TERMINAL_STATUSES.includes(status) &&
145
+ entry.terminalAt === null) {
146
+ entry.terminalAt = Date.now();
147
+ }
148
+ // No reopen-on-running: reports have no ordering guarantee, so a stale running
149
+ // snapshot must not resurrect a finished subagent's tail. New launches are new ids.
150
+ }
151
+ removeAgent(agentId) {
152
+ for (const [key, entry] of this.entries) {
153
+ if (entry.agentId === agentId)
154
+ this.entries.delete(key);
155
+ }
156
+ }
157
+ dispose() {
158
+ this.stop();
159
+ this.entries.clear();
160
+ }
161
+ /**
162
+ * Drain every entry once. Timer and manual (test) ticks serialize through a promise
163
+ * chain instead of skipping, so an awaited tick never no-ops behind an in-flight one.
164
+ */
165
+ tick() {
166
+ const run = this.queue.then(() => this.drain());
167
+ this.queue = run.catch(() => { });
168
+ return run;
169
+ }
170
+ async drain() {
171
+ const now = Date.now();
172
+ for (const [key, entry] of this.entries) {
173
+ if (!entry.tailer && now - entry.lastLocateAttempt >= LOCATE_RETRY_MS) {
174
+ entry.lastLocateAttempt = now;
175
+ entry.tailer = await this.locate(entry);
176
+ }
177
+ if (entry.tailer) {
178
+ try {
179
+ await drainPiSessionTailer(entry.tailer, (item, timestamp) => {
180
+ this.options.emit(entry.agentId, {
181
+ kind: "timeline",
182
+ id: entry.subagentId,
183
+ item,
184
+ ...(timestamp ? { timestamp } : {}),
185
+ });
186
+ });
187
+ }
188
+ catch {
189
+ // Transient read failures (rotation, permissions): retry next tick.
190
+ }
191
+ }
192
+ if (entry.terminalAt !== null &&
193
+ now >= entry.terminalAt + (this.options.terminalGraceMs ?? TERMINAL_DRAIN_GRACE_MS)) {
194
+ this.entries.delete(key);
195
+ }
196
+ }
197
+ }
198
+ async locate(entry) {
199
+ const file = entry.sessionFile ??
200
+ (entry.sessionId
201
+ ? await locatePiSubagentSessionFile(entry.sessionId, {
202
+ cwd: entry.cwd,
203
+ env: this.options.env,
204
+ homeDir: this.options.homeDir,
205
+ })
206
+ : null);
207
+ if (!file)
208
+ return null;
209
+ return { file, offset: 0, mapper: new PiHistoryMapper("pi") };
210
+ }
211
+ }
212
+ //# sourceMappingURL=subagent-session-tailer.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytetrue/server",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "BySpace backend server",
5
5
  "files": [
6
6
  "dist/server",
@@ -77,10 +77,10 @@
77
77
  "test:e2e:ui": "vitest --ui e2e.test.ts"
78
78
  },
79
79
  "dependencies": {
80
- "@bytetrue/client": "0.17.0",
81
- "@bytetrue/highlight": "0.17.0",
82
- "@bytetrue/protocol": "0.17.0",
83
- "@bytetrue/relay": "0.17.0"
80
+ "@bytetrue/client": "0.18.0",
81
+ "@bytetrue/highlight": "0.18.0",
82
+ "@bytetrue/protocol": "0.18.0",
83
+ "@bytetrue/relay": "0.18.0"
84
84
  },
85
85
  "devDependencies": {
86
86
  "@playwright/test": "^1.56.1",