pi-subagents 0.65.1 → 0.67.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 (148) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/researcher.md +23 -13
  5. package/agents/reviewer.md +3 -2
  6. package/docs/agents.md +20 -3
  7. package/docs/configuration.md +25 -5
  8. package/docs/extension-api.md +124 -18
  9. package/docs/missions.md +8 -0
  10. package/docs/models.md +59 -2
  11. package/docs/observability.md +46 -6
  12. package/docs/standalone-background.md +49 -0
  13. package/docs/tool-reference.md +20 -10
  14. package/docs/watchdog.md +35 -4
  15. package/docs/workflows.md +40 -19
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +2 -1
  18. package/prompts/parallel-review.md +1 -1
  19. package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
  20. package/skills/pi-subagents/SKILL.md +14 -0
  21. package/skills/pi-subagents/references/execution-controls.md +20 -5
  22. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  23. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  24. package/src/agents/advertised-agent-prompt.ts +94 -0
  25. package/src/agents/agent-management.ts +14 -1
  26. package/src/agents/agent-serializer.ts +2 -0
  27. package/src/agents/agents.ts +14 -0
  28. package/src/agents/builtin-names.ts +1 -0
  29. package/src/api/delegation.ts +4 -0
  30. package/src/api/preflight.ts +76 -45
  31. package/src/api/shared-types.ts +3 -1
  32. package/src/api/workflow-resources.ts +6 -0
  33. package/src/extension/fanout-child.ts +63 -4
  34. package/src/extension/index.ts +58 -8
  35. package/src/extension/public-execution.ts +4 -3
  36. package/src/extension/rpc.ts +8 -21
  37. package/src/extension/schemas.ts +71 -80
  38. package/src/extension/tool-description.ts +29 -81
  39. package/src/inspectors/actions.ts +148 -0
  40. package/src/inspectors/ghostty/actions.ts +74 -0
  41. package/src/inspectors/ghostty/plugin.ts +17 -0
  42. package/src/inspectors/herdr/actions.ts +99 -179
  43. package/src/inspectors/herdr/plugin.ts +20 -0
  44. package/src/inspectors/herdr/project-panes.ts +1 -1
  45. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  46. package/src/inspectors/plugins.ts +8 -0
  47. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  48. package/src/inspectors/types.ts +51 -0
  49. package/src/intercom/intercom-bridge.ts +50 -8
  50. package/src/intercom/native-supervisor-channel.ts +104 -67
  51. package/src/runs/background/active-async-capacity.ts +22 -18
  52. package/src/runs/background/async-execution.ts +45 -56
  53. package/src/runs/background/async-job-tracker.ts +35 -3
  54. package/src/runs/background/async-resume.ts +5 -9
  55. package/src/runs/background/async-status-snapshot.ts +10 -12
  56. package/src/runs/background/async-status.ts +17 -9
  57. package/src/runs/background/auto-drain.ts +44 -30
  58. package/src/runs/background/binary-bootstrap.ts +33 -0
  59. package/src/runs/background/chain-root-attachment.ts +8 -0
  60. package/src/runs/background/control-channel.ts +78 -44
  61. package/src/runs/background/fleet-view.ts +30 -2
  62. package/src/runs/background/notify.ts +117 -13
  63. package/src/runs/background/owned-process-tree.ts +35 -8
  64. package/src/runs/background/process-terminal.ts +23 -23
  65. package/src/runs/background/run-child-session.ts +121 -36
  66. package/src/runs/background/run-status.ts +78 -5
  67. package/src/runs/background/runner-aliases.ts +28 -9
  68. package/src/runs/background/runner-child-launch.ts +88 -0
  69. package/src/runs/background/runner-child-sessions.ts +5 -4
  70. package/src/runs/background/scheduled-runs.ts +40 -13
  71. package/src/runs/background/stale-run-reconciler.ts +3 -1
  72. package/src/runs/background/steering.ts +20 -2
  73. package/src/runs/background/subagent-runner.ts +458 -239
  74. package/src/runs/background/subagent-wait.ts +54 -8
  75. package/src/runs/background/wait-completions.ts +4 -0
  76. package/src/runs/background/wait-tool.ts +1 -1
  77. package/src/runs/foreground/async-steering-action.ts +37 -7
  78. package/src/runs/foreground/execution.ts +145 -56
  79. package/src/runs/foreground/prompt-audit.ts +3 -1
  80. package/src/runs/foreground/subagent-executor.ts +584 -297
  81. package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
  82. package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
  83. package/src/runs/shared/acceptance.ts +7 -4
  84. package/src/runs/shared/agent-contract.ts +1 -1
  85. package/src/runs/shared/async-status-projection.ts +51 -47
  86. package/src/runs/shared/capability-ceiling.ts +2 -0
  87. package/src/runs/shared/child-hooks.ts +167 -3
  88. package/src/runs/shared/child-launch.ts +28 -13
  89. package/src/runs/shared/child-lifecycle.ts +6 -3
  90. package/src/runs/shared/child-runtime-config.ts +3 -1
  91. package/src/runs/shared/child-session.ts +75 -8
  92. package/src/runs/shared/child-tool-plan.ts +124 -5
  93. package/src/runs/shared/completion-evidence.ts +2 -2
  94. package/src/runs/shared/completion-guard.ts +6 -3
  95. package/src/runs/shared/effective-system-prompt.ts +33 -0
  96. package/src/runs/shared/external-cli-runner.ts +9 -7
  97. package/src/runs/shared/host-step-status.ts +11 -11
  98. package/src/runs/shared/llm-intent-arbiter.ts +21 -11
  99. package/src/runs/shared/model-fallback.ts +12 -6
  100. package/src/runs/shared/nested-events.ts +5 -5
  101. package/src/runs/shared/orca-progress-tabs.ts +7 -1
  102. package/src/runs/shared/parallel-handoff.ts +57 -12
  103. package/src/runs/shared/parallel-utils.ts +2 -2
  104. package/src/runs/shared/pi-spawn.ts +10 -0
  105. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  106. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  107. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  108. package/src/runs/shared/run-fanout-budget.ts +8 -8
  109. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  110. package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
  111. package/src/runs/shared/task-intent.ts +46 -13
  112. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  113. package/src/runs/shared/worktree-setup-command.ts +190 -0
  114. package/src/runs/shared/worktree.ts +366 -208
  115. package/src/shared/fork-context.ts +15 -72
  116. package/src/shared/launch-contract.ts +65 -2
  117. package/src/shared/opencode-session-headers.ts +30 -0
  118. package/src/shared/types.ts +85 -61
  119. package/src/shared/utils.ts +7 -2
  120. package/src/shared/workflow-child-permit.ts +18 -13
  121. package/src/slash/delegation-adapters.ts +3 -1
  122. package/src/slash/delegation-request.ts +14 -0
  123. package/src/slash/slash-commands.ts +2 -1
  124. package/src/slash/subagents-admin.ts +11 -4
  125. package/src/tui/fleet-status.ts +164 -19
  126. package/src/tui/fleet.ts +27 -19
  127. package/src/tui/render.ts +172 -33
  128. package/src/watchdog/child-status.ts +8 -0
  129. package/src/watchdog/model-selection.ts +20 -0
  130. package/src/watchdog/permission-arbiter.ts +3 -1
  131. package/src/watchdog/register-child.ts +1 -0
  132. package/src/watchdog/register-main.ts +31 -27
  133. package/src/watchdog/review.ts +132 -67
  134. package/src/watchdog/runtime.ts +82 -20
  135. package/src/watchdog/scope.ts +1 -1
  136. package/src/watchdog/settings.ts +9 -3
  137. package/src/watchdog/tool-actions.ts +13 -12
  138. package/src/watchdog/turn-delta.ts +23 -0
  139. package/src/watchdog/types.ts +4 -0
  140. package/src/workflows/chat-progress.ts +3 -3
  141. package/src/workflows/scripted-workflow.ts +275 -17
  142. package/src/workflows/workflow-checklist.ts +13 -17
  143. package/src/workflows/workflow-child-summary.ts +57 -8
  144. package/src/workflows/workflow-preflight.ts +19 -19
  145. package/src/workflows/workflow-receipt.ts +3 -3
  146. package/src/workflows/workflow-resources.ts +96 -21
  147. package/src/workflows/workflow-settlement.ts +3 -0
  148. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -1,8 +1,12 @@
