@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.
Files changed (214) hide show
  1. package/README.md +110 -73
  2. package/dist/browser/cache/idb-sync-cache-key.d.ts +2 -0
  3. package/dist/browser/cache/idb-sync-cache-key.d.ts.map +1 -0
  4. package/dist/browser/cache/idb-sync-cache-key.js +7 -0
  5. package/dist/browser/cache/idb-sync-cache-key.js.map +1 -0
  6. package/dist/browser/cache/idb-sync-cache.d.ts +3 -3
  7. package/dist/browser/cache/idb-sync-cache.d.ts.map +1 -1
  8. package/dist/browser/cache/idb-sync-cache.js +49 -26
  9. package/dist/browser/cache/idb-sync-cache.js.map +1 -1
  10. package/dist/browser/session-sync/session-cache-ownership.d.ts +36 -0
  11. package/dist/browser/session-sync/session-cache-ownership.d.ts.map +1 -0
  12. package/dist/browser/session-sync/session-cache-ownership.js +66 -0
  13. package/dist/browser/session-sync/session-cache-ownership.js.map +1 -0
  14. package/dist/browser/session-sync/session-sync-registry.d.ts +8 -6
  15. package/dist/browser/session-sync/session-sync-registry.d.ts.map +1 -1
  16. package/dist/browser/session-sync/session-sync-registry.js +86 -33
  17. package/dist/browser/session-sync/session-sync-registry.js.map +1 -1
  18. package/dist/browser/session-sync/session-transcript-cache.d.ts +55 -0
  19. package/dist/browser/session-sync/session-transcript-cache.d.ts.map +1 -0
  20. package/dist/browser/session-sync/session-transcript-cache.js +69 -0
  21. package/dist/browser/session-sync/session-transcript-cache.js.map +1 -0
  22. package/dist/core/client/kortix.d.ts +72 -25
  23. package/dist/core/client/kortix.d.ts.map +1 -1
  24. package/dist/core/client/kortix.js +76 -17
  25. package/dist/core/client/kortix.js.map +1 -1
  26. package/dist/core/rest/platform-client/host-boundary.d.ts +7 -0
  27. package/dist/core/rest/platform-client/host-boundary.d.ts.map +1 -1
  28. package/dist/core/rest/platform-client/host-boundary.js +14 -0
  29. package/dist/core/rest/platform-client/host-boundary.js.map +1 -1
  30. package/dist/core/rest/projects-client/access.d.ts +1 -2
  31. package/dist/core/rest/projects-client/access.d.ts.map +1 -1
  32. package/dist/core/rest/projects-client/access.js +9 -6
  33. package/dist/core/rest/projects-client/access.js.map +1 -1
  34. package/dist/core/rest/projects-client/agent-config.d.ts +15 -7
  35. package/dist/core/rest/projects-client/agent-config.d.ts.map +1 -1
  36. package/dist/core/rest/projects-client/agent-config.js +43 -2
  37. package/dist/core/rest/projects-client/agent-config.js.map +1 -1
  38. package/dist/core/rest/projects-client/agent-scope.d.ts +4 -0
  39. package/dist/core/rest/projects-client/agent-scope.d.ts.map +1 -1
  40. package/dist/core/rest/projects-client/agent-scope.js +34 -1
  41. package/dist/core/rest/projects-client/agent-scope.js.map +1 -1
  42. package/dist/core/rest/projects-client/audit.d.ts +34 -0
  43. package/dist/core/rest/projects-client/audit.d.ts.map +1 -1
  44. package/dist/core/rest/projects-client/audit.js +49 -4
  45. package/dist/core/rest/projects-client/audit.js.map +1 -1
  46. package/dist/core/rest/projects-client/billing.d.ts +26 -14
  47. package/dist/core/rest/projects-client/billing.d.ts.map +1 -1
  48. package/dist/core/rest/projects-client/billing.js +4 -5
  49. package/dist/core/rest/projects-client/billing.js.map +1 -1
  50. package/dist/core/rest/projects-client/connectors.d.ts +106 -45
  51. package/dist/core/rest/projects-client/connectors.d.ts.map +1 -1
  52. package/dist/core/rest/projects-client/connectors.js +95 -58
  53. package/dist/core/rest/projects-client/connectors.js.map +1 -1
  54. package/dist/core/rest/projects-client/github.d.ts +7 -1
  55. package/dist/core/rest/projects-client/github.d.ts.map +1 -1
  56. package/dist/core/rest/projects-client/github.js.map +1 -1
  57. package/dist/core/rest/projects-client/iam.d.ts +17 -0
  58. package/dist/core/rest/projects-client/iam.d.ts.map +1 -1
  59. package/dist/core/rest/projects-client/iam.js +14 -0
  60. package/dist/core/rest/projects-client/iam.js.map +1 -1
  61. package/dist/core/rest/projects-client/index.d.ts +2 -0
  62. package/dist/core/rest/projects-client/index.d.ts.map +1 -1
  63. package/dist/core/rest/projects-client/index.js +2 -0
  64. package/dist/core/rest/projects-client/index.js.map +1 -1
  65. package/dist/core/rest/projects-client/model-enablement.d.ts +9 -0
  66. package/dist/core/rest/projects-client/model-enablement.d.ts.map +1 -0
  67. package/dist/core/rest/projects-client/model-enablement.js +16 -0
  68. package/dist/core/rest/projects-client/model-enablement.js.map +1 -0
  69. package/dist/core/rest/projects-client/policies.d.ts +18 -0
  70. package/dist/core/rest/projects-client/policies.d.ts.map +1 -1
  71. package/dist/core/rest/projects-client/policies.js.map +1 -1
  72. package/dist/core/rest/projects-client/projects.d.ts +87 -1
  73. package/dist/core/rest/projects-client/projects.d.ts.map +1 -1
  74. package/dist/core/rest/projects-client/projects.js +8 -0
  75. package/dist/core/rest/projects-client/projects.js.map +1 -1
  76. package/dist/core/rest/projects-client/sandbox.d.ts +50 -0
  77. package/dist/core/rest/projects-client/sandbox.d.ts.map +1 -1
  78. package/dist/core/rest/projects-client/sandbox.js.map +1 -1
  79. package/dist/core/rest/projects-client/session-costs.d.ts +102 -0
  80. package/dist/core/rest/projects-client/session-costs.d.ts.map +1 -0
  81. package/dist/core/rest/projects-client/session-costs.js +25 -0
  82. package/dist/core/rest/projects-client/session-costs.js.map +1 -0
  83. package/dist/core/rest/projects-client/session-sandbox.d.ts +0 -2
  84. package/dist/core/rest/projects-client/session-sandbox.d.ts.map +1 -1
  85. package/dist/core/rest/projects-client/session-sandbox.js +1 -10
  86. package/dist/core/rest/projects-client/session-sandbox.js.map +1 -1
  87. package/dist/core/rest/projects-client/sessions.d.ts +94 -44
  88. package/dist/core/rest/projects-client/sessions.d.ts.map +1 -1
  89. package/dist/core/rest/projects-client/sessions.js +9 -15
  90. package/dist/core/rest/projects-client/sessions.js.map +1 -1
  91. package/dist/core/rest/projects-client/setup-links.d.ts +37 -0
  92. package/dist/core/rest/projects-client/setup-links.d.ts.map +1 -1
  93. package/dist/core/rest/projects-client/setup-links.js +11 -0
  94. package/dist/core/rest/projects-client/setup-links.js.map +1 -1
  95. package/dist/core/session/index.d.ts +1 -1
  96. package/dist/core/session/index.d.ts.map +1 -1
  97. package/dist/core/session/index.js +1 -1
  98. package/dist/core/session/index.js.map +1 -1
  99. package/dist/core/session/preview-auth.d.ts +32 -0
  100. package/dist/core/session/preview-auth.d.ts.map +1 -0
  101. package/dist/core/session/preview-auth.js +53 -0
  102. package/dist/core/session/preview-auth.js.map +1 -0
  103. package/dist/core/session/server-store/url-helpers.d.ts.map +1 -1
  104. package/dist/core/session/server-store/url-helpers.js +5 -3
  105. package/dist/core/session/server-store/url-helpers.js.map +1 -1
  106. package/dist/core/session-sync/session-sync-controller.d.ts +12 -1
  107. package/dist/core/session-sync/session-sync-controller.d.ts.map +1 -1
  108. package/dist/core/session-sync/session-sync-controller.js +34 -2
  109. package/dist/core/session-sync/session-sync-controller.js.map +1 -1
  110. package/dist/index.d.ts +1 -2
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +0 -1
  113. package/dist/index.js.map +1 -1
  114. package/dist/kortix.esm.min.js +19 -24
  115. package/dist/kortix.global.js +19 -24
  116. package/dist/platform/auth-core.d.ts +5 -3
  117. package/dist/platform/auth-core.d.ts.map +1 -1
  118. package/dist/platform/auth-core.js +6 -4
  119. package/dist/platform/auth-core.js.map +1 -1
  120. package/dist/react/index.d.ts +1 -2
  121. package/dist/react/index.d.ts.map +1 -1
  122. package/dist/react/index.js +0 -1
  123. package/dist/react/index.js.map +1 -1
  124. package/dist/react/model-flatten.d.ts +20 -0
  125. package/dist/react/model-flatten.d.ts.map +1 -1
  126. package/dist/react/model-flatten.js +12 -0
  127. package/dist/react/model-flatten.js.map +1 -1
  128. package/dist/react/opencode.d.ts +4 -3
  129. package/dist/react/opencode.d.ts.map +1 -1
  130. package/dist/react/opencode.js +4 -3
  131. package/dist/react/opencode.js.map +1 -1
  132. package/dist/react/provider-selection.d.ts +9 -0
  133. package/dist/react/provider-selection.d.ts.map +1 -1
  134. package/dist/react/provider-selection.js +23 -0
  135. package/dist/react/provider-selection.js.map +1 -1
  136. package/dist/react/session-title-sync.d.ts +9 -5
  137. package/dist/react/session-title-sync.d.ts.map +1 -1
  138. package/dist/react/session-title-sync.js +14 -10
  139. package/dist/react/session-title-sync.js.map +1 -1
  140. package/dist/react/use-canonical-opencode-session.d.ts +2 -2
  141. package/dist/react/use-canonical-opencode-session.d.ts.map +1 -1
  142. package/dist/react/use-model-enablement.d.ts +25 -0
  143. package/dist/react/use-model-enablement.d.ts.map +1 -0
  144. package/dist/react/use-model-enablement.js +92 -0
  145. package/dist/react/use-model-enablement.js.map +1 -0
  146. package/dist/react/use-model-store.d.ts +6 -14
  147. package/dist/react/use-model-store.d.ts.map +1 -1
  148. package/dist/react/use-model-store.js +30 -52
  149. package/dist/react/use-model-store.js.map +1 -1
  150. package/dist/react/use-opencode-events/handle-event.d.ts.map +1 -1
  151. package/dist/react/use-opencode-events/handle-event.js +103 -41
  152. package/dist/react/use-opencode-events/handle-event.js.map +1 -1
  153. package/dist/react/use-opencode-local.d.ts +5 -4
  154. package/dist/react/use-opencode-local.d.ts.map +1 -1
  155. package/dist/react/use-opencode-local.js +20 -37
  156. package/dist/react/use-opencode-local.js.map +1 -1
  157. package/dist/react/use-opencode-sessions/keys.d.ts +3 -1
  158. package/dist/react/use-opencode-sessions/keys.d.ts.map +1 -1
  159. package/dist/react/use-opencode-sessions/keys.js +2 -0
  160. package/dist/react/use-opencode-sessions/keys.js.map +1 -1
  161. package/dist/react/use-opencode-sessions/sessions.d.ts.map +1 -1
  162. package/dist/react/use-opencode-sessions/sessions.js +16 -7
  163. package/dist/react/use-opencode-sessions/sessions.js.map +1 -1
  164. package/dist/react/use-opencode-sessions/shared.d.ts +3 -2
  165. package/dist/react/use-opencode-sessions/shared.d.ts.map +1 -1
  166. package/dist/react/use-opencode-sessions/shared.js +6 -3
  167. package/dist/react/use-opencode-sessions/shared.js.map +1 -1
  168. package/dist/react/use-opencode-sessions/sharing.js +2 -2
  169. package/dist/react/use-opencode-sessions/sharing.js.map +1 -1
  170. package/dist/react/use-session-sync.d.ts +13 -1
  171. package/dist/react/use-session-sync.d.ts.map +1 -1
  172. package/dist/react/use-session-sync.js +85 -17
  173. package/dist/react/use-session-sync.js.map +1 -1
  174. package/dist/react/use-session.d.ts +60 -40
  175. package/dist/react/use-session.d.ts.map +1 -1
  176. package/dist/react/use-session.js +160 -202
  177. package/dist/react/use-session.js.map +1 -1
  178. package/package.json +2 -2
  179. package/dist/core/acp/client.d.ts +0 -45
  180. package/dist/core/acp/client.d.ts.map +0 -1
  181. package/dist/core/acp/client.js +0 -277
  182. package/dist/core/acp/client.js.map +0 -1
  183. package/dist/core/acp/index.d.ts +0 -5
  184. package/dist/core/acp/index.d.ts.map +0 -1
  185. package/dist/core/acp/index.js +0 -5
  186. package/dist/core/acp/index.js.map +0 -1
  187. package/dist/core/acp/projection.d.ts +0 -23
  188. package/dist/core/acp/projection.d.ts.map +0 -1
  189. package/dist/core/acp/projection.js +0 -603
  190. package/dist/core/acp/projection.js.map +0 -1
  191. package/dist/core/acp/session-controller.d.ts +0 -90
  192. package/dist/core/acp/session-controller.d.ts.map +0 -1
  193. package/dist/core/acp/session-controller.js +0 -530
  194. package/dist/core/acp/session-controller.js.map +0 -1
  195. package/dist/core/acp/types.d.ts +0 -66
  196. package/dist/core/acp/types.d.ts.map +0 -1
  197. package/dist/core/acp/types.js +0 -21
  198. package/dist/core/acp/types.js.map +0 -1
  199. package/dist/core/session/runtime-transport.d.ts +0 -16
  200. package/dist/core/session/runtime-transport.d.ts.map +0 -1
  201. package/dist/core/session/runtime-transport.js +0 -30
  202. package/dist/core/session/runtime-transport.js.map +0 -1
  203. package/dist/react/session-busy.d.ts +0 -8
  204. package/dist/react/session-busy.d.ts.map +0 -1
  205. package/dist/react/session-busy.js +0 -4
  206. package/dist/react/session-busy.js.map +0 -1
  207. package/dist/react/use-acp-session-runtime.d.ts +0 -34
  208. package/dist/react/use-acp-session-runtime.d.ts.map +0 -1
  209. package/dist/react/use-acp-session-runtime.js +0 -84
  210. package/dist/react/use-acp-session-runtime.js.map +0 -1
  211. package/dist/react/use-admin-users.d.ts +0 -98
  212. package/dist/react/use-admin-users.d.ts.map +0 -1
  213. package/dist/react/use-admin-users.js +0 -121
  214. package/dist/react/use-admin-users.js.map +0 -1
