@opencode-ai/client 0.0.0-dev-17688 → 0.0.0-dev-17694
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/solid/data.d.ts +2 -0
- package/dist/solid/data.js +111 -44
- package/package.json +4 -4
package/dist/solid/data.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
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";
|
|
2
|
+
import { type SessionPromptInput } from "../promise";
|
|
2
3
|
export type DataSessionStatus = "idle" | "running";
|
|
3
4
|
export type CreateDataInput = {
|
|
4
5
|
readonly api: () => OpenCodeClient;
|
|
@@ -49,6 +50,7 @@ export declare function createData(config: CreateDataInput): {
|
|
|
49
50
|
sync(sessionID: string): Promise<void>;
|
|
50
51
|
invalidate(sessionID: string): void;
|
|
51
52
|
};
|
|
53
|
+
prompt(input: SessionPromptInput): Promise<import("../promise").SessionInboxUser>;
|
|
52
54
|
sync(sessionID: string, options?: {
|
|
53
55
|
children?: boolean;
|
|
54
56
|
}): Promise<void>;
|
package/dist/solid/data.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
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 { SessionMessage } from "@opencode-ai/schema/session-message";
|
|
6
7
|
import { isPermissionNotFoundError } from "../promise";
|
|
7
8
|
import { createStore, produce, reconcile } from "solid-js/store";
|
|
8
|
-
import { createEffect, createSignal, onCleanup } from "solid-js";
|
|
9
|
+
import { batch, createEffect, createSignal, onCleanup } from "solid-js";
|
|
9
10
|
const messageIDFromEvent = (eventID) => eventID.replace(/^evt_/, "msg_");
|
|
10
11
|
export function locationKey(location) {
|
|
11
12
|
return JSON.stringify([location.directory, location.workspaceID]);
|
|
@@ -75,11 +76,6 @@ export function createData(config) {
|
|
|
75
76
|
function setSessionActive(sessionID, status) {
|
|
76
77
|
setStore("session", "active", sessionID, status);
|
|
77
78
|
}
|
|
78
|
-
function addPending(item) {
|
|
79
|
-
if (store.session.pending[item.sessionID]?.some((pending) => pending.id === item.id))
|
|
80
|
-
return;
|
|
81
|
-
setStore("session", "pending", item.sessionID, [...(store.session.pending[item.sessionID] ?? []), item]);
|
|
82
|
-
}
|
|
83
79
|
function removePending(sessionID, inboxID) {
|
|
84
80
|
if (!inboxID)
|
|
85
81
|
return;
|
|
@@ -101,6 +97,56 @@ export function createData(config) {
|
|
|
101
97
|
return;
|
|
102
98
|
setStore("session", "pending", sessionID, index, { ...item, delivery });
|
|
103
99
|
}
|
|
100
|
+
// Inbox IDs of optimistic prompt admissions still awaiting their durable
|
|
101
|
+
// echo. This is the one deliberate piece of in-flight bookkeeping in this
|
|
102
|
+
// layer: it exists so a rejection only rolls back rows the server never
|
|
103
|
+
// acknowledged, and so a concurrent pending re-fetch cannot wipe a row the
|
|
104
|
+
// server does not know about yet. Entries clear on the enqueued echo or on
|
|
105
|
+
// rollback — not on POST success, which typically precedes the echo.
|
|
106
|
+
const outbox = new Set();
|
|
107
|
+
// Upsert an admitted inbox item into pending, input, and (for user and
|
|
108
|
+
// synthetic items) the visible transcript. Used by the inbox.enqueued
|
|
109
|
+
// handler and by optimistic prompt admission; the upsert is what reconciles
|
|
110
|
+
// the durable echo with an optimistic placeholder — the durable payload and
|
|
111
|
+
// times replace the client's guess.
|
|
112
|
+
function admitLocal(item) {
|
|
113
|
+
batch(() => {
|
|
114
|
+
const pending = store.session.pending[item.sessionID] ?? [];
|
|
115
|
+
const at = pending.findIndex((entry) => entry.id === item.id);
|
|
116
|
+
setStore("session", "pending", item.sessionID, at < 0 ? [...pending, item] : pending.map((entry, index) => (index === at ? item : entry)));
|
|
117
|
+
const input = store.session.input[item.sessionID] ?? [];
|
|
118
|
+
if (!input.includes(item.id))
|
|
119
|
+
setStore("session", "input", item.sessionID, [...input, item.id]);
|
|
120
|
+
if (item.type !== "user" && item.type !== "synthetic")
|
|
121
|
+
return;
|
|
122
|
+
message.update(item.sessionID, (draft, index) => {
|
|
123
|
+
const row = item.type === "user"
|
|
124
|
+
? { id: item.id, type: "user", ...item.payload, time: { created: item.timeCreated } }
|
|
125
|
+
: { id: item.id, type: "synthetic", ...item.payload, time: { created: item.timeCreated } };
|
|
126
|
+
const position = index.get(item.id);
|
|
127
|
+
if (position === undefined)
|
|
128
|
+
return message.append(draft, index, row);
|
|
129
|
+
draft[position] = row;
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
// Remove an inbox item from pending, input, and the visible transcript.
|
|
134
|
+
// Used by the inbox.cancelled handler and by optimistic rollback.
|
|
135
|
+
function retractLocal(sessionID, inboxID) {
|
|
136
|
+
batch(() => {
|
|
137
|
+
removePending(sessionID, inboxID);
|
|
138
|
+
if (!messageIndex.get(sessionID)?.has(inboxID))
|
|
139
|
+
return;
|
|
140
|
+
message.update(sessionID, (draft, index) => {
|
|
141
|
+
const position = index.get(inboxID);
|
|
142
|
+
if (position === undefined)
|
|
143
|
+
return;
|
|
144
|
+
draft.splice(position, 1);
|
|
145
|
+
index.delete(inboxID);
|
|
146
|
+
message.reindex(draft, index, position);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
104
150
|
const message = {
|
|
105
151
|
update(sessionID, fn) {
|
|
106
152
|
setStore("session", "message", produce((draft) => {
|
|
@@ -198,6 +244,7 @@ export function createData(config) {
|
|
|
198
244
|
}));
|
|
199
245
|
}
|
|
200
246
|
function removeSession(sessionID) {
|
|
247
|
+
store.session.pending[sessionID]?.forEach((item) => outbox.delete(item.id));
|
|
201
248
|
messageIndex.delete(sessionID);
|
|
202
249
|
sync.invalidate(`session:${sessionID}`);
|
|
203
250
|
sync.invalidate(`session.pending:${sessionID}`);
|
|
@@ -366,47 +413,16 @@ export function createData(config) {
|
|
|
366
413
|
updatePending(event.data.sessionID, event.data.inboxID, event.data.delivery);
|
|
367
414
|
return;
|
|
368
415
|
case "session.inbox.cancelled": {
|
|
369
|
-
|
|
370
|
-
if (messageIndex.get(event.data.sessionID)?.has(event.data.inboxID))
|
|
371
|
-
message.update(event.data.sessionID, (draft, index) => {
|
|
372
|
-
const position = index.get(event.data.inboxID);
|
|
373
|
-
if (position === undefined)
|
|
374
|
-
return;
|
|
375
|
-
draft.splice(position, 1);
|
|
376
|
-
index.delete(event.data.inboxID);
|
|
377
|
-
message.reindex(draft, index, position);
|
|
378
|
-
});
|
|
416
|
+
retractLocal(event.data.sessionID, event.data.inboxID);
|
|
379
417
|
return;
|
|
380
418
|
}
|
|
381
419
|
case "session.inbox.enqueued": {
|
|
382
|
-
|
|
383
|
-
|
|
420
|
+
outbox.delete(event.data.inboxID);
|
|
421
|
+
admitLocal({
|
|
384
422
|
id: event.data.inboxID,
|
|
385
423
|
sessionID: event.data.sessionID,
|
|
386
424
|
timeCreated: event.created,
|
|
387
|
-
...item,
|
|
388
|
-
});
|
|
389
|
-
if (!store.session.input[event.data.sessionID]?.includes(event.data.inboxID))
|
|
390
|
-
setStore("session", "input", event.data.sessionID, [
|
|
391
|
-
...(store.session.input[event.data.sessionID] ?? []),
|
|
392
|
-
event.data.inboxID,
|
|
393
|
-
]);
|
|
394
|
-
if (item.type !== "user" && item.type !== "synthetic")
|
|
395
|
-
return;
|
|
396
|
-
message.update(event.data.sessionID, (draft, index) => {
|
|
397
|
-
message.append(draft, index, item.type === "user"
|
|
398
|
-
? {
|
|
399
|
-
id: event.data.inboxID,
|
|
400
|
-
type: "user",
|
|
401
|
-
...item.payload,
|
|
402
|
-
time: { created: event.created },
|
|
403
|
-
}
|
|
404
|
-
: {
|
|
405
|
-
id: event.data.inboxID,
|
|
406
|
-
type: "synthetic",
|
|
407
|
-
...item.payload,
|
|
408
|
-
time: { created: event.created },
|
|
409
|
-
});
|
|
425
|
+
...event.data.item,
|
|
410
426
|
});
|
|
411
427
|
return;
|
|
412
428
|
}
|
|
@@ -927,14 +943,60 @@ export function createData(config) {
|
|
|
927
943
|
sync(sessionID) {
|
|
928
944
|
return sync.run(`session.pending:${sessionID}`, async () => {
|
|
929
945
|
const pending = await api().session.inbox.list({ sessionID });
|
|
930
|
-
|
|
931
|
-
|
|
946
|
+
// Keep optimistic rows still awaiting their echo: this fetch may
|
|
947
|
+
// have raced ahead of an in-flight admission the server does not
|
|
948
|
+
// know about yet.
|
|
949
|
+
const inflight = (store.session.pending[sessionID] ?? []).filter((item) => outbox.has(item.id) && !pending.some((row) => row.id === item.id));
|
|
950
|
+
const merged = inflight.length === 0 ? pending : [...pending, ...inflight];
|
|
951
|
+
setStore("session", "pending", sessionID, reconcile(merged));
|
|
952
|
+
setStore("session", "input", sessionID, reconcile(merged.filter((item) => item.type !== "compaction").map((item) => item.id)));
|
|
932
953
|
});
|
|
933
954
|
},
|
|
934
955
|
invalidate(sessionID) {
|
|
935
956
|
sync.invalidate(`session.pending:${sessionID}`);
|
|
936
957
|
},
|
|
937
958
|
},
|
|
959
|
+
// Optimistic prompt admission: render the prompt immediately under a
|
|
960
|
+
// client-minted ID, send it, and let the durable inbox.enqueued echo
|
|
961
|
+
// upsert that same ID with the server's payload. Server admission is
|
|
962
|
+
// idempotent per ID, so retrying with the identical payload cannot
|
|
963
|
+
// double-admit.
|
|
964
|
+
prompt(input) {
|
|
965
|
+
const id = input.id ?? SessionMessage.ID.create();
|
|
966
|
+
// A retry may reuse an ID that is already rendered — and possibly
|
|
967
|
+
// already durable. Admit optimistically only for new IDs so a failed
|
|
968
|
+
// retry cannot roll back acknowledged state.
|
|
969
|
+
const fresh = !messageIndex.get(input.sessionID)?.has(id) &&
|
|
970
|
+
!store.session.pending[input.sessionID]?.some((item) => item.id === id);
|
|
971
|
+
if (fresh) {
|
|
972
|
+
outbox.add(id);
|
|
973
|
+
admitLocal({
|
|
974
|
+
id,
|
|
975
|
+
sessionID: input.sessionID,
|
|
976
|
+
timeCreated: Date.now(),
|
|
977
|
+
type: "user",
|
|
978
|
+
delivery: input.delivery ?? "steer",
|
|
979
|
+
// Files and skills stay off the optimistic row: their durable
|
|
980
|
+
// forms are server-loaded (content, mime, resolution), so they
|
|
981
|
+
// fill in when the echo upserts the row.
|
|
982
|
+
payload: {
|
|
983
|
+
text: input.text,
|
|
984
|
+
agents: input.agents?.map((agent) => ({ ...agent })),
|
|
985
|
+
metadata: input.metadata,
|
|
986
|
+
},
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
// Wrapped so even a synchronous client failure reaches the rollback.
|
|
990
|
+
return Promise.resolve()
|
|
991
|
+
.then(() => api().session.prompt({ ...input, id }))
|
|
992
|
+
.catch((error) => {
|
|
993
|
+
// Roll back only rows this call admitted and the echo has not
|
|
994
|
+
// acknowledged: anything else is server state.
|
|
995
|
+
if (fresh && outbox.delete(id))
|
|
996
|
+
retractLocal(input.sessionID, id);
|
|
997
|
+
throw error;
|
|
998
|
+
});
|
|
999
|
+
},
|
|
938
1000
|
sync(sessionID, options) {
|
|
939
1001
|
return sync.run(options?.children ? `session.family:${sessionID}` : `session:${sessionID}`, async () => {
|
|
940
1002
|
const [info, children] = await Promise.all([
|
|
@@ -971,7 +1033,12 @@ export function createData(config) {
|
|
|
971
1033
|
sync(sessionID) {
|
|
972
1034
|
return sync.run(`session.message:${sessionID}`, async () => {
|
|
973
1035
|
const response = await api().message.list({ sessionID, limit: 200, order: "desc" });
|
|
974
|
-
const
|
|
1036
|
+
const fetched = response.data.toReversed();
|
|
1037
|
+
// Same protection as the pending sync: a re-fetch racing an
|
|
1038
|
+
// optimistic admission must not wipe the in-flight transcript row.
|
|
1039
|
+
const ids = new Set(fetched.map((item) => item.id));
|
|
1040
|
+
const inflight = (store.session.message[sessionID] ?? []).filter((item) => outbox.has(item.id) && !ids.has(item.id));
|
|
1041
|
+
const messages = inflight.length === 0 ? fetched : [...fetched, ...inflight];
|
|
975
1042
|
messageIndex.set(sessionID, new Map(messages.map((message, index) => [message.id, index])));
|
|
976
1043
|
setStore("session", "message", sessionID, reconcile(messages));
|
|
977
1044
|
setStore("session", "messageCursor", sessionID, response.cursor.next ?? undefined);
|
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-
|
|
4
|
+
"version": "0.0.0-dev-17694",
|
|
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-
|
|
61
|
-
"@opencode-ai/protocol": "0.0.0-dev-
|
|
60
|
+
"@opencode-ai/schema": "0.0.0-dev-17694",
|
|
61
|
+
"@opencode-ai/protocol": "0.0.0-dev-17694"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"effect": "4.0.0-beta.107",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@effect/platform-node": "4.0.0-beta.107",
|
|
77
|
-
"@opencode-ai/httpapi-codegen": "0.0.0-dev-
|
|
77
|
+
"@opencode-ai/httpapi-codegen": "0.0.0-dev-17694",
|
|
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",
|