@opencode-ai/client 0.0.0-dev-17829 → 0.0.0-dev-17843

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, FormInfo, IntegrationInfo, LocationRef, LocationGetOutput, McpResource, McpServer, ModelInfo, PermissionSavedInfo, PermissionRequest, PermissionReplyInput, Project, ProviderInfo, ReferenceInfo, SessionMessageInfo, SessionInfo, SessionInboxInfo, ShellInfo, SkillInfo, VcsInfo, OpenCodeEvent, OpenCodeClient, WebSearchProvider } from "../promise";
1
+ import type { AgentInfo, CommandInfo, FormInfo, IntegrationInfo, LocationRef, LocationGetOutput, McpResource, McpServer, ModelInfo, ModelRef, PermissionSavedInfo, PermissionRequest, PermissionReplyInput, Project, ProviderInfo, ReferenceInfo, SessionMessageInfo, SessionInfo, SessionInboxInfo, 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 = {
@@ -50,7 +50,20 @@ export declare function createData(config: CreateDataInput): {
50
50
  sync(sessionID: string): Promise<void>;
51
51
  invalidate(sessionID: string): void;
52
52
  };
53
- prompt(input: SessionPromptInput): Promise<import("../promise").SessionInboxUser>;
53
+ create(input: {
54
+ id?: string;
55
+ title?: string;
56
+ agent?: string;
57
+ model?: ModelRef;
58
+ location?: LocationRef;
59
+ projectID?: string;
60
+ }): {
61
+ id: string;
62
+ request: Promise<SessionInfo>;
63
+ };
64
+ prompt(input: SessionPromptInput & {
65
+ gate?: Promise<unknown>;
66
+ }): Promise<import("../promise").SessionInboxUser>;
54
67
  sync(sessionID: string, options?: {
55
68
  children?: boolean;
56
69
  }): Promise<void>;
@@ -3,6 +3,7 @@
3
3
  // merges, live/history overlays, or other race machinery here—last write wins.
4
4
  // Reconnect invalidates cached reads; active UI owners decide what to sync again.
5
5
  import { Worktree } from "@opencode-ai/schema/worktree";
6
+ import { SessionID } from "@opencode-ai/schema/session-id";
6
7
  import { SessionMessage } from "@opencode-ai/schema/session-message";
7
8
  import { isPermissionNotFoundError } from "../promise";
8
9
  import { createStore, produce, reconcile } from "solid-js/store";
@@ -105,6 +106,30 @@ export function createData(config) {
105
106
  // server does not know about yet. Entries clear on the enqueued echo or on
106
107
  // rollback — not on POST success, which typically precedes the echo.
107
108
  const outbox = new Set();
109
+ // Session IDs of optimistic create admissions still awaiting acknowledgement
110
+ // (the session.created echo or the create response itself). A failed create
111
+ // only rolls back a session the server never acknowledged. Unlike
112
+ // `creating`, this clears on the echo rather than request settlement.
113
+ const sessionOutbox = new Set();
114
+ // In-flight optimistic creates by session ID. prompt() gates its POST on
115
+ // this so a prompt sent to a still-creating session waits for the session
116
+ // to exist server-side instead of failing with "not found".
117
+ const creating = new Map();
118
+ // Per-session send chain: prompts must be admitted in submission order,
119
+ // and HTTP gives no ordering across concurrent POSTs. Each prompt waits
120
+ // for the previous prompt's POST (settled, so one failure does not block
121
+ // the next) before sending its own.
122
+ const sending = new Map();
123
+ // Register `promise` under `key` until it settles. A later registration
124
+ // replaces an earlier one; settlement only clears its own entry.
125
+ function track(map, key, promise) {
126
+ map.set(key, promise);
127
+ const settle = () => {
128
+ if (map.get(key) === promise)
129
+ map.delete(key);
130
+ };
131
+ void promise.then(settle, settle);
132
+ }
108
133
  // Upsert an admitted inbox item into pending, input, and (for user and
109
134
  // synthetic items) the visible transcript. Used by the inbox.enqueued
110
135
  // handler and by optimistic prompt admission; the upsert is what reconciles
@@ -251,6 +276,7 @@ export function createData(config) {
251
276
  store.session.pending[sessionID]?.forEach((item) => outbox.delete(item.id));
252
277
  messageIndex.delete(sessionID);
253
278
  sync.invalidate(`session:${sessionID}`);
279
+ sync.invalidate(`session.family:${sessionID}`);
254
280
  sync.invalidate(`session.pending:${sessionID}`);
255
281
  sync.invalidate(`session.message:${sessionID}`);
256
282
  sync.invalidate(`session.permission:${sessionID}`);
@@ -294,6 +320,7 @@ export function createData(config) {
294
320
  void result.project.sync().catch((error) => console.error("Failed to preload projects", error));
295
321
  return;
296
322
  case "session.created":
323
+ sessionOutbox.delete(event.data.sessionID);
297
324
  result.session.invalidate(event.data.sessionID);
298
325
  void result.session.sync(event.data.sessionID);
299
326
  // Band-aid: a newly created session starts empty, so live events can be its source of truth.
@@ -963,44 +990,103 @@ export function createData(config) {
963
990
  sync.invalidate(`session.pending:${sessionID}`);
964
991
  },
965
992
  },
993
+ // Optimistic session creation: admit a local record under a
994
+ // client-minted ID so a session view can mount immediately, then create
995
+ // the session on the server. The session.created echo re-syncs the
996
+ // record by ID, so the durable payload replaces the client's guess.
997
+ // Returns the ID synchronously along with the in-flight request:
998
+ // callers gate session-dependent sends on the request (prompt() gates
999
+ // itself on any in-flight create of its session automatically).
1000
+ create(input) {
1001
+ const { projectID, ...payload } = input;
1002
+ const id = payload.id ?? SessionID.create();
1003
+ const location = payload.location ?? defaultLocation();
1004
+ const fresh = !store.session.info[id];
1005
+ if (fresh) {
1006
+ const now = Date.now();
1007
+ sessionOutbox.add(id);
1008
+ result.session.remember({
1009
+ id,
1010
+ projectID: projectID ?? store.location[locationKey(location)]?.info?.project.id ?? "",
1011
+ agent: payload.agent,
1012
+ model: payload.model,
1013
+ cost: 0,
1014
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
1015
+ time: { created: now, updated: now },
1016
+ title: payload.title,
1017
+ location,
1018
+ });
1019
+ // A mounted optimistic session must not fetch its empty collections
1020
+ // before creation settles. The session.created echo re-syncs info.
1021
+ sync.complete(`session.family:${id}`);
1022
+ sync.complete(`session.pending:${id}`);
1023
+ sync.complete(`session.message:${id}`);
1024
+ }
1025
+ // Wrapped so even a synchronous client failure reaches the rollback.
1026
+ const request = Promise.resolve()
1027
+ .then(() => api().session.create({ ...payload, id, location }))
1028
+ .then((info) => {
1029
+ sessionOutbox.delete(id);
1030
+ result.session.remember(info);
1031
+ return info;
1032
+ })
1033
+ .catch((error) => {
1034
+ // Roll back only a record this call admitted and neither the echo
1035
+ // nor the response has acknowledged: anything else is server state.
1036
+ if (fresh && sessionOutbox.delete(id))
1037
+ removeSession(id);
1038
+ throw error;
1039
+ });
1040
+ if (fresh)
1041
+ track(creating, id, request);
1042
+ return { id, request };
1043
+ },
966
1044
  // Optimistic prompt admission: render the prompt immediately under a
967
1045
  // client-minted ID, send it, and let the durable inbox.enqueued echo
968
1046
  // upsert that same ID with the server's payload. Server admission is
969
1047
  // idempotent per ID, so retrying with the identical payload cannot
970
1048
  // double-admit.
971
1049
  prompt(input) {
972
- const id = input.id ?? SessionMessage.ID.create();
1050
+ const { gate, ...request } = input;
1051
+ const id = request.id ?? SessionMessage.ID.create();
973
1052
  // A retry may reuse an ID that is already rendered — and possibly
974
1053
  // already durable. Admit optimistically only for new IDs so a failed
975
1054
  // retry cannot roll back acknowledged state.
976
- const fresh = !messageIndex.get(input.sessionID)?.has(id) &&
977
- !store.session.pending[input.sessionID]?.some((item) => item.id === id);
1055
+ const fresh = !messageIndex.get(request.sessionID)?.has(id) &&
1056
+ !store.session.pending[request.sessionID]?.some((item) => item.id === id);
978
1057
  if (fresh) {
979
1058
  outbox.add(id);
980
1059
  admitLocal({
981
1060
  id,
982
- sessionID: input.sessionID,
1061
+ sessionID: request.sessionID,
983
1062
  timeCreated: Date.now(),
984
1063
  type: "user",
985
- delivery: input.delivery ?? "steer",
1064
+ delivery: request.delivery ?? "steer",
986
1065
  // Files and skills stay off the optimistic row: their durable
987
1066
  // forms are server-loaded (content, mime, resolution), so they
988
1067
  // fill in when the echo upserts the row.
989
1068
  payload: {
990
- text: input.text,
991
- agents: input.agents?.map((agent) => ({ ...agent })),
992
- metadata: input.metadata,
1069
+ text: request.text,
1070
+ agents: request.agents?.map((agent) => ({ ...agent })),
1071
+ metadata: request.metadata,
993
1072
  },
994
1073
  });
995
1074
  }
996
1075
  // Wrapped so even a synchronous client failure reaches the rollback.
997
- return Promise.resolve()
998
- .then(() => api().session.prompt({ ...input, id }))
999
- .catch((error) => {
1076
+ // The POST additionally waits for the caller's gate, for any
1077
+ // in-flight optimistic create of this session, and for the previous
1078
+ // prompt's POST: the row renders now, the send happens once the
1079
+ // session exists server-side and earlier prompts are admitted.
1080
+ const previous = sending.get(request.sessionID);
1081
+ const send = Promise.resolve()
1082
+ .then(() => Promise.all([gate, creating.get(request.sessionID), previous]))
1083
+ .then(() => api().session.prompt({ ...request, id }));
1084
+ track(sending, request.sessionID, send.then(() => undefined, () => undefined));
1085
+ return send.catch((error) => {
1000
1086
  // Roll back only rows this call admitted and the echo has not
1001
1087
  // acknowledged: anything else is server state.
1002
1088
  if (fresh && outbox.delete(id))
1003
- retractLocal(input.sessionID, id);
1089
+ retractLocal(request.sessionID, id);
1004
1090
  throw error;
1005
1091
  });
1006
1092
  },
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-17829",
4
+ "version": "0.0.0-dev-17843",
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-17829",
61
- "@opencode-ai/protocol": "0.0.0-dev-17829"
60
+ "@opencode-ai/schema": "0.0.0-dev-17843",
61
+ "@opencode-ai/protocol": "0.0.0-dev-17843"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "effect": "4.0.0-rc.110",
@@ -74,7 +74,7 @@
74
74
  },
75
75
  "devDependencies": {
76
76
  "@effect/platform-node": "4.0.0-rc.110",
77
- "@opencode-ai/httpapi-codegen": "0.0.0-dev-17829",
77
+ "@opencode-ai/httpapi-codegen": "0.0.0-dev-17843",
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",