@@ -21,10 +21,9 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
21
21
  import { useEffect, useMemo, useRef, useState } from 'react';
22
22
  import { getClient, RuntimeNotReadyError } from '../core/runtime/client.js';
23
23
  import { commitSessionRewind, messagesBeforeRewind, reconcileCommittedSessionRewind, } from '../core/session/rewind.js';
24
- import { createSessionRuntimePolicy, } from '../core/session/runtime-transport.js';
25
24
  import { useOpenCodeCompactionStore } from '../browser/stores/opencode-compaction-store.js';
26
25
  import { useOpenCodePendingStore } from '../browser/stores/opencode-pending-store.js';
27
- import { setOpenCodeHealth, setSandboxStatus } from '../browser/stores/sandbox-connection-store.js';
26
+ import { setOpenCodeHealth, setSandboxStatus, } from '../browser/stores/sandbox-connection-store.js';
28
27
  import { getSandboxUrlForExternalId } from '../browser/stores/server-store.js';
29
28
  import { useSyncStore } from '../browser/stores/sync-store.js';
30
29
  import { beginSessionPromptObservation, endSessionPromptObservation, } from '../browser/session-sync/session-sync-registry.js';
@@ -35,8 +34,6 @@ import { BillingError, parseBillingError } from '../core/http/api/errors.js';
35
34
  import { formatOpenCodeRuntimeError } from '../core/http/opencode-errors.js';
