@opencode-ai/client 0.0.0-dev-18592 → 0.0.0-dev-18598

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.
@@ -1,4 +1,4 @@
1
- import type { AgentInfo, CommandInfo, FormCancelInput, FormInfo, FormReplyInput, IntegrationInfo, LocationRef, LocationGetOutput, McpResource, McpServer, ModelInfo, ModelRef, PermissionSavedInfo, PermissionRequest, PermissionReplyInput, Project, ProviderInfo, ReferenceInfo, SessionMessageInfo, SessionInfo, SessionInboxInfo, ShellInfo, SkillInfo, VcsInfo, OpenCodeEvent, OpenCodeClient, WebSearchProvider } from "../promise";
1
+ import type { AgentInfo, CommandInfo, FormCancelInput, FormInfo, FormReplyInput, IntegrationInfo, LocationRef, LocationGetOutput, McpResource, McpServer, ModelInfo, ModelRef, PermissionSavedInfo, PermissionRequest, PermissionReplyInput, Project, ProviderInfo, ReferenceInfo, SessionMessageInfo, SessionInfo, SessionInboxInfo, SessionInboxCompaction, ShellInfo, SkillInfo, VcsInfo, OpenCodeEvent, OpenCodeClient, WebSearchProvider } from "../promise";
2
2
  import { type SessionPromptInput } from "../promise";
3
3
  export type DataSessionStatus = "idle" | "running";
4
4
  export type CreateDataInput = {
@@ -62,8 +62,13 @@ export declare function createData(config: CreateDataInput): {
62
62
  id: string;
63
63
  request: Promise<SessionInfo>;
64
64
  };
65
+ compact(input: {
66
+ sessionID: string;
67
+ model?: ModelRef;
68
+ }): Promise<SessionInboxCompaction>;
65
69
  prompt(input: SessionPromptInput & {
66
70
  gate?: Promise<unknown>;
71
+ prepare?: () => Promise<unknown>;
67
72
  }): Promise<import("../promise").SessionInboxUser>;
68
73
  sync(sessionID: string, options?: {
69
74
  children?: boolean;
@@ -164,12 +164,11 @@ export function createData(config) {
164
164
  return;
165
165
  setStore("session", "pending", sessionID, index, { ...item, delivery });
166
166
  }
167
- // Inbox IDs of optimistic prompt admissions still awaiting their durable
168
- // echo. This is the one deliberate piece of in-flight bookkeeping in this
169
- // layer: it exists so a rejection only rolls back rows the server never
170
- // acknowledged, and so a concurrent pending re-fetch cannot wipe a row the
171
- // server does not know about yet. Entries clear on the enqueued echo or on
172
- // rollback — not on POST success, which typically precedes the echo.
167
+ // Inbox IDs of optimistic admissions awaiting acknowledgement, so rejection
168
+ // only rolls back unacknowledged rows and a pending re-fetch cannot wipe a
169
+ // row the server does not know about yet. Prompts clear on their durable
170
+ // echo, positive pending read, or rollback; compactions also reconcile the
171
+ // POST's canonical ID.
173
172
  const outbox = new Set();
174
173
  // Session IDs of optimistic create admissions still awaiting acknowledgement
175
174
  // (the session.created echo or the create response itself). A failed create
@@ -180,11 +179,12 @@ export function createData(config) {
180
179
  // this so a prompt sent to a still-creating session waits for the session
181
180
  // to exist server-side instead of failing with "not found".
182
181
  const creating = new Map();
183
- // Per-session send chain: prompts must be admitted in submission order,
184
- // and HTTP gives no ordering across concurrent POSTs. Each prompt waits
185
- // for the previous prompt's POST (settled, so one failure does not block
186
- // the next) before sending its own.
182
+ // Per-session send chain: prompts and compactions must be admitted in
183
+ // submission order. Each waits for the previous POST to settle, so one
184
+ // failure does not block the next.
187
185
  const sending = new Map();
186
+ const compacting = new Map();
187
+ onCleanup(() => compacting.clear());
188
188
  // Register `promise` under `key` until it settles. A later registration
189
189
  // replaces an earlier one; settlement only clears its own entry.
190
190
  function track(map, key, promise) {
@@ -195,9 +195,19 @@ export function createData(config) {
195
195
  };
196
196
  void promise.then(settle, settle);
197
197
  }
198
+ // Capture creation before settlement clears its entry, so dependent RPCs still see a failed create.
199
+ function sendAdmission(sessionID, send, gate) {
200
+ const created = creating.get(sessionID);
201
+ const previous = sending.get(sessionID);
202
+ const request = Promise.resolve()
203
+ .then(() => Promise.all([gate, created, previous]))
204
+ .then(send);
205
+ track(sending, sessionID, request.catch(() => undefined));
206
+ return request;
207
+ }
198
208
  // Upsert an admitted inbox item into pending, input, and (for user and
199
209
  // synthetic items) the visible transcript. Used by the inbox.enqueued
200
- // handler and by optimistic prompt admission; the upsert is what reconciles
210
+ // handler and by optimistic admission; the upsert is what reconciles
201
211
  // the durable echo with an optimistic placeholder — the durable payload and
202
212
  // times replace the client's guess.
203
213
  function admitLocal(item) {
@@ -205,6 +215,8 @@ export function createData(config) {
205
215
  const pending = store.session.pending[item.sessionID] ?? [];
206
216
  const at = pending.findIndex((entry) => entry.id === item.id);
207
217
  setStore("session", "pending", item.sessionID, at < 0 ? [...pending, item] : pending.map((entry, index) => (index === at ? item : entry)));
218
+ if (item.type === "compaction")
219
+ return;
208
220
  const input = store.session.input[item.sessionID] ?? [];
209
221
  if (!input.includes(item.id))
210
222
  setStore("session", "input", item.sessionID, [...input, item.id]);
@@ -536,6 +548,7 @@ export function createData(config) {
536
548
  draft.push(existing);
537
549
  message.reindex(draft, index, position);
538
550
  });
551
+ compacting.get(event.data.sessionID)?.observed.add(event.data.inboxID);
539
552
  return;
540
553
  }
541
554
  case "session.inbox.delivery.changed":
@@ -543,6 +556,7 @@ export function createData(config) {
543
556
  return;
544
557
  case "session.inbox.cancelled": {
545
558
  retractLocal(event.data.sessionID, event.data.inboxID);
559
+ compacting.get(event.data.sessionID)?.observed.add(event.data.inboxID);
546
560
  return;
547
561
  }
548
562
  case "session.inbox.enqueued": {
@@ -553,6 +567,12 @@ export function createData(config) {
553
567
  timeCreated: event.created,
554
568
  ...event.data.item,
555
569
  });
570
+ if (event.data.item.type === "compaction") {
571
+ const active = compacting.get(event.data.sessionID);
572
+ active?.observed.add(event.data.inboxID);
573
+ if (active && active.id !== event.data.inboxID && outbox.delete(active.id))
574
+ removePending(event.data.sessionID, active.id);
575
+ }
556
576
  return;
557
577
  }
558
578
  case "session.instructions.updated":
@@ -853,6 +873,8 @@ export function createData(config) {
853
873
  time: { created: event.created },
854
874
  });
855
875
  });
876
+ if (event.data.inputID)
877
+ compacting.get(event.data.sessionID)?.observed.add(event.data.inputID);
856
878
  return;
857
879
  case "session.execution.succeeded":
858
880
  case "session.execution.failed":
@@ -953,6 +975,8 @@ export function createData(config) {
953
975
  }
954
976
  message.append(draft, index, failed);
955
977
  });
