@alook/daemon 0.1.50 → 0.1.51

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/README.md CHANGED
@@ -108,10 +108,12 @@ without killing the process.
108
108
 
109
109
  ### Persistent service runtime (OpenCode)
110
110
 
111
- OpenCode starts one authenticated, loopback-only v2 service per logical session.
112
- Root prompts and busy steers share the same service and vendor session. Durable
113
- session SSE is replayed by event id and sequence across reconnects; a separate
114
- live stream handles permissions.
111
+ OpenCode starts one authenticated, loopback-only service per logical session.
112
+ The actual CLI major selects its adapter: v1 retains `--pure` and durable event
113
+ replay; v2 uses its native API, persisted inbox/messages, and a new idle record
114
+ after all accepted inputs. Root prompts and busy steers share the same service
115
+ and vendor session. Live SSE handles permissions and tool progress. Unknown
116
+ majors or missing native capabilities fail explicitly.
115
117
 
116
118
  ---
117
119
 
@@ -123,7 +125,7 @@ live stream handles permissions.
123
125
  | **codex** | persistent session | JSON-RPC 2.0 (`app-server --listen stdio://`) | `safe_boundary_queue` | `initialize` → `thread/start`/`resume` | JSON-RPC notifications |
124
126
  | **pi** | persistent session | `@earendil-works/pi-coding-agent` | `steer` | `session.prompt()` | SDK callback |
125
127
  | **cursor** | persistent session | ACP JSON-RPC 2.0 (`cursor-agent acp`) | `steer` | `session/prompt` | `session/update` + correlated prompt response |
126
- | **opencode** | persistent session | authenticated HTTP + SSE (`opencode serve --pure`) | `steer` | v2 session prompt API | durable + live SSE |
128
+ | **opencode** | persistent session | authenticated HTTP + SSE (`opencode serve`; v1 adds `--pure`) | `steer` | CLI-major session prompt API | native recovery + live SSE |
127
129
 
128
130
  (Exact launch flags live in `agent-driver/src/adapters/<backend>/`.)
129
131
 