36
35
  import { extractGatewayErrorDetails } from '../core/turns/errors.js';
37
36
  import { useCanonicalOpenCodeSession } from './use-canonical-opencode-session.js';
38
- import { useAcpSessionRuntime } from './use-acp-session-runtime.js';
39
- import { resolveSessionBusy } from './session-busy.js';
40
37
  import { useOpenCodeEventStream } from './use-opencode-events/index.js';
41
38
  import { formatModelString } from './use-opencode-local.js';
42
39
  import { useProjectConfig } from './use-project-config.js';
@@ -47,7 +44,7 @@ import { useRuntimePhase } from './use-runtime-phase.js';
47
44
  import { clearStartStash, readStartStash } from './session-start-stash.js';
48
45
  import { useSessionPicks } from './use-session-picks.js';
49
46
  import { useSessionSync } from './use-session-sync.js';
50
- import { readAcpSessionTitle, refreshSessionTitleQueryUntilResolved, syncAcpSessionTitleQuery, } from './session-title-sync.js';
47
+ import { reconcileHydratedSessionTitle } from './session-title-sync.js';
51
48
  import { useVisibleAgents } from './use-visible-agents.js';
52
49
  import { rejectQuestion as rejectQuestionApi, replyToPermission, replyToQuestion, useAbortOpenCodeSession, useExecuteOpenCodeCommand, useSendOpenCodeMessage, } from './use-opencode-sessions/index.js';
53
50
  import { unwrap } from './use-opencode-sessions/shared.js';
@@ -69,12 +66,93 @@ export function shouldRetrySessionStart(failureCount, error, sessionId) {
69
66
  }
