@kortix/sdk 0.11.0 → 0.12.1
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 +110 -73
- package/dist/browser/cache/idb-sync-cache-key.d.ts +2 -0
- package/dist/browser/cache/idb-sync-cache-key.d.ts.map +1 -0
- package/dist/browser/cache/idb-sync-cache-key.js +7 -0
- package/dist/browser/cache/idb-sync-cache-key.js.map +1 -0
- package/dist/browser/cache/idb-sync-cache.d.ts +3 -3
- package/dist/browser/cache/idb-sync-cache.d.ts.map +1 -1
- package/dist/browser/cache/idb-sync-cache.js +49 -26
- package/dist/browser/cache/idb-sync-cache.js.map +1 -1
- package/dist/browser/session-sync/session-cache-ownership.d.ts +36 -0
- package/dist/browser/session-sync/session-cache-ownership.d.ts.map +1 -0
- package/dist/browser/session-sync/session-cache-ownership.js +66 -0
- package/dist/browser/session-sync/session-cache-ownership.js.map +1 -0
- package/dist/browser/session-sync/session-sync-registry.d.ts +8 -6
- package/dist/browser/session-sync/session-sync-registry.d.ts.map +1 -1
- package/dist/browser/session-sync/session-sync-registry.js +86 -33
- package/dist/browser/session-sync/session-sync-registry.js.map +1 -1
- package/dist/browser/session-sync/session-transcript-cache.d.ts +55 -0
- package/dist/browser/session-sync/session-transcript-cache.d.ts.map +1 -0
- package/dist/browser/session-sync/session-transcript-cache.js +69 -0
- package/dist/browser/session-sync/session-transcript-cache.js.map +1 -0
- package/dist/core/client/kortix.d.ts +72 -25
- package/dist/core/client/kortix.d.ts.map +1 -1
- package/dist/core/client/kortix.js +76 -17
- package/dist/core/client/kortix.js.map +1 -1
- package/dist/core/rest/platform-client/host-boundary.d.ts +7 -0
- package/dist/core/rest/platform-client/host-boundary.d.ts.map +1 -1
- package/dist/core/rest/platform-client/host-boundary.js +14 -0
- package/dist/core/rest/platform-client/host-boundary.js.map +1 -1
- package/dist/core/rest/projects-client/access.d.ts +1 -2
- package/dist/core/rest/projects-client/access.d.ts.map +1 -1
- package/dist/core/rest/projects-client/access.js +9 -6
- package/dist/core/rest/projects-client/access.js.map +1 -1
- package/dist/core/rest/projects-client/agent-config.d.ts +15 -7
- package/dist/core/rest/projects-client/agent-config.d.ts.map +1 -1
- package/dist/core/rest/projects-client/agent-config.js +43 -2
- package/dist/core/rest/projects-client/agent-config.js.map +1 -1
- package/dist/core/rest/projects-client/agent-scope.d.ts +4 -0
- package/dist/core/rest/projects-client/agent-scope.d.ts.map +1 -1
- package/dist/core/rest/projects-client/agent-scope.js +34 -1
- package/dist/core/rest/projects-client/agent-scope.js.map +1 -1
- package/dist/core/rest/projects-client/audit.d.ts +34 -0
- package/dist/core/rest/projects-client/audit.d.ts.map +1 -1
- package/dist/core/rest/projects-client/audit.js +49 -4
- package/dist/core/rest/projects-client/audit.js.map +1 -1
- package/dist/core/rest/projects-client/billing.d.ts +26 -14
- package/dist/core/rest/projects-client/billing.d.ts.map +1 -1
- package/dist/core/rest/projects-client/billing.js +4 -5
- package/dist/core/rest/projects-client/billing.js.map +1 -1
- package/dist/core/rest/projects-client/connectors.d.ts +106 -45
- package/dist/core/rest/projects-client/connectors.d.ts.map +1 -1
- package/dist/core/rest/projects-client/connectors.js +95 -58
- package/dist/core/rest/projects-client/connectors.js.map +1 -1
- package/dist/core/rest/projects-client/github.d.ts +7 -1
- package/dist/core/rest/projects-client/github.d.ts.map +1 -1
- package/dist/core/rest/projects-client/github.js.map +1 -1
- package/dist/core/rest/projects-client/iam.d.ts +17 -0
- package/dist/core/rest/projects-client/iam.d.ts.map +1 -1
- package/dist/core/rest/projects-client/iam.js +14 -0
- package/dist/core/rest/projects-client/iam.js.map +1 -1
- package/dist/core/rest/projects-client/index.d.ts +2 -0
- package/dist/core/rest/projects-client/index.d.ts.map +1 -1
- package/dist/core/rest/projects-client/index.js +2 -0
- package/dist/core/rest/projects-client/index.js.map +1 -1
- package/dist/core/rest/projects-client/model-enablement.d.ts +9 -0
- package/dist/core/rest/projects-client/model-enablement.d.ts.map +1 -0
- package/dist/core/rest/projects-client/model-enablement.js +16 -0
- package/dist/core/rest/projects-client/model-enablement.js.map +1 -0
- package/dist/core/rest/projects-client/policies.d.ts +18 -0
- package/dist/core/rest/projects-client/policies.d.ts.map +1 -1
- package/dist/core/rest/projects-client/policies.js.map +1 -1
- package/dist/core/rest/projects-client/projects.d.ts +87 -1
- package/dist/core/rest/projects-client/projects.d.ts.map +1 -1
- package/dist/core/rest/projects-client/projects.js +8 -0
- package/dist/core/rest/projects-client/projects.js.map +1 -1
- package/dist/core/rest/projects-client/sandbox.d.ts +50 -0
- package/dist/core/rest/projects-client/sandbox.d.ts.map +1 -1
- package/dist/core/rest/projects-client/sandbox.js.map +1 -1
- package/dist/core/rest/projects-client/session-costs.d.ts +102 -0
- package/dist/core/rest/projects-client/session-costs.d.ts.map +1 -0
- package/dist/core/rest/projects-client/session-costs.js +25 -0
- package/dist/core/rest/projects-client/session-costs.js.map +1 -0
- package/dist/core/rest/projects-client/session-sandbox.d.ts +0 -2
- package/dist/core/rest/projects-client/session-sandbox.d.ts.map +1 -1
- package/dist/core/rest/projects-client/session-sandbox.js +1 -10
- package/dist/core/rest/projects-client/session-sandbox.js.map +1 -1
- package/dist/core/rest/projects-client/sessions.d.ts +94 -44
- package/dist/core/rest/projects-client/sessions.d.ts.map +1 -1
- package/dist/core/rest/projects-client/sessions.js +9 -15
- package/dist/core/rest/projects-client/sessions.js.map +1 -1
- package/dist/core/rest/projects-client/setup-links.d.ts +37 -0
- package/dist/core/rest/projects-client/setup-links.d.ts.map +1 -1
- package/dist/core/rest/projects-client/setup-links.js +11 -0
- package/dist/core/rest/projects-client/setup-links.js.map +1 -1
- package/dist/core/session/index.d.ts +1 -1
- package/dist/core/session/index.d.ts.map +1 -1
- package/dist/core/session/index.js +1 -1
- package/dist/core/session/index.js.map +1 -1
- package/dist/core/session/preview-auth.d.ts +32 -0
- package/dist/core/session/preview-auth.d.ts.map +1 -0
- package/dist/core/session/preview-auth.js +53 -0
- package/dist/core/session/preview-auth.js.map +1 -0
- package/dist/core/session/server-store/url-helpers.d.ts.map +1 -1
- package/dist/core/session/server-store/url-helpers.js +5 -3
- package/dist/core/session/server-store/url-helpers.js.map +1 -1
- package/dist/core/session-sync/session-sync-controller.d.ts +12 -1
- package/dist/core/session-sync/session-sync-controller.d.ts.map +1 -1
- package/dist/core/session-sync/session-sync-controller.js +34 -2
- package/dist/core/session-sync/session-sync-controller.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/kortix.esm.min.js +19 -24
- package/dist/kortix.global.js +19 -24
- package/dist/platform/auth-core.d.ts +5 -3
- package/dist/platform/auth-core.d.ts.map +1 -1
- package/dist/platform/auth-core.js +6 -4
- package/dist/platform/auth-core.js.map +1 -1
- package/dist/react/index.d.ts +1 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +0 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/model-flatten.d.ts +20 -0
- package/dist/react/model-flatten.d.ts.map +1 -1
- package/dist/react/model-flatten.js +12 -0
- package/dist/react/model-flatten.js.map +1 -1
- package/dist/react/opencode.d.ts +4 -3
- package/dist/react/opencode.d.ts.map +1 -1
- package/dist/react/opencode.js +4 -3
- package/dist/react/opencode.js.map +1 -1
- package/dist/react/provider-selection.d.ts +9 -0
- package/dist/react/provider-selection.d.ts.map +1 -1
- package/dist/react/provider-selection.js +23 -0
- package/dist/react/provider-selection.js.map +1 -1
- package/dist/react/session-title-sync.d.ts +9 -5
- package/dist/react/session-title-sync.d.ts.map +1 -1
- package/dist/react/session-title-sync.js +14 -10
- package/dist/react/session-title-sync.js.map +1 -1
- package/dist/react/use-canonical-opencode-session.d.ts +2 -2
- package/dist/react/use-canonical-opencode-session.d.ts.map +1 -1
- package/dist/react/use-model-enablement.d.ts +25 -0
- package/dist/react/use-model-enablement.d.ts.map +1 -0
- package/dist/react/use-model-enablement.js +92 -0
- package/dist/react/use-model-enablement.js.map +1 -0
- package/dist/react/use-model-store.d.ts +6 -14
- package/dist/react/use-model-store.d.ts.map +1 -1
- package/dist/react/use-model-store.js +30 -52
- package/dist/react/use-model-store.js.map +1 -1
- package/dist/react/use-opencode-events/handle-event.d.ts.map +1 -1
- package/dist/react/use-opencode-events/handle-event.js +103 -41
- package/dist/react/use-opencode-events/handle-event.js.map +1 -1
- package/dist/react/use-opencode-local.d.ts +5 -4
- package/dist/react/use-opencode-local.d.ts.map +1 -1
- package/dist/react/use-opencode-local.js +20 -37
- package/dist/react/use-opencode-local.js.map +1 -1
- package/dist/react/use-opencode-sessions/keys.d.ts +3 -1
- package/dist/react/use-opencode-sessions/keys.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/keys.js +2 -0
- package/dist/react/use-opencode-sessions/keys.js.map +1 -1
- package/dist/react/use-opencode-sessions/sessions.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/sessions.js +16 -7
- package/dist/react/use-opencode-sessions/sessions.js.map +1 -1
- package/dist/react/use-opencode-sessions/shared.d.ts +3 -2
- package/dist/react/use-opencode-sessions/shared.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/shared.js +6 -3
- package/dist/react/use-opencode-sessions/shared.js.map +1 -1
- package/dist/react/use-opencode-sessions/sharing.js +2 -2
- package/dist/react/use-opencode-sessions/sharing.js.map +1 -1
- package/dist/react/use-session-sync.d.ts +13 -1
- package/dist/react/use-session-sync.d.ts.map +1 -1
- package/dist/react/use-session-sync.js +85 -17
- package/dist/react/use-session-sync.js.map +1 -1
- package/dist/react/use-session.d.ts +60 -40
- package/dist/react/use-session.d.ts.map +1 -1
- package/dist/react/use-session.js +160 -202
- package/dist/react/use-session.js.map +1 -1
- package/package.json +2 -2
- package/dist/core/acp/client.d.ts +0 -45
- package/dist/core/acp/client.d.ts.map +0 -1
- package/dist/core/acp/client.js +0 -277
- package/dist/core/acp/client.js.map +0 -1
- package/dist/core/acp/index.d.ts +0 -5
- package/dist/core/acp/index.d.ts.map +0 -1
- package/dist/core/acp/index.js +0 -5
- package/dist/core/acp/index.js.map +0 -1
- package/dist/core/acp/projection.d.ts +0 -23
- package/dist/core/acp/projection.d.ts.map +0 -1
- package/dist/core/acp/projection.js +0 -603
- package/dist/core/acp/projection.js.map +0 -1
- package/dist/core/acp/session-controller.d.ts +0 -90
- package/dist/core/acp/session-controller.d.ts.map +0 -1
- package/dist/core/acp/session-controller.js +0 -530
- package/dist/core/acp/session-controller.js.map +0 -1
- package/dist/core/acp/types.d.ts +0 -66
- package/dist/core/acp/types.d.ts.map +0 -1
- package/dist/core/acp/types.js +0 -21
- package/dist/core/acp/types.js.map +0 -1
- package/dist/core/session/runtime-transport.d.ts +0 -16
- package/dist/core/session/runtime-transport.d.ts.map +0 -1
- package/dist/core/session/runtime-transport.js +0 -30
- package/dist/core/session/runtime-transport.js.map +0 -1
- package/dist/react/session-busy.d.ts +0 -8
- package/dist/react/session-busy.d.ts.map +0 -1
- package/dist/react/session-busy.js +0 -4
- package/dist/react/session-busy.js.map +0 -1
- package/dist/react/use-acp-session-runtime.d.ts +0 -34
- package/dist/react/use-acp-session-runtime.d.ts.map +0 -1
- package/dist/react/use-acp-session-runtime.js +0 -84
- package/dist/react/use-acp-session-runtime.js.map +0 -1
- package/dist/react/use-admin-users.d.ts +0 -98
- package/dist/react/use-admin-users.d.ts.map +0 -1
- package/dist/react/use-admin-users.js +0 -121
- package/dist/react/use-admin-users.js.map +0 -1
package/README.md
CHANGED
|
@@ -24,9 +24,12 @@ npm install @kortix/sdk
|
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
```ts
|
|
27
|
-
import { createKortix } from
|
|
27
|
+
import { createKortix } from "@kortix/sdk";
|
|
28
28
|
|
|
29
|
-
const kortix = createKortix({
|
|
29
|
+
const kortix = createKortix({
|
|
30
|
+
backendUrl: "https://api.kortix.com/v1",
|
|
31
|
+
getToken,
|
|
32
|
+
});
|
|
30
33
|
await kortix.projects.list();
|
|
31
34
|
```
|
|
32
35
|
|
|
@@ -53,10 +56,10 @@ no build step required:
|
|
|
53
56
|
`@kortix/sdk` is the canonical entry — everything framework-free lives there.
|
|
54
57
|
Three others exist, each for a reason that fits in one sentence:
|
|
55
58
|
|
|
56
|
-
| Entry
|
|
57
|
-
|
|
58
|
-
| `@kortix/sdk/react`
|
|
59
|
-
| `@kortix/sdk/server`
|
|
59
|
+
| Entry | Why it can't live at root |
|
|
60
|
+
| ------------------------ | --------------------------- |
|
|
61
|
+
| `@kortix/sdk/react` | React is a peer dependency |
|
|
62
|
+
| `@kortix/sdk/server` | imports `node:async_hooks` |
|
|
60
63
|
| `@kortix/sdk/internal/*` | unsupported, outside semver |
|
|
61
64
|
|
|
62
65
|
Older subpaths (`@kortix/sdk/projects-client`, `/turns`, …) still work and are
|
|
@@ -70,46 +73,70 @@ table for the full list (20 of them).
|
|
|
70
73
|
## Quick start
|
|
71
74
|
|
|
72
75
|
```ts
|
|
73
|
-
import { createKortix } from
|
|
76
|
+
import { createKortix } from "@kortix/sdk";
|
|
74
77
|
|
|
75
78
|
const kortix = createKortix({
|
|
76
|
-
backendUrl:
|
|
77
|
-
getToken: () =>
|
|
79
|
+
backendUrl: "https://api.kortix.com/v1",
|
|
80
|
+
getToken: () =>
|
|
81
|
+
supabase.auth
|
|
82
|
+
.getSession()
|
|
83
|
+
.then((s) => s.data.session?.access_token ?? null),
|
|
78
84
|
});
|
|
79
85
|
|
|
80
86
|
// Projects
|
|
81
87
|
const projects = await kortix.projects.list();
|
|
82
|
-
const detail
|
|
83
|
-
await kortix.project(pid).secrets.upsert({ name:
|
|
88
|
+
const detail = await kortix.project(pid).detail();
|
|
89
|
+
await kortix.project(pid).secrets.upsert({ name: "STRIPE_API_KEY", value });
|
|
84
90
|
const visibleSessions = await kortix.project(pid).sessions.list();
|
|
85
|
-
const projectInventory = await kortix
|
|
91
|
+
const projectInventory = await kortix
|
|
92
|
+
.project(pid)
|
|
93
|
+
.sessions.list({ scope: "project" }); // manager only
|
|
86
94
|
const warm = await kortix.project(pid).sessions.ensureWarm();
|
|
87
|
-
await kortix
|
|
95
|
+
await kortix
|
|
96
|
+
.project(pid)
|
|
97
|
+
.sessions.claimWarm({ session_id: warm.session.session_id });
|
|
88
98
|
|
|
89
99
|
// Sessions (id-bound handle)
|
|
90
100
|
const s = kortix.session(pid, sid);
|
|
91
|
-
await s.
|
|
92
|
-
await s.
|
|
93
|
-
await s.
|
|
101
|
+
const cost = await s.cost(); // reads finalized LLM + compute cost; no runtime start
|
|
102
|
+
await s.send("Build me a widget"); // provisions/resumes if needed, then prompts
|
|
103
|
+
await s.rewind(userMessageId); // stages a reversible rollback on this session
|
|
104
|
+
await s.restoreRewind(); // restores the removed path before the next prompt
|
|
94
105
|
await s.previews();
|
|
95
106
|
|
|
96
|
-
// Lower level: the typed
|
|
107
|
+
// Lower level: the typed OpenCode REST compatibility client for THIS sandbox.
|
|
97
108
|
// `.runtime` throws until the runtime is resolved, and the runtime is keyed by
|
|
98
109
|
// the OpenCode session id (NOT the Kortix `sid`) — resolve both via ensureReady.
|
|
99
110
|
const { opencodeSessionId } = await s.ensureReady();
|
|
100
111
|
await s.runtime.session.prompt({ sessionID: opencodeSessionId, parts });
|
|
101
112
|
```
|
|
102
113
|
|
|
103
|
-
|
|
114
|
+
For OpenCode REST sessions, `send()` reads the persisted session model and
|
|
115
|
+
agent before the first prompt on a handle. This prevents a snapshot-inherited
|
|
116
|
+
OpenCode session from reusing stale snapshot defaults. A per-call choice
|
|
117
|
+
overrides a `setModel()` or `setAgent()` choice. A handle choice overrides the
|
|
118
|
+
persisted session default.
|
|
119
|
+
|
|
120
|
+
### React runtime
|
|
104
121
|
|
|
105
|
-
`useSession(projectId, sessionId)`
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
`useSession(projectId, sessionId)` opens the OpenCode REST runtime returned by
|
|
123
|
+
`POST /start`. The hook owns messages, rewind and restore, cancellation,
|
|
124
|
+
commands, permissions, and questions. Hosts do not construct runtime routes.
|
|
125
|
+
|
|
126
|
+
A server-rendered host can seed a known OpenCode pin while `/start` runs:
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
useSession(projectId, sessionId, {
|
|
130
|
+
initialOpenCodeSessionId: persistedSession.opencode_session_id,
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Use only a pin that the host authorized for the same `(projectId, sessionId)`.
|
|
135
|
+
The seed hydrates cached content. It does not choose the runtime identity.
|
|
136
|
+
The pin returned by `/start` always replaces a stale seed. The SDK also scopes
|
|
137
|
+
OpenCode query and synchronization controllers to the sandbox runtime. Two
|
|
138
|
+
sandboxes cannot share browser cache state when a snapshot exposes the same
|
|
139
|
+
OpenCode id during adoption.
|
|
113
140
|
|
|
114
141
|
## The facade surface
|
|
115
142
|
|
|
@@ -119,13 +146,13 @@ exhaustive — see `API-MAP.md` for the full per-domain surface:
|
|
|
119
146
|
| namespace | what |
|
|
120
147
|
|---|---|
|
|
121
148
|
| `kortix.projects` | list · get · detail · create · provision · update · archive · llmCatalog · modelPicker · sandboxTemplates · sessions (+ more: `listForAccount`, `sandboxHealth`, `createSession`) |
|
|
122
|
-
| `kortix.accounts` | list · get · create · members · invites · `tokens.{list,create,revoke}` (account-scoped CLI PATs, `kortix_pat_…`) · `audit.{log,export,webhooks.*}` (
|
|
123
|
-
| `kortix.billing` | entitlement/usage reads: `accountState` · `accountStateMinimal` · `transactions` · `transactionsSummary` · `creditBreakdown` · `usageHistory` · `tierConfigurations` — plus a curated mutation surface: `checkout.{createSession,confirmSession}` · `subscription.{createPortalSession,cancel,reactivate,scheduleDowngrade,cancelScheduledChange,prorationPreview}` · `credits.{purchase,autoTopupSettings,configureAutoTopup}` |
|
|
149
|
+
| `kortix.accounts` | list · get · create · members · invites · `tokens.{list,create,revoke}` (account-scoped CLI PATs, `kortix_pat_…`) · `audit.{log,export,webhooks.*}` (filterable project/session reconstruction log) (+ more: `updateName`, `leave`, `invite`, `removeMember`, `updateMemberRole`) |
|
|
150
|
+
| `kortix.billing` | entitlement/usage reads: `accountState` · `accountStateMinimal` · `transactions` · `transactionsSummary` · `creditBreakdown` · `usageHistory` · `usageRollup` · `sessionCosts.{list,get}` · `tierConfigurations` — plus a curated mutation surface: `checkout.{createSession,confirmSession}` · `subscription.{createPortalSession,cancel,reactivate,scheduleDowngrade,cancelScheduledChange,prorationPreview}` · `credits.{purchase,autoTopupSettings,configureAutoTopup}` |
|
|
124
151
|
| `kortix.marketplace` | public marketplace catalog browse + sources (not project-scoped): `items` · `item` · `itemFile` · `marketplaces` · `featured` · `sources.{list,add,remove}` — distinct from the install-scoped `project(id).marketplace` |
|
|
125
152
|
| `kortix.validateToken()` | pasted-API-key validation helper — `GET /accounts/me`, never throws, resolves `{valid, identity?, error?}` |
|
|
126
153
|
| `kortix.project(id)` | id-bound handle: `.secrets` · `.access` · `.connectors` · `.policies` · `.triggers` · `.files` · `.git` · `.changeRequests` (incl. `requestChanges`) · `.sessions` · `.tokens` (project-scoped CLI PATs — the `KORTIX_TOKEN` shape) · `.marketplace` / `.registry` (install/update/remove catalog items) · `.setupLinks.{requestSecret,requestConnector}` (agent-minted secret-entry / connector links) · `.validateManifest` · `.gitToken` · `.setDefaultAgent(name)` · `.session(sid)` (+ more namespaces: `.review`, `.approvals`, `.gateway` (incl. `.routing` and `.playground`), `.channels`, `.modelDefaults`, `.sandbox`) |
|
|
127
|
-
| `kortix.session(pid, sid)` | id-bound handle: lifecycle (`get`/`update`/`delete`/`start`/`restart`/`stop`/`setSharing`/`previews`/`commit`/`publicShares`/`ensureReady`) · `send`/`abort`/`rewind`/`restoreRewind`/`setModel`/`setAgent`
|
|
128
|
-
| `kortix.runtime()` | the
|
|
154
|
+
| `kortix.session(pid, sid)` | id-bound handle: lifecycle (`get`/`update`/`delete`/`start`/`restart`/`stop`/`setSharing`/`previews`/`commit`/`publicShares`/`ensureReady`) · finalized `cost()` · `send`/`abort`/`rewind`/`restoreRewind`/`setModel`/`setAgent` · `transcript()` · `.files` · runtime URL helpers (`health`/`previewUrl`/`proxyUrl`) · OpenCode REST compatibility escape hatches: `stream()` and `.runtime` |
|
|
155
|
+
| `kortix.runtime()` | the OpenCode v2 compatibility client for the active sandbox; use a session-scoped handle in multi-tenant code |
|
|
129
156
|
|
|
130
157
|
Runnable, self-contained scripts for the highest-value flows live in
|
|
131
158
|
[`examples/`](./examples): list projects with a PAT, send + stream, the
|
|
@@ -140,7 +167,7 @@ to the agent only as one `KORTIX_SESSION_CONTEXT` JSON envelope:
|
|
|
140
167
|
|
|
141
168
|
```ts
|
|
142
169
|
await kortix.project(projectId).sessions.create({
|
|
143
|
-
runtime_context: { workspace_id:
|
|
170
|
+
runtime_context: { workspace_id: "org_123", locale: "de" },
|
|
144
171
|
});
|
|
145
172
|
```
|
|
146
173
|
|
|
@@ -151,27 +178,27 @@ credential endpoint, and pass only the non-secret profile id at session create:
|
|
|
151
178
|
```ts
|
|
152
179
|
const project = kortix.project(projectId);
|
|
153
180
|
const profile = await project.connectors.profiles.reconcile({
|
|
154
|
-
connector_alias:
|
|
155
|
-
owner_type:
|
|
181
|
+
connector_alias: "customer-data",
|
|
182
|
+
owner_type: "external",
|
|
156
183
|
owner_id: wrapperUserId,
|
|
157
|
-
label:
|
|
184
|
+
label: "Customer data",
|
|
158
185
|
metadata: { tenant_ref: wrapperTenantReference },
|
|
159
186
|
});
|
|
160
187
|
|
|
161
188
|
// Omit `auth` when creating to apply source-advertised authentication.
|
|
162
189
|
const auth = await project.connectors.auth.discover({
|
|
163
|
-
slug:
|
|
164
|
-
provider:
|
|
165
|
-
spec:
|
|
190
|
+
slug: "hubspot",
|
|
191
|
+
provider: "postman",
|
|
192
|
+
spec: "https://github.com/HubSpot/HubSpot-public-api-spec-collection",
|
|
166
193
|
});
|
|
167
194
|
await project.connectors.profiles.updateCredential(profile.profile_id, {
|
|
168
195
|
value: shortLivedCapability,
|
|
169
|
-
kind:
|
|
196
|
+
kind: "secret",
|
|
170
197
|
});
|
|
171
198
|
await project.sessions.create({
|
|
172
|
-
runtime_context: { locale:
|
|
199
|
+
runtime_context: { locale: "de" },
|
|
173
200
|
connector_bindings: {
|
|
174
|
-
|
|
201
|
+
"customer-data": { profile_id: profile.profile_id },
|
|
175
202
|
},
|
|
176
203
|
});
|
|
177
204
|
```
|
|
@@ -182,8 +209,8 @@ token:
|
|
|
182
209
|
|
|
183
210
|
```ts
|
|
184
211
|
const profile = await project.connectors.profiles.reconcileMember({
|
|
185
|
-
connector_alias:
|
|
186
|
-
label:
|
|
212
|
+
connector_alias: "gmail",
|
|
213
|
+
label: "My Gmail",
|
|
187
214
|
});
|
|
188
215
|
await project.connectors.profiles.pipedreamConnect(profile.profile_id);
|
|
189
216
|
// Complete OAuth, then:
|
|
@@ -201,7 +228,8 @@ restart. Credentials are encrypted server-side and are never returned, placed
|
|
|
201
228
|
in `KORTIX_SESSION_CONTEXT`, or injected into the sandbox environment. Raw env
|
|
202
229
|
and MCP configuration are not session-create inputs.
|
|
203
230
|
|
|
204
|
-
`session.stream()` is a thin facade over the
|
|
231
|
+
For OpenCode REST sessions, `session.stream()` is a thin facade over the
|
|
232
|
+
framework-free `openEventStream`
|
|
205
233
|
primitive (also exported directly, for hosts that want to manage the client
|
|
206
234
|
themselves): it resolves THIS handle's own runtime (`ensureReady()`), connects
|
|
207
235
|
to that runtime's SSE endpoint, and hands you a `close()`-able handle. No React
|
|
@@ -217,6 +245,8 @@ const handle = await kortix.session(pid, sid).stream({
|
|
|
217
245
|
handle.close();
|
|
218
246
|
```
|
|
219
247
|
|
|
248
|
+
`session.stream()` emits OpenCode v2 events. Use `useSession()` in React.
|
|
249
|
+
|
|
220
250
|
`@kortix/sdk/react`'s `useOpenCodeEventStream` uses the exact same primitive
|
|
221
251
|
under the hood — it just also writes into the React Query cache.
|
|
222
252
|
|
|
@@ -234,12 +264,12 @@ in-flight request.
|
|
|
234
264
|
it statically imports `node:async_hooks`) fixes this with `AsyncLocalStorage`:
|
|
235
265
|
|
|
236
266
|
```ts
|
|
237
|
-
import { createScopedKortix } from
|
|
267
|
+
import { createScopedKortix } from "@kortix/sdk/server";
|
|
238
268
|
|
|
239
269
|
// Express/Hono/Bun.serve — any per-request handler. One scoped client PER
|
|
240
270
|
// REQUEST; each end user's token stays isolated to that request's own async
|
|
241
271
|
// call tree, even across `await`s, even under concurrency.
|
|
242
|
-
app.get(
|
|
272
|
+
app.get("/projects", async (req, res) => {
|
|
243
273
|
const kortix = createScopedKortix({
|
|
244
274
|
backendUrl: process.env.KORTIX_API_URL!,
|
|
245
275
|
getToken: async () => resolveKortixTokenFor(req), // per-end-user PAT/token
|
|
@@ -255,7 +285,7 @@ it never writes the process-global singleton. For middleware-style wrapping of
|
|
|
255
285
|
an entire request body instead, use the lower-level primitive:
|
|
256
286
|
|
|
257
287
|
```ts
|
|
258
|
-
import { runWithKortix } from
|
|
288
|
+
import { runWithKortix } from "@kortix/sdk/server";
|
|
259
289
|
|
|
260
290
|
app.use(async (req, res, next) => {
|
|
261
291
|
await runWithKortix(
|
|
@@ -286,8 +316,8 @@ renderer for **every** part kind at compile time, so a new part type is a
|
|
|
286
316
|
build error at your call site instead of a silent drop in production:
|
|
287
317
|
|
|
288
318
|
```tsx
|
|
289
|
-
import { renderParts, type PartRenderers } from
|
|
290
|
-
import { classifyTurn } from
|
|
319
|
+
import { renderParts, type PartRenderers } from "@kortix/sdk/react";
|
|
320
|
+
import { classifyTurn } from "@kortix/sdk/turns";
|
|
291
321
|
|
|
292
322
|
const renderers: PartRenderers<React.ReactNode> = {
|
|
293
323
|
text: (p) => <Markdown>{p.text}</Markdown>,
|
|
@@ -298,16 +328,21 @@ const renderers: PartRenderers<React.ReactNode> = {
|
|
|
298
328
|
patch: (p) => <DiffStat files={p.fileCount} />,
|
|
299
329
|
retry: (p) => <Note>{`retrying (attempt ${p.attempt})`}</Note>,
|
|
300
330
|
compaction: () => <Note>context compacted</Note>,
|
|
301
|
-
snapshot: () => null,
|
|
302
|
-
agent: () => null,
|
|
303
|
-
step: () => null,
|
|
304
|
-
unknown: () => null,
|
|
331
|
+
snapshot: () => null, // internal checkpoint hash — nothing to show
|
|
332
|
+
agent: () => null, // inline @mention, already in the sibling text
|
|
333
|
+
step: () => null, // model-step bookkeeping
|
|
334
|
+
unknown: () => null, // forward-compat: newer server than client
|
|
305
335
|
};
|
|
306
336
|
|
|
307
337
|
function Turn({ message }: { message: MessageWithParts }) {
|
|
308
338
|
const { parts, error, isEmpty } = classifyTurn(message);
|
|
309
339
|
if (isEmpty && !error) return null;
|
|
310
|
-
return
|
|
340
|
+
return (
|
|
341
|
+
<>
|
|
342
|
+
{renderParts(parts, renderers)}
|
|
343
|
+
{error && <TurnFailed {...error} />}
|
|
344
|
+
</>
|
|
345
|
+
);
|
|
311
346
|
}
|
|
312
347
|
```
|
|
313
348
|
|
|
@@ -346,17 +381,19 @@ The canonical server-side wrapper shape — catch a 402 and pass the payload
|
|
|
346
381
|
through to your own client for re-billing, instead of leaking a Kortix error:
|
|
347
382
|
|
|
348
383
|
```ts
|
|
349
|
-
import { ApiError, AuthError, BillingError } from
|
|
384
|
+
import { ApiError, AuthError, BillingError } from "@kortix/sdk";
|
|
350
385
|
|
|
351
386
|
try {
|
|
352
387
|
await kortix.session(pid, sid).send(prompt);
|
|
353
388
|
} catch (err) {
|
|
354
389
|
if (err instanceof BillingError) {
|
|
355
390
|
// 402 — surface the upgrade/cost payload under YOUR billing story.
|
|
356
|
-
return res.status(402).json({ reason:
|
|
391
|
+
return res.status(402).json({ reason: "quota", detail: err.detail });
|
|
357
392
|
}
|
|
358
|
-
if (err instanceof AuthError)
|
|
359
|
-
|
|
393
|
+
if (err instanceof AuthError)
|
|
394
|
+
return res.status(401).json({ error: "not authenticated" });
|
|
395
|
+
if (err instanceof ApiError)
|
|
396
|
+
return res.status(err.status ?? 502).json({ error: err.message });
|
|
360
397
|
throw err;
|
|
361
398
|
}
|
|
362
399
|
```
|
|
@@ -379,19 +416,19 @@ Stable, tree-shakeable surfaces (also reachable via the facade). Not exhaustive
|
|
|
379
416
|
`./sandbox-connection-store`, `./opencode-pending-store`, `./session/url`,
|
|
380
417
|
`./idb-sync-cache`):
|
|
381
418
|
|
|
382
|
-
| import
|
|
383
|
-
|
|
384
|
-
| `@kortix/sdk`
|
|
385
|
-
| `@kortix/sdk/server`
|
|
386
|
-
| `@kortix/sdk/react`
|
|
387
|
-
| `@kortix/sdk/turns`
|
|
388
|
-
| `@kortix/sdk/files`
|
|
389
|
-
| `@kortix/sdk/session`
|
|
390
|
-
| `@kortix/sdk/opencode-client`
|
|
391
|
-
| `@kortix/sdk/projects-client`
|
|
392
|
-
| `@kortix/sdk/auth`
|
|
393
|
-
| `@kortix/sdk/api-client`
|
|
394
|
-
| `@kortix/sdk/server-store` · `@kortix/sdk/sync-store` | active-sandbox state · live message/part/status store
|
|
419
|
+
| import | provides |
|
|
420
|
+
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
421
|
+
| `@kortix/sdk` | `createKortix`, `configureKortix`, `files`, the error classes, `classifyPart`/`classifyTurn`, `narrowChatEvent`, `openEventStream`, domain result types |
|
|
422
|
+
| `@kortix/sdk/server` | **Node/Bun only** — `runWithKortix`, `createScopedKortix`, `getScopedConfig` (per-request config isolation; see "Kortix as a Backend") |
|
|
423
|
+
| `@kortix/sdk/react` | every `useOpenCode*` hook + providers (reactive data), `useSession`, `useChatTurns`/`renderParts`, domain hooks (`useProjectSecrets`/`useProjectTriggers`/`useChangeRequests`) |
|
|
424
|
+
| `@kortix/sdk/turns` | framework-free part/turn classification (`classifyPart`, `classifyTurn`, `toolInfo`, turn grouping/cost helpers) |
|
|
425
|
+
| `@kortix/sdk/files` | workspace file ops (daemon `/file` + `/find`): `listFiles`, `readFile`, `readBlob`, `getFileStatus`, `findFiles`, `findText`, `uploadFile`, `deleteFile`, `mkdir`, `renameFile`, … |
|
|
426
|
+
| `@kortix/sdk/session` | a session's runtime surface — `getSessionHealth`/`isRuntimeReady` + proxy/preview URL builders (`rewriteLocalhostUrl`, `proxyLocalhostUrl`, `detectLocalhostUrls`, …) + preview-auth helpers. **No "sandbox" in the public surface** — a session owns its runtime |
|
|
427
|
+
| `@kortix/sdk/opencode-client` | `getClient`, `getClientForUrl` + the **full opencode v2 type surface** (`Event`, `Part`, `Message`, `Session`, `Pty`, `Config`, …) |
|
|
428
|
+
| `@kortix/sdk/projects-client` | the raw REST functions (the facade wraps these) |
|
|
429
|
+
| `@kortix/sdk/auth` | `authenticatedFetch`, token accessors |
|
|
430
|
+
| `@kortix/sdk/api-client` | the raw `backendApi` primitive — host code should go through the facade or another subpath module instead of calling this directly |
|
|
431
|
+
| `@kortix/sdk/server-store` · `@kortix/sdk/sync-store` | active-sandbox state · live message/part/status store |
|
|
395
432
|
|
|
396
433
|
## Configuration
|
|
397
434
|
|
|
@@ -447,6 +484,6 @@ pnpm --filter @kortix/sdk typecheck # package + examples/ (examples/tsconfig.js
|
|
|
447
484
|
pnpm --filter @kortix/sdk test # facade, files, react hooks, turns, transcript, session url/health, projects-client domains
|
|
448
485
|
```
|
|
449
486
|
|
|
450
|
-
See **`API-MAP.md`** for the complete endpoint catalogue
|
|
451
|
-
and
|
|
452
|
-
release.
|
|
487
|
+
See **`API-MAP.md`** for the complete endpoint catalogue. It covers the Kortix
|
|
488
|
+
REST API and OpenCode REST runtime. See **`CHANGELOG.md`** for
|
|
489
|
+
per-release changes.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idb-sync-cache-key.d.ts","sourceRoot":"","sources":["../../../src/browser/cache/idb-sync-cache-key.ts"],"names":[],"mappings":"AAAA,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,EACzB,kBAAkB,CAAC,EAAE,MAAM,GAC1B,MAAM,CAKR"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function buildSessionCacheKey(userScope, openCodeSessionId, kortixSessionScope) {
|
|
2
|
+
if (!kortixSessionScope) {
|
|
3
|
+
return `${userScope}:session:${openCodeSessionId}`;
|
|
4
|
+
}
|
|
5
|
+
return `${userScope}:kortix-session:${encodeURIComponent(kortixSessionScope)}`;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=idb-sync-cache-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idb-sync-cache-key.js","sourceRoot":"","sources":["../../../src/browser/cache/idb-sync-cache-key.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,oBAAoB,CAClC,SAAiB,EACjB,iBAAyB,EACzB,kBAA2B;IAE3B,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACxB,OAAO,GAAG,SAAS,YAAY,iBAAiB,EAAE,CAAC;IACrD,CAAC;IACD,OAAO,GAAG,SAAS,mBAAmB,kBAAkB,CAAC,kBAAkB,CAAC,EAAE,CAAC;AACjF,CAAC"}
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
* Schema: one object store "sessions" keyed by cacheKey, each entry holds
|
|
6
6
|
* { cacheKey, userId, sessionId, messages, parts, updatedAt }.
|
|
7
7
|
*/
|
|
8
|
-
export declare function saveSessionToIDB(sessionId: string, messages: any[], parts: Record<string, any[]
|
|
8
|
+
export declare function saveSessionToIDB(sessionId: string, messages: any[], parts: Record<string, any[]>, kortixSessionScope?: string): Promise<void>;
|
|
9
9
|
export declare function flushIDBWrites(): Promise<void>;
|
|
10
|
-
export declare function loadSessionFromIDB(sessionId: string): Promise<{
|
|
10
|
+
export declare function loadSessionFromIDB(sessionId: string, kortixSessionScope?: string): Promise<{
|
|
11
11
|
messages: any[];
|
|
12
12
|
parts: Record<string, any[]>;
|
|
13
13
|
} | null>;
|
|
14
14
|
export declare function loadAllSessionIdsFromIDB(): Promise<string[]>;
|
|
15
|
-
export declare function deleteSessionFromIDB(sessionId: string): Promise<void>;
|
|
15
|
+
export declare function deleteSessionFromIDB(sessionId: string, kortixSessionScope?: string): Promise<void>;
|
|
16
16
|
export declare function clearSessionIDBCache(): Promise<void>;
|
|
17
17
|
export declare function pruneIDBCache(): Promise<void>;
|
|
18
18
|
//# sourceMappingURL=idb-sync-cache.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"idb-sync-cache.d.ts","sourceRoot":"","sources":["../../../src/browser/cache/idb-sync-cache.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"idb-sync-cache.d.ts","sourceRoot":"","sources":["../../../src/browser/cache/idb-sync-cache.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAmHH,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,GAAG,EAAE,EACf,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,EAC5B,kBAAkB,CAAC,EAAE,MAAM,GAC1B,OAAO,CAAC,IAAI,CAAC,CAef;AAED,wBAAgB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAK9C;AAED,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,EACjB,kBAAkB,CAAC,EAAE,MAAM,GAC1B,OAAO,CAAC;IAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;CAAE,GAAG,IAAI,CAAC,CA4BnE;AAED,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAiBlE;AAED,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,EACjB,kBAAkB,CAAC,EAAE,MAAM,GAC1B,OAAO,CAAC,IAAI,CAAC,CAWf;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAa1D;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CA6BnD"}
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* { cacheKey, userId, sessionId, messages, parts, updatedAt }.
|
|
7
7
|
*/
|
|
8
8
|
import { platformConfig } from '../../core/http/config.js';
|
|
9
|
-
|
|
9
|
+
import { buildSessionCacheKey } from './idb-sync-cache-key.js';
|
|
10
|
+
const DB_NAME = 'kortix-session-cache';
|
|
10
11
|
const DB_VERSION = 2;
|
|
11
|
-
const STORE_NAME =
|
|
12
|
+
const STORE_NAME = 'sessions';
|
|
12
13
|
const MAX_CACHED_SESSIONS = 50;
|
|
13
14
|
const MAX_SESSION_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
14
15
|
async function getCurrentCacheScope() {
|
|
@@ -20,16 +21,13 @@ async function getCurrentCacheScope() {
|
|
|
20
21
|
return null;
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
|
-
function buildCacheKey(scope, sessionId) {
|
|
24
|
-
return `${scope}:session:${sessionId}`;
|
|
25
|
-
}
|
|
26
24
|
let dbPromise = null;
|
|
27
25
|
function openDB() {
|
|
28
26
|
if (dbPromise)
|
|
29
27
|
return dbPromise;
|
|
30
28
|
dbPromise = new Promise((resolve, reject) => {
|
|
31
|
-
if (typeof indexedDB ===
|
|
32
|
-
reject(new Error(
|
|
29
|
+
if (typeof indexedDB === 'undefined') {
|
|
30
|
+
reject(new Error('IndexedDB not available'));
|
|
33
31
|
return;
|
|
34
32
|
}
|
|
35
33
|
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
@@ -39,7 +37,7 @@ function openDB() {
|
|
|
39
37
|
db.deleteObjectStore(STORE_NAME);
|
|
40
38
|
}
|
|
41
39
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
42
|
-
db.createObjectStore(STORE_NAME, { keyPath:
|
|
40
|
+
db.createObjectStore(STORE_NAME, { keyPath: 'cacheKey' });
|
|
43
41
|
}
|
|
44
42
|
};
|
|
45
43
|
req.onsuccess = () => resolve(req.result);
|
|
@@ -53,17 +51,25 @@ function openDB() {
|
|
|
53
51
|
const pendingWrites = new Map();
|
|
54
52
|
let flushTimer = null;
|
|
55
53
|
const FLUSH_INTERVAL_MS = 500;
|
|
54
|
+
// The caps below are only real if something enforces them. Prune once per page
|
|
55
|
+
// load, off the back of the first flush, so the cache cannot grow forever
|
|
56
|
+
// without any host having to remember to call it.
|
|
57
|
+
let prunedThisLoad = false;
|
|
56
58
|
async function flushPendingWrites() {
|
|
57
59
|
flushTimer = null;
|
|
58
60
|
if (pendingWrites.size === 0)
|
|
59
61
|
return;
|
|
62
|
+
if (!prunedThisLoad) {
|
|
63
|
+
prunedThisLoad = true;
|
|
64
|
+
void pruneIDBCache();
|
|
65
|
+
}
|
|
60
66
|
const batch = new Map(pendingWrites);
|
|
61
67
|
pendingWrites.clear();
|
|
62
68
|
try {
|
|
63
69
|
const db = await openDB();
|
|
64
|
-
const tx = db.transaction(STORE_NAME,
|
|
70
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
65
71
|
const store = tx.objectStore(STORE_NAME);
|
|
66
|
-
for (const [cacheKey, { scope, sessionId, messages, parts }] of batch) {
|
|
72
|
+
for (const [cacheKey, { scope, sessionId, kortixSessionScope, messages, parts }] of batch) {
|
|
67
73
|
const partsForSession = {};
|
|
68
74
|
for (const msg of messages) {
|
|
69
75
|
if (parts[msg.id]) {
|
|
@@ -74,6 +80,7 @@ async function flushPendingWrites() {
|
|
|
74
80
|
cacheKey,
|
|
75
81
|
userId: scope,
|
|
76
82
|
sessionId,
|
|
83
|
+
kortixSessionScope,
|
|
77
84
|
messages,
|
|
78
85
|
parts: partsForSession,
|
|
79
86
|
updatedAt: Date.now(),
|
|
@@ -88,12 +95,18 @@ async function flushPendingWrites() {
|
|
|
88
95
|
// Non-critical
|
|
89
96
|
}
|
|
90
97
|
}
|
|
91
|
-
export async function saveSessionToIDB(sessionId, messages, parts) {
|
|
98
|
+
export async function saveSessionToIDB(sessionId, messages, parts, kortixSessionScope) {
|
|
92
99
|
const scope = await getCurrentCacheScope();
|
|
93
100
|
if (!scope)
|
|
94
101
|
return;
|
|
95
|
-
const cacheKey =
|
|
96
|
-
pendingWrites.set(cacheKey, {
|
|
102
|
+
const cacheKey = buildSessionCacheKey(scope, sessionId, kortixSessionScope);
|
|
103
|
+
pendingWrites.set(cacheKey, {
|
|
104
|
+
scope,
|
|
105
|
+
sessionId,
|
|
106
|
+
kortixSessionScope,
|
|
107
|
+
messages,
|
|
108
|
+
parts,
|
|
109
|
+
});
|
|
97
110
|
if (!flushTimer) {
|
|
98
111
|
flushTimer = setTimeout(flushPendingWrites, FLUSH_INTERVAL_MS);
|
|
99
112
|
}
|
|
@@ -104,25 +117,32 @@ export function flushIDBWrites() {
|
|
|
104
117
|
}
|
|
105
118
|
return flushPendingWrites();
|
|
106
119
|
}
|
|
107
|
-
export async function loadSessionFromIDB(sessionId) {
|
|
120
|
+
export async function loadSessionFromIDB(sessionId, kortixSessionScope) {
|
|
108
121
|
try {
|
|
109
122
|
const scope = await getCurrentCacheScope();
|
|
110
123
|
if (!scope)
|
|
111
124
|
return null;
|
|
112
125
|
const db = await openDB();
|
|
113
|
-
const tx = db.transaction(STORE_NAME,
|
|
126
|
+
const tx = db.transaction(STORE_NAME, 'readonly');
|
|
114
127
|
const store = tx.objectStore(STORE_NAME);
|
|
115
|
-
const req = store.get(
|
|
128
|
+
const req = store.get(buildSessionCacheKey(scope, sessionId, kortixSessionScope));
|
|
116
129
|
const result = await new Promise((resolve, reject) => {
|
|
117
130
|
req.onsuccess = () => resolve(req.result);
|
|
118
131
|
req.onerror = () => reject(req.error);
|
|
119
132
|
});
|
|
120
133
|
if (!result)
|
|
121
134
|
return null;
|
|
122
|
-
if (result.userId !== scope
|
|
135
|
+
if (result.userId !== scope)
|
|
123
136
|
return null;
|
|
137
|
+
if (kortixSessionScope) {
|
|
138
|
+
if (result.kortixSessionScope !== kortixSessionScope)
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
else if (result.sessionId !== sessionId) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
124
144
|
if (Date.now() - result.updatedAt > MAX_SESSION_AGE_MS) {
|
|
125
|
-
deleteSessionFromIDB(sessionId);
|
|
145
|
+
deleteSessionFromIDB(sessionId, kortixSessionScope);
|
|
126
146
|
return null;
|
|
127
147
|
}
|
|
128
148
|
return { messages: result.messages, parts: result.parts };
|
|
@@ -137,7 +157,7 @@ export async function loadAllSessionIdsFromIDB() {
|
|
|
137
157
|
if (!scope)
|
|
138
158
|
return [];
|
|
139
159
|
const db = await openDB();
|
|
140
|
-
const tx = db.transaction(STORE_NAME,
|
|
160
|
+
const tx = db.transaction(STORE_NAME, 'readonly');
|
|
141
161
|
const store = tx.objectStore(STORE_NAME);
|
|
142
162
|
const req = store.getAll();
|
|
143
163
|
const entries = await new Promise((resolve, reject) => {
|
|
@@ -150,14 +170,14 @@ export async function loadAllSessionIdsFromIDB() {
|
|
|
150
170
|
return [];
|
|
151
171
|
}
|
|
152
172
|
}
|
|
153
|
-
export async function deleteSessionFromIDB(sessionId) {
|
|
173
|
+
export async function deleteSessionFromIDB(sessionId, kortixSessionScope) {
|
|
154
174
|
try {
|
|
155
175
|
const scope = await getCurrentCacheScope();
|
|
156
176
|
if (!scope)
|
|
157
177
|
return;
|
|
158
178
|
const db = await openDB();
|
|
159
|
-
const tx = db.transaction(STORE_NAME,
|
|
160
|
-
tx.objectStore(STORE_NAME).delete(
|
|
179
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
180
|
+
tx.objectStore(STORE_NAME).delete(buildSessionCacheKey(scope, sessionId, kortixSessionScope));
|
|
161
181
|
}
|
|
162
182
|
catch {
|
|
163
183
|
// ignore
|
|
@@ -171,7 +191,7 @@ export async function clearSessionIDBCache() {
|
|
|
171
191
|
flushTimer = null;
|
|
172
192
|
}
|
|
173
193
|
const db = await openDB();
|
|
174
|
-
const tx = db.transaction(STORE_NAME,
|
|
194
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
175
195
|
tx.objectStore(STORE_NAME).clear();
|
|
176
196
|
}
|
|
177
197
|
catch {
|
|
@@ -181,7 +201,7 @@ export async function clearSessionIDBCache() {
|
|
|
181
201
|
export async function pruneIDBCache() {
|
|
182
202
|
try {
|
|
183
203
|
const db = await openDB();
|
|
184
|
-
const tx = db.transaction(STORE_NAME,
|
|
204
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
185
205
|
const store = tx.objectStore(STORE_NAME);
|
|
186
206
|
const req = store.getAll();
|
|
187
207
|
const entries = await new Promise((resolve, reject) => {
|
|
@@ -189,16 +209,19 @@ export async function pruneIDBCache() {
|
|
|
189
209
|
req.onerror = () => reject(req.error);
|
|
190
210
|
});
|
|
191
211
|
const now = Date.now();
|
|
212
|
+
// Delete by `cacheKey` — it is the store's keyPath. Deleting by `sessionId`
|
|
213
|
+
// matched no record, so prune silently kept every entry forever and the
|
|
214
|
+
// 50-session / 7-day caps were never enforced.
|
|
192
215
|
const stale = entries.filter((e) => now - e.updatedAt > MAX_SESSION_AGE_MS);
|
|
193
216
|
for (const e of stale) {
|
|
194
|
-
store.delete(e.
|
|
217
|
+
store.delete(e.cacheKey);
|
|
195
218
|
}
|
|
196
219
|
const fresh = entries
|
|
197
220
|
.filter((e) => now - e.updatedAt <= MAX_SESSION_AGE_MS)
|
|
198
221
|
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
199
222
|
if (fresh.length > MAX_CACHED_SESSIONS) {
|
|
200
223
|
for (const e of fresh.slice(MAX_CACHED_SESSIONS)) {
|
|
201
|
-
store.delete(e.
|
|
224
|
+
store.delete(e.cacheKey);
|
|
202
225
|
}
|
|
203
226
|
}
|
|
204
227
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"idb-sync-cache.js","sourceRoot":"","sources":["../../../src/browser/cache/idb-sync-cache.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"idb-sync-cache.js","sourceRoot":"","sources":["../../../src/browser/cache/idb-sync-cache.ts"],"names":[],"mappings":"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"}
|