@diologue/local-agent 0.1.5 → 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
@@ -81,9 +81,11 @@ var init_engine_release = __esm({
81
81
 
82
82
  // src/lib/engine-bundle.ts
83
83
  import { access as access2, constants } from "node:fs/promises";
84
+ import { readFileSync } from "node:fs";
85
+ import { createRequire } from "node:module";
84
86
  import path3 from "node:path";
85
87
  import { fileURLToPath } from "node:url";
86
- var engineRelease, __filename, __dirname, LOCAL_AGENT_ROOT, REPO_ROOT, BUNDLE_DIR_INSTALLED, BUNDLE_DIR_LOCAL_BUILD, ENGINE_BUNDLE_ENV, exists2, bundleFilename, findEngineBundle;
88
+ var engineRelease, __filename, __dirname, LOCAL_AGENT_ROOT, REPO_ROOT, BUNDLE_DIR_INSTALLED, BUNDLE_DIR_LOCAL_BUILD, ENGINE_BUNDLE_ENV, exists2, resolveOpencodeAiBinary, bundleFilename, findEngineBundle;
87
89
  var init_engine_bundle = __esm({
88
90
  "src/lib/engine-bundle.ts"() {
89
91
  "use strict";
@@ -111,6 +113,19 @@ var init_engine_bundle = __esm({
111
113
  return false;
112
114
  }
113
115
  };
116
+ resolveOpencodeAiBinary = () => {
117
+ try {
118
+ const require2 = createRequire(import.meta.url);
119
+ const pkgJsonPath = require2.resolve("opencode-ai/package.json");
120
+ const pkgDir = path3.dirname(pkgJsonPath);
121
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
122
+ const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.opencode;
123
+ if (!binRel) return null;
124
+ return path3.join(pkgDir, binRel);
125
+ } catch {
126
+ return null;
127
+ }
128
+ };
114
129
  bundleFilename = (platform = process.platform, arch = process.arch) => {
115
130
  const target = engineRelease.targets.find(
116
131
  (t) => t.platform === platform && t.arch === arch
@@ -125,6 +140,10 @@ var init_engine_bundle = __esm({
125
140
  return { path: fromEnv, source: "env" };
126
141
  }
127
142
  }
143
+ const opencodeAi = resolveOpencodeAiBinary();
144
+ if (opencodeAi && await exists2(opencodeAi)) {
145
+ return { path: opencodeAi, source: "opencode-ai" };
146
+ }
128
147
  const filename = bundleFilename();
129
148
  const installed = path3.join(BUNDLE_DIR_INSTALLED, filename);
130
149
  if (await exists2(installed)) {
@@ -148,7 +167,7 @@ import { access as access3, constants as constants2 } from "node:fs/promises";
148
167
  import { spawn as spawn2 } from "node:child_process";
149
168
  import path4 from "node:path";
150
169
  import { fileURLToPath as fileURLToPath2 } from "node:url";
151
- 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;
152
171
  var init_engine_locator_local = __esm({
153
172
  "src/adapters/engine-locator-local.ts"() {
154
173
  "use strict";
@@ -173,6 +192,37 @@ var init_engine_locator_local = __esm({
173
192
  return false;
174
193
  }
175
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
+ };
176
226
  defaultClientFactory = async (baseUrl) => {
177
227
  const sdk = await import("@opencode-ai/sdk");
178
228
  return sdk.createOpencodeClient({ baseUrl });
@@ -262,13 +312,12 @@ var init_engine_locator_local = __esm({
262
312
  const child = spawn2(command, args, {
263
313
  cwd,
264
314
  stdio: ["ignore", "pipe", "pipe"],
265
- // Inherit env so the engine sees any LOCAL_AGENT_* / Provider
266
- // credentials the parent has, plus inject the shim config via
267
- // OPENCODE_CONFIG-style env once V2.5 wires that. (For now the
268
- // config flows through createOpencodeServer in the npm path
269
- // and is wired post-spawn here TODO once we cut the npm
270
- // SDK entirely, we'll pipe the config in via stdin or args.)
271
- 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)
272
321
  });
273
322
  let url;
274
323
  try {
@@ -485,6 +534,9 @@ var createCorsMiddleware = (options) => {
485
534
  `Content-Type, ${LOCAL_AGENT_TOKEN_HEADER}`
486
535
  );
487
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
+ }
488
540
  }
489
541
  if (req.method === "OPTIONS") {
490
542
  res.status(204).end();
@@ -622,8 +674,56 @@ var getStatusShort = async (cwd) => {
622
674
  const out = await runGit(cwd, ["status", "--short", "-uall"]);
623
675
  return parseShortStatus(out);
624
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
+ };
625
718
  var getDiff = async (cwd) => {
626
- 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");
627
727
  };
628
728
  var runGitWithStdin = async (cwd, args, input) => {
629
729
  const { execFile: execFile2 } = await import("node:child_process");
@@ -663,6 +763,20 @@ var canApplyDiff = async (cwd, unified) => {
663
763
  var applyDiff = async (cwd, unified) => {
664
764
  await runGitWithStdin(cwd, ["apply"], unified);
665
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
+ };
666
780
  var parseDiffPaths = (unified) => {
667
781
  const paths = [];
668
782
  for (const line of unified.split("\n")) {
@@ -742,6 +856,9 @@ var applyPatchSchema = z.object({
742
856
  unified: z.string().min(1).max(8 * 1024 * 1024),
743
857
  baselineHash: z.string().optional()
744
858
  });
859
+ var revertPatchSchema = z.object({
860
+ unified: z.string().min(1).max(8 * 1024 * 1024)
861
+ });
745
862
  var buildRepoStatus = async (resolvedPath) => {
746
863
  const [branch, head, dirty] = await Promise.all([
747
864
  getBranch(resolvedPath),
@@ -891,6 +1008,40 @@ var createRepoRouter = (state) => {
891
1008
  throw err;
892
1009
  }
893
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
+ });
894
1045
  return router;
895
1046
  };
896
1047
 
@@ -900,6 +1051,9 @@ import { z as z2 } from "zod";
900
1051
 
901
1052
  // src/broker.ts
902
1053
  import { randomUUID } from "node:crypto";
1054
+ var logBroker = (message) => {
1055
+ console.error(`[llm-broker] ${message}`);
1056
+ };
903
1057
  var LlmBroker = class {
904
1058
  pending = /* @__PURE__ */ new Map();
905
1059
  emit;
@@ -929,13 +1083,18 @@ var LlmBroker = class {
929
1083
  return new Promise((resolve, reject) => {
930
1084
  const timer = setTimeout(() => {
931
1085
  this.pending.delete(requestId);
1086
+ logBroker(
1087
+ `timeout request=${requestId.slice(0, 8)} pending=${this.pending.size} after=${this.timeoutMs}ms`
1088
+ );
932
1089
  reject(
933
1090
  new Error(
934
1091
  `llm_broker_timeout: no response within ${this.timeoutMs}ms`
935
1092
  )
936
1093
  );
937
1094
  }, this.timeoutMs);
938
- timer.unref?.();
1095
+ if (this.timeoutMs >= 1e3) {
1096
+ timer.unref?.();
1097
+ }
939
1098
  this.pending.set(requestId, { resolve, reject, timer, observer });
940
1099
  const effectiveProvider = this.overrideProvider ?? payload.provider;
941
1100
  const effectiveModel = this.overrideModel ?? payload.model;
@@ -952,6 +1111,9 @@ var LlmBroker = class {
952
1111
  }
953
1112
  };
954
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
+ );
955
1117
  this.emit(envelope);
956
1118
  } catch (err) {
957
1119
  clearTimeout(timer);
@@ -976,7 +1138,14 @@ var LlmBroker = class {
976
1138
  * route (e.g. for providers that don't support streaming). */
977
1139
  fulfill(requestId, response) {
978
1140
  const entry = this.pending.get(requestId);
979
- 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
+ );
980
1149
  clearTimeout(entry.timer);
981
1150
  this.pending.delete(requestId);
982
1151
  if (entry.observer) {
@@ -1005,7 +1174,15 @@ var LlmBroker = class {
1005
1174
  * invoked; a `complete` chunk also resolves the pending Promise. */
1006
1175
  pushChunk(requestId, chunk) {
1007
1176
  const entry = this.pending.get(requestId);
1008
- 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
+ );
1009
1186
  if (entry.observer) {
1010
1187
  try {
1011
1188
  entry.observer(chunk);
@@ -1027,7 +1204,11 @@ var LlmBroker = class {
1027
1204
  * failure rather than a completion. */
1028
1205
  fail(requestId, message) {
1029
1206
  const entry = this.pending.get(requestId);
1030
- 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}`);
1031
1212
  clearTimeout(entry.timer);
1032
1213
  this.pending.delete(requestId);
1033
1214
  entry.reject(new Error(message));
@@ -1036,6 +1217,9 @@ var LlmBroker = class {
1036
1217
  /** Reject all in-flight requests. Called when the parent SSE stream
1037
1218
  * closes so a stranded request doesn't sit forever. */
1038
1219
  close(reason = "broker_closed") {
1220
+ if (this.pending.size > 0) {
1221
+ logBroker(`close reason=${reason} pending=${this.pending.size}`);
1222
+ }
1039
1223
  this.closed = true;
1040
1224
  for (const [, entry] of this.pending.entries()) {
1041
1225
  clearTimeout(entry.timer);
@@ -1092,6 +1276,9 @@ var writeEvent = (res, event) => {
1092
1276
  var writeDone = (res) => {
1093
1277
  res.write("data: [DONE]\n\n");
1094
1278
  };
1279
+ var logAgentRoute = (message) => {
1280
+ console.error(`[agent-route] ${message}`);
1281
+ };
1095
1282
  var createAgentRouter = (deps) => {
1096
1283
  const router = createRouter2();
1097
1284
  router.post("/message", async (req, res) => {
@@ -1105,6 +1292,9 @@ var createAgentRouter = (deps) => {
1105
1292
  res.status(409).json({ error: "no_repo_selected" });
1106
1293
  return;
1107
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
+ );
1108
1298
  res.setHeader("Content-Type", "text/event-stream");
1109
1299
  res.setHeader("Cache-Control", "no-cache, no-transform");
1110
1300
  res.setHeader("Connection", "keep-alive");
@@ -1130,21 +1320,33 @@ var createAgentRouter = (deps) => {
1130
1320
  prompt: parsed.data.prompt,
1131
1321
  history,
1132
1322
  signal: controller.signal,
1133
- broker: (payload) => broker.request(payload),
1323
+ broker: (payload, observer) => broker.request(payload, observer),
1134
1324
  preferredProvider: parsed.data.preferredProvider,
1135
1325
  preferredModel: parsed.data.preferredModel
1136
1326
  })) {
1137
1327
  if (controller.signal.aborted) {
1328
+ logAgentRoute(
1329
+ `message aborted session=${parsed.data.sessionId.slice(0, 8)}`
1330
+ );
1138
1331
  break;
1139
1332
  }
1333
+ logAgentRoute(
1334
+ `stream event session=${parsed.data.sessionId.slice(0, 8)} type=${event.type}`
1335
+ );
1140
1336
  writeEvent(res, event);
1141
1337
  }
1338
+ logAgentRoute(
1339
+ `message done session=${parsed.data.sessionId.slice(0, 8)}`
1340
+ );
1142
1341
  writeDone(res);
1143
1342
  } catch (err) {
1144
1343
  if (err && typeof err === "object" && "name" in err && err.name === "AbortError") {
1145
1344
  writeDone(res);
1146
1345
  } else {
1147
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
+ );
1148
1350
  writeEvent(res, { type: "error", message });
1149
1351
  writeDone(res);
1150
1352
  }
@@ -1161,7 +1363,10 @@ var llmResponseBodySchema = z2.object({
1161
1363
  text: z2.string(),
1162
1364
  provider: z2.string().min(1),
1163
1365
  model: z2.string().min(1),
1164
- 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(),
1165
1370
  error: z2.string().optional(),
1166
1371
  toolCalls: z2.array(
1167
1372
  z2.object({
@@ -2266,6 +2471,11 @@ var createEngineLocator = async (name) => {
2266
2471
  init_engine_locator_npm();
2267
2472
  var DIOLOGUE_PROVIDER_ID = "diologue";
2268
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
+ };
2269
2479
  var OpenCodeProcessAdapter = class {
2270
2480
  constructor(options = {}) {
2271
2481
  this.options = options;
@@ -2316,7 +2526,16 @@ var OpenCodeProcessAdapter = class {
2316
2526
  }
2317
2527
  }
2318
2528
  },
2319
- 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
+ }
2320
2539
  };
2321
2540
  }
2322
2541
  async ensureServer() {
@@ -2325,7 +2544,10 @@ var OpenCodeProcessAdapter = class {
2325
2544
  }
2326
2545
  this.serverPromise = (async () => {
2327
2546
  const locator = await this.resolveLocator();
2328
- 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;
2329
2551
  })();
2330
2552
  try {
2331
2553
  return await this.serverPromise;
@@ -2336,7 +2558,13 @@ var OpenCodeProcessAdapter = class {
2336
2558
  }
2337
2559
  async ensureOpencodeSession(handle, ourSessionId, repoPath, title) {
2338
2560
  const existing = this.sessionMap.get(ourSessionId);
2339
- 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}`);
2340
2568
  const created = await handle.client.session.create({
2341
2569
  body: { title },
2342
2570
  query: { directory: repoPath }
@@ -2346,6 +2574,9 @@ var OpenCodeProcessAdapter = class {
2346
2574
  throw new Error("opencode session.create returned no id");
2347
2575
  }
2348
2576
  this.sessionMap.set(ourSessionId, id);
2577
+ logAdapter(
2578
+ `created opencodeSession=${id.slice(0, 8)} session=${ourSessionId.slice(0, 8)}`
2579
+ );
2349
2580
  return id;
2350
2581
  }
2351
2582
  async *streamMessage(request) {
@@ -2391,16 +2622,16 @@ var OpenCodeProcessAdapter = class {
2391
2622
  }
2392
2623
  });
2393
2624
  const realCallback = callback;
2394
- streamScopedBroker.request = async (payload) => {
2395
- return realCallback(payload);
2625
+ streamScopedBroker.request = async (payload, observer) => {
2626
+ return realCallback(payload, observer);
2396
2627
  };
2397
2628
  activeBrokerHandle = bindActiveBroker(streamScopedBroker);
2398
- console.error(
2399
- `[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)}`
2400
2631
  );
2401
2632
  } else {
2402
- console.error(
2403
- `[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`
2404
2635
  );
2405
2636
  }
2406
2637
  const eventController = new AbortController();
@@ -2417,6 +2648,9 @@ var OpenCodeProcessAdapter = class {
2417
2648
  signal: eventController.signal
2418
2649
  });
2419
2650
  eventStream = res.stream;
2651
+ logAdapter(
2652
+ `subscribed events session=${request.sessionId.slice(0, 8)} opencodeSession=${openCodeSessionId.slice(0, 8)}`
2653
+ );
2420
2654
  } catch (err) {
2421
2655
  yield {
2422
2656
  type: "error",
@@ -2427,7 +2661,11 @@ var OpenCodeProcessAdapter = class {
2427
2661
  return;
2428
2662
  }
2429
2663
  const promptBody = {
2430
- parts: [{ type: "text", text: request.prompt }]
2664
+ parts: [
2665
+ { type: "text", text: `${EDIT_DIRECTIVE}
2666
+
2667
+ ${request.prompt}` }
2668
+ ]
2431
2669
  };
2432
2670
  if (streamScopedBroker && activeBrokerHandle) {
2433
2671
  promptBody.model = {
@@ -2435,22 +2673,63 @@ var OpenCodeProcessAdapter = class {
2435
2673
  modelID: DIOLOGUE_MODEL_ID
2436
2674
  };
2437
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
+ );
2438
2679
  const promptPromise = handle.client.session.prompt({
2439
2680
  path: { id: openCodeSessionId },
2440
2681
  query: { directory: request.repoPath },
2441
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;
2442
2688
  }).catch((err) => {
2443
- 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;
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
+ });
2444
2703
  });
2704
+ let emittedDone = false;
2705
+ const iterator = eventStream[Symbol.asyncIterator]();
2445
2706
  try {
2446
- for await (const item of eventStream) {
2707
+ while (!stopConsuming) {
2447
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;
2448
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
+ }
2449
2727
  for (const event of mapped.events) {
2450
2728
  if (event.type === "diff_proposed" && mapped.patchToFetch) {
2451
2729
  sawAnyPatch = true;
2452
2730
  lastPatchHash = mapped.patchToFetch.hash;
2453
2731
  }
2732
+ if (event.type === "done") emittedDone = true;
2454
2733
  yield event;
2455
2734
  }
2456
2735
  if (mapped.done) {
@@ -2459,31 +2738,42 @@ var OpenCodeProcessAdapter = class {
2459
2738
  }
2460
2739
  } finally {
2461
2740
  eventController.abort();
2741
+ try {
2742
+ await iterator.return?.();
2743
+ } catch {
2744
+ }
2462
2745
  request.signal?.removeEventListener("abort", stopOnAbort);
2463
2746
  activeBrokerHandle?.release();
2464
2747
  streamScopedBroker?.close("turn_ended");
2465
2748
  }
2466
- if (sawAnyPatch) {
2467
- try {
2468
- const diffRes = await handle.client.session.diff({
2469
- path: { id: openCodeSessionId },
2470
- query: { directory: request.repoPath }
2471
- });
2472
- const unified = diffRes.data?.unified ?? "";
2473
- if (unified) {
2474
- yield {
2475
- type: "diff_proposed",
2476
- unified,
2477
- files: parseUnifiedDiffFiles(unified)
2478
- };
2479
- }
2480
- } 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
+ };
2481
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
+ );
2482
2766
  }
2483
2767
  const promptResult = await promptPromise;
2484
2768
  if (promptResult instanceof Error) {
2485
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" };
2486
2775
  }
2776
+ void sawAnyPatch;
2487
2777
  void lastPatchHash;
2488
2778
  }
2489
2779
  async close() {