70
67
  return !isSessionStartError(error) && failureCount < 3;
71
68
  }
69
+ /**
70
+ * Gap between `/start` polls. The server long-polls each tick, so this is the
71
+ * pause between holds, not the latency to observe `ready`.
72
+ */
73
+ export const SESSION_START_POLL_MS = 1_500;
74
+ /**
75
+ * Should the `/start` boot poll fire again, given the last tick's outcome?
76
+ * `false` = stop: a terminal stage, or a terminal client error that no amount
77
+ * of polling can fix. Everything else keeps polling — including a `null`
78
+ * payload from a transient transport failure, since the box is still coming up.
79
+ */
80
+ export function shouldPollSessionStart(error, data) {
81
+ if (isSessionStartError(error))
82
+ return false;
83
+ const stage = data?.stage;
84
+ return stage === 'ready' || stage === 'failed' || stage === 'stopped'
85
+ ? false
86
+ : SESSION_START_POLL_MS;
87
+ }
88
+ /**
89
+ * TanStack Query pauses interval fetches while the document is hidden unless
90
+ * this option is true. Session readiness must continue because it gates the
91
+ * runtime switch, event stream, and queued-prompt replay.
92
+ */
93
+ export const SESSION_START_POLL_OPTIONS = {
94
+ refetchInterval: (query) => shouldPollSessionStart(query.state.error, query.state.data),
95
+ refetchIntervalInBackground: true,
96
+ };
72
97
  // `getClient()` (packages/sdk/src/opencode/client.ts) throws a
73
98
  // `RuntimeNotReadyError` with this exact message when the sandbox url hasn't
74
99
  // been resolved yet (session still starting). The string match is kept as a
75
100
  // fallback for callers that re-wrap the original error (losing the
76
101
  // `instanceof` chain) but still preserve its message.
77
102
  const RUNTIME_NOT_READY_MARKER = 'Server URL not ready';
103
+ /** The error body, wherever the transport parked it. */
104
+ function errorBody(error) {
105
+ if (!error || typeof error !== 'object')
106
+ return null;
107
+ const holder = error;
108
+ for (const key of ['data', 'details', 'body']) {
109
+ const candidate = holder[key];
110
+ if (candidate && typeof candidate === 'object')
111
+ return candidate;
112
+ }
113
+ return holder;
114
+ }
115
+ function errorBodyMessage(error) {
116
+ const body = errorBody(error);
117
+ for (const key of ['message', 'error']) {
118
+ const value = body?.[key];
119
+ if (typeof value === 'string' && value.trim())
120
+ return value.trim();
121
+ }
122
+ return null;
123
+ }
124
+ /**
125
+ * The connector profiles a `CONNECTOR_AUTHORIZATION_REQUIRED` refusal names, or
126
+ * null when this is not that refusal.
127
+ *
128
+ * Keyed on the CODE, never on the message: the prose is for humans and changes.
129
+ * A refusal that names no profile still returns null — a connector prompt that
130
+ * cannot say which connector is worse than the generic error it replaced.
131
+ */
132
+ function connectorRefusalProfiles(error) {
133
+ const body = errorBody(error);
134
+ if (body?.code !== 'CONNECTOR_AUTHORIZATION_REQUIRED')
135
+ return null;
136
+ const listed = [body.connector_profiles, body.connectorProfiles].find(Array.isArray);
137
+ const profiles = [];
138
+ for (const entry of listed ?? []) {
139
+ if (!entry || typeof entry !== 'object')
140
+ continue;
141
+ const profile = entry;
142
+ const strategy = profile.authorization_strategy ?? profile.authorizationStrategy;
143
+ if (typeof profile.id !== 'string' || typeof profile.slug !== 'string')
144
+ continue;
145
+ if (strategy !== 'project' && strategy !== 'user')
146
+ continue;
147
+ profiles.push({
148
+ id: profile.id,
149
+ slug: profile.slug,
150
+ name: typeof profile.name === 'string' && profile.name ? profile.name : profile.slug,
151
+ authorization_strategy: strategy,
152
+ });
153
+ }
154
+ return profiles.length > 0 ? profiles : null;
155
+ }
78
156
  /** Classify a thrown/rejected error from a send or a permission/question reply
79
157
  * into a `KortixSendError`. Pure — safe to unit test without a runtime. */