978
+ if (event.data.inputID)
979
+ compacting.get(event.data.sessionID)?.observed.add(event.data.inputID);
956
980
  return;
957
981
  case "permission.asked":
958
982
  if (store.session.permission[event.data.sessionID]?.some((request) => request.id === event.data.id))
@@ -1138,10 +1162,17 @@ export function createData(config) {
1138
1162
  sync(sessionID) {
1139
1163
  return sync.run(`session.pending:${sessionID}`, async () => {
1140
1164
  const pending = await api().session.inbox.list({ sessionID });
1165
+ // A positive read acknowledges admission even when its SSE echo is delayed.
1166
+ pending.forEach((item) => outbox.delete(item.id));
1167
+ // Compactions also coalesce by Session, not just by the proposed ID.
1168
+ if (pending.some((item) => item.type === "compaction"))
1169
+ store.session.pending[sessionID]
1170
+ ?.filter((item) => item.type === "compaction")
1171
+ .forEach((item) => outbox.delete(item.id));
1141
1172
  // Keep optimistic rows still awaiting their echo: this fetch may
1142
1173
  // have raced ahead of an in-flight admission the server does not
1143
1174
  // know about yet.
1144
- const inflight = (store.session.pending[sessionID] ?? []).filter((item) => outbox.has(item.id) && !pending.some((row) => row.id === item.id));
1175
+ const inflight = (store.session.pending[sessionID] ?? []).filter((item) => outbox.has(item.id));
1145
1176
  const merged = inflight.length === 0 ? pending : [...pending, ...inflight];
1146
1177
  batch(() => {
1147
1178
  setStore("session", "pending", sessionID, reconcile(merged));
@@ -1205,13 +1236,62 @@ export function createData(config) {
1205
1236
  track(creating, id, request);
1206
1237
  return { id, request };
1207
1238
  },
1239
+ compact(input) {
1240
+ const active = compacting.get(input.sessionID);
1241
+ if (active)
1242
+ return active.request;
1243
+ // A known pending control ID may be consumed while setup waits. Propose
1244
+ // a fresh ID and let the server coalesce, without duplicating its row.
1245
+ const id = SessionMessage.ID.create();
1246
+ if (!store.session.pending[input.sessionID]?.some((item) => item.type === "compaction")) {
1247
+ outbox.add(id);
1248
+ admitLocal({
1249
+ id,
1250
+ sessionID: input.sessionID,
1251
+ timeCreated: Date.now(),
1252
+ type: "compaction",
1253
+ delivery: "steer",
1254
+ payload: {},
1255
+ });
1256
+ }
1257
+ // Compaction admission can coalesce onto a different ID. Retire the
1258
+ // speculative row on an echo, and remember consumed IDs until the POST
1259
+ // settles so its older response cannot resurrect a queued row.
1260
+ const observed = new Set();
1261
+ const request = sendAdmission(input.sessionID, async () => {
1262
+ if (input.model)
1263
+ await api().session.switchModel({ sessionID: input.sessionID, model: input.model });
1264
+ return api().session.compact({ sessionID: input.sessionID, id });
1265
+ })
1266
+ .then((item) => {
1267
+ batch(() => {
1268
+ outbox.delete(id);
1269
+ if (item.id !== id)
1270
+ removePending(input.sessionID, id);
1271
+ if (!observed.has(item.id) && !messageIndex.get(input.sessionID)?.has(item.id))
1272
+ admitLocal(item);
1273
+ });
1274
+ return item;
1275
+ })
1276
+ .catch((error) => {
1277
+ if (outbox.delete(id))
1278
+ removePending(input.sessionID, id);
1279
+ throw error;
1280
+ })
1281
+ .finally(() => {
1282
+ if (compacting.get(input.sessionID)?.request === request)
1283
+ compacting.delete(input.sessionID);
1284
+ });
1285
+ compacting.set(input.sessionID, { id, observed, request });
1286
+ return request;
1287
+ },
1208
1288
  // Optimistic prompt admission: render the prompt immediately under a
1209
1289
  // client-minted ID, send it, and let the durable inbox.enqueued echo
1210
1290
  // upsert that same ID with the server's payload. Server admission is
1211
1291
  // idempotent per ID, so retrying with the identical payload cannot
1212
1292
  // double-admit.
1213
1293
  prompt(input) {
1214
- const { gate, ...request } = input;
1294
+ const { gate, prepare, ...request } = input;
1215
1295
  const id = request.id ?? SessionMessage.ID.create();
1216
1296
  // A retry may reuse an ID that is already rendered — and possibly
1217
1297
  // already durable. Admit optimistically only for new IDs so a failed
@@ -1236,18 +1316,11 @@ export function createData(config) {
1236
1316
  },
1237
1317
  });
1238
1318
  }
1239
- // Wrapped so even a synchronous client failure reaches the rollback.
1240
- // The POST additionally waits for the caller's gate, for any
1241
- // in-flight optimistic create of this session, and for the previous
1242
- // prompt's POST: the row renders now, the send happens once the
1243
- // session exists server-side and earlier prompts are admitted.
1244
- const previous = sending.get(request.sessionID);
1245
- const send = Promise.resolve()
1246
- .then(() => Promise.all([gate, creating.get(request.sessionID), previous]))
1247
- .then(() => api().session.prompt({ ...request, id }));
1248
- track(sending, request.sessionID, send.then(() => undefined, () => undefined));
1249
- return send.catch((error) => {
1250
- // Roll back only rows this call admitted and the echo has not
1319
+ return sendAdmission(request.sessionID, async () => {
1320
+ await prepare?.();
1321
+ return api().session.prompt({ ...request, id });
1322
+ }, gate).catch((error) => {
1323
+ // Roll back only rows this call admitted and the server has not
1251
1324
  // acknowledged: anything else is server state.
1252
1325
  if (fresh && outbox.delete(id))
1253
1326
  retractLocal(request.sessionID, id);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@opencode-ai/client",
4
- "version": "0.0.0-dev-18592",
4
+ "version": "0.0.0-dev-18598",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -57,8 +57,8 @@
57
57
  "typecheck": "tsgo --noEmit"
58
58
  },
59
59
  "dependencies": {
60
- "@opencode-ai/schema": "0.0.0-dev-18592",
61
- "@opencode-ai/protocol": "0.0.0-dev-18592"
60
+ "@opencode-ai/schema": "0.0.0-dev-18598",
61
+ "@opencode-ai/protocol": "0.0.0-dev-18598"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "effect": "4.0.0-rc.112",
@@ -74,7 +74,7 @@
74
74
  },
75
75
  "devDependencies": {
76
76
  "@effect/platform-node": "4.0.0-rc.112",
77
- "@opencode-ai/httpapi-codegen": "0.0.0-dev-18592",
77
+ "@opencode-ai/httpapi-codegen": "0.0.0-dev-18598",
78
78
  "@tsconfig/bun": "1.0.9",
79
79
  "@types/bun": "1.3.13",
80
80
  "@typescript/native-preview": "7.0.0-dev.20251207.1",