package/dist/cli/index.js CHANGED
@@ -32556,14 +32556,16 @@ class OpenCodeServiceLane {
32556
32556
  if (!this.identity || this.identity.generation !== generation || this.isProcessClosed(this.identity.process)) {
32557
32557
  if (/EADDRINUSE|address already in use/i.test(this.stderrBuffer))
32558
32558
  throw new OpenCodePortBindError("OpenCode service port bind raced");
32559
- throw new Error("OpenCode service exited before readiness");
32559
+ const process2 = this.identity?.process;
32560
+ const detail = scrubDriverErrorMessage(this.stderrBuffer.replaceAll(this.password, "[redacted]"), "no native stderr");
32561
+ throw new Error(`OpenCode service exited before readiness (code=${process2?.exitCode ?? "unknown"}, signal=${process2?.signalCode ?? "none"}): ${detail}`);
32560
32562
  }
32561
32563
  try {
32562
32564
  const healthTimeoutMs = Math.max(1, Math.min(1000, deadline - Date.now(), this.options.requestTimeoutMs ?? REQUEST_TIMEOUT_MS));
32563
- const { response, body } = await this.fetchJsonWithTimeout("/global/health", { method: "GET" }, "health", healthTimeoutMs);
32565
+ const { response, body } = await this.fetchJsonWithTimeout(this.healthPath, { method: "GET" }, "health", healthTimeoutMs);
32564
32566
  if (response.ok) {
32565
32567
  const health = record5(body);
32566
- if (health?.healthy !== true) {
32568
+ if (!this.acceptsHealth(health)) {
32567
32569
  throw new OpenCodeIncompatibleError("Installed OpenCode service did not report healthy status");
32568
32570
  }
32569
32571
  await this.verifyOpenApi();
@@ -32580,12 +32582,27 @@ class OpenCodeServiceLane {
32580
32582
  throw new Error("OpenCode service readiness timed out");
32581
32583
  }
32582
32584
  async verifyOpenApi() {
32583
- const { response, body } = await this.fetchJsonWithTimeout("/doc", { method: "GET" }, "OpenAPI");
32585
+ const { response, body } = await this.fetchJsonWithTimeout(this.openApiPath, { method: "GET" }, "OpenAPI");
32584
32586
  if (!response.ok)
32585
32587
  throw new OpenCodeIncompatibleError("Installed OpenCode service does not expose its OpenAPI document");
32586
32588
  const document = record5(body);
32587
32589
  const paths = record5(document?.paths);
32588
- const required2 = [
32590
+ const required2 = this.requiredApiPaths;
32591
+ if (!paths || required2.some((path10) => !record5(paths[path10]))) {
32592
+ throw new OpenCodeIncompatibleError("Installed OpenCode service is missing required session capabilities");
32593
+ }
32594
+ }
32595
+ get healthPath() {
32596
+ return "/global/health";
32597
+ }
32598
+ get openApiPath() {
32599
+ return "/doc";
32600
+ }
32601
+ acceptsHealth(health) {
32602
+ return health?.healthy === true;
32603
+ }
32604
+ get requiredApiPaths() {
32605
+ return [
32589
32606
  "/api/session",
32590
32607
  "/api/session/active",
32591
32608
  "/api/session/{sessionID}/prompt",
@@ -32597,12 +32614,12 @@ class OpenCodeServiceLane {
32597
32614
  "/api/session/{sessionID}/permission/{requestID}/reply",
32598
32615
  "/api/event"
32599
32616
  ];
32600
- if (!paths || required2.some((path10) => !record5(paths[path10]))) {
32601
- throw new OpenCodeIncompatibleError("Installed OpenCode service is missing required v2 session capabilities");
32602
- }
32617
+ }
32618
+ get selectedModel() {
32619
+ return parseModelRef(resolveLaunchFieldsOrDefault(this.ctx.config.runtimeConfig).model);
32603
32620
  }
32604
32621
  async openSession(resumeId) {
32605
- const model = parseModelRef(resolveLaunchFieldsOrDefault(this.ctx.config.runtimeConfig).model);
32622
+ const model = this.selectedModel;
32606
32623
  if (resumeId) {
32607
32624
  const { response: response2, body: body2 } = await this.fetchJsonWithTimeout(`/api/session/${encodeURIComponent(resumeId)}`, { method: "GET" }, "session resume");
32608
32625
  if (response2.status === 404) {
@@ -32945,7 +32962,7 @@ class OpenCodeServiceLane {
32945
32962
  async handleLiveEvent(value) {
32946
32963
  const event = record5(value);
32947
32964
  const data = record5(event?.data);
32948
- if (event?.type !== "permission.v2.asked" || data?.sessionID !== this.sessionId)
32965
+ if (event?.type !== this.permissionEventType || data?.sessionID !== this.sessionId)
32949
32966
  return;
32950
32967
  if (typeof data.id !== "string" || !/^per/.test(data.id)) {
32951
32968
  throw new OpenCodeProtocolError("OpenCode emitted a malformed permission request");
@@ -32968,13 +32985,19 @@ class OpenCodeServiceLane {
32968
32985
  }
32969
32986
  }
32970
32987
  }
32988
+ get permissionEventType() {
32989
+ return "permission.v2.asked";
32990
+ }
32991
+ get permissionReply() {
32992
+ return { reply: "once" };
32993
+ }
32971
32994
  async replyPermission(requestId) {
32972
32995
  if (this.handledPermissions.has(requestId))
32973
32996
  return;
32974
32997
  const existing = this.permissionFlights.get(requestId);
32975
32998
  if (existing)
32976
32999
  return existing;
32977
- const flight = this.requestNoContent(`/api/session/${encodeURIComponent(this.sessionId)}/permission/${encodeURIComponent(requestId)}/reply`, "POST", { reply: "once" }, "permission reply").catch((error62) => {
33000
+ const flight = this.requestNoContent(`/api/session/${encodeURIComponent(this.sessionId)}/permission/${encodeURIComponent(requestId)}/reply`, "POST", this.permissionReply, "permission reply").catch((error62) => {
32978
33001
  if (!(error62 instanceof OpenCodeHttpError) || error62.status !== 404)
32979
33002
  throw error62;
32980
33003
  }).then(() => {
@@ -33351,6 +33374,28 @@ async function legacyOpenCodeApiEnvironment(environment, model) {
33351
33374
  return credential?.type === "api" && typeof credential.key === "string" && credential.key.trim() ? { [name]: credential.key } : {};
33352
33375
  }
33353
33376
 
33377
+ // agent-driver/dist/adapters/opencode/compatibility.js
33378
+ var OPEN_CODE_V1 = {
33379
+ major: 1,
33380
+ modelArgs: ["models", "--pure"],
33381
+ serviceArgs: ["serve", "--pure"],
33382
+ sessionListArgs: (maxCount) => ["session", "list", "--format", "json", "--max-count", String(maxCount), "--pure"]
33383
+ };
33384
+ var OPEN_CODE_V2 = {
33385
+ major: 2,
33386
+ modelArgs: ["models", "--standalone"],
33387
+ serviceArgs: ["serve"],
33388
+ sessionListArgs: (maxCount) => ["api", "--standalone", "GET", `/api/session?parentID=null&order=desc&limit=${maxCount}`]
33389
+ };
33390
+ function resolveOpenCodeCompatibility(version3) {
33391
+ const match = version3?.trim().match(/^(?:opencode\s+)?v?(\d+)\.\d+\.\d+(?:[-+][\w.-]+)?$/i);
33392
+ if (match?.[1] === "1")
33393
+ return OPEN_CODE_V1;
33394
+ if (match?.[1] === "2")
33395
+ return OPEN_CODE_V2;
33396
+ throw new Error("Installed OpenCode CLI version is unrecognized or has an unsupported major version");
33397
+ }
33398
+
33354
33399
  // agent-driver/dist/adapters/opencode/recent-context.js
33355
33400
  var OPENCODE_DISCOVERY_TIMEOUT_MS = 15000;
33356
33401
  var OPENCODE_DISCOVERY_OUTPUT_MAX_BYTES = 4 * 1024 * 1024;
@@ -33384,6 +33429,7 @@ async function discoverOpenCodeRecentContext(request, dependencies = {}) {
33384
33429
  const collector = new RecentContextCollector(request, "unavailable");
33385
33430
  if (collector.satisfied)
33386
33431
  return collector.result();
33432
+ const compatibility = dependencies.compatibility ?? OPEN_CODE_V1;
33387
33433
  const startedAt = Date.now();
33388
33434
  const timeoutMs = dependencies.timeoutMs ?? OPENCODE_DISCOVERY_TIMEOUT_MS;
33389
33435
  let remainingOutputBytes = dependencies.outputMaxBytes ?? OPENCODE_DISCOVERY_OUTPUT_MAX_BYTES;
@@ -33392,7 +33438,7 @@ async function discoverOpenCodeRecentContext(request, dependencies = {}) {
33392
33438
  const remainingTimeoutMs = timeoutMs - (Date.now() - startedAt);
33393
33439
  if (remainingTimeoutMs <= 0)
33394
33440
  throw new Error("OpenCode discovery timed out");
33395
- const spec = resolveSpawnSpec("opencode", ["session", "list", "--format", "json", "--max-count", String(maxCount), "--pure"], request.command);
33441
+ const spec = resolveSpawnSpec("opencode", [...compatibility.sessionListArgs(maxCount)], request.command);
33396
33442
  const commandOutput = await collectCommandOutput(spec.command, spec.args, spec.shell, dependencies, remainingTimeoutMs, remainingOutputBytes);
33397
33443
  remainingOutputBytes -= commandOutput.outputBytes;
33398
33444
  let parsed;
@@ -33401,14 +33447,17 @@ async function discoverOpenCodeRecentContext(request, dependencies = {}) {
33401
33447
  } catch {
33402
33448
  throw new Error("OpenCode session listing returned invalid JSON");
33403
33449
  }
33450
+ if (compatibility.major === 2)
33451
+ parsed = asRecord(parsed)?.data;
33404
33452
  if (!Array.isArray(parsed))
33405
33453
  throw new Error("OpenCode session listing returned an invalid response");
33406
33454
  for (const value of parsed) {
33407
33455
  const session2 = asRecord(value);
33408
33456
  if (!session2)
33409
33457
  continue;
33410
- const modifiedAt = session2.updated ?? session2.updatedAt;
33411
- collector.add({ projectPath: session2.directory, modifiedAt });
33458
+ const modifiedAt = compatibility.major === 2 ? asRecord(session2.time)?.updated : session2.updated ?? session2.updatedAt;
33459
+ const projectPath = compatibility.major === 2 ? asRecord(session2.location)?.directory : session2.directory;
33460
+ collector.add({ projectPath, modifiedAt });
33412
33461
  }
33413
33462
  if (collector.satisfied || parsed.length < maxCount)
33414
33463
  break;
@@ -33420,6 +33469,419 @@ async function discoverOpenCodeRecentContext(request, dependencies = {}) {
33420
33469
  return collector.result();
33421
33470
  }
33422
33471
 
33472
+ // agent-driver/dist/adapters/opencode/v2-service-lane.js
33473
+ class OpenCodeV2ServiceLane extends OpenCodeServiceLane {
33474
+ root = null;
33475
+ v2AdmissionTail = Promise.resolve();
33476
+ v2EvaluationTimer;
33477
+ v2Evaluating = false;
33478
+ projectedMessages = new Set;
33479
+ liveTools = new Map;
33480
+ settledTools = new Set;
33481
+ usage = new SettledUsageProjector;
33482
+ get healthPath() {
33483
+ return "/api/info";
33484
+ }
33485
+ get openApiPath() {
33486
+ return "/openapi.json";
33487
+ }
33488
+ acceptsHealth(health) {
33489
+ if (typeof health?.version !== "string" || !Number.isInteger(health.pid) || Number(health.pid) < 1)
33490
+ return false;
33491
+ try {
33492
+ return resolveOpenCodeCompatibility(health.version).major === 2;
33493
+ } catch {
33494
+ return false;
33495
+ }
33496
+ }
33497
+ get requiredApiPaths() {
33498
+ return [
33499
+ "/api/session",
33500
+ "/api/session/active",
33501
+ "/api/session/{sessionID}",
33502
+ "/api/session/{sessionID}/prompt",
33503
+ "/api/session/{sessionID}/inbox",
33504
+ "/api/session/{sessionID}/message",
33505
+ "/api/session/{sessionID}/message/{messageID}",
33506
+ "/api/session/{sessionID}/interrupt",
33507
+ "/api/session/{sessionID}/model",
33508
+ "/api/session/{sessionID}/permission",
33509
+ "/api/session/{sessionID}/permission/{requestID}/reply",
33510
+ "/api/event"
33511
+ ];
33512
+ }
33513
+ get selectedModel() {
33514
+ const model = super.selectedModel;
33515
+ if (typeof model?.id !== "string")
33516
+ return model;
33517
+ const hash2 = model.id.lastIndexOf("#");
33518
+ return hash2 > 0 && hash2 < model.id.length - 1 ? { ...model, id: model.id.slice(0, hash2), variant: model.id.slice(hash2 + 1) } : model;
33519
+ }
33520
+ get permissionEventType() {
33521
+ return "permission.asked";
33522
+ }
33523
+ get permissionReply() {
33524
+ return { decision: "once" };
33525
+ }
33526
+ protocolFailure(message2, cause) {
33527
+ super.protocolFailure(cause === undefined ? message2 : `${message2}: ${scrubDriverErrorMessage(cause)}`);
33528
+ }
33529
+ async primeHistory() {}
33530
+ startStreams() {
33531
+ const identity = this.identity;
33532
+ this.liveTask = this.runLiveStream(identity).catch((error62) => {
33533
+ if (!this.stopping)
33534
+ this.protocolFailure("OpenCode CLI v2 live stream failed", error62);
33535
+ });
33536
+ }
33537
+ async recoverPendingPermissions() {
33538
+ await super.recoverPendingPermissions();
33539
+ const root = this.root;
33540
+ if (root)
33541
+ for (const message2 of await this.messagesAfter(root))
33542
+ this.projectTools(message2);
33543
+ this.schedule();
33544
+ }
33545
+ async handleLiveEvent(value) {
33546
+ await super.handleLiveEvent(value);
33547
+ const event = record5(value);
33548
+ const data = record5(event?.data);
33549
+ if (data?.sessionID !== this.sessionId || !this.root)
33550
+ return;
33551
+ switch (event?.type) {
33552
+ case "session.step.started":
33553
+ this.events.emit("runtime_event", {
33554
+ kind: "internal_progress",
33555
+ source: "opencode.service",
33556
+ itemType: "step_started"
33557
+ });
33558
+ break;
33559
+ case "session.tool.input.started": {
33560
+ const key = this.toolKey(data.assistantMessageID, data.id);
33561
+ if (key && typeof data.name === "string" && !this.liveTools.has(key) && !this.settledTools.has(key)) {
33562
+ this.liveTools.set(key, { name: data.name, called: false });
33563
+ }
33564
+ break;
33565
+ }
33566
+ case "session.tool.called":
33567
+ this.projectTool(data);
33568
+ break;
33569
+ case "session.tool.success":
33570
+ case "session.tool.failed": {
33571
+ this.finishTool(this.toolKey(data.assistantMessageID, data.id));
33572
+ break;
33573
+ }
33574
+ case "session.compaction.started":
33575
+ this.events.emit("runtime_event", { kind: "compaction_started" });
33576
+ break;
33577
+ case "session.compaction.failed":
33578
+ case "session.compaction.ended":
33579
+ this.events.emit("runtime_event", { kind: "compaction_finished" });
33580
+ break;
33581
+ default:
33582
+ break;
33583
+ }
33584
+ this.schedule();
33585
+ }
33586
+ async send(input2) {
33587
+ if (!this.ready || this.stopping || !this.sessionId || !this.identityMatches())
33588
+ return { ok: false, reason: "closed" };
33589
+ if (input2.mode === "idle") {
33590
+ if (this.root)
33591
+ return { ok: false, reason: "runtime_busy" };
33592
+ return this.beginRoot(input2);
33593
+ }
33594
+ const root = this.root;
33595
+ if (!root)
33596
+ return { ok: false, reason: "not_ready" };
33597
+ return this.admit(input2.text, "steer", this.newMessageId(), root);
33598
+ }
33599
+ async beginRoot(input2) {
33600
+ const page = await this.messagePage(undefined, 1);
33601
+ if (this.stopping || !this.identityMatches())
33602
+ return { ok: false, reason: "closed" };
33603
+ const receipt = input2.terminalOwner?.startsWith("msg_") ? input2.terminalOwner : this.newMessageId();
33604
+ const root = {
33605
+ receipt,
33606
+ baseline: typeof page.data[0]?.id === "string" ? page.data[0].id : null,
33607
+ inputs: new Set,
33608
+ generation: 0,
33609
+ pending: 0
33610
+ };
33611
+ this.root = root;
33612
+ return this.admit(input2.text, "queue", receipt, root);
33613
+ }
33614
+ admit(text2, delivery, id3, root) {
33615
+ root.generation += 1;
33616
+ root.pending += 1;
33617
+ const run = this.v2AdmissionTail.then(async () => {
33618
+ await this.waitForLiveStream();
33619
+ if (this.root !== root || this.stopping || !this.identityMatches())
33620
+ return { ok: false, reason: "closed" };
33621
+ const { response, body } = await this.fetchJsonWithTimeout(`/api/session/${encodeURIComponent(this.sessionId)}/prompt`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id: id3, text: text2, delivery, resume: true }) }, "prompt admission");
33622
+ if (!response.ok)
33623
+ throw new OpenCodeHttpError(response.status, "prompt admission");
33624
+ const input2 = record5(record5(body)?.data);
33625
+ if (input2?.id !== id3 || input2.sessionID !== this.sessionId || input2.type !== "user" || input2.delivery !== delivery) {
33626
+ throw new OpenCodeProtocolError("OpenCode CLI v2 returned an invalid prompt receipt");
33627
+ }
33628
+ if (this.root !== root || this.stopping || !this.identityMatches())
33629
+ return { ok: false, reason: "closed" };
33630
+ root.inputs.add(id3);
33631
+ return { ok: true, acceptedAs: delivery === "steer" ? "steer" : "prompt", receipt: id3 };
33632
+ }).catch((error62) => {
33633
+ if (this.root === root && !this.stopping)
33634
+ this.protocolFailure("OpenCode CLI v2 prompt admission failed", error62);
33635
+ throw error62;
33636
+ }).finally(() => {
33637
+ root.pending -= 1;
33638
+ this.schedule();
33639
+ });
33640
+ this.v2AdmissionTail = run.then(() => {}, () => {});
33641
+ return run;
33642
+ }
33643
+ async messagePage(cursor, limit = 100) {
33644
+ const query = new URLSearchParams({ limit: String(limit), ...cursor ? { cursor } : { order: "desc" } });
33645
+ const { response, body } = await this.fetchJsonWithTimeout(`/api/session/${encodeURIComponent(this.sessionId)}/message?${query}`, { method: "GET" }, "message recovery");
33646
+ if (!response.ok)
33647
+ throw new OpenCodeHttpError(response.status, "message recovery");
33648
+ const page = record5(body);
33649
+ const cursors = record5(page?.cursor);
33650
+ if (!Array.isArray(page?.data) || !cursors || page.data.some((item) => typeof record5(item)?.id !== "string")) {
33651
+ throw new OpenCodeProtocolError("OpenCode CLI v2 returned an invalid message page");
33652
+ }
33653
+ if (cursors.next !== undefined && cursors.next !== null && typeof cursors.next !== "string")
33654
+ throw new OpenCodeProtocolError("OpenCode CLI v2 returned an invalid message cursor");
33655
+ return { data: page.data, next: typeof cursors.next === "string" ? cursors.next : undefined };
33656
+ }
33657
+ async messagesAfter(root) {
33658
+ const messages = [];
33659
+ const cursors = new Set;
33660
+ let cursor;
33661
+ for (;; ) {
33662
+ const page = await this.messagePage(cursor);
33663
+ if (this.root !== root || this.stopping)
33664
+ return [];
33665
+ for (const message2 of page.data) {
33666
+ if (message2.id === root.baseline)
33667
+ return messages.reverse();
33668
+ messages.push(message2);
33669
+ }
33670
+ if (!page.next) {
33671
+ if (root.baseline !== null)
33672
+ throw new OpenCodeProtocolError("OpenCode CLI v2 message recovery lost its root boundary");
33673
+ return messages.reverse();
33674
+ }
33675
+ if (cursors.has(page.next))
33676
+ throw new OpenCodeProtocolError("OpenCode CLI v2 message cursor did not advance");
33677
+ cursors.add(page.next);
33678
+ cursor = page.next;
33679
+ }
33680
+ }
33681
+ schedule() {
33682
+ if (!this.root || this.stopping || this.v2EvaluationTimer || this.v2Evaluating)
33683
+ return;
33684
+ this.v2EvaluationTimer = setTimeout(() => {
33685
+ this.v2EvaluationTimer = undefined;
33686
+ this.evaluate();
33687
+ }, this.options.activePollMs ?? 100);
33688
+ this.v2EvaluationTimer.unref?.();
33689
+ }
33690
+ async isActive() {
33691
+ const { response, body } = await this.fetchJsonWithTimeout("/api/session/active", { method: "GET" }, "active session query");
33692
+ if (!response.ok)
33693
+ throw new OpenCodeHttpError(response.status, "active session query");
33694
+ const active = record5(record5(body)?.data);
33695
+ if (!active)
33696
+ throw new OpenCodeProtocolError("OpenCode CLI v2 returned invalid active session data");
33697
+ return active[this.sessionId] !== undefined;
33698
+ }
33699
+ async evaluate() {
33700
+ const root = this.root;
33701
+ const identity = this.identity;
33702
+ if (!root || !identity || this.stopping || root.pending > 0 || root.inputs.size === 0 || this.v2Evaluating)
33703
+ return;
33704
+ this.v2Evaluating = true;
33705
+ const generation = root.generation;
33706
+ const current = () => this.root === root && root.generation === generation && root.pending === 0 && this.isIdentity(identity);
33707
+ try {
33708
+ if (await this.isActive())
33709
+ return;
33710
+ if (!current())
33711
+ return;
33712
+ const messages = await this.messagesAfter(root);
33713
+ if (!current())
33714
+ return;
33715
+ let lastInput = -1;
33716
+ const delivered = new Set;
33717
+ for (const [index2, message2] of messages.entries()) {
33718
+ if (message2.type === "user" && typeof message2.id === "string" && root.inputs.has(message2.id)) {
33719
+ delivered.add(message2.id);
33720
+ lastInput = index2;
33721
+ }
33722
+ }
33723
+ const idle = messages.slice(lastInput + 1).reverse().find((message2) => message2.type === "idle");
33724
+ if (delivered.size !== root.inputs.size) {
33725
+ const { response, body } = await this.fetchJsonWithTimeout(`/api/session/${encodeURIComponent(this.sessionId)}/inbox`, { method: "GET" }, "pending input recovery");
33726
+ if (!response.ok)
33727
+ throw new OpenCodeHttpError(response.status, "pending input recovery");
33728
+ const pending = record5(body)?.data;
33729
+ if (!Array.isArray(pending))
33730
+ throw new OpenCodeProtocolError("OpenCode CLI v2 returned invalid inbox data");
33731
+ if (!current())
33732
+ return;
33733
+ const retained = new Set(pending.map((item) => record5(item)?.id));
33734
+ for (const id3 of root.inputs) {
33735
+ if (delivered.has(id3) || retained.has(id3))
33736
+ continue;
33737
+ const recovered = await this.fetchJsonWithTimeout(`/api/session/${encodeURIComponent(this.sessionId)}/message/${encodeURIComponent(id3)}`, { method: "GET" }, "accepted input recovery");
33738
+ if (!current())
33739
+ return;
33740
+ if (recovered.response.status === 404)
33741
+ throw new OpenCodeProtocolError("OpenCode CLI v2 lost an accepted input from both inbox and messages");
33742
+ if (!recovered.response.ok)
33743
+ throw new OpenCodeHttpError(recovered.response.status, "accepted input recovery");
33744
+ const message2 = record5(record5(recovered.body)?.data);
33745
+ if (message2?.id !== id3 || message2.type !== "user")
33746
+ throw new OpenCodeProtocolError("OpenCode CLI v2 returned an invalid recovered input");
33747
+ }
33748
+ return;
33749
+ }
33750
+ for (const message2 of messages) {
33751
+ this.projectTools(message2);
33752
+ this.projectMessage(message2);
33753
+ }
33754
+ if (!idle || await this.isActive() || !current())
33755
+ return;
33756
+ if (!["succeeded", "failed", "interrupted"].includes(String(idle.outcome))) {
33757
+ throw new OpenCodeProtocolError("OpenCode CLI v2 returned an invalid idle outcome");
33758
+ }
33759
+ this.root = null;
33760
+ if (idle.outcome === "failed") {
33761
+ const error62 = record5(idle.error);
33762
+ this.events.emit("runtime_event", {
33763
+ kind: "error",
33764
+ code: "opencode.turn_failed",
33765
+ message: typeof error62?.message === "string" ? error62.message : "OpenCode CLI v2 execution failed"
33766
+ });
33767
+ }
33768
+ this.events.emit("runtime_event", {
33769
+ kind: "turn_end",
33770
+ sessionId: this.sessionId,
33771
+ turnOwner: root.receipt
33772
+ });
33773
+ } catch (error62) {
33774
+ if (!current() || this.stopping)
33775
+ return;
33776
+ if (error62 instanceof OpenCodeProtocolError || error62 instanceof OpenCodeHttpError && error62.status < 500) {
33777
+ this.protocolFailure("OpenCode CLI v2 durable message recovery failed", error62);
33778
+ } else
33779
+ this.diagnostic("warning", "OpenCode CLI v2 message recovery temporarily unavailable; keeping the root active");
33780
+ } finally {
33781
+ this.v2Evaluating = false;
33782
+ this.schedule();
33783
+ }
33784
+ }
33785
+ toolKey(messageId, id3) {
33786
+ return typeof messageId === "string" && typeof id3 === "string" ? `${messageId}:${id3}` : undefined;
33787
+ }
33788
+ projectTool(data) {
33789
+ const key = this.toolKey(data.assistantMessageID, data.id);
33790
+ if (!key || this.settledTools.has(key))
33791
+ return;
33792
+ const tool = this.liveTools.get(key) ?? { name: typeof data.name === "string" ? data.name : "OpenCode tool", called: false };
33793
+ if (tool.called)
33794
+ return;
33795
+ tool.called = true;
33796
+ this.liveTools.set(key, tool);
33797
+ this.events.emit("runtime_event", { kind: "tool_call", callId: key, name: tool.name, input: data.input });
33798
+ }
33799
+ finishTool(key) {
33800
+ if (!key || this.settledTools.has(key))
33801
+ return;
33802
+ const tool = this.liveTools.get(key);
33803
+ if (!tool?.called)
33804
+ return;
33805
+ this.liveTools.delete(key);
33806
+ this.settledTools.add(key);
33807
+ this.events.emit("runtime_event", { kind: "tool_output", callId: key, name: tool.name });
33808
+ }
33809
+ projectTools(message2) {
33810
+ if (message2.type !== "assistant" || !Array.isArray(message2.content) || !this.root || this.stopping)
33811
+ return;
33812
+ for (const value of message2.content) {
33813
+ const tool = record5(value);
33814
+ const state = record5(tool?.state);
33815
+ if (tool?.type !== "tool" || !["running", "completed", "error"].includes(String(state?.status)))
33816
+ continue;
33817
+ this.projectTool({ assistantMessageID: message2.id, id: tool.id, name: tool.name, input: state?.input });
33818
+ if (state?.status !== "running")
33819
+ this.finishTool(this.toolKey(message2.id, tool.id));
33820
+ }
33821
+ }
33822
+ projectMessage(message2) {
33823
+ if (message2.type !== "assistant" || typeof message2.id !== "string" || this.projectedMessages.has(message2.id))
33824
+ return;
33825
+ const time3 = record5(message2.time);
33826
+ if (time3?.completed === undefined)
33827
+ return;
33828
+ this.projectedMessages.add(message2.id);
33829
+ if (Array.isArray(message2.content))
33830
+ for (const value of message2.content) {
33831
+ const content = record5(value);
33832
+ if (typeof content?.text !== "string" || !content.text)
33833
+ continue;
33834
+ if (content.type === "text")
33835
+ this.events.emit("runtime_event", { kind: "assistant_message_completed", text: content.text });
33836
+ if (content.type === "reasoning")
33837
+ this.events.emit("runtime_event", { kind: "assistant_reasoning_completed", text: content.text });
33838
+ }
33839
+ const tokens = record5(message2.tokens);
33840
+ if (!tokens || !this.sessionId)
33841
+ return;
33842
+ const cache = record5(tokens.cache);
33843
+ const identity = { runtime: "opencode", backendSessionId: this.sessionId, providerRecordId: message2.id };
33844
+ const usage = this.usage.project({
33845
+ ...identity,
33846
+ source: "opencode.v2",
33847
+ input: tokens.input,
33848
+ output: tokens.output,
33849
+ reasoning: tokens.reasoning,
33850
+ cacheRead: cache?.read,
33851
+ cacheWrite: cache?.write,
33852
+ inputIncludesCache: false,
33853
+ outputIncludesReasoning: false
33854
+ });
33855
+ if (usage)
33856
+ this.events.emit("runtime_event", usage);
33857
+ this.usage.release(identity);
33858
+ }
33859
+ async interrupt(_input) {
33860
+ const root = this.root;
33861
+ if (!root || !this.ready || this.stopping || !this.identityMatches())
33862
+ return false;
33863
+ root.generation += 1;
33864
+ root.pending += 1;
33865
+ try {
33866
+ await this.requestNoContent(`/api/session/${encodeURIComponent(this.sessionId)}/interrupt?resume=false`, "POST", undefined, "session interrupt");
33867
+ return true;
33868
+ } catch (error62) {
33869
+ if (!this.stopping)
33870
+ this.stop({ reason: "interrupt_timeout", forceAfterMs: 0 });
33871
+ throw error62;
33872
+ } finally {
33873
+ root.pending -= 1;
33874
+ this.schedule();
33875
+ }
33876
+ }
33877
+ stop(input2 = {}) {
33878
+ this.root = null;
33879
+ if (this.v2EvaluationTimer)
33880
+ clearTimeout(this.v2EvaluationTimer);
33881
+ return super.stop(input2);
33882
+ }
33883
+ }
33884
+
33423
33885
  // agent-driver/dist/adapters/opencode/index.js
33424
33886
  function createOpenCodeMessageId() {
33425
33887
  return `msg_${randomBytes4(16).toString("hex")}`;
@@ -33427,6 +33889,7 @@ function createOpenCodeMessageId() {
33427
33889
 
33428
33890
  class OpenCodeDriver {
33429
33891
  outputProbe;
33892
+ versionProbe;
33430
33893
  id = "opencode";
33431
33894
  instructionDelivery = { kind: "workspace_file", canonical: "AGENTS.md", aliases: ["CLAUDE.md"] };
33432
33895
  execution = {
@@ -33435,17 +33898,29 @@ class OpenCodeDriver {
33435
33898
  wakeStart: "immediate",
33436
33899
  terminalOwnership: "transport_request"
33437
33900
  };
33438
- constructor(outputProbe = probeCommandOutput) {
33901
+ constructor(outputProbe = probeCommandOutput, versionProbe = probeCliRuntime) {
33439
33902
  this.outputProbe = outputProbe;
33903
+ this.versionProbe = versionProbe;
33440
33904
  }
33441
33905
  discoverRecentContext(request) {
33442
- return discoverOpenCodeRecentContext(request);
33906
+ if (request.recentProjectsTopK === 0)
33907
+ return discoverOpenCodeRecentContext(request);
33908
+ const result = this.versionProbe("opencode", {}, request.command);
33909
+ if (result.status !== "healthy")
33910
+ throw new Error("Installed OpenCode CLI could not report its version");
33911
+ return discoverOpenCodeRecentContext(request, { compatibility: resolveOpenCodeCompatibility(result.version) });
33443
33912
  }
33444
33913
  probe(command) {
33445
- const result = probeCliRuntime("opencode", {}, command);
33914
+ const result = this.versionProbe("opencode", {}, command);
33446
33915
  if (result.status !== "healthy")
33447
33916
  return result;
33448
- const spec = resolveSpawnSpec("opencode", ["models", "--pure"], command);
33917
+ let compatibility;
33918
+ try {
33919
+ compatibility = resolveOpenCodeCompatibility(result.version);
33920
+ } catch {
33921
+ return { ...result, status: "unhealthy", lastError: "unsupported_opencode_cli_version" };
33922
+ }
33923
+ const spec = resolveSpawnSpec("opencode", [...compatibility.modelArgs], command);
33449
33924
  const output2 = this.outputProbe(spec.command, spec.args);
33450
33925
  return {
33451
33926
  ...result,
@@ -33456,20 +33931,29 @@ class OpenCodeDriver {
33456
33931
  return createOpenCodeMessageId();
33457
33932
  }
33458
33933
  async openLane(ctx, options) {
33459
- return new OpenCodeServiceLane(this, ctx, {
33934
+ const command = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig).command;
33935
+ const result = this.versionProbe("opencode", {}, command);
33936
+ if (result.status !== "healthy")
33937
+ throw new Error("Installed OpenCode CLI could not report its version");
33938
+ const compatibility = resolveOpenCodeCompatibility(result.version);
33939
+ const factory = {
33940
+ spawnService: (launch, port, password) => this.spawnService(launch, port, password, compatibility)
33941
+ };
33942
+ const Lane = compatibility.major === 1 ? OpenCodeServiceLane : OpenCodeV2ServiceLane;
33943
+ return new Lane(factory, ctx, {
33460
33944
  onRawStdoutLine: options?.onRawStdoutLine
33461
33945
  });
33462
33946
  }
33463
- async spawnService(ctx, port, password) {
33947
+ async spawnService(ctx, port, password, compatibility = OPEN_CODE_V1) {
33464
33948
  const { spawnEnv } = await prepareCliTransport(ctx, {
33465
- OPENCODE_SERVER_PASSWORD: password
33949
+ OPENCODE_SERVER_PASSWORD: password,
33950
+ ...compatibility.major === 2 ? { OPENCODE_PASSWORD: password } : {}
33466
33951
  });
33467
33952
  const fields2 = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
33468
33953
  Object.assign(spawnEnv, await legacyOpenCodeApiEnvironment(spawnEnv, fields2.model));
33469
33954
  const override = fields2.command;
33470
33955
  const args = [
33471
- "serve",
33472
- "--pure",
33956
+ ...compatibility.serviceArgs,
33473
33957
  "--hostname",
33474
33958
  "127.0.0.1",
33475
33959
  "--port",
package/dist/index.js CHANGED
@@ -7065,14 +7065,16 @@ class OpenCodeServiceLane {
7065
7065
  if (!this.identity || this.identity.generation !== generation || this.isProcessClosed(this.identity.process)) {
7066
7066
  if (/EADDRINUSE|address already in use/i.test(this.stderrBuffer))
7067
7067
  throw new OpenCodePortBindError("OpenCode service port bind raced");
7068
- throw new Error("OpenCode service exited before readiness");
7068
+ const process2 = this.identity?.process;
7069
+ const detail = scrubDriverErrorMessage(this.stderrBuffer.replaceAll(this.password, "[redacted]"), "no native stderr");
7070
+ throw new Error(`OpenCode service exited before readiness (code=${process2?.exitCode ?? "unknown"}, signal=${process2?.signalCode ?? "none"}): ${detail}`);
7069
7071
  }
7070
7072
  try {
7071
7073
  const healthTimeoutMs = Math.max(1, Math.min(1000, deadline - Date.now(), this.options.requestTimeoutMs ?? REQUEST_TIMEOUT_MS));
7072
- const { response, body } = await this.fetchJsonWithTimeout("/global/health", { method: "GET" }, "health", healthTimeoutMs);
7074
+ const { response, body } = await this.fetchJsonWithTimeout(this.healthPath, { method: "GET" }, "health", healthTimeoutMs);
7073
7075
  if (response.ok) {
7074
7076
  const health = record4(body);
7075
- if (health?.healthy !== true) {
7077
+ if (!this.acceptsHealth(health)) {
7076
7078
  throw new OpenCodeIncompatibleError("Installed OpenCode service did not report healthy status");
7077
7079
  }
7078
7080
  await this.verifyOpenApi();
@@ -7089,12 +7091,27 @@ class OpenCodeServiceLane {
7089
7091
  throw new Error("OpenCode service readiness timed out");
7090
7092
  }
7091
7093
  async verifyOpenApi() {
7092
- const { response, body } = await this.fetchJsonWithTimeout("/doc", { method: "GET" }, "OpenAPI");
7094
+ const { response, body } = await this.fetchJsonWithTimeout(this.openApiPath, { method: "GET" }, "OpenAPI");
7093
7095
  if (!response.ok)
7094
7096
  throw new OpenCodeIncompatibleError("Installed OpenCode service does not expose its OpenAPI document");
7095
7097
  const document = record4(body);
7096
7098
  const paths = record4(document?.paths);
7097
- const required = [
7099
+ const required = this.requiredApiPaths;
7100
+ if (!paths || required.some((path9) => !record4(paths[path9]))) {
7101
+ throw new OpenCodeIncompatibleError("Installed OpenCode service is missing required session capabilities");
7102
+ }
7103
+ }
7104
+ get healthPath() {
7105
+ return "/global/health";
7106
+ }
7107
+ get openApiPath() {
7108
+ return "/doc";
7109
+ }
7110
+ acceptsHealth(health) {
7111
+ return health?.healthy === true;
7112
+ }
7113
+ get requiredApiPaths() {
7114
+ return [
7098
7115
  "/api/session",
7099
7116
  "/api/session/active",
7100
7117
  "/api/session/{sessionID}/prompt",
@@ -7106,12 +7123,12 @@ class OpenCodeServiceLane {
7106
7123
  "/api/session/{sessionID}/permission/{requestID}/reply",
7107
7124
  "/api/event"
7108
7125
  ];
7109
- if (!paths || required.some((path9) => !record4(paths[path9]))) {
7110
- throw new OpenCodeIncompatibleError("Installed OpenCode service is missing required v2 session capabilities");
7111
- }
7126
+ }
7127
+ get selectedModel() {
7128
+ return parseModelRef(resolveLaunchFieldsOrDefault(this.ctx.config.runtimeConfig).model);
7112
7129
  }
7113
7130
  async openSession(resumeId) {
7114
- const model = parseModelRef(resolveLaunchFieldsOrDefault(this.ctx.config.runtimeConfig).model);
7131
+ const model = this.selectedModel;
7115
7132
  if (resumeId) {
7116
7133
  const { response: response2, body: body2 } = await this.fetchJsonWithTimeout(`/api/session/${encodeURIComponent(resumeId)}`, { method: "GET" }, "session resume");
7117
7134
  if (response2.status === 404) {
@@ -7454,7 +7471,7 @@ class OpenCodeServiceLane {
7454
7471
  async handleLiveEvent(value) {
7455
7472
  const event = record4(value);
7456
7473
  const data = record4(event?.data);
7457
- if (event?.type !== "permission.v2.asked" || data?.sessionID !== this.sessionId)
7474
+ if (event?.type !== this.permissionEventType || data?.sessionID !== this.sessionId)
7458
7475
  return;
7459
7476
  if (typeof data.id !== "string" || !/^per/.test(data.id)) {
7460
7477
  throw new OpenCodeProtocolError("OpenCode emitted a malformed permission request");
@@ -7477,13 +7494,19 @@ class OpenCodeServiceLane {
7477
7494
  }
7478
7495
  }
7479
7496
  }
7497
+ get permissionEventType() {
7498
+ return "permission.v2.asked";
7499
+ }
7500
+ get permissionReply() {
7501
+ return { reply: "once" };
7502
+ }
7480
7503
  async replyPermission(requestId) {
7481
7504
  if (this.handledPermissions.has(requestId))
7482
7505
  return;
7483
7506
  const existing = this.permissionFlights.get(requestId);
7484
7507
  if (existing)
7485
7508
  return existing;
7486
- const flight = this.requestNoContent(`/api/session/${encodeURIComponent(this.sessionId)}/permission/${encodeURIComponent(requestId)}/reply`, "POST", { reply: "once" }, "permission reply").catch((error) => {
7509
+ const flight = this.requestNoContent(`/api/session/${encodeURIComponent(this.sessionId)}/permission/${encodeURIComponent(requestId)}/reply`, "POST", this.permissionReply, "permission reply").catch((error) => {
7487
7510
  if (!(error instanceof OpenCodeHttpError) || error.status !== 404)
7488
7511
  throw error;
7489
7512
  }).then(() => {
@@ -7860,6 +7883,28 @@ async function legacyOpenCodeApiEnvironment(environment, model) {
7860
7883
  return credential?.type === "api" && typeof credential.key === "string" && credential.key.trim() ? { [name]: credential.key } : {};
7861
7884
  }
7862
7885
 
7886
+ // agent-driver/dist/adapters/opencode/compatibility.js
7887
+ var OPEN_CODE_V1 = {
7888
+ major: 1,
7889
+ modelArgs: ["models", "--pure"],
7890
+ serviceArgs: ["serve", "--pure"],
7891
+ sessionListArgs: (maxCount) => ["session", "list", "--format", "json", "--max-count", String(maxCount), "--pure"]
7892
+ };
7893
+ var OPEN_CODE_V2 = {
7894
+ major: 2,
7895
+ modelArgs: ["models", "--standalone"],
7896
+ serviceArgs: ["serve"],
7897
+ sessionListArgs: (maxCount) => ["api", "--standalone", "GET", `/api/session?parentID=null&order=desc&limit=${maxCount}`]
7898
+ };
7899
+ function resolveOpenCodeCompatibility(version) {
7900
+ const match = version?.trim().match(/^(?:opencode\s+)?v?(\d+)\.\d+\.\d+(?:[-+][\w.-]+)?$/i);
7901
+ if (match?.[1] === "1")
7902
+ return OPEN_CODE_V1;
7903
+ if (match?.[1] === "2")
7904
+ return OPEN_CODE_V2;
7905
+ throw new Error("Installed OpenCode CLI version is unrecognized or has an unsupported major version");
7906
+ }
7907
+
7863
7908
  // agent-driver/dist/adapters/opencode/recent-context.js
7864
7909
  var OPENCODE_DISCOVERY_TIMEOUT_MS = 15000;
7865
7910
  var OPENCODE_DISCOVERY_OUTPUT_MAX_BYTES = 4 * 1024 * 1024;
@@ -7893,6 +7938,7 @@ async function discoverOpenCodeRecentContext(request, dependencies = {}) {
7893
7938
  const collector = new RecentContextCollector(request, "unavailable");
7894
7939
  if (collector.satisfied)
7895
7940
  return collector.result();
7941
+ const compatibility = dependencies.compatibility ?? OPEN_CODE_V1;
7896
7942
  const startedAt = Date.now();
7897
7943
  const timeoutMs = dependencies.timeoutMs ?? OPENCODE_DISCOVERY_TIMEOUT_MS;
7898
7944
  let remainingOutputBytes = dependencies.outputMaxBytes ?? OPENCODE_DISCOVERY_OUTPUT_MAX_BYTES;
@@ -7901,7 +7947,7 @@ async function discoverOpenCodeRecentContext(request, dependencies = {}) {
7901
7947
  const remainingTimeoutMs = timeoutMs - (Date.now() - startedAt);
7902
7948
  if (remainingTimeoutMs <= 0)
7903
7949
  throw new Error("OpenCode discovery timed out");
7904
- const spec = resolveSpawnSpec("opencode", ["session", "list", "--format", "json", "--max-count", String(maxCount), "--pure"], request.command);
7950
+ const spec = resolveSpawnSpec("opencode", [...compatibility.sessionListArgs(maxCount)], request.command);
7905
7951
  const commandOutput = await collectCommandOutput(spec.command, spec.args, spec.shell, dependencies, remainingTimeoutMs, remainingOutputBytes);
7906
7952
  remainingOutputBytes -= commandOutput.outputBytes;
7907
7953
  let parsed;
@@ -7910,14 +7956,17 @@ async function discoverOpenCodeRecentContext(request, dependencies = {}) {
7910
7956
  } catch {
7911
7957
  throw new Error("OpenCode session listing returned invalid JSON");
7912
7958
  }
7959
+ if (compatibility.major === 2)
7960
+ parsed = asRecord(parsed)?.data;
7913
7961
  if (!Array.isArray(parsed))
7914
7962
  throw new Error("OpenCode session listing returned an invalid response");
7915
7963
  for (const value of parsed) {
7916
7964
  const session = asRecord(value);
7917
7965
  if (!session)
7918
7966
  continue;
7919
- const modifiedAt = session.updated ?? session.updatedAt;
7920
- collector.add({ projectPath: session.directory, modifiedAt });
7967
+ const modifiedAt = compatibility.major === 2 ? asRecord(session.time)?.updated : session.updated ?? session.updatedAt;
7968
+ const projectPath = compatibility.major === 2 ? asRecord(session.location)?.directory : session.directory;
7969
+ collector.add({ projectPath, modifiedAt });
7921
7970
  }
7922
7971
  if (collector.satisfied || parsed.length < maxCount)
7923
7972
  break;
@@ -7929,6 +7978,419 @@ async function discoverOpenCodeRecentContext(request, dependencies = {}) {
7929
7978
  return collector.result();
7930
7979
  }
7931
7980
 
7981
+ // agent-driver/dist/adapters/opencode/v2-service-lane.js
7982
+ class OpenCodeV2ServiceLane extends OpenCodeServiceLane {
7983
+ root = null;
7984
+ v2AdmissionTail = Promise.resolve();
7985
+ v2EvaluationTimer;
7986
+ v2Evaluating = false;
7987
+ projectedMessages = new Set;
7988
+ liveTools = new Map;
7989
+ settledTools = new Set;
7990
+ usage = new SettledUsageProjector;
7991
+ get healthPath() {
7992
+ return "/api/info";
7993
+ }
7994
+ get openApiPath() {
7995
+ return "/openapi.json";
7996
+ }
7997
+ acceptsHealth(health) {
7998
+ if (typeof health?.version !== "string" || !Number.isInteger(health.pid) || Number(health.pid) < 1)
7999
+ return false;
8000
+ try {
8001
+ return resolveOpenCodeCompatibility(health.version).major === 2;
8002
+ } catch {
8003
+ return false;
8004
+ }
8005
+ }
8006
+ get requiredApiPaths() {
8007
+ return [
8008
+ "/api/session",
8009
+ "/api/session/active",
8010
+ "/api/session/{sessionID}",
8011
+ "/api/session/{sessionID}/prompt",
8012
+ "/api/session/{sessionID}/inbox",
8013
+ "/api/session/{sessionID}/message",
8014
+ "/api/session/{sessionID}/message/{messageID}",
8015
+ "/api/session/{sessionID}/interrupt",
8016
+ "/api/session/{sessionID}/model",
8017
+ "/api/session/{sessionID}/permission",
8018
+ "/api/session/{sessionID}/permission/{requestID}/reply",
8019
+ "/api/event"
8020
+ ];
8021
+ }
8022
+ get selectedModel() {
8023
+ const model = super.selectedModel;
8024
+ if (typeof model?.id !== "string")
8025
+ return model;
8026
+ const hash = model.id.lastIndexOf("#");
8027
+ return hash > 0 && hash < model.id.length - 1 ? { ...model, id: model.id.slice(0, hash), variant: model.id.slice(hash + 1) } : model;
8028
+ }
8029
+ get permissionEventType() {
8030
+ return "permission.asked";
8031
+ }
8032
+ get permissionReply() {
8033
+ return { decision: "once" };
8034
+ }
8035
+ protocolFailure(message, cause) {
8036
+ super.protocolFailure(cause === undefined ? message : `${message}: ${scrubDriverErrorMessage(cause)}`);
8037
+ }
8038
+ async primeHistory() {}
8039
+ startStreams() {
8040
+ const identity = this.identity;
8041
+ this.liveTask = this.runLiveStream(identity).catch((error) => {
8042
+ if (!this.stopping)
8043
+ this.protocolFailure("OpenCode CLI v2 live stream failed", error);
8044
+ });
8045
+ }
8046
+ async recoverPendingPermissions() {
8047
+ await super.recoverPendingPermissions();
8048
+ const root = this.root;
8049
+ if (root)
8050
+ for (const message of await this.messagesAfter(root))
8051
+ this.projectTools(message);
8052
+ this.schedule();
8053
+ }
8054
+ async handleLiveEvent(value) {
8055
+ await super.handleLiveEvent(value);
8056
+ const event = record4(value);
8057
+ const data = record4(event?.data);
8058
+ if (data?.sessionID !== this.sessionId || !this.root)
8059
+ return;
8060
+ switch (event?.type) {
8061
+ case "session.step.started":
8062
+ this.events.emit("runtime_event", {
8063
+ kind: "internal_progress",
8064
+ source: "opencode.service",
8065
+ itemType: "step_started"
8066
+ });
8067
+ break;
8068
+ case "session.tool.input.started": {
8069
+ const key = this.toolKey(data.assistantMessageID, data.id);
8070
+ if (key && typeof data.name === "string" && !this.liveTools.has(key) && !this.settledTools.has(key)) {
8071
+ this.liveTools.set(key, { name: data.name, called: false });
8072
+ }
8073
+ break;
8074
+ }
8075
+ case "session.tool.called":
8076
+ this.projectTool(data);
8077
+ break;
8078
+ case "session.tool.success":
8079
+ case "session.tool.failed": {
8080
+ this.finishTool(this.toolKey(data.assistantMessageID, data.id));
8081
+ break;
8082
+ }
8083
+ case "session.compaction.started":
8084
+ this.events.emit("runtime_event", { kind: "compaction_started" });
8085
+ break;
8086
+ case "session.compaction.failed":
8087
+ case "session.compaction.ended":
8088
+ this.events.emit("runtime_event", { kind: "compaction_finished" });
8089
+ break;
8090
+ default:
8091
+ break;
8092
+ }
8093
+ this.schedule();
8094
+ }
8095
+ async send(input) {
8096
+ if (!this.ready || this.stopping || !this.sessionId || !this.identityMatches())
8097
+ return { ok: false, reason: "closed" };
8098
+ if (input.mode === "idle") {
8099
+ if (this.root)
8100
+ return { ok: false, reason: "runtime_busy" };
8101
+ return this.beginRoot(input);
8102
+ }
8103
+ const root = this.root;
8104
+ if (!root)
8105
+ return { ok: false, reason: "not_ready" };
8106
+ return this.admit(input.text, "steer", this.newMessageId(), root);
8107
+ }
8108
+ async beginRoot(input) {
8109
+ const page = await this.messagePage(undefined, 1);
8110
+ if (this.stopping || !this.identityMatches())
8111
+ return { ok: false, reason: "closed" };
8112
+ const receipt = input.terminalOwner?.startsWith("msg_") ? input.terminalOwner : this.newMessageId();
8113
+ const root = {
8114
+ receipt,
8115
+ baseline: typeof page.data[0]?.id === "string" ? page.data[0].id : null,
8116
+ inputs: new Set,
8117
+ generation: 0,
8118
+ pending: 0
8119
+ };
8120
+ this.root = root;
8121
+ return this.admit(input.text, "queue", receipt, root);
8122
+ }
8123
+ admit(text, delivery, id, root) {
8124
+ root.generation += 1;
8125
+ root.pending += 1;
8126
+ const run = this.v2AdmissionTail.then(async () => {
8127
+ await this.waitForLiveStream();
8128
+ if (this.root !== root || this.stopping || !this.identityMatches())
8129
+ return { ok: false, reason: "closed" };
8130
+ const { response, body } = await this.fetchJsonWithTimeout(`/api/session/${encodeURIComponent(this.sessionId)}/prompt`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id, text, delivery, resume: true }) }, "prompt admission");
8131
+ if (!response.ok)
8132
+ throw new OpenCodeHttpError(response.status, "prompt admission");
8133
+ const input = record4(record4(body)?.data);
8134
+ if (input?.id !== id || input.sessionID !== this.sessionId || input.type !== "user" || input.delivery !== delivery) {
8135
+ throw new OpenCodeProtocolError("OpenCode CLI v2 returned an invalid prompt receipt");
8136
+ }
8137
+ if (this.root !== root || this.stopping || !this.identityMatches())
8138
+ return { ok: false, reason: "closed" };
8139
+ root.inputs.add(id);
8140
+ return { ok: true, acceptedAs: delivery === "steer" ? "steer" : "prompt", receipt: id };
8141
+ }).catch((error) => {
8142
+ if (this.root === root && !this.stopping)
8143
+ this.protocolFailure("OpenCode CLI v2 prompt admission failed", error);
8144
+ throw error;
8145
+ }).finally(() => {
8146
+ root.pending -= 1;
8147
+ this.schedule();
8148
+ });
8149
+ this.v2AdmissionTail = run.then(() => {}, () => {});
8150
+ return run;
8151
+ }
8152
+ async messagePage(cursor, limit = 100) {
8153
+ const query = new URLSearchParams({ limit: String(limit), ...cursor ? { cursor } : { order: "desc" } });
8154
+ const { response, body } = await this.fetchJsonWithTimeout(`/api/session/${encodeURIComponent(this.sessionId)}/message?${query}`, { method: "GET" }, "message recovery");
8155
+ if (!response.ok)
8156
+ throw new OpenCodeHttpError(response.status, "message recovery");
8157
+ const page = record4(body);
8158
+ const cursors = record4(page?.cursor);
8159
+ if (!Array.isArray(page?.data) || !cursors || page.data.some((item) => typeof record4(item)?.id !== "string")) {
8160
+ throw new OpenCodeProtocolError("OpenCode CLI v2 returned an invalid message page");
8161
+ }
8162
+ if (cursors.next !== undefined && cursors.next !== null && typeof cursors.next !== "string")
8163
+ throw new OpenCodeProtocolError("OpenCode CLI v2 returned an invalid message cursor");
8164
+ return { data: page.data, next: typeof cursors.next === "string" ? cursors.next : undefined };
8165
+ }
8166
+ async messagesAfter(root) {
8167
+ const messages = [];
8168
+ const cursors = new Set;
8169
+ let cursor;
8170
+ for (;; ) {
8171
+ const page = await this.messagePage(cursor);
8172
+ if (this.root !== root || this.stopping)
8173
+ return [];
8174
+ for (const message of page.data) {
8175
+ if (message.id === root.baseline)
8176
+ return messages.reverse();
8177
+ messages.push(message);
8178
+ }
8179
+ if (!page.next) {
8180
+ if (root.baseline !== null)
8181
+ throw new OpenCodeProtocolError("OpenCode CLI v2 message recovery lost its root boundary");
8182
+ return messages.reverse();
8183
+ }
8184
+ if (cursors.has(page.next))
8185
+ throw new OpenCodeProtocolError("OpenCode CLI v2 message cursor did not advance");
8186
+ cursors.add(page.next);
8187
+ cursor = page.next;
8188
+ }
8189
+ }
8190
+ schedule() {
8191
+ if (!this.root || this.stopping || this.v2EvaluationTimer || this.v2Evaluating)
8192
+ return;
8193
+ this.v2EvaluationTimer = setTimeout(() => {
8194
+ this.v2EvaluationTimer = undefined;
8195
+ this.evaluate();
8196
+ }, this.options.activePollMs ?? 100);
8197
+ this.v2EvaluationTimer.unref?.();
8198
+ }
8199
+ async isActive() {
8200
+ const { response, body } = await this.fetchJsonWithTimeout("/api/session/active", { method: "GET" }, "active session query");
8201
+ if (!response.ok)
8202
+ throw new OpenCodeHttpError(response.status, "active session query");
8203
+ const active = record4(record4(body)?.data);
8204
+ if (!active)
8205
+ throw new OpenCodeProtocolError("OpenCode CLI v2 returned invalid active session data");
8206
+ return active[this.sessionId] !== undefined;
8207
+ }
8208
+ async evaluate() {
8209
+ const root = this.root;
8210
+ const identity = this.identity;
8211
+ if (!root || !identity || this.stopping || root.pending > 0 || root.inputs.size === 0 || this.v2Evaluating)
8212
+ return;
8213
+ this.v2Evaluating = true;
8214
+ const generation = root.generation;
8215
+ const current = () => this.root === root && root.generation === generation && root.pending === 0 && this.isIdentity(identity);
8216
+ try {
8217
+ if (await this.isActive())
8218
+ return;
8219
+ if (!current())
8220
+ return;
8221
+ const messages = await this.messagesAfter(root);
8222
+ if (!current())
8223
+ return;
8224
+ let lastInput = -1;
8225
+ const delivered = new Set;
8226
+ for (const [index, message] of messages.entries()) {
8227
+ if (message.type === "user" && typeof message.id === "string" && root.inputs.has(message.id)) {
8228
+ delivered.add(message.id);
8229
+ lastInput = index;
8230
+ }
8231
+ }
8232
+ const idle = messages.slice(lastInput + 1).reverse().find((message) => message.type === "idle");
8233
+ if (delivered.size !== root.inputs.size) {
8234
+ const { response, body } = await this.fetchJsonWithTimeout(`/api/session/${encodeURIComponent(this.sessionId)}/inbox`, { method: "GET" }, "pending input recovery");
8235
+ if (!response.ok)
8236
+ throw new OpenCodeHttpError(response.status, "pending input recovery");
8237
+ const pending = record4(body)?.data;
8238
+ if (!Array.isArray(pending))
8239
+ throw new OpenCodeProtocolError("OpenCode CLI v2 returned invalid inbox data");
8240
+ if (!current())
8241
+ return;
8242
+ const retained = new Set(pending.map((item) => record4(item)?.id));
8243
+ for (const id of root.inputs) {
8244
+ if (delivered.has(id) || retained.has(id))
8245
+ continue;
8246
+ const recovered = await this.fetchJsonWithTimeout(`/api/session/${encodeURIComponent(this.sessionId)}/message/${encodeURIComponent(id)}`, { method: "GET" }, "accepted input recovery");
8247
+ if (!current())
8248
+ return;
8249
+ if (recovered.response.status === 404)
8250
+ throw new OpenCodeProtocolError("OpenCode CLI v2 lost an accepted input from both inbox and messages");
8251
+ if (!recovered.response.ok)
8252
+ throw new OpenCodeHttpError(recovered.response.status, "accepted input recovery");
8253
+ const message = record4(record4(recovered.body)?.data);
8254
+ if (message?.id !== id || message.type !== "user")
8255
+ throw new OpenCodeProtocolError("OpenCode CLI v2 returned an invalid recovered input");
8256
+ }
8257
+ return;
8258
+ }
8259
+ for (const message of messages) {
8260
+ this.projectTools(message);
8261
+ this.projectMessage(message);
8262
+ }
8263
+ if (!idle || await this.isActive() || !current())
8264
+ return;
8265
+ if (!["succeeded", "failed", "interrupted"].includes(String(idle.outcome))) {
8266
+ throw new OpenCodeProtocolError("OpenCode CLI v2 returned an invalid idle outcome");
8267
+ }
8268
+ this.root = null;
8269
+ if (idle.outcome === "failed") {
8270
+ const error = record4(idle.error);
8271
+ this.events.emit("runtime_event", {
8272
+ kind: "error",
8273
+ code: "opencode.turn_failed",
8274
+ message: typeof error?.message === "string" ? error.message : "OpenCode CLI v2 execution failed"
8275
+ });
8276
+ }
8277
+ this.events.emit("runtime_event", {
8278
+ kind: "turn_end",
8279
+ sessionId: this.sessionId,
8280
+ turnOwner: root.receipt
8281
+ });
8282
+ } catch (error) {
8283
+ if (!current() || this.stopping)
8284
+ return;
8285
+ if (error instanceof OpenCodeProtocolError || error instanceof OpenCodeHttpError && error.status < 500) {
8286
+ this.protocolFailure("OpenCode CLI v2 durable message recovery failed", error);
8287
+ } else
8288
+ this.diagnostic("warning", "OpenCode CLI v2 message recovery temporarily unavailable; keeping the root active");
8289
+ } finally {
8290
+ this.v2Evaluating = false;
8291
+ this.schedule();
8292
+ }
8293
+ }
8294
+ toolKey(messageId, id) {
8295
+ return typeof messageId === "string" && typeof id === "string" ? `${messageId}:${id}` : undefined;
8296
+ }
8297
+ projectTool(data) {
8298
+ const key = this.toolKey(data.assistantMessageID, data.id);
8299
+ if (!key || this.settledTools.has(key))
8300
+ return;
8301
+ const tool = this.liveTools.get(key) ?? { name: typeof data.name === "string" ? data.name : "OpenCode tool", called: false };
8302
+ if (tool.called)
8303
+ return;
8304
+ tool.called = true;
8305
+ this.liveTools.set(key, tool);
8306
+ this.events.emit("runtime_event", { kind: "tool_call", callId: key, name: tool.name, input: data.input });
8307
+ }
8308
+ finishTool(key) {
8309
+ if (!key || this.settledTools.has(key))
8310
+ return;
8311
+ const tool = this.liveTools.get(key);
8312
+ if (!tool?.called)
8313
+ return;
8314
+ this.liveTools.delete(key);
8315
+ this.settledTools.add(key);
8316
+ this.events.emit("runtime_event", { kind: "tool_output", callId: key, name: tool.name });
8317
+ }
8318
+ projectTools(message) {
8319
+ if (message.type !== "assistant" || !Array.isArray(message.content) || !this.root || this.stopping)
8320
+ return;
8321
+ for (const value of message.content) {
8322
+ const tool = record4(value);
8323
+ const state = record4(tool?.state);
8324
+ if (tool?.type !== "tool" || !["running", "completed", "error"].includes(String(state?.status)))
8325
+ continue;
8326
+ this.projectTool({ assistantMessageID: message.id, id: tool.id, name: tool.name, input: state?.input });
8327
+ if (state?.status !== "running")
8328
+ this.finishTool(this.toolKey(message.id, tool.id));
8329
+ }
8330
+ }
8331
+ projectMessage(message) {
8332
+ if (message.type !== "assistant" || typeof message.id !== "string" || this.projectedMessages.has(message.id))
8333
+ return;
8334
+ const time = record4(message.time);
8335
+ if (time?.completed === undefined)
8336
+ return;
8337
+ this.projectedMessages.add(message.id);
8338
+ if (Array.isArray(message.content))
8339
+ for (const value of message.content) {
8340
+ const content = record4(value);
8341
+ if (typeof content?.text !== "string" || !content.text)
8342
+ continue;
8343
+ if (content.type === "text")
8344
+ this.events.emit("runtime_event", { kind: "assistant_message_completed", text: content.text });
8345
+ if (content.type === "reasoning")
8346
+ this.events.emit("runtime_event", { kind: "assistant_reasoning_completed", text: content.text });
8347
+ }
8348
+ const tokens = record4(message.tokens);
8349
+ if (!tokens || !this.sessionId)
8350
+ return;
8351
+ const cache = record4(tokens.cache);
8352
+ const identity = { runtime: "opencode", backendSessionId: this.sessionId, providerRecordId: message.id };
8353
+ const usage = this.usage.project({
8354
+ ...identity,
8355
+ source: "opencode.v2",
8356
+ input: tokens.input,
8357
+ output: tokens.output,
8358
+ reasoning: tokens.reasoning,
8359
+ cacheRead: cache?.read,
8360
+ cacheWrite: cache?.write,
8361
+ inputIncludesCache: false,
8362
+ outputIncludesReasoning: false
8363
+ });
8364
+ if (usage)
8365
+ this.events.emit("runtime_event", usage);
8366
+ this.usage.release(identity);
8367
+ }
8368
+ async interrupt(_input) {
8369
+ const root = this.root;
8370
+ if (!root || !this.ready || this.stopping || !this.identityMatches())
8371
+ return false;
8372
+ root.generation += 1;
8373
+ root.pending += 1;
8374
+ try {
8375
+ await this.requestNoContent(`/api/session/${encodeURIComponent(this.sessionId)}/interrupt?resume=false`, "POST", undefined, "session interrupt");
8376
+ return true;
8377
+ } catch (error) {
8378
+ if (!this.stopping)
8379
+ this.stop({ reason: "interrupt_timeout", forceAfterMs: 0 });
8380
+ throw error;
8381
+ } finally {
8382
+ root.pending -= 1;
8383
+ this.schedule();
8384
+ }
8385
+ }
8386
+ stop(input = {}) {
8387
+ this.root = null;
8388
+ if (this.v2EvaluationTimer)
8389
+ clearTimeout(this.v2EvaluationTimer);
8390
+ return super.stop(input);
8391
+ }
8392
+ }
8393
+
7932
8394
  // agent-driver/dist/adapters/opencode/index.js
7933
8395
  function createOpenCodeMessageId() {
7934
8396
  return `msg_${randomBytes4(16).toString("hex")}`;
@@ -7936,6 +8398,7 @@ function createOpenCodeMessageId() {
7936
8398
 
7937
8399
  class OpenCodeDriver {
7938
8400
  outputProbe;
8401
+ versionProbe;
7939
8402
  id = "opencode";
7940
8403
  instructionDelivery = { kind: "workspace_file", canonical: "AGENTS.md", aliases: ["CLAUDE.md"] };
7941
8404
  execution = {
@@ -7944,17 +8407,29 @@ class OpenCodeDriver {
7944
8407
  wakeStart: "immediate",
7945
8408
  terminalOwnership: "transport_request"
7946
8409
  };
7947
- constructor(outputProbe = probeCommandOutput) {
8410
+ constructor(outputProbe = probeCommandOutput, versionProbe = probeCliRuntime) {
7948
8411
  this.outputProbe = outputProbe;
8412
+ this.versionProbe = versionProbe;
7949
8413
  }
7950
8414
  discoverRecentContext(request) {
7951
- return discoverOpenCodeRecentContext(request);
8415
+ if (request.recentProjectsTopK === 0)
8416
+ return discoverOpenCodeRecentContext(request);
8417
+ const result = this.versionProbe("opencode", {}, request.command);
8418
+ if (result.status !== "healthy")
8419
+ throw new Error("Installed OpenCode CLI could not report its version");
8420
+ return discoverOpenCodeRecentContext(request, { compatibility: resolveOpenCodeCompatibility(result.version) });
7952
8421
  }
7953
8422
  probe(command) {
7954
- const result = probeCliRuntime("opencode", {}, command);
8423
+ const result = this.versionProbe("opencode", {}, command);
7955
8424
  if (result.status !== "healthy")
7956
8425
  return result;
7957
- const spec = resolveSpawnSpec("opencode", ["models", "--pure"], command);
8426
+ let compatibility;
8427
+ try {
8428
+ compatibility = resolveOpenCodeCompatibility(result.version);
8429
+ } catch {
8430
+ return { ...result, status: "unhealthy", lastError: "unsupported_opencode_cli_version" };
8431
+ }
8432
+ const spec = resolveSpawnSpec("opencode", [...compatibility.modelArgs], command);
7958
8433
  const output = this.outputProbe(spec.command, spec.args);
7959
8434
  return {
7960
8435
  ...result,
@@ -7965,20 +8440,29 @@ class OpenCodeDriver {
7965
8440
  return createOpenCodeMessageId();
7966
8441
  }
7967
8442
  async openLane(ctx, options) {
7968
- return new OpenCodeServiceLane(this, ctx, {
8443
+ const command = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig).command;
8444
+ const result = this.versionProbe("opencode", {}, command);
8445
+ if (result.status !== "healthy")
8446
+ throw new Error("Installed OpenCode CLI could not report its version");
8447
+ const compatibility = resolveOpenCodeCompatibility(result.version);
8448
+ const factory = {
8449
+ spawnService: (launch, port, password) => this.spawnService(launch, port, password, compatibility)
8450
+ };
8451
+ const Lane = compatibility.major === 1 ? OpenCodeServiceLane : OpenCodeV2ServiceLane;
8452
+ return new Lane(factory, ctx, {
7969
8453
  onRawStdoutLine: options?.onRawStdoutLine
7970
8454
  });
7971
8455
  }
7972
- async spawnService(ctx, port, password) {
8456
+ async spawnService(ctx, port, password, compatibility = OPEN_CODE_V1) {
7973
8457
  const { spawnEnv } = await prepareCliTransport(ctx, {
7974
- OPENCODE_SERVER_PASSWORD: password
8458
+ OPENCODE_SERVER_PASSWORD: password,
8459
+ ...compatibility.major === 2 ? { OPENCODE_PASSWORD: password } : {}
7975
8460
  });
7976
8461
  const fields2 = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
7977
8462
  Object.assign(spawnEnv, await legacyOpenCodeApiEnvironment(spawnEnv, fields2.model));
7978
8463
  const override = fields2.command;
7979
8464
  const args = [
7980
- "serve",
7981
- "--pure",
8465
+ ...compatibility.serviceArgs,
7982
8466
  "--hostname",
7983
8467
  "127.0.0.1",
7984
8468
  "--port",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alook/daemon",
3
- "version": "0.1.50",
3
+ "version": "0.1.51",
4
4
  "description": "Alook agent daemon — host-side runtime backend, process manager, credential proxy, and control plane.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/alookai/alook#readme",