80
158
  export function classifySendError(error) {
@@ -89,10 +167,15 @@ export function classifySendError(error) {
89
167
  if (error && typeof error === 'object') {
90
168
  const parsed = parseBillingError(error);
91
169
  if (parsed instanceof BillingError) {
170
+ return { kind: 'billing', message: parsed.message, billing: parsed, cause: error };
171
+ }
172
+ const connectors = connectorRefusalProfiles(error);
173
+ if (connectors) {
92
174
  return {
93
- kind: 'billing',
94
- message: parsed.message,
95
- billing: parsed,
175
+ kind: 'connector',
176
+ message: errorBodyMessage(error) ??
177
+ 'Connect the required connector profiles before continuing this session.',
178
+ connectors,
96
179
  cause: error,
97
180
  };
98
181
  }
@@ -138,25 +221,24 @@ export function sendStateOnStart(text) {
138
221
  export function sendStateOnError(error) {
139
222
  return { pending: null, sendError: classifySendError(error) };
140
223
  }
141
- /**
142
- * Keep the REST compatibility transcript reconciler active after
143
- * `promptAsync()` accepts a prompt.
144
- *
145
- * The runtime can accept the prompt before the browser receives its first SSE
146
- * status event. Without this optimistic busy state, the 10-second liveness
147
- * reconciliation never starts. The transcript can then stay at the user
148
- * message even though the agent completed the turn.
149
- */
150
- export function beginRestPromptObservation(sessionId) {
151
- beginSessionPromptObservation(sessionId);
224
+ export function beginRestPromptObservation(sessionId, runtimeScope) {
225
+ beginSessionPromptObservation(sessionId, runtimeScope);
152
226
  useSyncStore.getState().setStatus(sessionId, { type: 'busy' });
153
227
  }
154
- /** Clear the optimistic REST busy state when the prompt never starts or stops. */
155
- export function endRestPromptObservation(sessionId) {
156
- endSessionPromptObservation(sessionId);
228
+ export function endRestPromptObservation(sessionId, runtimeScope) {
229
+ endSessionPromptObservation(sessionId, runtimeScope);
157
230
  useSyncStore.getState().setStatus(sessionId, { type: 'idle' });
158
231
  }
159
- /** Stage a reversible OpenCode rollback on one canonical session. */
232
+ export async function sendRestPromptWithObservation(sessionId, runtimeScope, sendPrompt) {
233
+ beginRestPromptObservation(sessionId, runtimeScope);
234
+ try {
235
+ await sendPrompt();
236
+ }
237
+ catch (error) {
238
+ endRestPromptObservation(sessionId, runtimeScope);
239
+ throw error;
240
+ }
241
+ }
160
242
  export async function rewindOpenCodeSession(sessionId, messageId) {
161
243
  if (!messageId)
162
244
  throw new Error('Session rewind requires a message id');
@@ -165,7 +247,6 @@ export async function rewindOpenCodeSession(sessionId, messageId) {
165
247
  messageID: messageId,
166
248
  }));
167
249
  }
168
- /** Restore a staged OpenCode rollback before a replacement prompt commits it. */
169
250
  export async function restoreOpenCodeSessionRewind(sessionId) {
170
251
  unwrap(await getClient().session.unrevert({ sessionID: sessionId }));
171
252
  }
@@ -228,7 +309,7 @@ const DISABLED_CHAT_ENGINE_SYNC = {
228
309
  export function useSession(projectId, sessionId, options = {}) {
229
310
  const queryClient = useQueryClient();
230
311
  const titleRefreshAbortRef = useRef(null);
231
- const { waitMs = 15_000, replayStartStash = true, enabled = true, chatEngine = true, initialOpenCodeSessionId = null, runtimeTransport: runtimeTransportOverride, } = options;
312
+ const { waitMs = 15_000, replayStartStash = true, enabled = true, chatEngine = true, initialOpenCodeSessionId = null, } = options;
232
313
  // 1. Drive /start until the runtime is ready (the server long-polls each tick).
233
314
  const start = useQuery({
234
315
  queryKey: sessionStartKey(projectId, sessionId),
@@ -238,12 +319,7 @@ export function useSession(projectId, sessionId, options = {}) {
238
319
  retryDelay: (failureCount, error) => isSessionStartError(error) && error.status === 404
239
320
  ? FRESH_START_404_RETRY_DELAY_MS
240
321
  : Math.min(1000 * 2 ** failureCount, 5000),
241
- refetchInterval: (q) => {
242
- if (isSessionStartError(q.state.error))
243
- return false;
244
- const stage = q.state.data?.stage;
245
- return stage === 'ready' || stage === 'failed' || stage === 'stopped' ? false : 1500;
246
- },
322
+ ...SESSION_START_POLL_OPTIONS,
247
323
  });
248
324
  const startData = start.data ?? null;
249
325
  const startError = isSessionStartError(start.error) ? start.error : null;
@@ -251,9 +327,6 @@ export function useSession(projectId, sessionId, options = {}) {
251
327
  const sandbox = startData?.sandbox ?? null;
252
328
  const startReady = stage === 'ready';
253
329
  const terminal = stage === 'failed' || stage === 'stopped';
254
- const runtimePolicy = createSessionRuntimePolicy(startData?.runtime_transport, runtimeTransportOverride);
255
- const runtimeTransport = runtimePolicy.transport;
256
- const usesAcp = runtimePolicy.useAcp;
257
330
  // 2. Point the SDK's runtime at this session's sandbox once ready. Track WHICH
258
331
  // sandbox we switched to (not a bare bool) so navigating between sessions (this
259
332
  // hook instance is reused) re-gates instead of binding the new session to the
@@ -287,7 +360,7 @@ export function useSession(projectId, sessionId, options = {}) {
287
360
  // 4. Open the live SSE stream. This was a provider component (OpenCodeEvent
288
361
  // StreamProvider); calling the underlying hook here means the host mounts
289
362
  // nothing. It self-gates on the connection store's healthy flag (seeded above).
290
- useOpenCodeEventStream({ enabled: runtimePolicy.streamOpenCodeEvents });
363
+ useOpenCodeEventStream({ enabled: switched });
291
364
  // 5. Resolve the canonical OpenCode root id (server-owned; /start hands it over)
292
365
  // and sync messages off it.
293
366
  const canonicalSession = useCanonicalOpenCodeSession({
@@ -295,22 +368,10 @@ export function useSession(projectId, sessionId, options = {}) {
295
368
  sessionId,
296
369
  pinFromStart: startData?.opencode_session_id ?? null,
297
370
  initialPin: initialOpenCodeSessionId,
298
- listRuntimeSessions: runtimePolicy.listOpenCodeSessions,
371
+ listRuntimeSessions: switched,
299
372
  });
300
373
  const { rootSessionId } = canonicalSession;
301
374
  const ocSessionId = rootSessionId ?? '';
302
- const runtimeUrl = sandbox?.external_id ? getSandboxUrlForExternalId(sandbox.external_id) : null;
303
- const acpRuntime = useAcpSessionRuntime({
304
- runtimeUrl,
305
- sessionId: rootSessionId,
306
- enabled: enabled && switched && usesAcp,
307
- });
308
- const acpSessionTitle = readAcpSessionTitle(acpRuntime.projection.sessionInfo);
309
- useEffect(() => {
310
- if (!usesAcp || !acpSessionTitle)
311
- return;
312
- syncAcpSessionTitleQuery(queryClient, projectId, sessionId, { title: acpSessionTitle });
313
- }, [queryClient, projectId, sessionId, usesAcp, acpSessionTitle]);
314
375
  useEffect(() => () => {
315
376
  titleRefreshAbortRef.current?.abort();
316
377
  titleRefreshAbortRef.current = null;
@@ -320,29 +381,17 @@ export function useSession(projectId, sessionId, options = {}) {
320
381
  // is off — `useSessionSync('')` fetches/polls nothing (its effects no-op on
321
382
  // a falsy/non-canonical session id) — and use a fixed, type-stable empty
322
383
  // result instead of whatever it happens to return for that starved call.
323
- const rawSync = useSessionSync(chatEngine && runtimePolicy.syncOpenCodeMessages ? ocSessionId : '');
324
- const acpSync = useMemo(() => ({
325
- messages: acpRuntime.projection.messages,
326
- status: acpRuntime.projection.status,
327
- isBusy: acpRuntime.projection.status.type !== 'idle',
328
- isLoading: !acpRuntime.ready,
329
- diffs: [],
330
- todos: acpRuntime.projection.todos,
331
- hasOlder: false,
332
- isLoadingOlder: false,
333
- loadOlder: async () => { },
334
- }), [acpRuntime.projection, acpRuntime.ready]);
335
- const sync = !chatEngine ? DISABLED_CHAT_ENGINE_SYNC : usesAcp ? acpSync : rawSync;
384
+ const rawSync = useSessionSync(chatEngine ? ocSessionId : '', {
385
+ kortixSessionScope: `${projectId}/${sessionId}`,
386
+ networkEnabled: switched,
387
+ });
388
+ const sync = chatEngine ? rawSync : DISABLED_CHAT_ENGINE_SYNC;
336
389
  const runtimePhase = useRuntimePhase();
337
390
  const [restRewind, setRestRewind] = useState(null);
338
391
  const [rewindPending, setRewindPending] = useState(false);
339
392
  const [rewindError, setRewindError] = useState(null);
340
- const rewindMessageId = usesAcp
341
- ? (acpRuntime.rewindState?.messageId ?? null)
342
- : restRewind?.staged
343
- ? restRewind.messageId
344
- : null;
345
- const messages = useMemo(() => usesAcp ? sync.messages : messagesBeforeRewind(sync.messages, restRewind?.messageId ?? null), [usesAcp, sync.messages, restRewind?.messageId]);
393
+ const rewindMessageId = restRewind?.staged ? restRewind.messageId : null;
394
+ const messages = useMemo(() => messagesBeforeRewind(sync.messages, restRewind?.messageId ?? null), [sync.messages, restRewind?.messageId]);
346
395
  useEffect(() => {
347
396
  setRestRewind(null);
348
397
  setRewindPending(false);
@@ -350,7 +399,7 @@ export function useSession(projectId, sessionId, options = {}) {
350
399
  }, [sessionId, ocSessionId]);
351
400
  useEffect(() => {
352
401
  setRestRewind((current) => reconcileCommittedSessionRewind(sync.messages, current));
353
- }, [restRewind, sync.messages]);
402
+ }, [sync.messages]);
354
403
  // 5b. Self-heal a missed `question.asked` SSE event (a `question` tool part
355
404
  // rendering as running with nothing in the pending store) — see
356
405
  // `useQuestionSelfHeal` for why this is distinct from the SSE reconnect-gap
@@ -358,24 +407,21 @@ export function useSession(projectId, sessionId, options = {}) {
358
407
  // is off — see that option's jsdoc: a host mounting its own chat surface
359
408
  // already runs its own copy of this poller for the same session.
360
409
  useQuestionSelfHeal(ocSessionId, sync.messages, {
361
- enabled: !usesAcp && chatEngine && !!ocSessionId,
410
+ enabled: switched && chatEngine && !!ocSessionId,
362
411
  });
363
412
  usePermissionSelfHeal(ocSessionId, sync.messages, {
364
- enabled: !usesAcp && chatEngine && !!ocSessionId,
413
+ enabled: switched && chatEngine && !!ocSessionId,
365
414
  });
366
415
  // 6. Interactive prompts live in the pending store (the SSE writes them there,
367
416
  // keyed by request id carrying sessionID). useSessionSync does NOT surface them.
368
417
  const questionMap = useOpenCodePendingStore((s) => s.questions);
369
418
  const permissionMap = useOpenCodePendingStore((s) => s.permissions);
370
- const isCompacting = useOpenCodeCompactionStore((state) => !usesAcp && Boolean(state.compactingBySession[ocSessionId]));
419
+ const isCompacting = useOpenCodeCompactionStore((state) => switched && Boolean(state.compactingBySession[ocSessionId]));
371
420
  const removeQuestion = useOpenCodePendingStore((s) => s.removeQuestion);
372
421
  const removePermission = useOpenCodePendingStore((s) => s.removePermission);
373
- const questions = useMemo(() => usesAcp
374
- ? acpRuntime.projection.questions
375
- : Object.values(questionMap).filter((q) => q.sessionID === ocSessionId), [usesAcp, acpRuntime.projection.questions, questionMap, ocSessionId]);
376
- const permissions = useMemo(() => usesAcp
377
- ? acpRuntime.projection.permissions
378
- : Object.values(permissionMap).filter((p) => p.sessionID === ocSessionId), [usesAcp, acpRuntime.projection.permissions, permissionMap, ocSessionId]);
422
+ const questions = useMemo(() => switched ? Object.values(questionMap).filter((q) => q.sessionID === ocSessionId) : [], [questionMap, ocSessionId, switched]);
423
+ const permissions = useMemo(() => switched ? Object.values(permissionMap).filter((p) => p.sessionID === ocSessionId) : [], [permissionMap, ocSessionId, switched]);
424
+ const runtimeActionReady = switched && !!rootSessionId;
379
425
  // 7. Server-side capabilities + per-session picks (all pre-runtime — no sandbox).
380
426
  const models = useProjectModels(projectId);
381
427
  const agents = useVisibleAgents({ projectId });
@@ -389,6 +435,21 @@ export function useSession(projectId, sessionId, options = {}) {
389
435
  // lands (count grows) — robust to server-normalized text where a text-equality
390
436
  // match would clear too early or never (wedging the composer). 30s backstop.
391
437
  const userMsgCount = useMemo(() => messages.filter((m) => m.info.role === 'user').length, [messages]);
438
+ useEffect(() => {
439
+ if (!chatEngine || userMsgCount <= 0)
440
+ return;
441
+ titleRefreshAbortRef.current?.abort();
442
+ const controller = new AbortController();
443
+ titleRefreshAbortRef.current = controller;
444
+ void reconcileHydratedSessionTitle(queryClient, projectId, sessionId, userMsgCount, {
445
+ signal: controller.signal,
446
+ }).finally(() => {
447
+ if (titleRefreshAbortRef.current === controller) {
448
+ titleRefreshAbortRef.current = null;
449
+ }
450
+ });
451
+ return () => controller.abort();
452
+ }, [chatEngine, projectId, queryClient, sessionId, userMsgCount]);
392
453
  const [sendState, setSendState] = useState(IDLE_SEND_STATE);
393
454
  const pending = sendState.pending;
394
455
  const pendingBaseCount = useRef(0);
@@ -404,82 +465,41 @@ export function useSession(projectId, sessionId, options = {}) {
404
465
  return () => clearTimeout(t);
405
466
  }, [pending]);
406
467
  const sendParts = async (parts, override) => {
407
- if (!ocSessionId)
468
+ if (!runtimeActionReady)
408
469
  throw new RuntimeNotReadyError();
409
- titleRefreshAbortRef.current?.abort();
410
- const titleRefreshAbort = new AbortController();
411
- titleRefreshAbortRef.current = titleRefreshAbort;
412
- void refreshSessionTitleQueryUntilResolved(queryClient, projectId, sessionId, {
413
- signal: titleRefreshAbort.signal,
414
- }).finally(() => {
415
- if (titleRefreshAbortRef.current === titleRefreshAbort) {
416
- titleRefreshAbortRef.current = null;
417
- }
418
- });
419
470
  const model = override?.model ?? picks.model;
420
471
  const agent = override?.agent ?? picks.agent;
421
472
  const variant = override?.variant;
422
- if (usesAcp) {
423
- const acpParts = parts.map((part) => {
424
- if (part.type === 'text') {
425
- return { type: 'text', text: part.text };
426
- }
427
- if (part.type === 'agent') {
428
- return { type: 'text', text: `@${part.name}` };
429
- }
430
- return {
431
- type: 'resource_link',
432
- uri: part.url,
433
- name: part.filename,
434
- mimeType: part.mime,
435
- };
436
- });
437
- await acpRuntime.send(acpParts, {
438
- model: model ? formatModelString(model) : null,
439
- agent,
440
- });
441
- return;
442
- }
443
473
  const opts = {
444
474
  ...(model ? { model } : {}),
445
475
  ...(agent ? { agent } : {}),
446
476
  ...(variant ? { variant } : {}),
447
477
  ...(override?.directory ? { directory: override.directory } : {}),
448
478
  };
449
- await sendMutation.mutateAsync({
479
+ await sendRestPromptWithObservation(ocSessionId, sandbox?.external_id ?? undefined, () => sendMutation.mutateAsync({
450
480
  sessionId: ocSessionId,
451
481
  parts,
452
482
  ...(Object.keys(opts).length ? { options: opts } : {}),
453
- });
483
+ }));
454
484
  setRestRewind(commitSessionRewind);
455
485
  };
456
486
  const send = (text, override) => {
457
- if (!ocSessionId)
487
+ if (!runtimeActionReady)
458
488
  return;
459
489
  pendingBaseCount.current = userMsgCount;
460
- if (!usesAcp)
461
- beginRestPromptObservation(ocSessionId);
462
490
  setSendState(sendStateOnStart(text));
463
491
  void sendParts([{ type: 'text', text }], override).catch((error) => {
464
- if (!usesAcp)
465
- endRestPromptObservation(ocSessionId);
466
492
  setSendState(sendStateOnError(error));
467
493
  });
468
494
  };
469
495
  // Run a project slash-command (server-side `/command`), distinct from a prompt.
470
496
  const runCommand = (command, args, options = {}) => {
471
- if (!ocSessionId)
497
+ if (!runtimeActionReady)
472
498
  return Promise.resolve();
473
- if (usesAcp) {
474
- return acpRuntime.runCommand(command, args, {
475
- model: options.model ? formatModelString(options.model) : null,
476
- agent: options.agent,
477
- });
478
- }
479
499
  return commandMutation.mutateAsync(buildSessionCommandInput(ocSessionId, command, args, options));
480
500
  };
481
501
  const rewind = async (messageId) => {
482
- if (!ocSessionId)
502
+ if (!runtimeActionReady)
483
503
  throw new RuntimeNotReadyError();
484
504
  if (!messageId)
485
505
  throw new Error('Session rewind requires a message id');
@@ -489,13 +509,8 @@ export function useSession(projectId, sessionId, options = {}) {
489
509
  setRewindPending(true);
490
510
  setRewindError(null);
491
511
  try {
492
- if (usesAcp) {
493
- await acpRuntime.rewind(messageId);
494
- }
495
- else {
496
- await rewindOpenCodeSession(ocSessionId, messageId);
497
- setRestRewind({ messageId, staged: true });
498
- }
512
+ await rewindOpenCodeSession(ocSessionId, messageId);
513
+ setRestRewind({ messageId, staged: true });
499
514
  }
500
515
  catch (error) {
501
516
  const classified = classifySendError(error);
@@ -507,20 +522,15 @@ export function useSession(projectId, sessionId, options = {}) {
507
522
  }
508
523
  };
509
524
  const restoreRewind = async () => {
510
- if (!ocSessionId)
525
+ if (!runtimeActionReady)
511
526
  throw new RuntimeNotReadyError();
512
527
  if (!rewindMessageId || rewindPending)
513
528
  return;
514
529
  setRewindPending(true);
515
530
  setRewindError(null);
516
531
  try {
517
- if (usesAcp) {
518
- await acpRuntime.restoreRewind();
519
- }
520
- else {
521
- await restoreOpenCodeSessionRewind(ocSessionId);
522
- setRestRewind(null);
523
- }
532
+ await restoreOpenCodeSessionRewind(ocSessionId);
533
+ setRestRewind(null);
524
534
  }
525
535
  catch (error) {
526
536
  const classified = classifySendError(error);
@@ -533,49 +543,15 @@ export function useSession(projectId, sessionId, options = {}) {
533
543
  };
534
544
  // The one true cancel: abort the run AND drop any pending prompt + open prompts.
535
545
  const cancel = () => {
536
- if (ocSessionId) {
537
- if (usesAcp)
538
- void acpRuntime.cancel();
539
- else {
540
- endRestPromptObservation(ocSessionId);
541
- abortMutation.mutate(ocSessionId);
542
- }
546
+ if (runtimeActionReady) {
547
+ endRestPromptObservation(ocSessionId);
548
+ abortMutation.mutate(ocSessionId);
543
549
  }
544
550
  questions.forEach((q) => removeQuestion(q.id));
545
551
  permissions.forEach((p) => removePermission(p.id));
546
552
  setSendState(IDLE_SEND_STATE);
547
553
  };
548
- const answerSessionQuestion = async (requestId, answers) => {
549
- if (!usesAcp)
550
- return answerQuestion(requestId, answers);
551
- try {
552
- await acpRuntime.answerQuestion(requestId, answers);
553
- }
554
- catch (error) {
555
- throw classifySendError(error);
556
- }
557
- };
558
- const rejectSessionQuestion = async (requestId) => {
559
- if (!usesAcp)
560
- return rejectQuestion(requestId);
561
- try {
562
- await acpRuntime.rejectQuestion(requestId);
563
- }
564
- catch (error) {
565
- throw classifySendError(error);
566
- }
567
- };
568
- const answerSessionPermission = async (requestId, reply, message) => {
569
- if (!usesAcp)
570
- return answerPermission(requestId, reply, message);
571
- try {
572
- await acpRuntime.answerPermission(requestId, reply);
573
- }
574
- catch (error) {
575
- throw classifySendError(error);
576
- }
577
- };
578
- const runtimeSessionError = usesAcp ? acpRuntime.error : canonicalSession.error;
554
+ const runtimeSessionError = canonicalSession.error;
579
555
  const phase = terminal || startError || runtimeSessionError ? 'error' : switched ? 'ready' : 'starting';
580
556
  // 10. Replay the new-session hand-off once ready + thread empty (exactly once).
581
557
  // Force-disabled when `chatEngine` is off: this reads `sync.isLoading`/
@@ -599,11 +575,7 @@ export function useSession(projectId, sessionId, options = {}) {
599
575
  picks.setModel(stash.model);
600
576
  if (stash.agent)
601
577
  picks.setAgent(stash.agent);
602
- send(stash.prompt, {
603
- model: stash.model,
604
- agent: stash.agent,
605
- variant: stash.variant,
606
- });
578
+ send(stash.prompt, { model: stash.model, agent: stash.agent, variant: stash.variant });
607
579
  // eslint-disable-next-line react-hooks/exhaustive-deps
608
580
  }, [phase, sync.isLoading, sync.messages.length, sessionId, replayStartStash, chatEngine]);
609
581
  return {
@@ -611,10 +583,7 @@ export function useSession(projectId, sessionId, options = {}) {
611
583
  sessionId,
612
584
  /** Canonical OpenCode root id, or null while resolving. */
613
585
  opencodeSessionId: rootSessionId ?? null,
614
- /** Server-selected SDK client transport. */
615
- runtimeTransport,
616
- /** Runtime sessions available for legacy deep-link selection.
617
- * ACP exposes only the canonical session during this canary. */
586
+ runtimeTransport: 'rest',
618
587
  runtimeSessions: canonicalSession.sessions,
619
588
  runtimeSessionsLoading: canonicalSession.isLoading,
620
589
  runtimeSessionsListed: canonicalSession.listed,
@@ -643,12 +612,7 @@ export function useSession(projectId, sessionId, options = {}) {
643
612
  startError,
644
613
  /** Granular boot phase (connecting|booting|ready|unreachable) for detailed UI. */
645
614
  runtimePhase,
646
- isBusy: resolveSessionBusy({
647
- syncBusy: sync.isBusy,
648
- hasPendingText: !!pending,
649
- usesAcp,
650
- acpSending: acpRuntime.sending,
651
- }),
615
+ isBusy: sync.isBusy || !!pending,
652
616
  isCompacting,
653
617
  isLoading: sync.isLoading,
654
618
  isError: terminal || !!startError || !!runtimeSessionError,
@@ -659,15 +623,12 @@ export function useSession(projectId, sessionId, options = {}) {
659
623
  /** Pending optimistic message text, or null. */
660
624
  pending,
661
625
  /** True while the current `send` mutation is in flight. */
662
- isSending: usesAcp ? acpRuntime.sending : sendMutation.isPending,
626
+ isSending: sendMutation.isPending,
663
627
  /** Last `send` failure, typed (billing / runtime-not-ready / runtime-error),
664
628
  * or null. Reset on every new `send` call. */
665
629
  sendError: sendState.sendError,
666
- /** Selected user message while a reversible history rewind is staged. */
667
630
  rewindMessageId,
668
- /** True while the runtime stages or restores a history rewind. */
669
631
  rewindPending,
670
- /** Last history rewind failure, or null. */
671
632
  rewindError,
672
633
  // server-side capabilities (pre-runtime)
673
634
  models,
@@ -677,22 +638,19 @@ export function useSession(projectId, sessionId, options = {}) {
677
638
  picks,
678
639
  // actions
679
640
  send,
680
- /** Send text and file prompt parts through the selected SDK transport. */
681
641
  sendParts,
682
- /** Rewind this canonical session to one user message. */
683
642
  rewind,
684
- /** Restore the removed path before a replacement prompt commits it. */
685
643
  restoreRewind,
686
644
  cancel,
687
645
  runCommand,
688
646
  /** Answer an agent question through the server and drop it from pending
689
647
  * state on success; throws a `KortixSendError` and leaves it pending on
690
648
  * failure. */
691
- answerQuestion: answerSessionQuestion,
649
+ answerQuestion,
692
650
  /** Reject an agent question through the server (see `answerQuestion`). */
693
- rejectQuestion: rejectSessionQuestion,
651
+ rejectQuestion,
694
652
  /** Answer an agent permission request through the server (see `answerQuestion`). */
695
- answerPermission: answerSessionPermission,
653
+ answerPermission,
696
654
  /** @deprecated Drops the question from local state WITHOUT replying to the
697
655
  * server — the agent run stays blocked waiting on it. Use `answerQuestion`
698
656
  * / `rejectQuestion` instead. */