1
- import { randomUUID } from "node:crypto";
1
+ // Forking a parent transcript into a child must drop Anthropic's signed and redacted
2
+ // thinking blocks: a thinking signature is bound to the session that produced it and
3
+ // cannot be replayed into a branch. Stripping them is required; disabling the child's
4
+ // thinking is not. Pi >= 0.85.0 recovers from signed-thinking mismatches on the
5
+ // transport, so a sanitized transcript is safe to resume with thinking enabled and the
6
+ // child keeps the level it asked for, reasoning fresh from its first turn.
2
7
  import * as fs from "node:fs";
3
8
  import * as path from "node:path";
4
9
  import { SessionManager } from "@earendil-works/pi-coding-agent";
5
- import { findModelInfo, type ModelInfo } from "./model-info.ts";
6
10
 
7
11
  type SubagentExecutionContext = "fresh" | "fork";
8
12
 
@@ -19,7 +23,6 @@ interface BranchSessionEntry {
19
23
  api?: string;
20
24
  model?: string;
21
25
  };
22
- thinkingLevel?: string;
23
26
  }
24
27
 
25
28
  interface BranchSessionManager {
@@ -39,20 +42,11 @@ interface ForkContextResolverOptions {
39
42
  openSession?: (path: string, sessionDir?: string) => BranchSessionManager;
40
43
  /** Rewrite a created fork before its path can be used to spawn a child. */
41
44
  pruneSession?: (sessionFile: string) => Promise<void>;
42
- /** Decide per child index whether a sanitized transcript must also disable the child's
43
- * thinking. Defaults to true (the pre-existing conservative behavior) when omitted. */
44
- forceThinkingOffForIndex?: (index: number) => boolean;
45
- }
46
-
47
- interface ForkContextResolution {
48
- sessionFile: string;
49
- thinkingOverride?: "off";
50
45
  }
51
46
 
52
47
  interface ForkContextResolver {
53
48
  prepareSessionForIndex(index?: number): Promise<void>;
54
49
  sessionFileForIndex(index?: number): string | undefined;
55
- thinkingOverrideForIndex(index?: number): "off" | undefined;
56
50
  }
57
51
 
58
52
  export function resolveSubagentContext(value: unknown): SubagentExecutionContext {
@@ -101,20 +95,6 @@ export function canPreferForkFromSnapshot(input: PreferredForkSnapshot): boolean
101
95
  }
102
96
  }
