@diologue/local-agent 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -167,7 +167,7 @@ import { access as access3, constants as constants2 } from "node:fs/promises";
167
167
  import { spawn as spawn2 } from "node:child_process";
168
168
  import path4 from "node:path";
169
169
  import { fileURLToPath as fileURLToPath2 } from "node:url";
170
- var __filename2, __dirname2, REPO_ROOT2, DEFAULT_ENGINE_DIR, ENTRY_REL, DEFAULT_STARTUP_TIMEOUT_MS, LISTENING_REGEX, defaultRuntimeCheck, exists3, defaultClientFactory, LocalEngineLocator, truncate;
170
+ var __filename2, __dirname2, REPO_ROOT2, DEFAULT_ENGINE_DIR, ENTRY_REL, DEFAULT_STARTUP_TIMEOUT_MS, LISTENING_REGEX, defaultRuntimeCheck, exists3, isRecord, parseExistingInlineConfig, mergeInlineConfig, buildEngineEnv, defaultClientFactory, LocalEngineLocator, truncate;
171
171
  var init_engine_locator_local = __esm({
172
172
  "src/adapters/engine-locator-local.ts"() {
173
173
  "use strict";
@@ -192,6 +192,37 @@ var init_engine_locator_local = __esm({
192
192
  return false;
193
193
  }
194
194
  };
195
+ isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
196
+ parseExistingInlineConfig = (existing) => {
197
+ if (!existing) return {};
198
+ try {
199
+ const parsed = JSON.parse(existing);
200
+ return isRecord(parsed) ? parsed : {};
201
+ } catch {
202
+ return {};
203
+ }
204
+ };
205
+ mergeInlineConfig = (existing, injected) => {
206
+ if (!injected) return existing;
207
+ const base = parseExistingInlineConfig(existing);
208
+ const baseProvider = isRecord(base.provider) ? base.provider : {};
209
+ return JSON.stringify({
210
+ ...base,
211
+ ...injected,
212
+ provider: {
213
+ ...baseProvider,
214
+ ...injected.provider ?? {}
215
+ }
216
+ });
217
+ };
218
+ buildEngineEnv = (config) => {
219
+ const env = { ...process.env };
220
+ const content = mergeInlineConfig(env.OPENCODE_CONFIG_CONTENT, config);
221
+ if (content) {
222
+ env.OPENCODE_CONFIG_CONTENT = content;
223
+ }
224
+ return env;
225
+ };
195
226
  defaultClientFactory = async (baseUrl) => {
196
227
  const sdk = await import("@opencode-ai/sdk");
197
228
  return sdk.createOpencodeClient({ baseUrl });
@@ -281,13 +312,12 @@ var init_engine_locator_local = __esm({
281
312
  const child = spawn2(command, args, {
282
313
  cwd,
283
314
  stdio: ["ignore", "pipe", "pipe"],
284
- // Inherit env so the engine sees any LOCAL_AGENT_* / Provider
285
- // credentials the parent has, plus inject the shim config via
286
- // OPENCODE_CONFIG-style env once V2.5 wires that. (For now the
287
- // config flows through createOpencodeServer in the npm path
288
- // and is wired post-spawn here TODO once we cut the npm
289
- // SDK entirely, we'll pipe the config in via stdin or args.)
290
- env: process.env
315
+ // Inherit env so the engine sees any LOCAL_AGENT_* / provider
316
+ // credentials the parent has, then inject the brokered provider as
317
+ // inline opencode config. Without this, the default local engine path
318
+ // does not know the diologue/diologue-routed model, so the prompt
319
+ // fails before edit tools can create files in the selected repo.
320
+ env: buildEngineEnv(options.config)
291
321
  });
292
322
  let url;
293
323
  try {
@@ -504,6 +534,9 @@ var createCorsMiddleware = (options) => {
504
534
  `Content-Type, ${LOCAL_AGENT_TOKEN_HEADER}`
505
535
  );
506
536
  res.setHeader("Access-Control-Max-Age", "300");
537
+ if (req.header("access-control-request-private-network") === "true") {
538
+ res.setHeader("Access-Control-Allow-Private-Network", "true");
539
+ }
507
540
  }
508
541
  if (req.method === "OPTIONS") {
509
542
  res.status(204).end();
@@ -641,8 +674,56 @@ var getStatusShort = async (cwd) => {
641
674
  const out = await runGit(cwd, ["status", "--short", "-uall"]);
642
675
  return parseShortStatus(out);
643
676
  };
677
+ var runGitAllowingExit = (cwd, args, allowedExitCodes) => new Promise((resolve, reject) => {
678
+ execFile(
679
+ "git",
680
+ args,
681
+ {
682
+ cwd,
683
+ maxBuffer: 16 * 1024 * 1024,
684
+ env: { ...process.env, GIT_PAGER: "cat", PAGER: "cat" }
685
+ },
686
+ (err, stdout, stderr) => {
687
+ if (!err) {
688
+ resolve(stdout);
689
+ return;
690
+ }
691
+ const e = err;
692
+ const exitCode = e.code ?? -1;
693
+ if (allowedExitCodes.has(exitCode)) {
694
+ resolve(stdout);
695
+ return;
696
+ }
697
+ reject(new GitCommandError(args, exitCode, stderr));
698
+ }
699
+ );
700
+ });
701
+ var getUntrackedFiles = async (cwd) => {
702
+ const out = await runGit(cwd, [
703
+ "ls-files",
704
+ "--others",
705
+ "--exclude-standard",
706
+ "-z"
707
+ ]);
708
+ return out.split("\0").filter(Boolean);
709
+ };
710
+ var getUntrackedFileDiff = async (cwd, file) => {
711
+ const diff = await runGitAllowingExit(
712
+ cwd,
713
+ ["diff", "--no-index", "--", "/dev/null", file],
714
+ /* @__PURE__ */ new Set([1])
715
+ );
716
+ return diff.replace(/^diff --git a\/dev\/null b\/(.+)$/m, "diff --git a/$1 b/$1").replace(/^--- \/dev\/null$/m, "--- /dev/null");
717
+ };
644
718
  var getDiff = async (cwd) => {
645
- return runGit(cwd, ["diff", "HEAD"]);
719
+ const [trackedDiff, untrackedFiles] = await Promise.all([
720
+ runGit(cwd, ["diff", "HEAD"]),
721
+ getUntrackedFiles(cwd)
722
+ ]);
723
+ const untrackedDiffs = await Promise.all(
724
+ untrackedFiles.map((file) => getUntrackedFileDiff(cwd, file))
725
+ );
726
+ return [trackedDiff, ...untrackedDiffs].filter((part) => part.trim().length > 0).join("\n");
646
727
  };
647
728
  var runGitWithStdin = async (cwd, args, input) => {
648
729
  const { execFile: execFile2 } = await import("node:child_process");
@@ -682,6 +763,20 @@ var canApplyDiff = async (cwd, unified) => {
682
763
  var applyDiff = async (cwd, unified) => {
683
764
  await runGitWithStdin(cwd, ["apply"], unified);
684
765
  };
766
+ var canRevertDiff = async (cwd, unified) => {
767
+ try {
768
+ await runGitWithStdin(cwd, ["apply", "--reverse", "--check"], unified);
769
+ return { ok: true };
770
+ } catch (err) {
771
+ if (err instanceof GitCommandError) {
772
+ return { ok: false, stderr: err.stderr };
773
+ }
774
+ throw err;
775
+ }
776
+ };
777
+ var revertDiff = async (cwd, unified) => {
778
+ await runGitWithStdin(cwd, ["apply", "--reverse"], unified);
779
+ };
685
780
  var parseDiffPaths = (unified) => {
686
781
  const paths = [];
687
782
  for (const line of unified.split("\n")) {
@@ -761,6 +856,9 @@ var applyPatchSchema = z.object({
761
856
  unified: z.string().min(1).max(8 * 1024 * 1024),
762
857
  baselineHash: z.string().optional()
763
858
  });
859
+ var revertPatchSchema = z.object({
860
+ unified: z.string().min(1).max(8 * 1024 * 1024)
861
+ });
764
862
  var buildRepoStatus = async (resolvedPath) => {
765
863
  const [branch, head, dirty] = await Promise.all([
766
864
  getBranch(resolvedPath),
@@ -910,6 +1008,40 @@ var createRepoRouter = (state) => {
910
1008
  throw err;
911
1009
  }
912
1010
  });
1011
+ router.post("/revert", async (req, res) => {
1012
+ const repo = requireSelectedRepo(state, res);
1013
+ if (!repo) return;
1014
+ const parsed = revertPatchSchema.safeParse(req.body);
1015
+ if (!parsed.success) {
1016
+ res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
1017
+ return;
1018
+ }
1019
+ try {
1020
+ const check = await canRevertDiff(repo.path, parsed.data.unified);
1021
+ if (!check.ok) {
1022
+ res.status(409).json({
1023
+ error: "diff_does_not_revert",
1024
+ message: "These changes can't be reverted cleanly. The working tree may have changed since the agent wrote them, or they were already reverted.",
1025
+ stderr: check.stderr
1026
+ });
1027
+ return;
1028
+ }
1029
+ await revertDiff(repo.path, parsed.data.unified);
1030
+ const paths = parseDiffPaths(parsed.data.unified);
1031
+ const body = {
1032
+ ok: true,
1033
+ filesChanged: paths.length,
1034
+ paths
1035
+ };
1036
+ res.json(body);
1037
+ } catch (err) {
1038
+ if (err instanceof GitCommandError) {
1039
+ sendGitError(res, err);
1040
+ return;
1041
+ }
1042
+ throw err;
1043
+ }
1044
+ });
913
1045
  return router;
914
1046
  };
915
1047
 
@@ -919,6 +1051,9 @@ import { z as z2 } from "zod";
919
1051
 
920
1052
  // src/broker.ts
921
1053
  import { randomUUID } from "node:crypto";
1054
+ var logBroker = (message) => {
1055
+ console.error(`[llm-broker] ${message}`);
1056
+ };
922
1057
  var LlmBroker = class {
923
1058
  pending = /* @__PURE__ */ new Map();
924
1059
  emit;
@@ -948,13 +1083,18 @@ var LlmBroker = class {
948
1083
  return new Promise((resolve, reject) => {
949
1084
  const timer = setTimeout(() => {
950
1085
  this.pending.delete(requestId);
1086
+ logBroker(
1087
+ `timeout request=${requestId.slice(0, 8)} pending=${this.pending.size} after=${this.timeoutMs}ms`
1088
+ );
951
1089
  reject(
952
1090
  new Error(
953
1091
  `llm_broker_timeout: no response within ${this.timeoutMs}ms`
954
1092
  )
955
1093
  );
956
1094
  }, this.timeoutMs);
957
- timer.unref?.();
1095
+ if (this.timeoutMs >= 1e3) {
1096
+ timer.unref?.();
1097
+ }
958
1098
  this.pending.set(requestId, { resolve, reject, timer, observer });
959
1099
  const effectiveProvider = this.overrideProvider ?? payload.provider;
960
1100
  const effectiveModel = this.overrideModel ?? payload.model;
@@ -971,6 +1111,9 @@ var LlmBroker = class {
971
1111
  }
972
1112
  };
973
1113
  try {
1114
+ logBroker(
1115
+ `emit request=${requestId.slice(0, 8)} provider=${effectiveProvider ?? "(default)"} model=${effectiveModel ?? "(default)"} messages=${payload.messages.length} tools=${payload.tools?.length ?? 0}`
1116
+ );
974
1117
  this.emit(envelope);
975
1118
  } catch (err) {
976
1119
  clearTimeout(timer);
@@ -995,7 +1138,14 @@ var LlmBroker = class {
995
1138
  * route (e.g. for providers that don't support streaming). */
996
1139
  fulfill(requestId, response) {
997
1140
  const entry = this.pending.get(requestId);
998
- if (!entry) return false;
1141
+ if (!entry) {
1142
+ logBroker(`fulfill_miss request=${requestId.slice(0, 8)}`);
1143
+ return false;
1144
+ }
1145
+ const toolNames = (response.toolCalls ?? []).map((tc) => tc.function?.name ?? "?").join(",");
1146
+ logBroker(
1147
+ `fulfill request=${requestId.slice(0, 8)} text=${response.text.length} toolCalls=${response.toolCalls?.length ?? 0}${toolNames ? ` tools=[${toolNames}]` : ""} finish=${response.finishReason ?? "(none)"}`
1148
+ );
999
1149
  clearTimeout(entry.timer);
1000
1150
  this.pending.delete(requestId);
1001
1151
  if (entry.observer) {
@@ -1024,7 +1174,15 @@ var LlmBroker = class {
1024
1174
  * invoked; a `complete` chunk also resolves the pending Promise. */
1025
1175
  pushChunk(requestId, chunk) {
1026
1176
  const entry = this.pending.get(requestId);
1027
- if (!entry) return false;
1177
+ if (!entry) {
1178
+ logBroker(
1179
+ `chunk_miss request=${requestId.slice(0, 8)} type=${chunk.type}`
1180
+ );
1181
+ return false;
1182
+ }
1183
+ logBroker(
1184
+ `chunk request=${requestId.slice(0, 8)} type=${chunk.type}${chunk.type === "text_delta" ? ` chars=${chunk.text.length}` : ""}${chunk.type === "tool_call_delta" ? ` index=${chunk.index} name=${chunk.name ?? "(delta)"} argsDelta=${chunk.argumentsDelta?.length ?? 0}` : ""}`
1185
+ );
1028
1186
  if (entry.observer) {
1029
1187
  try {
1030
1188
  entry.observer(chunk);
@@ -1046,7 +1204,11 @@ var LlmBroker = class {
1046
1204
  * failure rather than a completion. */
1047
1205
  fail(requestId, message) {
1048
1206
  const entry = this.pending.get(requestId);
1049
- if (!entry) return false;
1207
+ if (!entry) {
1208
+ logBroker(`fail_miss request=${requestId.slice(0, 8)}`);
1209
+ return false;
1210
+ }
1211
+ logBroker(`fail request=${requestId.slice(0, 8)} message=${message}`);
1050
1212
  clearTimeout(entry.timer);
1051
1213
  this.pending.delete(requestId);
1052
1214
  entry.reject(new Error(message));
@@ -1055,6 +1217,9 @@ var LlmBroker = class {
1055
1217
  /** Reject all in-flight requests. Called when the parent SSE stream
1056
1218
  * closes so a stranded request doesn't sit forever. */
1057
1219
  close(reason = "broker_closed") {
1220
+ if (this.pending.size > 0) {
1221
+ logBroker(`close reason=${reason} pending=${this.pending.size}`);
1222
+ }
1058
1223
  this.closed = true;
1059
1224
  for (const [, entry] of this.pending.entries()) {
1060
1225
  clearTimeout(entry.timer);
@@ -1111,6 +1276,9 @@ var writeEvent = (res, event) => {
1111
1276
  var writeDone = (res) => {
1112
1277
  res.write("data: [DONE]\n\n");
1113
1278
  };
1279
+ var logAgentRoute = (message) => {
1280
+ console.error(`[agent-route] ${message}`);
1281
+ };
1114
1282
  var createAgentRouter = (deps) => {
1115
1283
  const router = createRouter2();
1116
1284
  router.post("/message", async (req, res) => {
@@ -1124,6 +1292,9 @@ var createAgentRouter = (deps) => {
1124
1292
  res.status(409).json({ error: "no_repo_selected" });
1125
1293
  return;
1126
1294
  }
1295
+ logAgentRoute(
1296
+ `message start session=${parsed.data.sessionId.slice(0, 8)} repo=${repo.path} promptChars=${parsed.data.prompt.length} provider=${parsed.data.preferredProvider ?? "(default)"} model=${parsed.data.preferredModel ?? "(default)"}`
1297
+ );
1127
1298
  res.setHeader("Content-Type", "text/event-stream");
1128
1299
  res.setHeader("Cache-Control", "no-cache, no-transform");
1129
1300
  res.setHeader("Connection", "keep-alive");
@@ -1149,21 +1320,33 @@ var createAgentRouter = (deps) => {
1149
1320
  prompt: parsed.data.prompt,
1150
1321
  history,
1151
1322
  signal: controller.signal,
1152
- broker: (payload) => broker.request(payload),
1323
+ broker: (payload, observer) => broker.request(payload, observer),
1153
1324
  preferredProvider: parsed.data.preferredProvider,
1154
1325
  preferredModel: parsed.data.preferredModel
1155
1326
  })) {
1156
1327
  if (controller.signal.aborted) {
1328
+ logAgentRoute(
1329
+ `message aborted session=${parsed.data.sessionId.slice(0, 8)}`
1330
+ );
1157
1331
  break;
1158
1332
  }
1333
+ logAgentRoute(
1334
+ `stream event session=${parsed.data.sessionId.slice(0, 8)} type=${event.type}`
1335
+ );
1159
1336
  writeEvent(res, event);
1160
1337
  }
1338
+ logAgentRoute(
1339
+ `message done session=${parsed.data.sessionId.slice(0, 8)}`
1340
+ );
1161
1341
  writeDone(res);
1162
1342
  } catch (err) {
1163
1343
  if (err && typeof err === "object" && "name" in err && err.name === "AbortError") {
1164
1344
  writeDone(res);
1165
1345
  } else {
1166
1346
  const message = err instanceof Error ? err.message : "Adapter stream failed";
1347
+ logAgentRoute(
1348
+ `message error session=${parsed.data.sessionId.slice(0, 8)} error=${message}`
1349
+ );
1167
1350
  writeEvent(res, { type: "error", message });
1168
1351
  writeDone(res);
1169
1352
  }
@@ -1180,7 +1363,10 @@ var llmResponseBodySchema = z2.object({
1180
1363
  text: z2.string(),
1181
1364
  provider: z2.string().min(1),
1182
1365
  model: z2.string().min(1),
1183
- tokenUsage: z2.object({ input: z2.number().nonnegative(), output: z2.number().nonnegative() }).optional(),
1366
+ tokenUsage: z2.object({
1367
+ input: z2.number().nonnegative(),
1368
+ output: z2.number().nonnegative()
1369
+ }).optional(),
1184
1370
  error: z2.string().optional(),
1185
1371
  toolCalls: z2.array(
1186
1372
  z2.object({
@@ -2285,6 +2471,11 @@ var createEngineLocator = async (name) => {
2285
2471
  init_engine_locator_npm();
2286
2472
  var DIOLOGUE_PROVIDER_ID = "diologue";
2287
2473
  var DIOLOGUE_MODEL_ID = "diologue-routed";
2474
+ var END_OF_TURN_GRACE_MS = 600;
2475
+ var EDIT_DIRECTIVE = "You are an autonomous coding agent operating on a real git repository. Carry out the request by editing files directly with your tools (write / edit / patch / bash) \u2014 do not just describe the change, outline steps, or print code for the user to copy. Read only what you need, make the edits on disk, and finish once the change has actually been written.";
2476
+ var logAdapter = (message) => {
2477
+ console.error(`[opencode-adapter] ${message}`);
2478
+ };
2288
2479
  var OpenCodeProcessAdapter = class {
2289
2480
  constructor(options = {}) {
2290
2481
  this.options = options;
@@ -2335,7 +2526,16 @@ var OpenCodeProcessAdapter = class {
2335
2526
  }
2336
2527
  }
2337
2528
  },
2338
- model: `${DIOLOGUE_PROVIDER_ID}/${DIOLOGUE_MODEL_ID}`
2529
+ model: `${DIOLOGUE_PROVIDER_ID}/${DIOLOGUE_MODEL_ID}`,
2530
+ // Auto-approve tool execution. opencode gates edit/bash/webfetch
2531
+ // behind a permission prompt; this helper runs headless with no
2532
+ // approver, so without this the agent's writes are never applied to
2533
+ // the repo and a turn finishes having changed nothing.
2534
+ permission: {
2535
+ edit: "allow",
2536
+ bash: "allow",
2537
+ webfetch: "allow"
2538
+ }
2339
2539
  };
2340
2540
  }
2341
2541
  async ensureServer() {
@@ -2344,7 +2544,10 @@ var OpenCodeProcessAdapter = class {
2344
2544
  }
2345
2545
  this.serverPromise = (async () => {
2346
2546
  const locator = await this.resolveLocator();
2347
- return locator.start({ config: this.buildShimConfig() });
2547
+ logAdapter(`starting engine helperBase=${this.resolveHelperBaseUrl()}`);
2548
+ const handle = await locator.start({ config: this.buildShimConfig() });
2549
+ logAdapter(`engine ready url=${handle.url}`);
2550
+ return handle;
2348
2551
  })();
2349
2552
  try {
2350
2553
  return await this.serverPromise;
@@ -2355,7 +2558,13 @@ var OpenCodeProcessAdapter = class {
2355
2558
  }
2356
2559
  async ensureOpencodeSession(handle, ourSessionId, repoPath, title) {
2357
2560
  const existing = this.sessionMap.get(ourSessionId);
2358
- if (existing) return existing;
2561
+ if (existing) {
2562
+ logAdapter(
2563
+ `reuse opencodeSession=${existing.slice(0, 8)} session=${ourSessionId.slice(0, 8)} repo=${repoPath}`
2564
+ );
2565
+ return existing;
2566
+ }
2567
+ logAdapter(`create session=${ourSessionId.slice(0, 8)} repo=${repoPath}`);
2359
2568
  const created = await handle.client.session.create({
2360
2569
  body: { title },
2361
2570
  query: { directory: repoPath }
@@ -2365,6 +2574,9 @@ var OpenCodeProcessAdapter = class {
2365
2574
  throw new Error("opencode session.create returned no id");
2366
2575
  }
2367
2576
  this.sessionMap.set(ourSessionId, id);
2577
+ logAdapter(
2578
+ `created opencodeSession=${id.slice(0, 8)} session=${ourSessionId.slice(0, 8)}`
2579
+ );
2368
2580
  return id;
2369
2581
  }
2370
2582
  async *streamMessage(request) {
@@ -2410,16 +2622,16 @@ var OpenCodeProcessAdapter = class {
2410
2622
  }
2411
2623
  });
2412
2624
  const realCallback = callback;
2413
- streamScopedBroker.request = async (payload) => {
2414
- return realCallback(payload);
2625
+ streamScopedBroker.request = async (payload, observer) => {
2626
+ return realCallback(payload, observer);
2415
2627
  };
2416
2628
  activeBrokerHandle = bindActiveBroker(streamScopedBroker);
2417
- console.error(
2418
- `[opencode-adapter] bound active broker token="${activeBrokerHandle.token.slice(0, 12)}\u2026" session=${request.sessionId.slice(0, 8)}`
2629
+ logAdapter(
2630
+ `bound active broker token="${activeBrokerHandle.token.slice(0, 12)}\u2026" session=${request.sessionId.slice(0, 8)}`
2419
2631
  );
2420
2632
  } else {
2421
- console.error(
2422
- `[opencode-adapter] WARNING: request.broker is missing for session=${request.sessionId.slice(0, 8)} \u2014 shim will 401`
2633
+ logAdapter(
2634
+ `WARNING request.broker missing session=${request.sessionId.slice(0, 8)}; shim will 401`
2423
2635
  );
2424
2636
  }
2425
2637
  const eventController = new AbortController();
@@ -2436,6 +2648,9 @@ var OpenCodeProcessAdapter = class {
2436
2648
  signal: eventController.signal
2437
2649
  });
2438
2650
  eventStream = res.stream;
2651
+ logAdapter(
2652
+ `subscribed events session=${request.sessionId.slice(0, 8)} opencodeSession=${openCodeSessionId.slice(0, 8)}`
2653
+ );
2439
2654
  } catch (err) {
2440
2655
  yield {
2441
2656
  type: "error",
@@ -2446,7 +2661,11 @@ var OpenCodeProcessAdapter = class {
2446
2661
  return;
2447
2662
  }
2448
2663
  const promptBody = {
2449
- parts: [{ type: "text", text: request.prompt }]
2664
+ parts: [
2665
+ { type: "text", text: `${EDIT_DIRECTIVE}
2666
+
2667
+ ${request.prompt}` }
2668
+ ]
2450
2669
  };
2451
2670
  if (streamScopedBroker && activeBrokerHandle) {
2452
2671
  promptBody.model = {
@@ -2454,22 +2673,63 @@ var OpenCodeProcessAdapter = class {
2454
2673
  modelID: DIOLOGUE_MODEL_ID
2455
2674
  };
2456
2675
  }
2676
+ logAdapter(
2677
+ `prompt start session=${request.sessionId.slice(0, 8)} opencodeSession=${openCodeSessionId.slice(0, 8)} promptChars=${request.prompt.length} provider=${request.preferredProvider ?? "(default)"} model=${request.preferredModel ?? "(default)"}`
2678
+ );
2457
2679
  const promptPromise = handle.client.session.prompt({
2458
2680
  path: { id: openCodeSessionId },
2459
2681
  query: { directory: request.repoPath },
2460
2682
  body: promptBody
2683
+ }).then((value) => {
2684
+ logAdapter(
2685
+ `prompt accepted session=${request.sessionId.slice(0, 8)} opencodeSession=${openCodeSessionId.slice(0, 8)}`
2686
+ );
2687
+ return value;
2461
2688
  }).catch((err) => {
2462
- return err instanceof Error ? err : new Error(String(err));
2689
+ const error = err instanceof Error ? err : new Error(String(err));
2690
+ logAdapter(
2691
+ `prompt failed session=${request.sessionId.slice(0, 8)} opencodeSession=${openCodeSessionId.slice(0, 8)} error=${error.message}`
2692
+ );
2693
+ return error;
2463
2694
  });
2695
+ let stopConsuming = false;
2696
+ const stopSignal = new Promise((resolve) => {
2697
+ void promptPromise.finally(() => {
2698
+ setTimeout(() => {
2699
+ stopConsuming = true;
2700
+ resolve("stop");
2701
+ }, END_OF_TURN_GRACE_MS);
2702
+ });
2703
+ });
2704
+ let emittedDone = false;
2705
+ const iterator = eventStream[Symbol.asyncIterator]();
2464
2706
  try {
2465
- for await (const item of eventStream) {
2707
+ while (!stopConsuming) {
2466
2708
  if (request.signal?.aborted) break;
2709
+ const nextP = iterator.next();
2710
+ const next = await Promise.race([
2711
+ nextP,
2712
+ stopSignal.then(() => null)
2713
+ ]);
2714
+ if (next === null) {
2715
+ void nextP.catch(() => void 0);
2716
+ break;
2717
+ }
2718
+ if (next.done) break;
2719
+ const rawItem = next.value;
2720
+ const item = "payload" in rawItem && rawItem.payload ? rawItem.payload : rawItem;
2467
2721
  const mapped = mapEvent(item, mapState);
2722
+ if (mapped.events.length > 0 || mapped.done) {
2723
+ logAdapter(
2724
+ `event ${item.type} mapped=${mapped.events.map((event) => event.type).join(",") || "(none)"} done=${mapped.done} session=${request.sessionId.slice(0, 8)}`
2725
+ );
2726
+ }
2468
2727
  for (const event of mapped.events) {
2469
2728
  if (event.type === "diff_proposed" && mapped.patchToFetch) {
2470
2729
  sawAnyPatch = true;
2471
2730
  lastPatchHash = mapped.patchToFetch.hash;
2472
2731
  }
2732
+ if (event.type === "done") emittedDone = true;
2473
2733
  yield event;
2474
2734
  }
2475
2735
  if (mapped.done) {
@@ -2478,31 +2738,42 @@ var OpenCodeProcessAdapter = class {
2478
2738
  }
2479
2739
  } finally {
2480
2740
  eventController.abort();
2741
+ try {
2742
+ await iterator.return?.();
2743
+ } catch {
2744
+ }
2481
2745
  request.signal?.removeEventListener("abort", stopOnAbort);
2482
2746
  activeBrokerHandle?.release();
2483
2747
  streamScopedBroker?.close("turn_ended");
2484
2748
  }
2485
- if (sawAnyPatch) {
2486
- try {
2487
- const diffRes = await handle.client.session.diff({
2488
- path: { id: openCodeSessionId },
2489
- query: { directory: request.repoPath }
2490
- });
2491
- const unified = diffRes.data?.unified ?? "";
2492
- if (unified) {
2493
- yield {
2494
- type: "diff_proposed",
2495
- unified,
2496
- files: parseUnifiedDiffFiles(unified)
2497
- };
2498
- }
2499
- } catch {
2749
+ try {
2750
+ const diffRes = await handle.client.session.diff({
2751
+ path: { id: openCodeSessionId },
2752
+ query: { directory: request.repoPath }
2753
+ });
2754
+ const unified = diffRes.data?.unified ?? "";
2755
+ if (unified) {
2756
+ yield {
2757
+ type: "diff_proposed",
2758
+ unified,
2759
+ files: parseUnifiedDiffFiles(unified)
2760
+ };
2500
2761
  }
2762
+ } catch (err) {
2763
+ logAdapter(
2764
+ `diff fetch failed session=${request.sessionId.slice(0, 8)} error=${err instanceof Error ? err.message : String(err)}`
2765
+ );
2501
2766
  }
2502
2767
  const promptResult = await promptPromise;
2503
2768
  if (promptResult instanceof Error) {
2504
2769
  yield { type: "error", message: promptResult.message, recoverable: true };
2770
+ } else {
2771
+ logAdapter(`turn complete session=${request.sessionId.slice(0, 8)}`);
2772
+ }
2773
+ if (!emittedDone) {
2774
+ yield { type: "done" };
2505
2775
  }
2776
+ void sawAnyPatch;
2506
2777
  void lastPatchHash;
2507
2778
  }
2508
2779
  async close() {