103
97
 
104
- /** Decide whether a resolved child model uses Anthropic's provider or message API, which
105
- * requires the sanitized fork to disable thinking. Unknown models stay conservative. */
106
- export function forkedChildRequiresThinkingOff(
107
- model: string | undefined,
108
- availableModels?: ModelInfo[],
109
- preferredProvider?: string,
110
- ): boolean {
111
- if (!model) return true;
112
- const info = findModelInfo(model, availableModels, preferredProvider);
113
- if (!info) return true;
114
- return info.provider.toLowerCase() === "anthropic"
115
- || info.api?.toLowerCase() === "anthropic-messages";
116
- }
117
-
118
98
  function isUnsafeAnthropicThinkingBlock(message: BranchSessionEntry["message"], block: unknown): boolean {
119
99
  if (!message || !block || typeof block !== "object" || !("type" in block)) return false;
120
100
  const provider = typeof message.provider === "string" ? message.provider.toLowerCase() : "";
@@ -128,28 +108,6 @@ function isUnsafeAnthropicThinkingBlock(message: BranchSessionEntry["message"],
128
108
  return record.redacted === true || (typeof signature === "string" && signature.length > 0);
129
109
  }
130
110
 
131
- function createEntryId(entries: BranchSessionEntry[]): string {
132
- const ids = new Set(entries.map((entry) => entry.id).filter((id): id is string => typeof id === "string"));
133
- for (let attempt = 0; attempt < 100; attempt++) {
134
- const id = randomUUID().slice(0, 8);
135
- if (!ids.has(id)) return id;
136
- }
137
- return randomUUID();
138
- }
139
-
140
- function appendThinkingOffEntry(entries: BranchSessionEntry[]): void {
141
- const last = entries[entries.length - 1];
142
- if (last?.type === "thinking_level_change" && last.thinkingLevel === "off") return;
143
- const parent = [...entries].reverse().find((entry) => typeof entry.id === "string");
144
- entries.push({
145
- type: "thinking_level_change",
146
- id: createEntryId(entries),
147
- parentId: parent?.id ?? null,
148
- timestamp: new Date().toISOString(),
149
- thinkingLevel: "off",
150
- });
151
- }
152
-
153
111
  function sanitizeUnsafeThinkingBlocks(entries: BranchSessionEntry[]): boolean {
154
112
  let sanitized = false;
155
113
  for (const entry of entries) {
@@ -183,7 +141,6 @@ export function createForkContextResolver(
183
141
  return {
184
142
  prepareSessionForIndex: async () => {},
185
143
  sessionFileForIndex: () => undefined,
186
- thinkingOverrideForIndex: () => undefined,
187
144
  };
188
145
  }
189
146
 
@@ -217,12 +174,12 @@ export function createForkContextResolver(
217
174
  path.basename(parentSessionFile, ".jsonl"),
218
175
  "forks",
219
176
  );
220
- const cachedResolutions = new Map<number, ForkContextResolution>();
177
+ const cachedSessionFiles = new Map<number, string>();
221
178
  const preparedIndexes = new Set<number>();
222
179
  const preparationPromises = new Map<number, Promise<void>>();
223
180
 
224
- const resolveFork = (index = 0): ForkContextResolution => {
225
- const cached = cachedResolutions.get(index);
181
+ const resolveFork = (index = 0): string => {
182
+ const cached = cachedSessionFiles.get(index);
226
183
  if (cached) return cached;
227
184
  try {
228
185
  if (!fs.existsSync(parentSessionFile)) {
@@ -233,34 +190,23 @@ export function createForkContextResolver(
233
190
  if (!sessionFile) {
234
191
  throw new Error("Session manager did not return a forked session file.");
235
192
  }
236
- const forceThinkingOff = (sanitized: boolean): boolean =>
237
- sanitized && (options.forceThinkingOffForIndex?.(index) ?? true);
238
- let thinkingOverride: "off" | undefined;
239
193
  if (!fs.existsSync(sessionFile)) {
240
194
  const header = sourceManager.getHeader?.();
241
195
  const entries = sourceManager.getEntries?.();
242
196
  if (!header || !entries) {
243
197
  throw new Error(`Session manager returned a forked session file that does not exist and cannot be persisted by fallback: ${sessionFile}`);
244
198
  }
245
- if (forceThinkingOff(sanitizeUnsafeThinkingBlocks(entries))) {
246
- appendThinkingOffEntry(entries);
247
- thinkingOverride = "off";
248
- }
199
+ sanitizeUnsafeThinkingBlocks(entries);
249
200
  fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
250
201
  fs.writeFileSync(sessionFile, `${[header, ...entries].map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
251
202
  } else {
252
203
  const entries = readSessionEntries(sessionFile);
253
204
  if (sanitizeUnsafeThinkingBlocks(entries)) {
254
- if (forceThinkingOff(true)) {
255
- appendThinkingOffEntry(entries);
256
- thinkingOverride = "off";
257
- }
258
205
  fs.writeFileSync(sessionFile, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
259
206
  }
260
207
  }
261
- const resolution = { sessionFile, ...(thinkingOverride ? { thinkingOverride } : {}) };
262
- cachedResolutions.set(index, resolution);
263
- return resolution;
208
+ cachedSessionFiles.set(index, sessionFile);
209
+ return sessionFile;
264
210
  } catch (error) {
265
211
  const cause = error instanceof Error ? error : new Error(String(error));
266
212
  throw new Error(`Failed to create forked subagent session: ${cause.message}`, { cause });
@@ -269,11 +215,11 @@ export function createForkContextResolver(
269
215
 
270
216
  return {
271
217
  async prepareSessionForIndex(index = 0): Promise<void> {
272
- const resolution = resolveFork(index);
218
+ const sessionFile = resolveFork(index);
273
219
  if (!options.pruneSession || preparedIndexes.has(index)) return;
274
220
  let preparation = preparationPromises.get(index);
275
221
  if (!preparation) {
276
- preparation = options.pruneSession(resolution.sessionFile).then(() => {
222
+ preparation = options.pruneSession(sessionFile).then(() => {
277
223
  preparedIndexes.add(index);
278
224
  });
279
225
  preparationPromises.set(index, preparation);
@@ -284,10 +230,7 @@ export function createForkContextResolver(
284
230
  if (options.pruneSession && !preparedIndexes.has(index)) {
285
231
  throw new Error(`Pruned fork session ${index} was used before pruning completed.`);
286
232
  }
287
- return resolveFork(index).sessionFile;
288
- },
289
- thinkingOverrideForIndex(index = 0): "off" | undefined {
290
- return resolveFork(index).thinkingOverride;
233
+ return resolveFork(index);
291
234
  },
292
235
  };
293
236
  }
@@ -1,10 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import type { AgentConfig } from "../agents/agents.ts";
4
+ import type { PiLaunchToolPlan } from "../runs/shared/child-tool-plan.ts";
4
5
  import type { ExtensionBindings } from "../runs/shared/extension-bindings.ts";
5
6
 
6
7
  export const AGENT_DEFINITION_PROJECTION_VERSION = 1 as const;
7
- export const LAUNCH_BINDING_PROJECTION_VERSION = 1 as const;
8
+ // v2: the Intercom bridge prompt and tools are part of the binding on every
9
+ // path, and the bridge text no longer names the parent session.
10
+ export const LAUNCH_BINDING_PROJECTION_VERSION = 2 as const;
8
11
 
9
12
  function stableJson(value: unknown): string {
10
13
  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
@@ -75,8 +78,9 @@ export function projectAgentDefinition(agent: AgentConfig): Record<string, unkno
75
78
  };
76
79
  }
77
80
 
81
+ /** Digest of the parsed definition; a runtime overlay that already captured it wins over re-hashing the overlaid copy. */
78
82
  export function agentDefinitionDigest(agent: AgentConfig): string {
79
- return stableJsonDigest(projectAgentDefinition(agent));
83
+ return agent.definitionDigest ?? stableJsonDigest(projectAgentDefinition(agent));
80
84
  }
81
85
 
82
86
  export interface LaunchBindingInput {
@@ -136,3 +140,62 @@ export function projectLaunchBinding(input: LaunchBindingInput): Record<string,
136
140
  export function launchBindingDigest(input: LaunchBindingInput): string {
137
141
  return stableJsonDigest(projectLaunchBinding(input));
138
142
  }
143
+
144
+ type LaunchBindingPromptMode = Pick<LaunchBindingInput, "systemPromptMode" | "inheritProjectContext" | "inheritGlobalContext" | "inheritSkills">;
145
+
146
+ /**
147
+ * Who the child is: the launched agent, or the identity a persisted step
148
+ * already captured when a runner recomputes the binding per model attempt.
149
+ */
150
+ export type LaunchBindingIdentity =
151
+ | { agent: AgentConfig }
152
+ | ({ definitionDigest: string } & LaunchBindingPromptMode);
153
+
154
+ export type LaunchBindingSource = LaunchBindingIdentity
155
+ & Pick<LaunchBindingInput, "fast" | "thinking" | "skills" | "outputPath" | "outputMode" | "structuredOutputSchema" | "extensionBindings">
156
+ & {
157
+ task: string;
158
+ modelCandidates: string[];
159
+ /** Effective child system prompt before runtime acceptance prose. */
160
+ systemPrompt: string;
161
+ toolPlan: Pick<PiLaunchToolPlan, "effectiveToolAllowlist" | "excludeTools" | "extensionArgs" | "effectiveMcpTools">;
162
+ };
163
+
164
+ export interface LaunchBinding {
165
+ definitionDigest: string;
166
+ launchContractDigest: string;
167
+ }
168
+
169
+ /**
170
+ * Assemble launch identity from resolved preflight or execution inputs.
171
+ * The stable projection omits undefined optional fields.
172
+ */
173
+ export function resolveLaunchBinding(source: LaunchBindingSource): LaunchBinding {
174
+ const identity = "agent" in source ? {
175
+ definitionDigest: agentDefinitionDigest(source.agent),
176
+ systemPromptMode: source.agent.systemPromptMode,
177
+ inheritProjectContext: source.agent.inheritProjectContext,
178
+ inheritGlobalContext: source.agent.inheritGlobalContext,
179
+ inheritSkills: source.agent.inheritSkills,
180
+ } : source;
181
+ return {
182
+ definitionDigest: identity.definitionDigest,
183
+ launchContractDigest: launchBindingDigest({
184
+ ...identity,
185
+ task: source.task,
186
+ modelCandidates: source.modelCandidates,
187
+ fast: source.fast,
188
+ thinking: source.thinking || undefined,
189
+ systemPrompt: source.systemPrompt,
190
+ skills: source.skills,
191
+ tools: source.toolPlan.effectiveToolAllowlist,
192
+ excludeTools: source.toolPlan.excludeTools.length > 0 ? source.toolPlan.excludeTools : undefined,
193
+ extensions: source.toolPlan.extensionArgs,
194
+ mcpDirectTools: source.toolPlan.effectiveMcpTools,
195
+ outputPath: source.outputPath || undefined,
196
+ outputMode: source.outputMode,
197
+ structuredOutputSchema: source.structuredOutputSchema || undefined,
198
+ extensionBindings: source.extensionBindings || undefined,
199
+ }),
200
+ };
201
+ }
@@ -0,0 +1,30 @@
1
+ import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
2
+
3
+ const OPENCODE_HOST = "opencode.ai";
4
+
5
+ function matchesOpenCodeHost(baseUrl: string): boolean {
6
+ try {
7
+ return new URL(baseUrl).hostname === OPENCODE_HOST;
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ /**
14
+ * OpenCode session-routing headers for internal subagent model calls.
15
+ *
16
+ * Pi's own session path emits these from coding-agent's provider-attribution
17
+ * merge, but subagent-internal calls (watchdog review, permission arbiter,
18
+ * task-mutation arbiter, prompt audit) stream through bare Agents that bypass
19
+ * that path. Without them OpenCode falls back to client-IP affinity and loses
20
+ * prompt-cache routing (see pi issue #4847). Returns undefined for every other
21
+ * provider so non-OpenCode requests stay byte-identical.
22
+ */
23
+ export function opencodeSessionHeaders(
24
+ model: Pick<Model<Api>, "provider" | "baseUrl">,
25
+ sessionId: string | undefined,
26
+ ): ProviderHeaders | undefined {
27
+ if (!sessionId) return undefined;
28
+ if (model.provider !== "opencode" && model.provider !== "opencode-go" && !matchesOpenCodeHost(model.baseUrl)) return undefined;
29
+ return { "x-opencode-session": sessionId, "x-opencode-client": "pi" };
30
+ }