@threadbase-sh/streamer 1.63.0 → 1.64.0

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 (296) hide show
  1. package/dist/agent/agent-client.d.ts +14 -0
  2. package/dist/agent/agent-client.d.ts.map +1 -0
  3. package/dist/agent/agent-config.d.ts +24 -0
  4. package/dist/agent/agent-config.d.ts.map +1 -0
  5. package/dist/agent/conversation-writer.d.ts +13 -0
  6. package/dist/agent/conversation-writer.d.ts.map +1 -0
  7. package/dist/agent/dedupe.d.ts +6 -0
  8. package/dist/agent/dedupe.d.ts.map +1 -0
  9. package/dist/agent/errors.d.ts +23 -0
  10. package/dist/agent/errors.d.ts.map +1 -0
  11. package/dist/agent/handle-send-agent-input.d.ts +21 -0
  12. package/dist/agent/handle-send-agent-input.d.ts.map +1 -0
  13. package/dist/agent/handle-start-agent-session.d.ts +21 -0
  14. package/dist/agent/handle-start-agent-session.d.ts.map +1 -0
  15. package/dist/agent/history-mapper.d.ts +4 -0
  16. package/dist/agent/history-mapper.d.ts.map +1 -0
  17. package/dist/agent/payload-guard.d.ts +29 -0
  18. package/dist/agent/payload-guard.d.ts.map +1 -0
  19. package/dist/api/app.d.ts +28 -0
  20. package/dist/api/app.d.ts.map +1 -0
  21. package/dist/api/handlers/conversations.handlers.d.ts +69 -0
  22. package/dist/api/handlers/conversations.handlers.d.ts.map +1 -0
  23. package/dist/api/handlers/http-helpers.d.ts +50 -0
  24. package/dist/api/handlers/http-helpers.d.ts.map +1 -0
  25. package/dist/api/handlers/sessions.handlers.d.ts +230 -0
  26. package/dist/api/handlers/sessions.handlers.d.ts.map +1 -0
  27. package/dist/api/middleware/auth.middleware.d.ts +5 -0
  28. package/dist/api/middleware/auth.middleware.d.ts.map +1 -0
  29. package/dist/api/middleware/cors.middleware.d.ts +5 -0
  30. package/dist/api/middleware/cors.middleware.d.ts.map +1 -0
  31. package/dist/api/middleware/error.middleware.d.ts +4 -0
  32. package/dist/api/middleware/error.middleware.d.ts.map +1 -0
  33. package/dist/api/routes/backup.routes.d.ts +5 -0
  34. package/dist/api/routes/backup.routes.d.ts.map +1 -0
  35. package/dist/api/routes/browse.routes.d.ts +5 -0
  36. package/dist/api/routes/browse.routes.d.ts.map +1 -0
  37. package/dist/api/routes/cacheAlert.routes.d.ts +5 -0
  38. package/dist/api/routes/cacheAlert.routes.d.ts.map +1 -0
  39. package/dist/api/routes/config.routes.d.ts +5 -0
  40. package/dist/api/routes/config.routes.d.ts.map +1 -0
  41. package/dist/api/routes/conversations.routes.d.ts +5 -0
  42. package/dist/api/routes/conversations.routes.d.ts.map +1 -0
  43. package/dist/api/routes/devices.routes.d.ts +17 -0
  44. package/dist/api/routes/devices.routes.d.ts.map +1 -0
  45. package/dist/api/routes/diagnostics.routes.d.ts +5 -0
  46. package/dist/api/routes/diagnostics.routes.d.ts.map +1 -0
  47. package/dist/api/routes/health.routes.d.ts +5 -0
  48. package/dist/api/routes/health.routes.d.ts.map +1 -0
  49. package/dist/api/routes/logs.routes.d.ts +3 -0
  50. package/dist/api/routes/logs.routes.d.ts.map +1 -0
  51. package/dist/api/routes/misc.routes.d.ts +77 -0
  52. package/dist/api/routes/misc.routes.d.ts.map +1 -0
  53. package/dist/api/routes/pair.routes.d.ts +5 -0
  54. package/dist/api/routes/pair.routes.d.ts.map +1 -0
  55. package/dist/api/routes/progress.routes.d.ts +39 -0
  56. package/dist/api/routes/progress.routes.d.ts.map +1 -0
  57. package/dist/api/routes/projects.routes.d.ts +5 -0
  58. package/dist/api/routes/projects.routes.d.ts.map +1 -0
  59. package/dist/api/routes/providers.routes.d.ts +19 -0
  60. package/dist/api/routes/providers.routes.d.ts.map +1 -0
  61. package/dist/api/routes/scanner.routes.d.ts +5 -0
  62. package/dist/api/routes/scanner.routes.d.ts.map +1 -0
  63. package/dist/api/routes/sessions.routes.d.ts +6 -0
  64. package/dist/api/routes/sessions.routes.d.ts.map +1 -0
  65. package/dist/api/routes/ws.routes.d.ts +7 -0
  66. package/dist/api/routes/ws.routes.d.ts.map +1 -0
  67. package/dist/api/types/api-deps.d.ts +114 -0
  68. package/dist/api/types/api-deps.d.ts.map +1 -0
  69. package/dist/auth.d.ts +81 -0
  70. package/dist/auth.d.ts.map +1 -0
  71. package/dist/browse.d.ts +17 -0
  72. package/dist/browse.d.ts.map +1 -0
  73. package/dist/claude-flags.d.ts +76 -0
  74. package/dist/claude-flags.d.ts.map +1 -0
  75. package/dist/cli.cjs +20 -5
  76. package/dist/cli.cjs.map +1 -1
  77. package/dist/codex-pty-runner.d.ts +90 -0
  78. package/dist/codex-pty-runner.d.ts.map +1 -0
  79. package/dist/config/update-config.d.ts +13 -0
  80. package/dist/config/update-config.d.ts.map +1 -0
  81. package/dist/conversation-cache.d.ts +336 -0
  82. package/dist/conversation-cache.d.ts.map +1 -0
  83. package/dist/db/config.d.ts +11 -0
  84. package/dist/db/config.d.ts.map +1 -0
  85. package/dist/db/index.d.ts +5 -0
  86. package/dist/db/index.d.ts.map +1 -0
  87. package/dist/db/migrations.d.ts +3 -0
  88. package/dist/db/migrations.d.ts.map +1 -0
  89. package/dist/db/pool.d.ts +5 -0
  90. package/dist/db/pool.d.ts.map +1 -0
  91. package/dist/db/query-timing.d.ts +52 -0
  92. package/dist/db/query-timing.d.ts.map +1 -0
  93. package/dist/db/repositories/cacheMetadata.repository.d.ts +12 -0
  94. package/dist/db/repositories/cacheMetadata.repository.d.ts.map +1 -0
  95. package/dist/db/repositories/conversations.repository.d.ts +27 -0
  96. package/dist/db/repositories/conversations.repository.d.ts.map +1 -0
  97. package/dist/db/repositories/devices.repository.d.ts +137 -0
  98. package/dist/db/repositories/devices.repository.d.ts.map +1 -0
  99. package/dist/db/repositories/managed-sessions.repository.d.ts +127 -0
  100. package/dist/db/repositories/managed-sessions.repository.d.ts.map +1 -0
  101. package/dist/db/repositories/projects.repository.d.ts +28 -0
  102. package/dist/db/repositories/projects.repository.d.ts.map +1 -0
  103. package/dist/db/repositories/push.repository.d.ts +186 -0
  104. package/dist/db/repositories/push.repository.d.ts.map +1 -0
  105. package/dist/db/repositories/sessions.repository.d.ts +16 -0
  106. package/dist/db/repositories/sessions.repository.d.ts.map +1 -0
  107. package/dist/db/runtime-store.d.ts +80 -0
  108. package/dist/db/runtime-store.d.ts.map +1 -0
  109. package/dist/db/sqlite-migrate.d.ts +13 -0
  110. package/dist/db/sqlite-migrate.d.ts.map +1 -0
  111. package/dist/db/upload-records.d.ts +11 -0
  112. package/dist/db/upload-records.d.ts.map +1 -0
  113. package/dist/e2ee/noise.d.ts +261 -0
  114. package/dist/e2ee/noise.d.ts.map +1 -0
  115. package/dist/e2ee/pair-payload.d.ts +60 -0
  116. package/dist/e2ee/pair-payload.d.ts.map +1 -0
  117. package/dist/e2ee/pair-request.d.ts +24 -0
  118. package/dist/e2ee/pair-request.d.ts.map +1 -0
  119. package/dist/external-tails.d.ts +91 -0
  120. package/dist/external-tails.d.ts.map +1 -0
  121. package/dist/feature-flags.d.ts +212 -0
  122. package/dist/feature-flags.d.ts.map +1 -0
  123. package/dist/handlers/handleListProjects.d.ts +3 -0
  124. package/dist/handlers/handleListProjects.d.ts.map +1 -0
  125. package/dist/index.d.ts +18 -2571
  126. package/dist/index.d.ts.map +1 -0
  127. package/dist/lifecycle/constants.d.ts +21 -0
  128. package/dist/lifecycle/constants.d.ts.map +1 -0
  129. package/dist/lifecycle/process-liveness.d.ts +7 -0
  130. package/dist/lifecycle/process-liveness.d.ts.map +1 -0
  131. package/dist/live-session-manager.d.ts +59 -0
  132. package/dist/live-session-manager.d.ts.map +1 -0
  133. package/dist/logger.d.ts +14 -0
  134. package/dist/logger.d.ts.map +1 -0
  135. package/dist/pair-store.d.ts +60 -0
  136. package/dist/pair-store.d.ts.map +1 -0
  137. package/dist/platform.d.ts +25 -0
  138. package/dist/platform.d.ts.map +1 -0
  139. package/dist/process-discovery.d.ts +35 -0
  140. package/dist/process-discovery.d.ts.map +1 -0
  141. package/dist/providers.d.ts +7 -0
  142. package/dist/providers.d.ts.map +1 -0
  143. package/dist/pty-host/protocol.d.ts +270 -0
  144. package/dist/pty-host/protocol.d.ts.map +1 -0
  145. package/dist/pty-host/remote-session-runner.d.ts +133 -0
  146. package/dist/pty-host/remote-session-runner.d.ts.map +1 -0
  147. package/dist/pty-host/socket.d.ts +54 -0
  148. package/dist/pty-host/socket.d.ts.map +1 -0
  149. package/dist/pty-host/spawn-host.d.ts +30 -0
  150. package/dist/pty-host/spawn-host.d.ts.map +1 -0
  151. package/dist/pty-manager.d.ts +65 -0
  152. package/dist/pty-manager.d.ts.map +1 -0
  153. package/dist/pty-shared.d.ts +27 -0
  154. package/dist/pty-shared.d.ts.map +1 -0
  155. package/dist/scanner-manager.d.ts +126 -0
  156. package/dist/scanner-manager.d.ts.map +1 -0
  157. package/dist/schemas/cacheAlert.schema.d.ts +14 -0
  158. package/dist/schemas/cacheAlert.schema.d.ts.map +1 -0
  159. package/dist/schemas/claudeFlags.schema.d.ts +7 -0
  160. package/dist/schemas/claudeFlags.schema.d.ts.map +1 -0
  161. package/dist/schemas/project.schema.d.ts +18 -0
  162. package/dist/schemas/project.schema.d.ts.map +1 -0
  163. package/dist/schemas/updateConfig.schema.d.ts +19 -0
  164. package/dist/schemas/updateConfig.schema.d.ts.map +1 -0
  165. package/dist/seal.d.ts +14 -0
  166. package/dist/seal.d.ts.map +1 -0
  167. package/dist/server-identity.d.ts +43 -0
  168. package/dist/server-identity.d.ts.map +1 -0
  169. package/dist/server-wiring.d.ts +184 -0
  170. package/dist/server-wiring.d.ts.map +1 -0
  171. package/dist/server.d.ts +390 -0
  172. package/dist/server.d.ts.map +1 -0
  173. package/dist/services/backup/backup.d.ts +103 -0
  174. package/dist/services/backup/backup.d.ts.map +1 -0
  175. package/dist/services/cache/cacheMetadata.d.ts +8 -0
  176. package/dist/services/cache/cacheMetadata.d.ts.map +1 -0
  177. package/dist/services/cache-integrity/alertStore.d.ts +24 -0
  178. package/dist/services/cache-integrity/alertStore.d.ts.map +1 -0
  179. package/dist/services/cache-integrity/backup.d.ts +8 -0
  180. package/dist/services/cache-integrity/backup.d.ts.map +1 -0
  181. package/dist/services/cache-integrity/cacheIntegrityMonitor.d.ts +71 -0
  182. package/dist/services/cache-integrity/cacheIntegrityMonitor.d.ts.map +1 -0
  183. package/dist/services/conversations/conversationWatcher.d.ts +99 -0
  184. package/dist/services/conversations/conversationWatcher.d.ts.map +1 -0
  185. package/dist/services/conversations/findSearchTarget.d.ts +30 -0
  186. package/dist/services/conversations/findSearchTarget.d.ts.map +1 -0
  187. package/dist/services/conversations/isAgentConversation.d.ts +8 -0
  188. package/dist/services/conversations/isAgentConversation.d.ts.map +1 -0
  189. package/dist/services/conversations/pruneAgentConversations.d.ts +8 -0
  190. package/dist/services/conversations/pruneAgentConversations.d.ts.map +1 -0
  191. package/dist/services/conversations/refreshConversationCache.d.ts +27 -0
  192. package/dist/services/conversations/refreshConversationCache.d.ts.map +1 -0
  193. package/dist/services/conversations/shouldRefreshProjectsFromHdd.d.ts +33 -0
  194. package/dist/services/conversations/shouldRefreshProjectsFromHdd.d.ts.map +1 -0
  195. package/dist/services/diagnostics/diagnostics.d.ts +66 -0
  196. package/dist/services/diagnostics/diagnostics.d.ts.map +1 -0
  197. package/dist/services/projectChats/deriveProjectChatTitle.d.ts +15 -0
  198. package/dist/services/projectChats/deriveProjectChatTitle.d.ts.map +1 -0
  199. package/dist/services/projects/ensureProjectsForConversations.d.ts +15 -0
  200. package/dist/services/projects/ensureProjectsForConversations.d.ts.map +1 -0
  201. package/dist/services/providers/capabilities.d.ts +105 -0
  202. package/dist/services/providers/capabilities.d.ts.map +1 -0
  203. package/dist/services/providers/providerHealth.d.ts +72 -0
  204. package/dist/services/providers/providerHealth.d.ts.map +1 -0
  205. package/dist/services/push/apnsClient.d.ts +105 -0
  206. package/dist/services/push/apnsClient.d.ts.map +1 -0
  207. package/dist/services/push/expoPushSender.d.ts +37 -0
  208. package/dist/services/push/expoPushSender.d.ts.map +1 -0
  209. package/dist/services/push/liveActivityContentState.d.ts +60 -0
  210. package/dist/services/push/liveActivityContentState.d.ts.map +1 -0
  211. package/dist/services/push/liveActivityNotifier.d.ts +49 -0
  212. package/dist/services/push/liveActivityNotifier.d.ts.map +1 -0
  213. package/dist/services/push/liveActivityRenewal.d.ts +63 -0
  214. package/dist/services/push/liveActivityRenewal.d.ts.map +1 -0
  215. package/dist/services/push/liveActivitySender.d.ts +88 -0
  216. package/dist/services/push/liveActivitySender.d.ts.map +1 -0
  217. package/dist/services/push/waitingInputNotifier.d.ts +41 -0
  218. package/dist/services/push/waitingInputNotifier.d.ts.map +1 -0
  219. package/dist/services/questions/answersToKeystrokes.d.ts +8 -0
  220. package/dist/services/questions/answersToKeystrokes.d.ts.map +1 -0
  221. package/dist/services/questions/codexGateAnswers.d.ts +14 -0
  222. package/dist/services/questions/codexGateAnswers.d.ts.map +1 -0
  223. package/dist/services/questions/codexScreen.d.ts +74 -0
  224. package/dist/services/questions/codexScreen.d.ts.map +1 -0
  225. package/dist/services/questions/detectAskUserQuestion.d.ts +6 -0
  226. package/dist/services/questions/detectAskUserQuestion.d.ts.map +1 -0
  227. package/dist/services/questions/detectPermissionGate.d.ts +62 -0
  228. package/dist/services/questions/detectPermissionGate.d.ts.map +1 -0
  229. package/dist/services/questions/detectQuestionFromScreen.d.ts +30 -0
  230. package/dist/services/questions/detectQuestionFromScreen.d.ts.map +1 -0
  231. package/dist/services/questions/detectShellPrompt.d.ts +18 -0
  232. package/dist/services/questions/detectShellPrompt.d.ts.map +1 -0
  233. package/dist/services/questions/parseAgentPhase.d.ts +12 -0
  234. package/dist/services/questions/parseAgentPhase.d.ts.map +1 -0
  235. package/dist/services/questions/parseStatusLine.d.ts +19 -0
  236. package/dist/services/questions/parseStatusLine.d.ts.map +1 -0
  237. package/dist/services/questions/questionBroadcast.d.ts +18 -0
  238. package/dist/services/questions/questionBroadcast.d.ts.map +1 -0
  239. package/dist/services/questions/resolveAnswer.d.ts +16 -0
  240. package/dist/services/questions/resolveAnswer.d.ts.map +1 -0
  241. package/dist/services/search/searchQuery.d.ts +78 -0
  242. package/dist/services/search/searchQuery.d.ts.map +1 -0
  243. package/dist/services/security/capabilities.d.ts +50 -0
  244. package/dist/services/security/capabilities.d.ts.map +1 -0
  245. package/dist/services/sessions/autoResumeOnBoot.d.ts +28 -0
  246. package/dist/services/sessions/autoResumeOnBoot.d.ts.map +1 -0
  247. package/dist/services/sessions/codexRolloutOwner.d.ts +55 -0
  248. package/dist/services/sessions/codexRolloutOwner.d.ts.map +1 -0
  249. package/dist/services/sessions/conversationBusy.d.ts +32 -0
  250. package/dist/services/sessions/conversationBusy.d.ts.map +1 -0
  251. package/dist/services/sessions/idempotency.d.ts +67 -0
  252. package/dist/services/sessions/idempotency.d.ts.map +1 -0
  253. package/dist/services/sessions/reconcileSessions.d.ts +63 -0
  254. package/dist/services/sessions/reconcileSessions.d.ts.map +1 -0
  255. package/dist/services/sessions/rehydrateSessions.d.ts +68 -0
  256. package/dist/services/sessions/rehydrateSessions.d.ts.map +1 -0
  257. package/dist/services/sessions/resumeIdentity.d.ts +16 -0
  258. package/dist/services/sessions/resumeIdentity.d.ts.map +1 -0
  259. package/dist/session-registry-boot.d.ts +142 -0
  260. package/dist/session-registry-boot.d.ts.map +1 -0
  261. package/dist/session-store.d.ts +32 -0
  262. package/dist/session-store.d.ts.map +1 -0
  263. package/dist/session-watchers.d.ts +60 -0
  264. package/dist/session-watchers.d.ts.map +1 -0
  265. package/dist/types.d.ts +616 -0
  266. package/dist/types.d.ts.map +1 -0
  267. package/dist/uploads.d.ts +16 -0
  268. package/dist/uploads.d.ts.map +1 -0
  269. package/dist/utils/bootToken.d.ts +3 -0
  270. package/dist/utils/bootToken.d.ts.map +1 -0
  271. package/dist/utils/canonicalizeFilePath.d.ts +75 -0
  272. package/dist/utils/canonicalizeFilePath.d.ts.map +1 -0
  273. package/dist/utils/canonicalizeProjectPath.d.ts +13 -0
  274. package/dist/utils/canonicalizeProjectPath.d.ts.map +1 -0
  275. package/dist/utils/codexConversationLine.d.ts +38 -0
  276. package/dist/utils/codexConversationLine.d.ts.map +1 -0
  277. package/dist/utils/conversationEtag.d.ts +19 -0
  278. package/dist/utils/conversationEtag.d.ts.map +1 -0
  279. package/dist/utils/dates.d.ts +12 -0
  280. package/dist/utils/dates.d.ts.map +1 -0
  281. package/dist/utils/debounce.d.ts +15 -0
  282. package/dist/utils/debounce.d.ts.map +1 -0
  283. package/dist/utils/deriveSessionName.d.ts +10 -0
  284. package/dist/utils/deriveSessionName.d.ts.map +1 -0
  285. package/dist/utils/fileIdentity.d.ts +36 -0
  286. package/dist/utils/fileIdentity.d.ts.map +1 -0
  287. package/dist/utils/isScannedSnapshotStale.d.ts +16 -0
  288. package/dist/utils/isScannedSnapshotStale.d.ts.map +1 -0
  289. package/dist/utils/scanProgressThrottle.d.ts +12 -0
  290. package/dist/utils/scanProgressThrottle.d.ts.map +1 -0
  291. package/dist/version.d.ts +3 -0
  292. package/dist/version.d.ts.map +1 -0
  293. package/dist/ws-hub.d.ts +15 -0
  294. package/dist/ws-hub.d.ts.map +1 -0
  295. package/package.json +3 -4
  296. package/dist/index.d.cts +0 -2571
package/dist/index.d.ts CHANGED
@@ -1,2571 +1,18 @@
1
- import { Client } from '@temporalio/client';
2
- import { UserInputSignal, Stage } from '@threadbase-sh/agent-types';
3
- import * as hono_types from 'hono/types';
4
- import { Hono } from 'hono';
5
- import { Logger as Logger$1 } from 'pino';
6
- import { HttpBindings } from '@hono/node-server';
7
- import { ServerResponse, IncomingMessage } from 'http';
8
- import { WebSocket } from 'ws';
9
- import { ConversationMessage, FileStatEntry, ConversationMeta } from '@threadbase-sh/scanner';
10
- import Database from 'better-sqlite3';
11
- import { Stats } from 'fs';
12
- import { z } from 'zod';
13
- import { Pool } from 'pg';
14
-
15
- interface AgentClient {
16
- startSession(sessionId: string): Promise<string>;
17
- sendUserInput(sessionId: string, payload: UserInputSignal): Promise<void>;
18
- endSession(sessionId: string): Promise<void>;
19
- getSessionStage(sessionId: string): Promise<string>;
20
- }
21
- interface AgentClientOpts {
22
- temporalClient: Client;
23
- taskQueue: string;
24
- }
25
- declare function createAgentClient({ temporalClient, taskQueue }: AgentClientOpts): AgentClient;
26
-
27
- interface AgentConfig {
28
- enabled: boolean;
29
- temporal: {
30
- address: string;
31
- namespace: string;
32
- taskQueue: string;
33
- };
34
- webhook: {
35
- hmacSecret: string;
36
- timestampSkewSeconds: number;
37
- };
38
- dedupe: {
39
- perSessionCapacity: number;
40
- };
41
- payload: {
42
- limitBytes: number;
43
- trajectoryLogBytes: number;
44
- trajectoryLogTurns: number;
45
- };
46
- sessionBusyRetryMs: number;
47
- conversationsDir: string;
48
- }
49
- declare function readAgentConfig(env?: NodeJS.ProcessEnv): AgentConfig;
50
-
51
- interface AppendArgs {
52
- sessionId: string;
53
- turnId: string;
54
- content: string;
55
- reviewerOverruled?: boolean;
56
- }
57
- interface ConversationWriter {
58
- appendAssistantTurn(args: AppendArgs): Promise<void>;
59
- }
60
- declare function createConversationWriter(opts: {
61
- baseDir: string;
62
- }): ConversationWriter;
63
-
64
- interface ProgressDedupeLRU {
65
- hasSeen(eventId: string): boolean;
66
- readonly size: number;
67
- }
68
- declare function createProgressDedupeLRU(capacity: number): ProgressDedupeLRU;
69
-
70
- type LogDest = "console" | "pino" | "both";
71
- type LogLevel = "debug" | "info" | "warn" | "error";
72
- interface Logger {
73
- debug(msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;
74
- info(msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;
75
- warn(msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;
76
- error(msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;
77
- log(level: LogLevel, msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;
78
- pino: Logger$1;
79
- }
80
-
81
- /** Claude Code `--permission-mode` values, as accepted by CLI v2.1.x. */
82
- declare const PERMISSION_MODES: readonly ["acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan"];
83
- type PermissionMode = (typeof PERMISSION_MODES)[number];
84
- /** Claude Code `--effort` levels, as accepted by CLI v2.1.x. */
85
- declare const EFFORT_LEVELS: readonly ["low", "medium", "high", "xhigh", "max"];
86
- type EffortLevel = (typeof EFFORT_LEVELS)[number];
87
- type FlagValueType = "boolean" | "string" | "enum" | "list";
88
- /** How risky enabling a flag is. Drives the client's confirmation UX. */
89
- type FlagRisk = "low" | "elevated" | "dangerous";
90
- interface FlagDefinition {
91
- /** Stable config/wire key. Never the CLI spelling. */
92
- id: string;
93
- /** The literal CLI token, e.g. "--add-dir". */
94
- flag: string;
95
- valueType: FlagValueType;
96
- /** Allowed values when valueType === "enum". */
97
- enumValues?: readonly string[];
98
- /**
99
- * Baseline risk. `permissionMode` is the exception: it is only dangerous for
100
- * the values in DANGEROUS_PERMISSION_MODES, so clients must call
101
- * `flagValueRisk()` rather than reading this field directly.
102
- */
103
- risk: FlagRisk;
104
- }
105
- type ClaudeFlagValue = string | string[] | boolean;
106
- type ClaudeFlagValues = Record<string, ClaudeFlagValue>;
107
-
108
- /** Spec for one flag in the keyed registry. The id is the object key, not a field. */
109
- interface FeatureFlagSpec {
110
- /** Shipped to clients alongside the values so a UI can render it. */
111
- description: string;
112
- default: boolean;
113
- /** Full env var name. */
114
- env: string;
115
- }
116
- /** Wire shape: the keyed spec plus its id, for GET /api/config/feature-flags. */
117
- interface FeatureFlagDefinition extends FeatureFlagSpec {
118
- /** Stable config/wire key. Used in server.yaml, on the CLI, and over HTTP. */
119
- id: FeatureFlagId;
120
- }
121
- /**
122
- * Values arriving from a config source. PARTIAL on purpose: server.yaml, the
123
- * CLI and ServerConfig each speak about the flags they mention and stay silent
124
- * on the rest, which is what lets the precedence chain fall through.
125
- */
126
- type FeatureFlagValues = Partial<Record<FeatureFlagId, boolean>>;
127
- /** Every flag, resolved. Total — see resolveFeatureFlags(). */
128
- type ResolvedFeatureFlags = Record<FeatureFlagId, boolean>;
129
- /**
130
- * Which rung of the precedence chain decided a flag.
131
- *
132
- * Kept because the resolved boolean alone cannot answer "why is this on?", and
133
- * that is the first question asked of a flag that is on when nobody expected it.
134
- * `override` is the legacy ServerConfig field (see resolveFeatureFlags).
135
- */
136
- type FeatureFlagSource = "override" | "env" | "cli" | "yaml" | "default";
137
- /**
138
- * The registry, keyed by flag name.
139
- *
140
- * Prefer `FEATURE_FLAGS.ptyHost` over a string lookup. A typo is a compile
141
- * error; `findFeatureFlag("…")` is only for untrusted yaml/CLI tokens.
142
- *
143
- * `as const` + `keyof` is what makes `flags.ptyHsot` a compile error instead of
144
- * `undefined`. A TS enum would add a runtime object without a stronger type.
145
- */
146
- declare const FEATURE_FLAGS: {
147
- readonly codexSystemPrompt: {
148
- readonly description: string;
149
- readonly default: false;
150
- readonly env: "THREADBASE_FEATURE_CODEX_SYSTEM_PROMPT";
151
- };
152
- readonly sessionRehydration: {
153
- readonly description: string;
154
- readonly default: true;
155
- readonly env: "THREADBASE_FEATURE_SESSION_REHYDRATION";
156
- };
157
- readonly liveActivityPush: {
158
- readonly description: string;
159
- readonly default: false;
160
- readonly env: "THREADBASE_FEATURE_LIVE_ACTIVITY_PUSH";
161
- };
162
- readonly e2ee: {
163
- readonly description: string;
164
- readonly default: false;
165
- readonly env: "THREADBASE_FEATURE_E2EE";
166
- };
167
- readonly ptyHost: {
168
- readonly description: string;
169
- readonly default: false;
170
- readonly env: "THREADBASE_FEATURE_PTY_HOST";
171
- };
172
- };
173
- /**
174
- * The registry's ids as a union, derived rather than declared.
175
- *
176
- * This is what makes `flags.ptyHsot` a compile error instead of `undefined`.
177
- * Under the old `Record<string, boolean>` an index signature accepted any key,
178
- * so a typo in a consumer read as falsy and silently disabled the feature it
179
- * was meant to gate — the exact failure the "total map" contract exists to
180
- * prevent, reachable through the one door that contract left open.
181
- */
182
- type FeatureFlagId = keyof typeof FEATURE_FLAGS;
183
-
184
- declare const CLAUDE_CODE_PROVIDER: "claude-code";
185
- declare const CODEX_CLI_PROVIDER: "codex-cli";
186
- type ProviderName = typeof CLAUDE_CODE_PROVIDER | typeof CODEX_CLI_PROVIDER;
187
- declare function isProviderName(value: unknown): value is ProviderName;
188
- declare function isProviderResumable(_provider: string | null | undefined, availabilityResumable: boolean): boolean;
189
-
190
- type SessionStatus = "running" | "waiting_input" | "idle";
191
- /**
192
- * Phase axis *inside* `status === "running"` — what the agent is doing during a
193
- * turn. Deliberately a separate field rather than new SessionStatus members:
194
- * VALID_STATUSES rejects unknown values and the store drops sessions outside
195
- * the requested set, so a new status string would make those sessions vanish
196
- * from already-shipped apps. Additive fields are safe; additive values in a
197
- * union a shipped client filters on are not.
198
- *
199
- * The full set is defined here even though Codex only ever emits `working`
200
- * (its status bar is binary — Ready/Working, and claiming otherwise would be
201
- * invention). Defining it up front keeps a two-valued provider from fixing the
202
- * field's shape before Claude's richer footer lands. Consumers must ignore an
203
- * unrecognised value rather than coerce it.
204
- *
205
- * This union lives in exactly one place. Two independently-maintained copies of
206
- * a TUI-derived grammar have already drifted once (tb-mobile PR #647).
207
- */
208
- type AgentPhase = "thinking" | "streaming" | "hooks" | "acting" | "working";
209
- /**
210
- * Process-lifetime axis for a managed session (C1 durable session runtime).
211
- * Orthogonal to SessionStatus — see SessionResponse.lifecycle for why the two
212
- * are separate, and docs/architecture/2026-07-24-durable-session-runtime.md.
213
- */
214
- type SessionLifecycle = "attached" | "starting" | "detached" | "orphaned" | "resumable" | "completed" | "failed";
215
- /**
216
- * How a SessionStatus was derived (C3).
217
- * See docs/architecture/2026-07-24-session-state-confidence.md.
218
- *
219
- * The runners already compute this at every transition — it was written to a log
220
- * line and discarded, so a status reached by a timer expiring was indistinguishable
221
- * on the wire from one reached by observing a prompt marker.
222
- */
223
- type StatusSource$1 = "spawn" | "prompt-marker" | "screen-marker" | "user-input" | "process-exit" | "timeout-fallback" | "quiet-fallback" | "shutdown";
224
- /**
225
- * How much to trust the status.
226
- *
227
- * `observed` — something in the stream or the process told us.
228
- * `inferred` — a timer expired and we picked the most likely state.
229
- *
230
- * Deliberately two buckets rather than a numeric score: a percentage would imply
231
- * a calibration we have no data to support. The point is that a guess must never
232
- * be presented as an observation.
233
- */
234
- type StatusConfidence = "observed" | "inferred";
235
- /** Confidence implied by each source. Inference is exactly the timer-driven paths. */
236
- declare function confidenceForSource(source: StatusSource$1): StatusConfidence;
237
- interface ManagedSession {
238
- id: string;
239
- provider?: ProviderName;
240
- projectId?: string;
241
- projectPath: string;
242
- projectName: string;
243
- branch: string;
244
- status: SessionStatus;
245
- startedAt: Date;
246
- completedAt: Date | null;
247
- promptCount: number;
248
- lastOutput: string;
249
- failureReason?: string;
250
- /**
251
- * Machine-readable companion to `failureReason`, set only when the runner
252
- * recognised the failure. Currently just `codex_active_writer` (Codex's
253
- * single-writer lock), which the resume path maps to a structured 409 rather
254
- * than reporting a spawn as successful. Absent for every other failure.
255
- */
256
- failureCode?: string;
257
- sessionName?: string;
258
- model?: string;
259
- account?: string;
260
- messageCount?: number;
261
- preview?: string;
262
- firstMessageText?: string;
263
- firstMessageAt?: Date;
264
- lastMessageText?: string;
265
- lastMessageAt?: Date;
266
- lastActivityAt?: Date;
267
- /**
268
- * How `status` was derived, and when (C3). Confidence is derived from the
269
- * source via confidenceForSource — storing both would let them disagree.
270
- */
271
- statusSource?: StatusSource$1;
272
- statusUpdatedAt?: Date;
273
- /**
274
- * Agent phase within a running turn, scraped from the rendered screen.
275
- * Optional internally (every existing construction site predates it), but
276
- * managedToResponse emits it unconditionally as `?? null` — on the wire
277
- * absence must never be a third state, because the client merges session
278
- * frames and a merge cannot express a removed key.
279
- *
280
- * Cleared in markReady() for the running -> waiting_input turn end, which is
281
- * the only exit a runner observes on screen. Every other way out of `running`
282
- * — handleExit, putOnHold, failStartup — is enforced by SessionStore
283
- * .updateManaged() instead: a phase exists only while the status is
284
- * `running`, so leaving it clears the field.
285
- */
286
- subStatus?: AgentPhase | null;
287
- filePath?: string;
288
- resumedFromConversationId?: string;
289
- /**
290
- * This session was seeded from the durable registry at boot, not spawned by
291
- * this run — there is no PTY behind it and never was one in this process.
292
- *
293
- * INTERNAL. It never reaches the wire: `managedToResponse` translates it into
294
- * the already-defined `ownership: "historical"` + `lifecycle: "resumable"`
295
- * pair, so no client needs to learn a new field to render a recovered session.
296
- * Cleared implicitly when a resume overwrites the stub with a real session.
297
- */
298
- rehydrated?: boolean;
299
- /**
300
- * This live session was re-adopted from the pty-host during boot rather than
301
- * spawned by this streamer process. Internal lifecycle provenance only.
302
- */
303
- reconciled?: boolean;
304
- /**
305
- * What this session was doing when the streamer stopped it, for a stub whose
306
- * `status` had to flatten to `idle`. Only ever set from a registry row whose
307
- * `status_source` is `shutdown` — the one source that means "we ended this",
308
- * as opposed to the agent finishing or a crash freezing the row mid-turn.
309
- */
310
- interruptedStatus?: "running" | "waiting_input";
311
- /**
312
- * Set once a live session's underlying persisted conversation file is
313
- * discovered after the fact (currently: fresh Codex sessions, whose
314
- * rollout id isn't known until the CLI creates its own JSONL). Distinct
315
- * from `resumedFromConversationId` (resume flow) and `conversationId` on
316
- * `SessionResponse` (stable mobile deep-link alias, always === id for the
317
- * lifetime of a live PTY) — must never be written into either of those.
318
- */
319
- boundConversationId?: string;
320
- /**
321
- * Source conversation this session was FORKED from (`codex fork`). Distinct
322
- * from `resumedFromConversationId`: a resume continues one conversation, a
323
- * fork starts a second one whose history diverges from the source at the fork
324
- * point — and the source keeps its own owner, which is the entire point.
325
- */
326
- forkedFromConversationId?: string;
327
- /**
328
- * Multi-agent mode only. Per-session in-memory LRU of progress event ids
329
- * seen by the webhook receiver. Used to drop Temporal-replay duplicates
330
- * before they reach the WebSocket. See spec §7.1.
331
- */
332
- progressDedupeIds?: ProgressDedupeLRU;
333
- /** Multi-agent: current stage of the active turn (advisory; advisory wire field). */
334
- stage?: Stage | string;
335
- /** Multi-agent: ms since the session last emitted a stage transition. */
336
- stalledSinceMs?: number;
337
- /** Multi-agent: 1 or 2 when stage === "rework". */
338
- reworkAttempt?: number;
339
- /**
340
- * Multi-agent: id of the in-flight turn, or null when idle. Set when
341
- * `POST /api/sessions/:id/input` accepts a request; cleared by the webhook
342
- * receiver on stage=done or terminal_failure. Undefined in PTY mode.
343
- */
344
- currentTurnId?: string | null;
345
- /**
346
- * Multi-agent resume: stable identity of the underlying conversation
347
- * (the JSONL filename), distinct from `id` which is per-orchestrator-instance.
348
- * Undefined in PTY mode (PTY uses `resumedFromConversationId` instead).
349
- */
350
- conversationId?: string;
351
- }
352
- interface DiscoveredProcess {
353
- pid: number;
354
- projectPath: string;
355
- projectName: string;
356
- branch: string;
357
- conversationId: string | null;
358
- startedAt: Date;
359
- }
360
- interface AskOption {
361
- label: string;
362
- description: string;
363
- preview?: string;
364
- }
365
- interface AskQuestion {
366
- question: string;
367
- header: string;
368
- multiSelect: boolean;
369
- options: AskOption[];
370
- }
371
- interface PermissionOption {
372
- index: number;
373
- label: string;
374
- answerKeys?: string;
375
- }
376
- interface UserMessage {
377
- text: string;
378
- ts: number;
379
- }
380
- type WSMessage = {
381
- type: "terminal_output";
382
- sessionId: string;
383
- data: string;
384
- seq?: number;
385
- } | {
386
- type: "session_update";
387
- session?: SessionResponse;
388
- sessionId?: string;
389
- turnId?: string;
390
- stage?: Stage | string;
391
- stalledSinceMs?: number;
392
- reworkAttempt?: number;
393
- }
394
- /**
395
- * Agent phase changed within a running turn. Scoped to that session's
396
- * subscribers, like terminal_output and user_message.
397
- *
398
- * A minimal frame rather than a SessionResponse copy, deliberately:
399
- * managedToResponse recomputes `elapsedMs` from `new Date()` on every call
400
- * for a live session, so a session copy would differ on every tick whether
401
- * or not the phase changed — and a client that merges frames would get a
402
- * fresh object identity several times a second, re-rendering every consumer
403
- * for the whole turn.
404
- *
405
- * `phase` is always present and is `null` when there is no phase. Absence
406
- * must never carry meaning: clients merge session state, and a merge cannot
407
- * express a removed key, so an omitted field would keep its previous value
408
- * and the indicator would latch on a finished turn.
409
- */
410
- | {
411
- type: "session_phase";
412
- sessionId: string;
413
- phase: AgentPhase | null;
414
- updatedAt: string;
415
- } | {
416
- type: "session_list";
417
- sessions: readonly SessionResponse[];
418
- } | {
419
- type: "conversation_event";
420
- sessionId: string;
421
- line: string;
422
- } | {
423
- type: "conversation_events";
424
- sessionId: string;
425
- lines: string[];
426
- seqs?: (number | null)[];
427
- } | {
428
- type: "conversation_updated";
429
- conversationId: string;
430
- messageCount: number;
431
- lastActivity: string;
432
- ownership: "external" | "managed";
433
- } | {
434
- type: "question";
435
- sessionId: string;
436
- toolUseId: string;
437
- questions: AskQuestion[];
438
- } | {
439
- type: "question_cancelled";
440
- sessionId: string;
441
- toolUseId: string;
442
- } | {
443
- type: "permission";
444
- sessionId: string;
445
- prompt?: string;
446
- detail?: string;
447
- options: PermissionOption[];
448
- cursor?: number;
449
- } | {
450
- type: "permission_cancelled";
451
- sessionId: string;
452
- } | {
453
- type: "ping";
454
- ts: number;
455
- } | {
456
- type: "user_message";
457
- sessionId: string;
458
- text: string;
459
- ts: number;
460
- } | {
461
- type: "terminal_replay";
462
- sessionId: string;
463
- lines: string[];
464
- userMessages?: UserMessage[];
465
- seq?: number;
466
- } | {
467
- type: "session_ready";
468
- session: SessionResponse;
469
- } | {
470
- type: "agent_output";
471
- sessionId: string;
472
- turnId: string;
473
- role: "worker" | "reviewer" | "signoff";
474
- content: string;
475
- partial?: boolean;
476
- reviewerOverruled?: boolean;
477
- stage?: Stage | string;
478
- reworkAttempt?: number;
479
- } | {
480
- type: "turn_failure";
481
- sessionId: string;
482
- turnId: string;
483
- reason: string;
484
- } | {
485
- type: "cache_ready";
486
- } | {
487
- type: "scan_progress";
488
- scanned: number;
489
- total: number;
490
- } | {
491
- type: "cache_alert";
492
- fingerprint: string;
493
- severity: "high" | "low";
494
- missingCount: number;
495
- totalRows: number;
496
- detectedAt: string;
497
- sample: {
498
- id: string;
499
- title?: string;
500
- }[];
501
- } | {
502
- type: "cache_alert_resolved";
503
- fingerprint: string;
504
- action: CacheAlertResolveAction;
505
- };
506
- /** The four cache-integrity resolution actions (POST /api/cache/alert/resolve). */
507
- type CacheAlertResolveAction = "prune_all" | "prune_selected" | "ignore" | "reset_rescan";
508
- type ServerWarmupState = "startup" | "cache_reset" | "conversation_refresh";
509
- interface ServerWarmingUpResponse {
510
- error: "Server is warming up";
511
- code: "SERVER_WARMING_UP";
512
- warmupState: ServerWarmupState;
513
- }
514
- interface SessionResponse {
515
- id: string;
516
- conversationId: string;
517
- provider?: ProviderName;
518
- projectId?: string;
519
- status: SessionStatus;
520
- projectPath: string;
521
- projectName: string;
522
- branch: string;
523
- lastOutput: string;
524
- elapsedMs: number;
525
- promptCount: number;
526
- startedAt: string;
527
- completedAt: string | null;
528
- ptyAttached: boolean;
529
- /**
530
- * Process-lifetime axis, orthogonal to `status` (C1).
531
- *
532
- * `status` answers "what is the agent doing" (running / waiting_input /
533
- * idle); `lifecycle` answers "does this process still exist and do we own
534
- * it". They were conflated before: `idle` meant finished, killed-to-save-
535
- * resources, and externally-discovered all at once, so a client could not
536
- * tell a completed session from one we terminated.
537
- *
538
- * Additive and optional — `ptyAttached` keeps its meaning (=== "attached"),
539
- * so a client that ignores this behaves exactly as it did before. `"starting"`
540
- * is additive in the same way: a session we hold no PTY for and have observed
541
- * no exit for reports it instead of the `"completed"` it used to, so a client
542
- * can tell "not attached yet" from "ended" (tb-mobile #508).
543
- */
544
- /**
545
- * How `status` was derived and how far to trust it (C3). Additive: `status`
546
- * keeps its exact meaning, so a client ignoring these behaves as before.
547
- * An `inferred` confidence means a timer expired and we assumed — not that
548
- * anything in the stream confirmed the state.
549
- */
550
- statusSource?: StatusSource$1;
551
- statusConfidence?: StatusConfidence;
552
- statusUpdatedAt?: string;
553
- lifecycle?: SessionLifecycle;
554
- /** How `lifecycle` was determined, so stale values are visible not implied. */
555
- lifecycleSource?: "spawn" | "exit" | "probe" | "reconcile";
556
- lifecycleUpdatedAt?: string;
557
- /**
558
- * ADDITIVE (plan Phase 5). What a recovered session was doing when the
559
- * streamer stopped it, so a client can say "interrupted mid-response" instead
560
- * of the `idle` its `status` is forced to report — a stub holds no PTY, and a
561
- * novel SessionStatus value would be dropped by `?status=` filtering on
562
- * already-shipped clients. Present only on rows whose `status_source` was
563
- * `shutdown`; older clients ignore it.
564
- */
565
- interruptedStatus?: "running" | "waiting_input";
566
- failureReason?: string;
567
- pid?: number;
568
- sessionName?: string;
569
- model?: string;
570
- /**
571
- * Reasoning-effort tier scraped from the live PTY status line (e.g. "high").
572
- * Live sessions only — absent for historical/resumable conversations.
573
- */
574
- effort?: string;
575
- /**
576
- * Active permission mode from the live PTY status line (e.g. "accept edits on").
577
- * Live sessions only.
578
- */
579
- permissionMode?: string;
580
- /**
581
- * Agent phase within a running turn, scraped from the rendered PTY screen.
582
- *
583
- * NOT optional, and always serialised — `null` when there is no phase. A
584
- * client that merges session frames (`{...prev, ...next}`) cannot express a
585
- * removed key, so an omitted field would keep its previous value and the
586
- * indicator would latch on a finished turn. That is the bug tb-mobile PR #647
587
- * shipped; absence must never carry meaning here.
588
- *
589
- * Consequently this must NOT be moved into the `...(x != null && { x })`
590
- * guard block in managedToResponse: `!= null` catches null and undefined
591
- * alike and would convert an explicit clear back into absence.
592
- */
593
- subStatus: AgentPhase | null;
594
- account?: string;
595
- messageCount?: number;
596
- preview?: string;
597
- firstMessageText?: string;
598
- firstMessageAt?: string;
599
- lastMessageText?: string;
600
- lastMessageAt?: string;
601
- lastActivityAt?: string;
602
- filePath?: string;
603
- resumedFromConversationId?: string;
604
- /** See `ManagedSession.forkedFromConversationId`. Additive; older clients ignore it. */
605
- forkedFromConversationId?: string;
606
- /** See `ManagedSession.boundConversationId` — never repurposes `conversationId`. */
607
- boundConversationId?: string;
608
- /**
609
- * Who owns the underlying process. Additive — older clients ignore it, and it
610
- * deliberately does NOT introduce a new `status` value: `VALID_STATUSES`
611
- * rejects unknown values in `?status=` and the store drops sessions outside
612
- * the requested set, so a new status string would make these sessions vanish
613
- * from already-shipped apps.
614
- * managed — this streamer spawned and holds the PTY
615
- * external — a process we discovered but do not own (read-only)
616
- * historical — a cached conversation, no process known
617
- */
618
- ownership?: SessionOwnership;
619
- /**
620
- * Whether the process is believed to be running. Only ever "alive" when
621
- * discovery actually saw it; never guessed from file activity.
622
- */
623
- processLiveness?: ProcessLiveness;
624
- /**
625
- * INFERRED from JSONL writes, never authoritative: "active_writing" means the
626
- * transcript grew recently, which cannot distinguish a generating agent from
627
- * one blocked on a permission gate (gates are screen-only). Absent for
628
- * sessions we own — their `status` is the authoritative signal.
629
- */
630
- activity?: SessionActivity;
631
- }
632
- type SessionOwnership = "managed" | "external" | "historical";
633
- type ProcessLiveness = "alive" | "gone" | "unknown";
634
- interface SessionActivity {
635
- state: "active_writing" | "quiet";
636
- lastEventAt: string;
637
- source: "jsonl";
638
- }
639
- interface ConversationListResponse {
640
- conversations: unknown[];
641
- hasMore: boolean;
642
- offset: number;
643
- total: number;
644
- }
645
- type SessionSortKey = "startedAt" | "lastActivityAt" | "projectName" | "status";
646
- type SortOrder = "asc" | "desc";
647
- interface SessionListPage {
648
- /** Response copies, like `SessionStore.list()` — see the note there. */
649
- sessions: readonly Readonly<SessionResponse>[];
650
- nextCursor: string | null;
651
- total: number;
652
- }
653
- interface SessionListQuery {
654
- limit: number;
655
- cursor?: string;
656
- sortBy: SessionSortKey;
657
- order: SortOrder;
658
- status?: SessionStatus[];
659
- }
660
- interface SessionCursor {
661
- k: string | number;
662
- id: string;
663
- }
664
- interface ServerConfig {
665
- port: number;
666
- host?: string;
667
- apiKey?: string;
668
- /** 'cli' when --api-key was passed; rotation persists in-memory only and reverts on restart */
669
- apiKeySource?: "config" | "cli";
670
- localNoAuth?: boolean;
671
- verbose?: boolean;
672
- logMenubarRequests?: boolean;
673
- browseRoot?: string;
674
- publicUrl?: string;
675
- browserCors?: string;
676
- disableDb?: boolean;
677
- scanProfiles?: Array<{
678
- id: string;
679
- label: string;
680
- configDir: string;
681
- enabled: boolean;
682
- emoji: string;
683
- }>;
684
- codexRoots?: string[];
685
- scannerPersistent?: boolean;
686
- skipStartupWarmup?: boolean;
687
- ptyGracePeriodMs?: number;
688
- /**
689
- * Re-start interrupted sessions at boot instead of listing them for the user
690
- * to tap (plan Phase 7). A boolean here rather than the tri-state the loader
691
- * returns: by the time a ServerConfig is built the "never asked" case has
692
- * already resolved to false. Precedence: explicit here → `auto_resume_on_boot`
693
- * in server.yaml → false. Never enabled implicitly — it is the one setting
694
- * that starts an agent nobody asked for in that moment.
695
- */
696
- autoResumeOnBoot?: boolean;
697
- cacheDir?: string;
698
- runtimeDbPath?: string;
699
- tailSize?: number;
700
- directoryScanDebounceMs?: number;
701
- defaultSystemPrompt?: string;
702
- codexSystemPromptEnabled?: boolean;
703
- featureFlags?: FeatureFlagValues;
704
- defaultPermissionMode?: PermissionMode;
705
- defaultModel?: string;
706
- defaultEffort?: EffortLevel;
707
- claudeFlags?: ClaudeFlagValues;
708
- claudeExtraArgs?: string;
709
- }
710
- interface PTYManagerOptions {
711
- onOutput?: (sessionId: string, data: string) => void;
712
- onStatusChange?: (session: ManagedSession) => void;
713
- onReady?: (session: ManagedSession) => void;
714
- onPermissionChange?: (sessionId: string, gate: {
715
- prompt?: string;
716
- detail?: string;
717
- options: PermissionOption[];
718
- cursor?: number;
719
- } | null) => void;
720
- /**
721
- * Fired when the agent's phase within a running turn changes, including to
722
- * `null` at turn end. Additive; absent in tests that omit it.
723
- *
724
- * Deliberately NOT routed through onStatusChange, even though that callback
725
- * already exists and is already relayed across the pty-host boundary. Its
726
- * handler writes a DB row per invocation with no same-status guard, refreshes
727
- * the scanner index, broadcasts globally, and pokes the APNs and push
728
- * notifiers — machinery built for a handful of transitions per session, not
729
- * for a signal that can fire every SCRAPE_THROTTLE_MS.
730
- *
731
- * The server must broadcast this to that session's subscribers only
732
- * (wsHub.broadcastToClients), as a minimal frame rather than a SessionResponse
733
- * copy: managedToResponse recomputes elapsedMs on every call, so a session
734
- * copy would differ every tick and re-render every client consumer of that
735
- * session for the whole turn.
736
- */
737
- onPhaseChange?: (sessionId: string, phase: AgentPhase | null) => void;
738
- onLiveQuestion?: (sessionId: string, questions: AskQuestion[]) => void;
739
- onLiveQuestionGone?: (sessionId: string) => void;
740
- onUserMessage?: (sessionId: string, text: string, ts: number) => void;
741
- logger?: Logger;
742
- }
743
- interface StartSessionOptions {
744
- projectPath: string;
745
- projectName?: string;
746
- branch?: string;
747
- /**
748
- * Provider-side id to resume from, when it differs from `sessionId`. Codex
749
- * keys a fresh session by a local placeholder UUID and only learns its real
750
- * rollout id once it writes the file, so resuming needs the rollout id in
751
- * argv while the session keeps the placeholder the client navigated to.
752
- * Absent means "resume by the session id", which is what every Claude session
753
- * does — PTYManager ignores this field entirely.
754
- */
755
- resumeId?: string;
756
- permissionMode?: PermissionMode;
757
- model?: string;
758
- effort?: EffortLevel;
759
- claudeFlags?: ClaudeFlagValues;
760
- claudeExtraArgs?: string;
761
- }
762
- interface StartForkSessionOptions {
763
- /**
764
- * Provider-side id of the conversation to fork FROM (for Codex, the rollout
765
- * id — never a local placeholder). The forked session gets its own id; the
766
- * two identities are deliberately kept distinct.
767
- */
768
- forkFromId: string;
769
- projectPath: string;
770
- projectName?: string;
771
- branch?: string;
772
- }
773
- interface StartFreshSessionOptions {
774
- projectPath: string;
775
- projectName?: string;
776
- systemPrompt?: string;
777
- permissionMode?: PermissionMode;
778
- model?: string;
779
- effort?: EffortLevel;
780
- claudeFlags?: ClaudeFlagValues;
781
- claudeExtraArgs?: string;
782
- }
783
- interface SessionRunner {
784
- start(sessionId: string, options: StartSessionOptions): Promise<ManagedSession>;
785
- startFresh(options: StartFreshSessionOptions): Promise<ManagedSession>;
786
- sendInput(sessionId: string, input: string): number;
787
- sendKeys(sessionId: string, keys: string): void;
788
- cancel(sessionId: string): void;
789
- killPid(pid: number): void;
790
- putOnHold(sessionId: string): void;
791
- getOutput(sessionId: string): string;
792
- getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;
793
- getInputHistory(sessionId: string): UserMessage[];
794
- getPid(sessionId: string): number | null;
795
- getSession(sessionId: string): ManagedSession | null;
796
- hasSession(sessionId: string): boolean;
797
- listSessions(): ManagedSession[];
798
- dispose(): void;
799
- }
800
-
801
- /**
802
- * Scoped device capabilities (C5).
803
- * See docs/architecture/2026-07-24-device-identity-and-capabilities.md.
804
- *
805
- * Authorization was all-or-nothing: authMiddleware asked one question — is this
806
- * token the API key — and answered 401 or full access. A device paired merely to
807
- * glance at session status held exactly the authority of the one driving the
808
- * agent, because there was only ever one credential and no principal to scope.
809
- */
810
- declare const CAPABILITIES: readonly ["history:read", "session:control", "fs:browse", "fs:upload", "notifications", "admin"];
811
- type Capability = (typeof CAPABILITIES)[number];
812
- type CapabilityPreset = "full" | "read-only";
813
- /**
814
- * The principal behind a request.
815
- *
816
- * `legacy` is the shared API key: it predates device identity, so it carries the
817
- * full preset and no device id. Keeping it working is what lets this ship
818
- * without breaking every already-paired device.
819
- */
820
- interface Principal {
821
- kind: "device" | "legacy";
822
- deviceId?: string;
823
- capabilities: Capability[];
824
- }
825
-
826
- interface LineSpan {
827
- /** Absolute byte offset of the line's first byte in the file. */
828
- byteOffset: number;
829
- /** Byte length of the line content, excluding the trailing "\n". */
830
- byteLength: number;
831
- /** The decoded line text (no trailing "\n"). */
832
- text: string;
833
- }
834
-
835
- interface ConversationCacheOptions {
836
- filterAgentConversations?: boolean;
837
- agentEntrypoints?: ReadonlySet<string>;
838
- onAgentFileDetected?: (filePath: string) => void;
839
- }
840
- interface ConversationListItem {
841
- id: string;
842
- filePath: string;
843
- projectId: string | null;
844
- projectPath: string | null;
845
- projectName: string | null;
846
- title: string | null;
847
- model: string | null;
848
- account: string | null;
849
- branch: string | null;
850
- messageCount: number;
851
- lastActivity: string;
852
- firstMessage: string | null;
853
- lastMessage: string | null;
854
- preview: string | null;
855
- source: string | null;
856
- provider: "claude-code" | "codex-cli";
857
- }
858
- interface CachedTailMessage {
859
- role: string;
860
- timestamp: string;
861
- text: string;
862
- content?: unknown[];
863
- }
864
- interface CachedTail {
865
- conversationId: string;
866
- messages: CachedTailMessage[];
867
- tailSize: number;
868
- }
869
- /** A row of `conversation_file_state` — per-file offset-index resume state. */
870
- interface FileStateRow {
871
- path: string;
872
- identity: string;
873
- size: number;
874
- mtime_ms: number;
875
- byte_offset: number;
876
- last_message_index: number;
877
- }
878
- /** A row of `conversation_message_index` — one indexed message's byte span. */
879
- interface MessageIndexRow {
880
- conversation_id: string;
881
- message_index: number;
882
- byte_offset: number;
883
- byte_length: number;
884
- uuid: string | null;
885
- role: string | null;
886
- ts: number | null;
887
- }
888
- interface ScannerMeta {
889
- id: string;
890
- sessionId?: string;
891
- filePath: string;
892
- projectPath?: string;
893
- projectName?: string;
894
- title?: string;
895
- sessionName?: string;
896
- model?: string;
897
- account?: string;
898
- gitBranch?: string;
899
- messageCount?: number;
900
- timestamp?: string;
901
- firstMessage?: unknown;
902
- lastMessage?: unknown;
903
- preview?: string;
904
- provider?: "claude-code" | "codex-cli";
905
- }
906
- declare class ConversationCache {
907
- private db;
908
- private tailSize;
909
- private fileIndex;
910
- private fileIndexLoaded;
911
- private indexParseState;
912
- private backfillInFlight;
913
- private tailSeq;
914
- private nameSeq;
915
- private stmts;
916
- private migrationsDir?;
917
- private filterAgentConversations;
918
- private agentEntrypoints;
919
- private onAgentFileDetected?;
920
- private constructor();
921
- /**
922
- * Expose the underlying handle so projects/cache_metadata repositories can
923
- * share the same connection. Internal API; not part of the public surface.
924
- */
925
- getDatabase(): Database.Database;
926
- getFileState(path: string): FileStateRow | null;
927
- upsertFileState(row: FileStateRow): void;
928
- /** Drop a file's index rows + file_state (truncation / identity change). */
929
- deleteFileIndex(path: string, conversationId: string): void;
930
- /** Append/replace index rows in one transaction. */
931
- appendMessageIndexRows(rows: MessageIndexRow[]): void;
932
- /** Rows for message_index in [fromIndex, toIndex), ordered ascending. */
933
- getMessageIndexWindow(conversationId: string, fromIndex: number, toIndex: number): MessageIndexRow[];
934
- getIndexedMessageCount(conversationId: string): number;
935
- /**
936
- * Conversation id for a JSONL path — the filename stem (matches the pseudo-id
937
- * updateFromLine derives and the uuid the detail read path resolves). The
938
- * offset index keys on this so the window select and the cursor agree.
939
- */
940
- static conversationIdForFile(filePath: string): string;
941
- private isIndexableFile;
942
- /**
943
- * Incremental offset-index writer: extend the index for a burst of appended
944
- * lines (one watcher read) using their byte spans. Each line is classified
945
- * with the scanner's parseJsonlLine (a running per-file reducer state), so the
946
- * message ordering can never drift from the scanner's. Message lines get an
947
- * index row at the next message_index; non-message lines (summary/sidecar)
948
- * get no row but still advance byte_offset. file_state is updated to the end
949
- * of the last consumed span.
950
- *
951
- * Requires an up-to-date `stat` (identity/size/mtime) for the file so the read
952
- * path can detect truncation/replacement.
953
- *
954
- * `readFrom` is the absolute byte offset the watcher read started at, and
955
- * `endOffset` is where it ended (readFrom + consumed, i.e. the watcher's new
956
- * entry.offset). CONTIGUITY GUARD: the read must begin exactly where the index
957
- * left off (`readFrom === existing.byte_offset`, or 0 with no state). If it
958
- * doesn't — the watcher attached at EOF after the server was down, or an
959
- * append raced an in-flight backfill — extending would assign wrong
960
- * message_index values over a hole. In that case this writes nothing and
961
- * returns null so the caller drops the index and backfills.
962
- *
963
- * On success returns the message_index assigned to each input span (null for a
964
- * non-message line) so the caller can stamp WS `seq`. Empty array when spans
965
- * is empty. `endOffset` is stored verbatim as byte_offset so the watcher's
966
- * offset and file_state.byte_offset are the same number by construction.
967
- */
968
- extendMessageIndex(filePath: string, spans: LineSpan[], stat: Stats, readFrom: number, endOffset: number): (number | null)[] | null;
969
- clearIndexParseState(filePath: string): void;
970
- /**
971
- * On-demand full backfill of the offset index for a file with no/stale
972
- * file_state (cold conversation, or after a truncation/replacement). Rebuilds
973
- * from byte 0: drops any existing rows, walks the whole file in chunks with a
974
- * running parse state, yields to the event loop every ~1000 lines so a large
975
- * file never blocks, and writes index rows + file_state.
976
- *
977
- * Single-flighted per path: concurrent callers await the same walk. The
978
- * triggering detail request is served by the scanner fallback while this runs.
979
- */
980
- backfillIndex(filePath: string): Promise<void>;
981
- private runBackfill;
982
- /**
983
- * Windowed detail read straight from the offset index — the hot path.
984
- * Returns the parsed messages for message_index in [fromIndex, toIndex) plus
985
- * the total indexed count, or null when the index can't serve this file (no
986
- * file_state, identity/size mismatch = truncation/replacement, or cold index)
987
- * so the caller falls back to the scanner and enqueues a backfill.
988
- *
989
- * On a match it SQL-selects the window's byte ranges and preads exactly those
990
- * ranges from the JSONL (never the whole file), parsing only the sliced lines.
991
- * Returns messages in the same ConversationMessage shape parseJsonlLine
992
- * produces during a scan, so the payload is identical to the scanner path.
993
- */
994
- readMessageWindow(filePath: string, fromIndex: number, toIndex: number): {
995
- messages: ConversationMessage[];
996
- total: number;
997
- fromIndex: number;
998
- } | null;
999
- private agentEntrypointsKey;
1000
- private classifyAgentFile;
1001
- isAgentFileCached(filePath: string): boolean;
1002
- static open(dbPath: string, tailSize?: number, migrationsDir?: string, options?: ConversationCacheOptions): ConversationCache;
1003
- close(): void;
1004
- getPopularProjects(limit: number): Array<{
1005
- path: string;
1006
- name: string;
1007
- sessionCount: number;
1008
- }>;
1009
- /** Every project with at least one cached conversation, most recently active
1010
- * first. Paths are the raw `project_path` values, which is what
1011
- * /api/conversations?project= matches on exactly — so a summary row is
1012
- * always joinable against the page it describes. */
1013
- listProjectSummaries(opts: {
1014
- limit: number;
1015
- offset: number;
1016
- }): {
1017
- projects: Array<{
1018
- path: string;
1019
- name: string;
1020
- conversationCount: number;
1021
- lastActivity: string;
1022
- }>;
1023
- total: number;
1024
- };
1025
- private ensureFileIndex;
1026
- updateFromLine(filePath: string, rawLine: string): void;
1027
- /**
1028
- * Batched form of updateFromLine: applies a burst of newly-appended lines
1029
- * (one chokidar read) in a single transaction with one message_count bump,
1030
- * one meta write, and one tail read/write — instead of 2-4 synchronous
1031
- * writes per line. Semantics are identical to replaying each line through
1032
- * updateFromLine in order: the agent filter short-circuits the whole batch,
1033
- * project context is backfilled last-wins, message_count increases by the
1034
- * number of surviving message lines, and last_activity/last_message reflect
1035
- * the newest message line by timestamp (a monotonic guard keeps them from
1036
- * moving backward when an interleaved writer appends an older line — P0.3).
1037
- */
1038
- updateFromLines(filePath: string, rawLines: string[]): void;
1039
- upsertFromScannerMeta(metas: ScannerMeta[]): string[];
1040
- deleteByFilePath(filePath: string): boolean;
1041
- populateTailFromFile(convId: string, filePath: string): boolean;
1042
- listConversations(opts: {
1043
- project?: string;
1044
- provider?: string;
1045
- limit: number;
1046
- offset: number;
1047
- }): {
1048
- conversations: ConversationListItem[];
1049
- total: number;
1050
- };
1051
- /** Returns a map of filePath → { mtimeMs, size } for all rows that have
1052
- * stat data stored. Used by the server to build the statCache passed to
1053
- * ConversationScanner.scan() so unchanged files are skipped. */
1054
- getFileStats(): Map<string, {
1055
- mtimeMs: number;
1056
- size: number;
1057
- }>;
1058
- getScannerStatCache(): Map<string, {
1059
- stat: FileStatEntry;
1060
- meta: ConversationMeta;
1061
- }>;
1062
- /**
1063
- * Conversation id for a JSONL path, or null when no row exists yet. Resolves
1064
- * by file_path (NOT conversationIdForFile) so codex rollout files — named
1065
- * rollout-<ts>-<uuid>.jsonl, whose stem is not the row id — resolve correctly.
1066
- */
1067
- getIdByFilePath(filePath: string): string | null;
1068
- getMetaById(id: string): ConversationListItem | null;
1069
- setConversationProjectId(conversationId: string, projectId: string): void;
1070
- markAsStreamer(id: string): void;
1071
- getLatestConversation(): {
1072
- id: string;
1073
- lastActivity: string | null;
1074
- } | null;
1075
- hasOrphanProjectId(): boolean;
1076
- listConversationsForProjectBackfill(): Array<{
1077
- id: string;
1078
- projectPath: string | null;
1079
- projectId: string | null;
1080
- lastActivity: string | null;
1081
- }>;
1082
- getConversationTail(id: string): CachedTail | null;
1083
- hasConversation(id: string): boolean;
1084
- upsertSessionName(sessionId: string, name: string): void;
1085
- getSessionName(sessionId: string): string | null;
1086
- listSessionNames(): Record<string, string>;
1087
- invalidate(id?: string): void;
1088
- /**
1089
- * Drop the cached row for a file. Two callers with opposite intent:
1090
- * - a directory-watch "change" event (the file was appended to) — pass
1091
- * `skipIfTailed: true`, which NEVER deletes (upsert-or-leave). A change
1092
- * event fires on every external append; deleting here flickers the
1093
- * conversation out of /api/conversations — whether it's a live-tailed row
1094
- * the updateFromLines/warm-up path just wrote (CRITICAL #2; both watchers
1095
- * fire on the same append with no ordering guarantee) OR a refresh-created
1096
- * untailed row (a ?refresh=1 upsert never populates a tail, so the old
1097
- * "delete when untailed" behavior made it vanish on its next append with no
1098
- * client action). The live-tail path owns the row's content and the
1099
- * debounced rescan re-derives metadata, so leaving the row loses nothing.
1100
- * - a genuine unlink (the file is gone) — leave `skipIfTailed` false so the
1101
- * row is always removed, otherwise a deleted session ghosts in the cache.
1102
- */
1103
- invalidateByFilePath(filePath: string, opts?: {
1104
- skipIfTailed?: boolean;
1105
- }): string | null;
1106
- /**
1107
- * Drop rows whose `file_path` no longer exists on disk AND which have no
1108
- * cached tail to fall back to. Rows with a tail are left alone so
1109
- * `handleGetConversation` can still serve the cached tail even when the
1110
- * JSONL has been deleted.
1111
- */
1112
- pruneGhostFiles(exists?: (filePath: string) => boolean): string[];
1113
- /**
1114
- * Reconcile the cache against the authoritative set of conversation file
1115
- * paths a fresh scan surfaced: drop any cached row whose `file_path` is not
1116
- * in `livePaths` (removed from disk, or now filtered out — e.g. became an
1117
- * agent JSONL). This is the "removed conversations" half of a ?refresh=1
1118
- * reconcile; the additions/updates half is upsertFromScannerMeta.
1119
- *
1120
- * Skip semantics depend on whether the file still exists on disk:
1121
- * - File GONE from disk → always removed, tail or not. This matches the old
1122
- * invalidate()+rebuild behavior (a deleted conversation must disappear on
1123
- * refresh) and keeps refresh=1 truthful about removals. NOTE: this is an
1124
- * INTENTIONAL divergence from pruneGhostFiles(), which KEEPS tailed ghosts
1125
- * so their cached history stays viewable on a background prune. refresh=1
1126
- * has the opposite contract (mobile relies on removals being reflected), so
1127
- * do not "unify" the two — they serve different purposes.
1128
- * - File STILL on disk but absent from `livePaths` → the CRITICAL #2 race:
1129
- * the scan snapshot predates a just-created (and now live-tailed) file.
1130
- * A tailed row here is actively maintained from real content, so it is
1131
- * kept — dropping it would flicker the active conversation out of
1132
- * /api/conversations. An untailed on-disk row not in the snapshot is a
1133
- * transient scan/discovery gap; it is left alone (not removed) and the
1134
- * next reconcile picks it up, rather than risk removing a real file the
1135
- * scan simply hasn't surfaced yet.
1136
- * Returns the removed IDs.
1137
- */
1138
- reconcileDeletions(livePaths: Set<string>, opts?: {
1139
- exists?: (filePath: string) => boolean;
1140
- }): string[];
1141
- /**
1142
- * Read-only: list cached rows whose `file_path` no longer exists on disk.
1143
- * Unlike pruneGhostFiles/reconcileDeletions this mutates nothing — it just
1144
- * reports drift for the CacheIntegrityMonitor to classify. `tailed` flags
1145
- * rows that still have cached history (which pruneGhostFiles would keep).
1146
- */
1147
- listMissingFiles(exists?: (filePath: string) => boolean): {
1148
- id: string;
1149
- filePath: string;
1150
- title: string | null;
1151
- tailed: boolean;
1152
- }[];
1153
- /**
1154
- * Drop the given conversation ids outright — main row, tail, and message
1155
- * index — regardless of whether they have a tail. Used by the cache-integrity
1156
- * resolution actions (prune_all / prune_selected). Returns the count dropped.
1157
- */
1158
- dropRowsById(ids: string[]): number;
1159
- /**
1160
- * Wipe all cached conversation state — meta, tails, and message index — and
1161
- * reset the in-memory file index. Only called by the `reset_rescan`
1162
- * resolution action, which repopulates from a fresh disk scan afterward.
1163
- */
1164
- clearAll(): void;
1165
- }
1166
-
1167
- type CacheMetadataKey = "last_conversation_id" | "last_conversation_created_at" | "projects_last_indexed_at" | "conversations_last_indexed_at" | "conversations_dirty";
1168
- declare class CacheMetadataRepository {
1169
- private get;
1170
- private upsert;
1171
- private del;
1172
- constructor(db: Database.Database);
1173
- getCacheMetadata(key: CacheMetadataKey): string | null;
1174
- setCacheMetadata(key: CacheMetadataKey, value: string): void;
1175
- deleteCacheMetadata(key: CacheMetadataKey): void;
1176
- }
1177
-
1178
- /**
1179
- * Thin repository wrapper around ConversationCache for the project-id flow.
1180
- * The cache is the source of truth for conversation rows; this just exposes
1181
- * a stable, repo-style API for services that don't want to know about the
1182
- * cache class directly.
1183
- */
1184
- declare class ConversationsRepository {
1185
- private cache;
1186
- constructor(cache: ConversationCache);
1187
- updateConversationProjectId(args: {
1188
- conversationId: string;
1189
- projectId: string;
1190
- }): void;
1191
- listConversationsForProjectBackfill(): {
1192
- id: string;
1193
- projectPath: string | null;
1194
- projectId: string | null;
1195
- lastActivity: string | null;
1196
- }[];
1197
- getLatestConversation(): {
1198
- id: string;
1199
- lastActivity: string | null;
1200
- } | null;
1201
- hasOrphanRows(): boolean;
1202
- }
1203
-
1204
- /**
1205
- * Paired-device registry (C5).
1206
- * See docs/architecture/2026-07-24-device-identity-and-capabilities.md.
1207
- *
1208
- * Stores identity and authority for each paired device. Never stores a usable
1209
- * credential: only the SHA-256 of a device token, so reading this table cannot
1210
- * impersonate a device.
1211
- */
1212
- interface DeviceRow {
1213
- device_id: string;
1214
- public_key: string;
1215
- token_hash: string;
1216
- name: string | null;
1217
- capabilities: string;
1218
- created_at: number;
1219
- last_seen_at: number | null;
1220
- revoked_at: number | null;
1221
- /** Noise static public key, base64. Null on every row that predates E2EE. */
1222
- e2ee_static_pub: string | null;
1223
- /** The downgrade lock. 1 once this device has completed a handshake. */
1224
- e2ee_required: number;
1225
- e2ee_version: number | null;
1226
- }
1227
- /** A device as reported over the API. Deliberately carries no credential. */
1228
- interface DeviceView {
1229
- deviceId: string;
1230
- name: string | null;
1231
- capabilities: Capability[];
1232
- createdAt: number;
1233
- lastSeenAt: number | null;
1234
- revokedAt: number | null;
1235
- /**
1236
- * Whether this device is pinned to encryption.
1237
- *
1238
- * A boolean, never the key and never a hash of it: `GET /api/devices` exists
1239
- * so a user can spot a device they did not pair, and answering "is this one
1240
- * encrypted" needs no key material to do it. Publishing the static key would
1241
- * hand an attacker who reached this endpoint the value that identifies a
1242
- * device.
1243
- */
1244
- e2ee: boolean;
1245
- }
1246
- interface RegisteredDevice {
1247
- deviceId: string;
1248
- /** Returned to the client exactly once, at pairing. Never persisted raw. */
1249
- deviceToken: string;
1250
- capabilities: Capability[];
1251
- }
1252
- declare class DevicesRepository {
1253
- private insertStmt;
1254
- private byTokenHashStmt;
1255
- private byIdStmt;
1256
- private listStmt;
1257
- private revokeStmt;
1258
- private touchStmt;
1259
- private deleteStmt;
1260
- private deleteRevokedStmt;
1261
- private byE2eeStaticPubStmt;
1262
- private repairStmt;
1263
- constructor(db: Database.Database);
1264
- /**
1265
- * Record a newly paired device and mint its token.
1266
- *
1267
- * The raw token is returned to the caller and never stored — this is the only
1268
- * moment it exists outside the client.
1269
- */
1270
- register(args: {
1271
- publicKey: string;
1272
- name?: string | null;
1273
- preset?: CapabilityPreset;
1274
- now?: number;
1275
- /**
1276
- * The device's Noise static public key, base64, when pairing completed a
1277
- * handshake. Absent on a plaintext pairing, which stays exactly as it is
1278
- * today: no key, no pin, no behaviour change.
1279
- */
1280
- e2eeStaticPub?: string;
1281
- e2eeVersion?: number;
1282
- }): RegisteredDevice;
1283
- /** The device that owns a Noise static key, or null. */
1284
- getByE2eeStaticPub(staticPub: string): DeviceRow | null;
1285
- /**
1286
- * Resolve a presented token to a device, or null.
1287
- *
1288
- * Returns null for a revoked device, so revocation takes effect on the very
1289
- * next request with no cache to go stale.
1290
- */
1291
- authenticate(token: string): DeviceRow | null;
1292
- get(deviceId: string): DeviceRow | null;
1293
- /** All devices, including revoked ones — an audit surface needs the history. */
1294
- list(): DeviceView[];
1295
- /** Revoke one device. Others are untouched — no key rotation, no collateral. */
1296
- revoke(deviceId: string, now?: number): boolean;
1297
- /**
1298
- * Erase one device's record outright.
1299
- *
1300
- * Deliberately separate from `revoke`, which is a soft delete that keeps the
1301
- * row so `list()` can show what happened. That audit trail is the right
1302
- * default — but it meant a `devices` row, including the user-supplied `name`
1303
- * ("Ronen's iPhone"), had no removal path at all once the registry moved to
1304
- * runtime.db, which no command deletes. This is that path.
1305
- *
1306
- * Erasure is NOT revocation: deleting a row frees its `token_hash`, so a
1307
- * device whose token is still on a phone somewhere stops being *known* rather
1308
- * than being *refused*. Revoke first, delete second, is the safe order, and
1309
- * `deleteRevoked()` exists so that is the easy thing to do.
1310
- */
1311
- delete(deviceId: string): boolean;
1312
- /**
1313
- * Erase every already-revoked device. The bulk companion to `delete`, and the
1314
- * one that is safe by construction: a revoked device is already refused, so
1315
- * removing its row cannot restore access to anything.
1316
- *
1317
- * Returns the number of rows removed.
1318
- */
1319
- deleteRevoked(): number;
1320
- touch(deviceId: string, now?: number): void;
1321
- }
1322
-
1323
- /**
1324
- * Durable registry of managed sessions (C1 Phase 2).
1325
- * See docs/architecture/2026-07-24-durable-session-runtime.md.
1326
- *
1327
- * Distinct from SessionsRepository, which wraps the in-memory SessionStore for
1328
- * the live request path. This one exists so session identity and provenance
1329
- * outlive the streamer process: without it, a restart loses startedAt,
1330
- * promptCount, sessionName, the Codex rollout binding, and failureReason, and
1331
- * the boot reconciler has nothing to reconcile against.
1332
- *
1333
- * Deliberately not stored here: the PTY output ring buffer and xterm screen.
1334
- * Their authoritative copy is the provider's JSONL, and writing 64KiB of ANSI
1335
- * on every chunk to duplicate it would be a write storm for no gain.
1336
- */
1337
- /** How a persisted status was obtained — never inferred, always recorded. */
1338
- type StatusSource = "spawn" | "transition" | "exit" | "shutdown" | "probe" | "reconcile";
1339
-
1340
- interface ManagedSessionRow {
1341
- session_id: string;
1342
- provider: string;
1343
- pid: number | null;
1344
- cmdline: string | null;
1345
- project_path: string;
1346
- project_name: string;
1347
- branch: string;
1348
- status: string;
1349
- status_source: string;
1350
- status_updated_at: number;
1351
- started_at: number;
1352
- completed_at: number | null;
1353
- last_activity_at: number | null;
1354
- prompt_count: number;
1355
- session_name: string | null;
1356
- project_id: string | null;
1357
- bound_conversation_id: string | null;
1358
- resumed_from_conversation_id: string | null;
1359
- failure_reason: string | null;
1360
- streamer_instance_id: string;
1361
- /**
1362
- * Which machine boot `pid` was recorded during (migration 002). Optional
1363
- * because rows written before it exists read back as null/absent, which the
1364
- * reconciler treats exactly like a mismatch — never like a match.
1365
- */
1366
- boot_token?: string | null;
1367
- }
1368
- interface RecordSpawnInput {
1369
- session: ManagedSession;
1370
- pid: number | null;
1371
- cmdline: string | null;
1372
- streamerInstanceId: string;
1373
- }
1374
- declare class ManagedSessionsRepository {
1375
- private upsertStmt;
1376
- private updateStatusStmt;
1377
- private bindStmt;
1378
- private getStmt;
1379
- private listNonTerminalStmt;
1380
- private listAllStmt;
1381
- private pruneTerminalStmt;
1382
- private listRecoverableStmt;
1383
- private deleteStmt;
1384
- constructor(db: Database.Database);
1385
- /** Record a session at spawn, or refresh every field of an existing row. */
1386
- recordSpawn({ session, pid, cmdline, streamerInstanceId }: RecordSpawnInput): void;
1387
- /**
1388
- * Persist a status transition. `source` is required rather than defaulted:
1389
- * a status whose provenance is unknown is the thing this table exists to
1390
- * prevent, and the reconciler reads it to decide how much to trust the value.
1391
- */
1392
- recordStatus(sessionId: string, status: SessionStatus, source: StatusSource, fields?: {
1393
- completedAt?: Date | null;
1394
- lastActivityAt?: Date | null;
1395
- promptCount?: number;
1396
- failureReason?: string | null;
1397
- /** Null/omitted keeps whatever is stored — it never clears a known name. */
1398
- sessionName?: string | null;
1399
- }): void;
1400
- /**
1401
- * Persist the Codex rollout id discovered after spawn.
1402
- *
1403
- * Its own statement rather than a `recordSpawn` re-run: the binding arrives
1404
- * while the session is live, and re-upserting would also rewrite `cmdline`
1405
- * with an id that is *not* in a fresh Codex process's argv, turning the
1406
- * reconciler's identity check into a false `orphaned`. Without this write the
1407
- * binding lives only in memory and dies with the streamer — which is the
1408
- * whole reason a restarted Codex session could not be resumed (G6).
1409
- */
1410
- recordBinding(sessionId: string, boundConversationId: string): void;
1411
- get(sessionId: string): ManagedSessionRow | null;
1412
- /**
1413
- * Rows with no recorded completion — the reconciler's probe set.
1414
- *
1415
- * Capped. Callers must compare the result length against the limit and say so
1416
- * when it clips: a silently truncated probe set reads as "we checked
1417
- * everything" when it did not.
1418
- */
1419
- listNonTerminal(limit?: number): ManagedSessionRow[];
1420
- /** Every row, most recently touched first, for the diagnostics surface. */
1421
- listAll(limit?: number): ManagedSessionRow[];
1422
- /**
1423
- * Delete terminal rows older than `olderThanMs`, returning how many went.
1424
- *
1425
- * Only rows carrying a `completed_at` are eligible, so nothing the reconciler
1426
- * or rehydrator might still want is reachable from here — a row without one
1427
- * is by definition unfinished business, however old it looks.
1428
- */
1429
- pruneTerminal(olderThanMs?: number): number;
1430
- /**
1431
- * Rows a restart could bring back: still open, or closed by our own shutdown,
1432
- * and touched no longer ago than `sinceMs`. Newest first, capped — the caller
1433
- * decides which of these actually deserve rehydrating (`shouldRehydrate`).
1434
- */
1435
- listRecoverable({ sinceMs, limit }: {
1436
- sinceMs: number;
1437
- limit: number;
1438
- }): ManagedSessionRow[];
1439
- delete(sessionId: string): void;
1440
- }
1441
-
1442
- /**
1443
- * Persisted Project row. Mirrors the projects SQLite table.
1444
- */
1445
- declare const ProjectSchema: z.ZodObject<{
1446
- id: z.ZodString;
1447
- path: z.ZodString;
1448
- name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1449
- lastConversationId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1450
- lastConversationCreatedAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1451
- lastIndexedAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1452
- latestMessageAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1453
- latestMessageId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1454
- createdAt: z.ZodString;
1455
- updatedAt: z.ZodString;
1456
- }, z.core.$strip>;
1457
- type Project = z.infer<typeof ProjectSchema>;
1458
-
1459
- interface UpsertProjectInput {
1460
- lastConversationId?: string | null;
1461
- lastConversationCreatedAt?: string | null;
1462
- latestMessageAt?: string | null;
1463
- latestMessageId?: string | null;
1464
- name?: string | null;
1465
- }
1466
- declare class ProjectsRepository {
1467
- private getByPath;
1468
- private getById;
1469
- private listAll;
1470
- private insert;
1471
- private update;
1472
- constructor(db: Database.Database);
1473
- getProjectByPath(rawPath: string): Project | null;
1474
- getProjectById(id: string): Project | null;
1475
- listProjects(): Project[];
1476
- /**
1477
- * Insert a project at the given canonical path, or update its metadata
1478
- * if one already exists. Returns the persisted Project row.
1479
- *
1480
- * Idempotent: passing the same path twice returns the same project id.
1481
- */
1482
- upsertProjectByPath(rawPath: string, input?: UpsertProjectInput): Project;
1483
- }
1484
-
1485
- /**
1486
- * Token kinds. A device supplies three non-interchangeable types, and
1487
- * conflating them fails only at send time with no signal at registration:
1488
- *
1489
- * - `expo` — Expo relay token, for ordinary push notifications.
1490
- * - `liveactivity_start` — ActivityKit push-to-start token. App-wide, one per
1491
- * device, long-lived. Starts an activity when none exists.
1492
- * - `liveactivity_update` — ActivityKit per-activity update token, issued by
1493
- * iOS after an activity starts, scoped to that one activity, short-lived.
1494
- */
1495
- declare const PUSH_TOKEN_KINDS: readonly ["expo", "liveactivity_start", "liveactivity_update"];
1496
- type PushTokenKind = (typeof PUSH_TOKEN_KINDS)[number];
1497
- interface PushTokenRow {
1498
- token: string;
1499
- platform: string;
1500
- device_id: string | null;
1501
- registered_at: number;
1502
- last_success_at: number | null;
1503
- last_failure_at: number | null;
1504
- last_failure_code: string | null;
1505
- failure_streak: number;
1506
- revoked_at: number | null;
1507
- kind: PushTokenKind;
1508
- activity_id: string | null;
1509
- session_id: string | null;
1510
- expires_at: number | null;
1511
- stale_date: number | null;
1512
- started_at: number | null;
1513
- renewed_at: number | null;
1514
- }
1515
- /**
1516
- * Health as reported to a client. Deliberately omits the token itself — a push
1517
- * token is a delivery credential, and a health endpoint has no reason to echo
1518
- * one back.
1519
- */
1520
- interface PushTokenHealth {
1521
- platform: string;
1522
- deviceId: string | null;
1523
- registeredAt: number;
1524
- lastSuccessAt: number | null;
1525
- lastFailureAt: number | null;
1526
- lastFailureCode: string | null;
1527
- failureStreak: number;
1528
- revokedAt: number | null;
1529
- /**
1530
- * Never delivered vs delivering vs failing vs revoked vs expired. The
1531
- * distinction the user actually needs: "not yet" and "broken" look identical
1532
- * without it.
1533
- */
1534
- state: "never-delivered" | "healthy" | "failing" | "dead" | "revoked" | "expired";
1535
- kind: PushTokenKind;
1536
- /** Present only for per-activity Live Activity tokens. */
1537
- activityId: string | null;
1538
- sessionId: string | null;
1539
- expiresAt: number | null;
1540
- }
1541
- declare class PushRepository {
1542
- private upsertStmt;
1543
- private getStmt;
1544
- private listActiveStmt;
1545
- private listAllStmt;
1546
- private successStmt;
1547
- private failureStmt;
1548
- private revokeStmt;
1549
- private claimEventStmt;
1550
- private markDeliveredStmt;
1551
- private listByKindSessionStmt;
1552
- private listByKindStmt;
1553
- private listRenewableStmt;
1554
- private claimRenewalStmt;
1555
- private expireStmt;
1556
- private expireSessionActivitiesStmt;
1557
- constructor(db: Database.Database);
1558
- /**
1559
- * Register or refresh a token.
1560
- *
1561
- * `kind` defaults to Expo so a released client posting `{ token, platform }`
1562
- * keeps working — tb-mobile cannot be force-updated, and every client
1563
- * predating Live Activities is registering an Expo relay token.
1564
- *
1565
- * Several rows per device is normal and intended: a device runs one activity
1566
- * per live session, each with its own update token. The token itself is the
1567
- * primary key, so distinct activities never collide.
1568
- */
1569
- register(args: {
1570
- token: string;
1571
- platform: string;
1572
- deviceId?: string | null;
1573
- kind?: PushTokenKind;
1574
- activityId?: string | null;
1575
- sessionId?: string | null;
1576
- expiresAt?: number | null;
1577
- staleDate?: number | null;
1578
- startedAt?: number | null;
1579
- now?: number;
1580
- }): void;
1581
- get(token: string): PushTokenRow | null;
1582
- /**
1583
- * Expo tokens eligible for delivery — not revoked, not past the failure limit.
1584
- *
1585
- * Deliberately Expo-only. ActivityKit tokens go over direct APNs with a
1586
- * different topic and are rejected by Expo's relay, so the ordinary
1587
- * notification fan-out must not see them.
1588
- */
1589
- listDeliverable(): PushTokenRow[];
1590
- /** Live-activity tokens for one session, eligible for delivery. */
1591
- listForSession(kind: PushTokenKind, sessionId: string, now?: number): PushTokenRow[];
1592
- /**
1593
- * Every deliverable token of one kind.
1594
- *
1595
- * Used for push-to-start, which is app-wide rather than session-scoped: the
1596
- * activity does not exist yet, so there is no per-activity token to look up.
1597
- */
1598
- listByKind(kind: PushTokenKind, now?: number): PushTokenRow[];
1599
- /** Unrenewed activities with a renewal deadline, soonest first. */
1600
- listRenewable(): PushTokenRow[];
1601
- /**
1602
- * Claim a row for renewal.
1603
- *
1604
- * Returns true exactly once per row. A restart re-arms timers from the
1605
- * persisted deadline, so the same renewal can be attempted twice; the loser
1606
- * gets false and must not send. Doing this as a conditional UPDATE rather
1607
- * than read-then-write avoids the race where both attempts observe
1608
- * "not yet renewed".
1609
- */
1610
- claimRenewal(token: string, now?: number): boolean;
1611
- /** Mark one token expired, so it stops being a delivery target. */
1612
- expire(token: string, now?: number): void;
1613
- /**
1614
- * Expire every live activity for a session.
1615
- *
1616
- * Called when the session ends. Without this, a per-activity token outlives
1617
- * its session and a later renewal sweep would resurrect an activity for a
1618
- * session that is already gone.
1619
- */
1620
- expireSessionActivities(sessionId: string, now?: number): void;
1621
- /** Every token, including dead and revoked ones, for the health report. */
1622
- listHealth(now?: number): PushTokenHealth[];
1623
- recordSuccess(token: string, now?: number): void;
1624
- recordFailure(token: string, code: string, now?: number): void;
1625
- revoke(token: string, now?: number): boolean;
1626
- /**
1627
- * Claim an event id for delivery.
1628
- *
1629
- * Returns true exactly once per event id. A retry, a reconnect
1630
- * reconciliation, or two triggers firing for the same underlying event all
1631
- * get false and must not notify — the user should never be told twice about
1632
- * one thing.
1633
- */
1634
- claimEvent(eventId: string, sessionId: string | null, now?: number): boolean;
1635
- markDelivered(eventId: string, now?: number): void;
1636
- }
1637
-
1638
- declare class SessionStore {
1639
- private managed;
1640
- private discovered;
1641
- addManaged(session: ManagedSession): void;
1642
- /**
1643
- * Multi-agent mode only. Attach a dedupe LRU to a session record. Idempotent —
1644
- * calling twice keeps the existing LRU (and its contents).
1645
- */
1646
- initAgentSession(sessionId: string, dedupeCapacity: number): void;
1647
- updateManaged(sessionId: string, updates: Partial<ManagedSession>): ManagedSession | null;
1648
- removeManaged(sessionId: string): boolean;
1649
- /**
1650
- * The **live** stored record — mutating it mutates the store. Paired with
1651
- * `get()`, which hands back a throwaway response copy. Prefer
1652
- * `updateManaged()` for writes; this is for readers that need the internal
1653
- * shape (Date fields, `rehydrated`, …) rather than the wire shape.
1654
- */
1655
- getManaged(sessionId: string): ManagedSession | null;
1656
- setDiscovered(processes: DiscoveredProcess[]): void;
1657
- /**
1658
- * The **live** stored records — mutating an element mutates the store. Paired
1659
- * with `list()`, which hands back throwaway response copies.
1660
- */
1661
- listManaged(): ManagedSession[];
1662
- list(ptyAttachedIds: Set<string>): readonly Readonly<SessionResponse>[];
1663
- get(sessionId: string, ptyAttachedIds: Set<string>): Readonly<SessionResponse> | null;
1664
- paginate(ptyAttachedIds: Set<string>, query: SessionListQuery): SessionListPage;
1665
- }
1666
-
1667
- /**
1668
- * Sessions live in-memory in SessionStore (Postgres-backed persistence
1669
- * was dropped per the SQLite-only direction). This repository wraps the
1670
- * store with a stable, project-id-aware API for the new services.
1671
- */
1672
- declare class SessionsRepository {
1673
- private store;
1674
- constructor(store: SessionStore);
1675
- updateSessionProjectId(args: {
1676
- sessionId: string;
1677
- projectId: string;
1678
- }): void;
1679
- listManagedSessions(): ManagedSession[];
1680
- }
1681
-
1682
- /**
1683
- * `~/.threadbase/runtime.db` — the authoritative, non-derived half of the
1684
- * streamer's SQLite state.
1685
- *
1686
- * Deliberately a separate file from `cache/cache.db`, and deliberately not
1687
- * under `cache/`. The conversation cache is rebuildable from the provider
1688
- * JSONLs at any time, which is why "delete the cache and restart" is reasonable
1689
- * support advice and why the integrity monitor offers a reset-and-rescan
1690
- * action. The managed-session registry is not rebuildable from anything on
1691
- * disk, so it must not sit one plausible instruction away from deletion — nor
1692
- * share a handle whose failure (a better-sqlite3 ABI mismatch, most commonly)
1693
- * would silently disable session persistence along with the cache.
1694
- *
1695
- * Runs its own migrations from `src/db/runtime-migrations/`, tracked in this
1696
- * file's own `schema_migrations` table.
1697
- */
1698
- declare class RuntimeStore {
1699
- private readonly db;
1700
- private constructor();
1701
- static open(dbPath: string, migrationsDir?: string): RuntimeStore;
1702
- getDatabase(): Database.Database;
1703
- /**
1704
- * One-time move of `managed_sessions` rows out of a pre-split `cache.db`.
1705
- *
1706
- * Returns the number of rows copied.
1707
- */
1708
- importLegacyManagedSessions(source: Database.Database): number;
1709
- /**
1710
- * One-time move of `devices` rows out of `cache.db`, where the registry used
1711
- * to live (migration `011_create_devices.sql`).
1712
- *
1713
- * Losing this table invalidates every device token ever issued, and cache.db
1714
- * is the file `tb-streamer cache clear` deletes and the integrity monitor
1715
- * rebuilds — see `runtime-migrations/003_create_devices.sql`.
1716
- *
1717
- * Unlike `managed_sessions`, this one MOVES rather than copies: the source
1718
- * rows are deleted once the copy is verified. A `devices` row carries a
1719
- * user-supplied label ("Ronen's iPhone"), and leaving a second copy of that
1720
- * on disk indefinitely — in the one file the user is told to delete when
1721
- * something goes wrong — is more retained personal data than the rollback
1722
- * path is worth. Recovering from a rollback is re-scanning a pairing QR.
1723
- *
1724
- * The delete is conditional on the copy being complete: `INSERT OR IGNORE`
1725
- * can silently skip a row, so the destination count must match what was read
1726
- * before anything is removed. A mismatch keeps the source and reports
1727
- * `purged: false` rather than throwing — the import itself still succeeded,
1728
- * and keeping data is the safe direction to fail in.
1729
- */
1730
- importLegacyDevices(source: Database.Database): {
1731
- copied: number;
1732
- purged: boolean;
1733
- };
1734
- /**
1735
- * Copy a whole table out of a pre-split `cache.db` into this file.
1736
- *
1737
- * The copy itself is non-destructive — the source is left in place, so an
1738
- * older streamer rolled back onto the same machine still finds its data.
1739
- * `importLegacyDevices` deletes the source afterwards for its own reasons;
1740
- * `managed_sessions` does not. Runs only when this file's table is empty, so
1741
- * a second boot is a no-op rather than a re-copy that would resurrect rows
1742
- * deleted since.
1743
- *
1744
- * The table name is interpolated into SQL, so it is typed as a closed union
1745
- * rather than `string` — the set of tables that can ever be lifted is known
1746
- * at compile time, and that is what keeps a caller from making this a hole.
1747
- */
1748
- private importLegacyTable;
1749
- close(): void;
1750
- }
1751
-
1752
- interface HostHeartbeatState {
1753
- registryState: "known" | "unknown";
1754
- referencedSessionIds: string[];
1755
- }
1756
- /**
1757
- * A duplex line channel. Deliberately not a socket: the runner is tested
1758
- * against an in-memory pair, and PR 7 supplies the real one over a unix socket
1759
- * (POSIX) or named pipe (Windows).
1760
- */
1761
- interface HostTransport {
1762
- send(line: string): void;
1763
- onLine(handler: (line: string) => void): void;
1764
- onClose(handler: () => void): void;
1765
- close(): void;
1766
- }
1767
-
1768
- declare class LiveSessionManager {
1769
- private runners;
1770
- private remoteRunner;
1771
- private options;
1772
- constructor(options?: PTYManagerOptions);
1773
- useRemoteRunner(transport: HostTransport): Promise<ManagedSession[]>;
1774
- isRemote(): boolean;
1775
- startRemoteHeartbeat(getState: () => HostHeartbeatState): void;
1776
- start(sessionId: string, options: StartSessionOptions & {
1777
- provider?: ProviderName;
1778
- }): Promise<ManagedSession>;
1779
- startFresh(options: StartFreshSessionOptions & {
1780
- provider?: ProviderName;
1781
- }): Promise<ManagedSession>;
1782
- /**
1783
- * Fork an existing conversation into a new session. Codex-only: `codex fork`
1784
- * has no Claude Code equivalent, and there is no safe generic fallback — a
1785
- * silent downgrade to resume would attach to the very writer the caller is
1786
- * trying to leave alone.
1787
- */
1788
- startFork(options: StartForkSessionOptions & {
1789
- provider?: ProviderName;
1790
- }): Promise<ManagedSession>;
1791
- sendInput(sessionId: string, input: string): number;
1792
- sendKeys(sessionId: string, keys: string): void;
1793
- cancel(sessionId: string): void;
1794
- killPid(pid: number): void;
1795
- putOnHold(sessionId: string): void;
1796
- getOutput(sessionId: string): string;
1797
- getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;
1798
- getInputHistory(sessionId: string): UserMessage[];
1799
- getSession(sessionId: string): ManagedSession | null;
1800
- getPid(sessionId: string): number | null;
1801
- hasSession(sessionId: string): boolean;
1802
- listSessions(): ManagedSession[];
1803
- dispose(): void;
1804
- private runnerFor;
1805
- /**
1806
- * Refuse before spawning when the provider's CLI is not on this machine.
1807
- *
1808
- * Without this the spawn "succeeds": on POSIX execvp fails inside the forked
1809
- * child, so a session appears, exits ~12ms later with code 1 and no output,
1810
- * and the caller is told only that it "exited before becoming ready" — or,
1811
- * on the Claude resume path, is told nothing at all, since that path answers
1812
- * 200 before the process has had a chance to die. Every start route funnels
1813
- * through here, so one check covers start, resume, adopt and fork.
1814
- *
1815
- * 503, not 500: the request was well-formed and the fault is this machine's
1816
- * environment. `code` is what mobile branches on (it reads `errBody.code`),
1817
- * and `PROVIDER_NOT_INSTALLED` is a remediation string it already knows.
1818
- */
1819
- private assertProviderInstalled;
1820
- private assertSupportedProvider;
1821
- private activeRunners;
1822
- }
1823
-
1824
- declare class WSHub {
1825
- private clients;
1826
- private pingTimer;
1827
- private pongTimers;
1828
- addClient(ws: WebSocket): void;
1829
- broadcast(message: WSMessage): void;
1830
- broadcastToClients(clients: Iterable<WebSocket>, message: WSMessage): void;
1831
- unicast(ws: WebSocket, message: WSMessage): void;
1832
- get connectionCount(): number;
1833
- dispose(): void;
1834
- private startPing;
1835
- }
1836
-
1837
- /** One missing conversation the alert covers. */
1838
- interface MissingEntry {
1839
- id: string;
1840
- filePath: string;
1841
- title: string | null;
1842
- tailed: boolean;
1843
- }
1844
- interface PendingAlert {
1845
- fingerprint: string;
1846
- severity: "high" | "low";
1847
- detectedAt: string;
1848
- missingCount: number;
1849
- totalRows: number;
1850
- backupPath?: string;
1851
- /** Capped at 1000 entries in the persisted file. */
1852
- missing: MissingEntry[];
1853
- }
1854
-
1855
- type ResolveAction = CacheAlertResolveAction;
1856
- /** The `cache_alert` WS variant, narrowed from the WSMessage union. */
1857
- type CacheAlertWsMessage = Extract<WSMessage, {
1858
- type: "cache_alert";
1859
- }>;
1860
- type ResolveResult = {
1861
- ok: true;
1862
- action: ResolveAction;
1863
- pruned?: number;
1864
- backupPath?: string;
1865
- } | {
1866
- alreadyResolved: true;
1867
- } | {
1868
- conflict: true;
1869
- currentFingerprint: string;
1870
- };
1871
- declare class CacheIntegrityMonitor {
1872
- private readonly cache;
1873
- private readonly wsHub;
1874
- private readonly log;
1875
- private readonly cacheDir;
1876
- private readonly rescan?;
1877
- private readonly runDuringReset?;
1878
- private _pending;
1879
- private ignoredIds;
1880
- private deferredUnlinks;
1881
- private unlinkTimes;
1882
- constructor(cache: ConversationCache, wsHub: WSHub, log: Logger, cacheDir: string, rescan?: (() => Promise<ScannerMeta[]>) | undefined, runDuringReset?: (<T>(operation: () => Promise<T>) => Promise<T>) | undefined);
1883
- get pending(): PendingAlert | null;
1884
- private persist;
1885
- private classifySeverity;
1886
- private sampleOf;
1887
- private buildWsMessage;
1888
- wsMessage(): CacheAlertWsMessage | null;
1889
- healthzField(): {
1890
- severity: "high" | "low";
1891
- missingCount: number;
1892
- fingerprint: string;
1893
- detectedAt: string;
1894
- } | undefined;
1895
- /**
1896
- * Scan the cache for rows whose file is gone, excluding ids the user chose to
1897
- * ignore. If none remain, clear any stale pending alert and return (the caller
1898
- * decides whether to run pruneGhostFiles). Otherwise classify severity, persist
1899
- * the pending record, back up on high severity, and broadcast the alert.
1900
- */
1901
- runDetection(detectedAt?: string): Promise<void>;
1902
- /** Queue an unlink while an alert is pending — the row is not invalidated. */
1903
- deferUnlink(filePath: string): void;
1904
- /**
1905
- * Record a live unlink while NO alert is pending. Crossing the storm threshold
1906
- * (>= 10 unlinks within 30s) re-triggers detection.
1907
- */
1908
- recordUnlink(filePath: string): void;
1909
- private ensureBackup;
1910
- private clearPending;
1911
- private applyDeferredUnlinks;
1912
- private broadcastResolved;
1913
- /**
1914
- * Apply the human's chosen resolution. Idempotent per fingerprint: no pending
1915
- * alert → alreadyResolved; a different fingerprint → conflict. See the spec's
1916
- * four-action semantics.
1917
- */
1918
- resolve(fingerprint: string, action: ResolveAction, ids?: string[]): Promise<ResolveResult>;
1919
- }
1920
-
1921
- /** Result for a single registry row. Pure data — nothing here acts. */
1922
- interface ReconcileVerdict {
1923
- sessionId: string;
1924
- lifecycle: SessionLifecycle;
1925
- /** Why this verdict was reached, for logs and the diagnostics surface. */
1926
- reason: string;
1927
- }
1928
-
1929
- type ApiDeps = {
1930
- apiKey: string;
1931
- localNoAuth: boolean;
1932
- logMenubarRequests: boolean;
1933
- rotateApiKey: () => {
1934
- newKey: string;
1935
- persisted: boolean;
1936
- };
1937
- claudeFlagsConfig: () => {
1938
- registry: readonly FlagDefinition[];
1939
- values: ClaudeFlagValues;
1940
- extraArgs: string | null;
1941
- persisted: boolean;
1942
- };
1943
- setClaudeFlagsConfig: (values: ClaudeFlagValues, extraArgs: string | undefined) => {
1944
- values: ClaudeFlagValues;
1945
- extraArgs: string | null;
1946
- persisted: boolean;
1947
- };
1948
- featureFlagsConfig: () => {
1949
- registry: readonly FeatureFlagDefinition[];
1950
- values: ResolvedFeatureFlags;
1951
- sources: Record<FeatureFlagId, FeatureFlagSource>;
1952
- };
1953
- publicUrl: string | null;
1954
- browseRoot: string | null;
1955
- browserCors: string | undefined;
1956
- ptyManager: LiveSessionManager;
1957
- sessionStore: SessionStore;
1958
- wsHub: WSHub;
1959
- cache: () => ConversationCache | null;
1960
- cacheMonitor: () => CacheIntegrityMonitor | null;
1961
- /** Push registration + delivery state (C7). Null when the cache DB is unavailable. */
1962
- pushRepo: () => PushRepository | null;
1963
- /**
1964
- * Whether Live Activity push is actually running on this server — the same
1965
- * fact the boot log reports as `live_activity.enabled`. Asked of the server
1966
- * rather than re-derived from the environment, because APNs credentials alone
1967
- * do not enable it: the sender is only wired when the token store opened too.
1968
- */
1969
- liveActivityPushEnabled: () => boolean;
1970
- /** Paired-device registry (C5). Null when the cache DB is unavailable. */
1971
- devicesRepo: () => DevicesRepository | null;
1972
- projectsRepo: () => ProjectsRepository | null;
1973
- conversationsRepo: () => ConversationsRepository | null;
1974
- sessionsRepo: () => SessionsRepository | null;
1975
- cacheMetadataRepo: () => CacheMetadataRepository | null;
1976
- /** Authoritative session registry (~/.threadbase/runtime.db). Null when it failed to open. */
1977
- runtimeStore: () => RuntimeStore | null;
1978
- /** Rows of that registry. Null when it failed to open. */
1979
- managedSessionsRepo: () => ManagedSessionsRepository | null;
1980
- /** This boot's reconcile verdicts, by session id. Empty before it has run. */
1981
- sessionVerdicts: () => Map<string, ReconcileVerdict>;
1982
- ptyAttachedIds: () => Set<string>;
1983
- handleListSessions: (url: URL, res: ServerResponse) => Promise<void>;
1984
- handleSessionsCount: (res: ServerResponse) => void;
1985
- handleGetRecentSessions: (url: URL, res: ServerResponse) => void;
1986
- handleGetSessionNames: (res: ServerResponse) => void;
1987
- handleGetSession: (sessionId: string, res: ServerResponse) => Promise<void>;
1988
- handleGetOutput: (sessionId: string, res: ServerResponse) => void;
1989
- handleSendInput: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1990
- handleSendAnswer: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1991
- handleCancel: (sessionId: string, res: ServerResponse) => void;
1992
- handleStopSession: (sessionId: string, res: ServerResponse) => Promise<void>;
1993
- handleSetSessionName: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1994
- handleSetSessionModel: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1995
- handleSetSessionEffort: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1996
- handleUploadFile: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1997
- handleAdopt: (sessionId: string, res: ServerResponse) => Promise<void>;
1998
- handleFork: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1999
- handleResume: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
2000
- handleStartSession: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
2001
- handleListConversations: (url: URL, res: ServerResponse) => Promise<void>;
2002
- handleConversationsCount: (url: URL, res: ServerResponse) => Promise<void>;
2003
- handleGetConversation: (id: string, url: URL, res: ServerResponse, ifNoneMatch?: string) => Promise<void>;
2004
- handleSearch: (url: URL, res: ServerResponse) => Promise<void>;
2005
- handleSearchTarget: (id: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
2006
- handleListProjects: (url: URL, res: ServerResponse) => void;
2007
- handleGetPopularProjects: (url: URL, res: ServerResponse) => void;
2008
- handleGetProjectSummaries: (url: URL, res: ServerResponse) => void;
2009
- handlePairStart: (res: ServerResponse) => void;
2010
- handlePairExchange: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
2011
- handleBrowse: (url: URL, res: ServerResponse) => Promise<void>;
2012
- handleMkdir: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
2013
- handleWsOpen: (ws: WebSocket) => void;
2014
- handleWsMessage: (ws: WebSocket, raw: unknown, principal: Principal | null) => void;
2015
- handleWsClose: (ws: WebSocket) => void;
2016
- agentClient: AgentClient | null;
2017
- conversationWriter: ConversationWriter | null;
2018
- agentConfig: AgentConfig;
2019
- };
2020
-
2021
- type AppEnv = {
2022
- Bindings: HttpBindings;
2023
- Variables: {
2024
- requestId?: string;
2025
- validatedBody?: unknown;
2026
- validatedQuery?: unknown;
2027
- /** Who is making this request (C5). Set by authMiddleware. */
2028
- principal?: Principal;
2029
- };
2030
- };
2031
-
2032
- interface AgentDeps {
2033
- sessionStore: {
2034
- getManaged: (sessionId: string) => {
2035
- id: string;
2036
- progressDedupeIds?: {
2037
- hasSeen: (id: string) => boolean;
2038
- };
2039
- currentTurnId?: string | null;
2040
- } | null;
2041
- };
2042
- wsHub: {
2043
- broadcast: (m: WSMessage) => void;
2044
- };
2045
- conversationWriter: {
2046
- appendAssistantTurn: (a: {
2047
- sessionId: string;
2048
- turnId: string;
2049
- content: string;
2050
- reviewerOverruled?: boolean;
2051
- }) => Promise<void>;
2052
- } | null;
2053
- agentConfig: {
2054
- enabled: boolean;
2055
- webhook: {
2056
- hmacSecret: string;
2057
- timestampSkewSeconds: number;
2058
- };
2059
- dedupe: {
2060
- perSessionCapacity: number;
2061
- };
2062
- };
2063
- }
2064
- declare const createProgressRoutes: (deps: ApiDeps & AgentDeps) => Hono<AppEnv, hono_types.BlankSchema, "/">;
2065
-
2066
- declare function generateApiKey(): string;
2067
- declare function validateApiKey(provided: string, expected: string): boolean;
2068
- declare function loadOrCreateApiKey(): string;
2069
-
2070
- interface DbConfig {
2071
- connectionString: string;
2072
- max: number;
2073
- ssl?: string;
2074
- statementTimeout?: number;
2075
- instanceId: string;
2076
- }
2077
- declare function isDbEnabled(): boolean;
2078
- declare function getDbConfig(): DbConfig | null;
2079
-
2080
- declare function maskConnectionString(url: string): string;
2081
- declare function createPool(config: DbConfig): Promise<Pool>;
2082
-
2083
- declare function discoverClaudeProcesses(): Promise<DiscoveredProcess[]>;
2084
-
2085
- declare class PTYManager implements SessionRunner {
2086
- private sessions;
2087
- private onOutput;
2088
- private onStatusChange;
2089
- private onReady;
2090
- private onPermissionChange;
2091
- private onPhaseChange;
2092
- private onLiveQuestion;
2093
- private onLiveQuestionGone;
2094
- private onUserMessage;
2095
- private permissionOpen;
2096
- private oscTail;
2097
- private lastDetectAt;
2098
- private lastScreenQuestionKey;
2099
- private shellPromptOpen;
2100
- private closedGateKey;
2101
- private pendingReady;
2102
- private queuedInputs;
2103
- private log;
2104
- private firstChunkAt;
2105
- private chunkIndex;
2106
- private lastChunkAt;
2107
- private quietCheckers;
2108
- private readyFallbackTimers;
2109
- private startPromises;
2110
- constructor(options?: PTYManagerOptions);
2111
- start(sessionId: string, options: StartSessionOptions): Promise<ManagedSession>;
2112
- private doStart;
2113
- startFresh(options: StartFreshSessionOptions): Promise<ManagedSession>;
2114
- sendKeys(sessionId: string, keys: string): void;
2115
- sendInput(sessionId: string, input: string): number;
2116
- private writeSubmit;
2117
- private flushQueuedInputs;
2118
- cancel(sessionId: string): void;
2119
- killPid(pid: number): void;
2120
- putOnHold(sessionId: string): void;
2121
- getOutput(sessionId: string): string;
2122
- getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;
2123
- getInputHistory(sessionId: string): UserMessage[];
2124
- getPid(sessionId: string): number | null;
2125
- private recordUserMessage;
2126
- getSession(sessionId: string): ManagedSession | null;
2127
- hasSession(sessionId: string): boolean;
2128
- listSessions(): ManagedSession[];
2129
- dispose(): void;
2130
- private handleOutput;
2131
- private detectLivePrompts;
2132
- private handleQuiet;
2133
- private recheckReadyFromScreen;
2134
- private armReadyFallback;
2135
- private clearReadyFallback;
2136
- /**
2137
- * Record the agent's phase and notify only on a real change.
2138
- *
2139
- * The change guard is load-bearing, not an optimisation: the scrape pass runs
2140
- * on every chunk, so an unguarded setter would fire the callback — and the
2141
- * WS frame behind it — several times a second for the entire duration of a
2142
- * turn while reporting the same value.
2143
- */
2144
- private setPhase;
2145
- private markReady;
2146
- private handleExit;
2147
- }
2148
-
2149
- interface ConversationWatcherEvents {
2150
- /** Fires once per new newline-terminated line appended to a watched file. */
2151
- onNewLine?: (filePath: string, line: string) => void;
2152
- /**
2153
- * Fires once per chokidar read with ALL new lines from that read, batched.
2154
- * When set, it REPLACES the per-line onNewLine dispatch for that file —
2155
- * callers pick one. Lets a burst of appended lines collapse into a single
2156
- * downstream cache write + WebSocket broadcast.
2157
- */
2158
- onNewLines?: (filePath: string, lines: string[]) => void;
2159
- /**
2160
- * Like onNewLines but also carries each line's absolute byte span in the
2161
- * file (for the offset index). Fires ALONGSIDE onNewLines/onNewLine (it does
2162
- * not replace them) so the cache tail write and the index extend can both
2163
- * consume the same read. `readFrom` is the absolute byte offset the read
2164
- * started at; `spans` are complete lines only (a torn trailing line is held
2165
- * for the next read).
2166
- */
2167
- onNewLineSpans?: (filePath: string, spans: LineSpan[], readFrom: number, endOffset: number) => void;
2168
- /** Fires when chokidar reports an add/change/unlink at the directory level. */
2169
- onConversationChanged?: (filePath: string) => void | Promise<void>;
2170
- /** Fires when a tailed file is deleted (per-file watcher unlink event). */
2171
- onFileDeleted?: (filePath: string) => void;
2172
- /**
2173
- * Fires when a tailed file shrank below our read offset (in-place truncation
2174
- * or replacement by a shorter file). The tail has already reset to byte 0;
2175
- * the consumer must discard any byte-offset index built for the old content,
2176
- * which no longer describes this file.
2177
- */
2178
- onTruncated?: (filePath: string) => void;
2179
- /** Reported errors per file. */
2180
- onError?: (filePath: string, error: Error) => void;
2181
- }
2182
- /**
2183
- * Chokidar-backed replacement for src/file-watcher.ts.
2184
- *
2185
- * - watch(filePath) → tail a single JSONL file, emitting onNewLine
2186
- * for each appended line.
2187
- * - watchDirectory(dir) → mark cache dirty on add/change/unlink events
2188
- * for any file inside the directory.
2189
- *
2190
- * Per the refactor plan, file watching is an OPTIMIZATION; correctness
2191
- * still relies on refresh=1 / the latest HDD conversation id check.
2192
- */
2193
- declare class ConversationWatcher {
2194
- private files;
2195
- private directories;
2196
- private onNewLine;
2197
- private onNewLines;
2198
- private onNewLineSpans;
2199
- private onConversationChanged;
2200
- private onFileDeleted;
2201
- private onTruncated;
2202
- private onError;
2203
- constructor(events?: ConversationWatcherEvents);
2204
- watch(filePath: string): void;
2205
- unwatch(filePath: string): void;
2206
- /**
2207
- * Re-drive the tail read for a file that's already being tailed. A per-file
2208
- * chokidar handle can die silently (fs.watch stops firing after inode churn)
2209
- * while the coarser directory watcher keeps reporting changes — calling this
2210
- * from the directory-event path makes the tail self-healing. Reads are
2211
- * offset-based and coalesced, so a redundant poke after a normal change
2212
- * event is a cheap stat + no-op. Returns false for untailed paths.
2213
- */
2214
- poke(filePath: string): boolean;
2215
- /**
2216
- * Watch a directory of conversation JSONL files. Fires
2217
- * onConversationChanged for any add/change/unlink event so the caller
2218
- * can mark the cache dirty without scanning everything immediately.
2219
- *
2220
- * **This costs one OS watch handle per file under `directory`, not one per
2221
- * directory.** chokidar recurses the tree and registers a separate fs.watch
2222
- * per entry, because a directory watch alone does not report writes to files
2223
- * inside it — and per-file `change` events are exactly what the caller needs
2224
- * (they drive poke()'s tail self-heal and the external-tail attach). So the
2225
- * handle count tracks the size of the conversation corpus on disk, not the
2226
- * number of live sessions, and it does not shrink until transcripts are
2227
- * deleted. `ignoreInitial` suppresses the startup *events*, not the walk.
2228
- *
2229
- * Measured 2026-08-09 on the live macOS instance: 2131 open .jsonl fds
2230
- * against 2133 files under the watched roots — 1:1, ~88% of all fds on the
2231
- * process, at 2.0% of that box's 122 880 per-process ceiling. Comfortable
2232
- * there. **Linux is the tight one**: these are inotify watches billed to the
2233
- * per-user `max_user_watches`, which can be 8192 and is shared with every
2234
- * other watcher the user runs. Exhaustion surfaces as ENOSPC on the `error`
2235
- * event — which is why server.ts wires onError rather than leaving it unset.
2236
- *
2237
- * Before trading handles for a bound here, note the regression it invites: a
2238
- * conversation excluded from the walk (by age or by an LRU cap) is one whose
2239
- * external appends produce no event at all, and that failure is silent.
2240
- */
2241
- watchDirectory(directory: string): void;
2242
- unwatchDirectory(directory: string): void;
2243
- dispose(): void;
2244
- private readNewLines;
2245
- }
2246
-
2247
- declare class StreamerServer {
2248
- private httpServer;
2249
- private ptyManager;
2250
- private sessionStore;
2251
- private wsHub;
2252
- private fileWatcher;
2253
- private sessionFileMap;
2254
- private externalTails;
2255
- private externalTailManager;
2256
- private pendingLineSeqs;
2257
- private pendingQuestions;
2258
- private contendedSessions;
2259
- private selfPtyEndedAt;
2260
- private pendingQuestionKey;
2261
- private pendingPermission;
2262
- private pendingPermissionKey;
2263
- private scannerManager;
2264
- private sessionWatchers;
2265
- private registryBoot;
2266
- private conversationHandlers;
2267
- private sessionHandlers;
2268
- private binding;
2269
- private activeWarmups;
2270
- private nextWarmupId;
2271
- private inFlightCacheWrites;
2272
- private apiKey;
2273
- private apiKeySource;
2274
- private localNoAuth;
2275
- private logMenubarRequests;
2276
- private verbose;
2277
- private scanProfiles;
2278
- private dbPool;
2279
- private dbInstanceId;
2280
- private disableDb;
2281
- private host;
2282
- private skipStartupWarmup;
2283
- private autoResumeOnBoot;
2284
- private browseRoot;
2285
- private publicUrl;
2286
- private browserCors;
2287
- private pairTokens;
2288
- private exchangeAttempts;
2289
- private sessionStartAttempts;
2290
- private sessionInputAttempts;
2291
- private ptyGracePeriodMs;
2292
- private defaultSystemPrompt;
2293
- private featureFlags;
2294
- private featureFlagSources;
2295
- private codexSystemPromptEnabled;
2296
- private defaultPermissionMode;
2297
- private defaultModel;
2298
- private defaultEffort;
2299
- private claudeFlags;
2300
- private claudeExtraArgs;
2301
- private claudeFlagsPersistable;
2302
- private ptyGraceTimers;
2303
- private ptyGraceDeferCounts;
2304
- private sessionSubscribers;
2305
- private lastAgentChunkAt;
2306
- private terminalSeq;
2307
- private idempotency;
2308
- private sessionVerdicts;
2309
- private idleReaperTimer;
2310
- private clientIdToWs;
2311
- private wsToClientId;
2312
- private cache;
2313
- private cacheMonitor;
2314
- private projectsRepo;
2315
- private conversationsRepo;
2316
- private sessionsRepo;
2317
- private managedSessionsRepo;
2318
- private runtimeStore;
2319
- private readonly streamerInstanceId;
2320
- private cacheMetadataRepo;
2321
- private pushRepo;
2322
- private devicesRepo;
2323
- private apnsClient;
2324
- private liveActivityNotifier;
2325
- private liveActivityRenewal;
2326
- private waitingInputNotifier;
2327
- private discoveryCache;
2328
- private discoveryInFlight;
2329
- private cacheDir;
2330
- private runtimeDbPath;
2331
- private tailSize;
2332
- private directoryDebounceMs;
2333
- private codexRoots;
2334
- private includeAgents;
2335
- private agentEntrypoints;
2336
- private honoApp;
2337
- private log;
2338
- private agentConfig;
2339
- private agentClient;
2340
- private sessionStatusBus;
2341
- constructor(config: ServerConfig & {
2342
- apiKey: string;
2343
- });
2344
- private ptyAttachedIds;
2345
- private rememberSelfPtyEnded;
2346
- /**
2347
- * Send a session_list to only the client that triggered this HTTP request
2348
- * (identified by X-Client-Id header → registered WS socket). Falls back to
2349
- * a full broadcast if no match exists (old clients, or no WS registered yet).
2350
- */
2351
- private broadcastOrUnicastSessionList;
2352
- private sessionListPayload;
2353
- /**
2354
- * Overlay boot-reconciliation verdicts onto session responses.
2355
- *
2356
- * A session left by a previous run is not in the in-memory store, so
2357
- * SessionStore cannot classify it — it only ever sees what this run spawned.
2358
- * Discovery may still surface the process, in which case the reconciler knows
2359
- * strictly more about it than discovery does: it can tell `detached` (alive
2360
- * and confirmed ours) from `orphaned` (alive but identity unconfirmed), which
2361
- * a pid enumeration alone cannot.
2362
- *
2363
- * Only applied when the session is NOT live here: a session this run owns has
2364
- * an authoritative lifecycle already, and a stale verdict must never override
2365
- * it.
2366
- */
2367
- private withReconciledLifecycle;
2368
- private addSessionSubscriber;
2369
- /**
2370
- * Bring up Live Activity push, if credentials are present (Feature 12).
2371
- *
2372
- * APNS_KEY absent is the ordinary case on a dev machine and in CI, so this
2373
- * logs once at info and leaves the feature off rather than failing: the server
2374
- * must not refuse to boot over a missing optional push credential.
2375
- *
2376
- * The key is read from the environment as PEM contents and never from a path
2377
- * on disk; neither it nor any device token is ever logged.
2378
- */
2379
- private initLiveActivityPush;
2380
- /**
2381
- * Bring up "your turn" notifications over Expo's relay (#528).
2382
- *
2383
- * Unconditional, unlike Live Activity push: Expo holds the app's APNs and FCM
2384
- * credentials, so a self-hosted streamer needs no credential of its own. The
2385
- * access token is optional and only relevant if the Expo project has enhanced
2386
- * security enabled — requiring one would lock out every self-hoster, since
2387
- * they do not own the project. It is never logged.
2388
- */
2389
- private initWaitingInputPush;
2390
- /** Whether any live socket is subscribed to this session — "someone is looking". */
2391
- private hasSessionSubscriber;
2392
- /**
2393
- * Release PTYs whose agent has been silent past IDLE_REAP_AFTER_MS.
2394
- *
2395
- * This is the bound that lets handleWsClose stop arming kill timers. The
2396
- * distinction that matters: the old timer measured how long nobody was
2397
- * *watching*, which is uncorrelated with whether work is in flight. This
2398
- * measures how long the *agent* has produced nothing, and only ever considers
2399
- * sessions that are already settled — a `running` PTY is skipped regardless of
2400
- * age, so a long silent turn is never interrupted.
2401
- *
2402
- * Exposed (not private) so tests can drive one sweep deterministically instead
2403
- * of waiting on the interval.
2404
- */
2405
- reapIdleSessions(now?: number): string[];
2406
- private startGraceTimer;
2407
- get port(): number;
2408
- private currentWarmupState;
2409
- private beginWarmup;
2410
- private finishWarmup;
2411
- private withWarmup;
2412
- private rejectIfWarmingUp;
2413
- /**
2414
- * Say which provider CLIs this machine can actually launch.
2415
- *
2416
- * The operator cannot discover this case unaided: under launchd/Task
2417
- * Scheduler the service inherits a stripped PATH, so a CLI that works
2418
- * perfectly in their terminal is invisible to the service, and every session
2419
- * start dies milliseconds in. `/api/diagnostics` answers it too, but only for
2420
- * someone who already suspects it.
2421
- *
2422
- * Availability only, never a version — `--version` costs a process spawn per
2423
- * provider (85ms for claude here) and belongs on the first request that wants
2424
- * it, not on boot.
2425
- *
2426
- * Called AFTER the port is bound, which is not cosmetic. This is the first
2427
- * caller of the exe resolvers in the process, so the memo is cold by
2428
- * definition and each provider pays one synchronous `which` / `where.exe`
2429
- * (platform.ts) with a 3s timeout. On POSIX that is 3ms found, 7ms missing.
2430
- * Windows is the risk — `where.exe` is slower, `execFileSync` blocks the
2431
- * event loop, and Task Scheduler's stripped PATH is exactly where a miss
2432
- * pays the full timeout — so the worst case is ~6s of two blocking lookups.
2433
- * After `listen()` that delays the first requests on a box that cannot start
2434
- * a session anyway; before it, it would have delayed binding the port.
2435
- */
2436
- private logProviderAvailability;
2437
- listen(port: number, opts?: {
2438
- awaitReady?: boolean;
2439
- }): Promise<void>;
2440
- private bindWithRetry;
2441
- private bindWithRetryLoop;
2442
- private trackCacheWrite;
2443
- close(): Promise<void>;
2444
- private handleRequest;
2445
- private handlePairStart;
2446
- private handlePairExchange;
2447
- private rotateApiKey;
2448
- /**
2449
- * The registry ships with the values so a client renders the list from one
2450
- * round-trip, same as getClaudeFlagsConfig().
2451
- *
2452
- * Deliberately no `persisted` field: unlike claude-flags there is no PUT, and
2453
- * the absence of that field is the signal that this endpoint is read-only.
2454
- */
2455
- private getFeatureFlagsConfig;
2456
- private getClaudeFlagsConfig;
2457
- /**
2458
- * Replace the per-server flag set. Applies to the NEXT spawn — a live PTY
2459
- * keeps the argv it was started with.
2460
- *
2461
- * Mirrors rotateApiKey(): when the values were pinned by a CLI flag we still
2462
- * apply them in memory but skip the server.yaml write, because the flag would
2463
- * win again on restart and silently revert them.
2464
- *
2465
- * Logged with old→new at info level on purpose: this can disable the
2466
- * permission prompts entirely, so it needs a forensic trail.
2467
- */
2468
- private setClaudeFlagsConfig;
2469
- /**
2470
- * The three spawn options that a configured claude-flag can override, with
2471
- * the boot-time CLI/yaml default as the fallback. Spread into every
2472
- * start/resume/adopt call so all three paths agree.
2473
- *
2474
- * These ids are excluded from buildFlagArgs (SPAWN_POSITIONAL_FLAG_IDS)
2475
- * precisely because they arrive here instead — the PTY spawn paths pass them
2476
- * as explicit positionals, so emitting them from the allowlist too would
2477
- * duplicate the flag.
2478
- *
2479
- * Narrowed with the type guards rather than cast: ClaudeFlagValues is a loose
2480
- * Record by design, and while validateFlagValues already guarantees the shape
2481
- * on the way in, TypeScript cannot see that through the record.
2482
- */
2483
- private spawnFlagOverrides;
2484
- private checkRateLimit;
2485
- private checkExchangeRateLimit;
2486
- private checkSessionStartRateLimit;
2487
- private checkSessionInputRateLimit;
2488
- private handleSessionsCount;
2489
- /**
2490
- * Live Codex sessions keep `SessionResponse.conversationId === managed.id`
2491
- * (stable deep-link / PTY key) and store the rollout UUID separately as
2492
- * `boundConversationId`. REST history is indexed under the rollout UUID, so
2493
- * resolve the placeholder → bound id before looking up the scanner.
2494
- */
2495
- private resolveConversationLookupId;
2496
- /** File path for a live managed session (placeholder id or bound Codex id). */
2497
- private findLiveSessionFilePath;
2498
- /** True when a conversation UUID is the bound rollout of a live PTY session. */
2499
- private isBoundConversationLive;
2500
- /**
2501
- * Broadcast conversation JSONL lines to WS clients. Codex rollout lines are
2502
- * normalized to the Claude `type:user|assistant` shape mobile understands;
2503
- * Claude lines pass through unchanged so seq alignment stays intact.
2504
- */
2505
- private broadcastConversationLines;
2506
- /**
2507
- * Resolve a client-supplied session/conversation id into everything needed to
2508
- * launch against it: the id the PROVIDER filed the history under, that
2509
- * history's path, the project cwd, and which CLI owns it.
2510
- *
2511
- * Shared by resume and fork so the two can never disagree about identity —
2512
- * which for Codex is the whole difficulty: the id a client navigated to may
2513
- * be a local placeholder, and only the registry knows the rollout id behind
2514
- * it.
2515
- */
2516
- private resolveConversationTarget;
2517
- /**
2518
- * Block until a freshly spawned session reaches `waiting_input` (ready) or
2519
- * `idle` (failed), or until `timeoutMs` elapses with the process still alive.
2520
- *
2521
- * "timeout" is not an error: it is the pre-existing asynchronous contract —
2522
- * the session keeps booting and the caller answers with a pending shape.
2523
- */
2524
- private waitForStartupOutcome;
2525
- /**
2526
- * Drop every trace of a managed session: in-memory store, durable registry
2527
- * row, and the collision-probe markers that would otherwise outlive it.
2528
- *
2529
- * Used when a start never became usable (`abandonFailedStart`) and when stop
2530
- * is asked to discard an empty session that has no cached conversation. The
2531
- * registry delete is load-bearing — `rehydrateSessions` will bring the row
2532
- * back on the next boot if it remains.
2533
- *
2534
- * Callers that kill the PTY (`putOnHold`) must do that *first*: onStatusChange
2535
- * on idle writes `selfPtyEndedAt` and a registry status, and those have to
2536
- * be cleared here afterwards.
2537
- */
2538
- private forgetSession;
2539
- /**
2540
- * Drop every trace of a session that never became usable.
2541
- *
2542
- * The runner has already torn itself down (failStartup / handleExit); what
2543
- * remains is server-side bookkeeping that would otherwise leave a dead
2544
- * session in the list, a registry row claiming a spawn, and a `selfPtyEndedAt`
2545
- * marker that would suppress the mtime collision signal on the NEXT resume —
2546
- * i.e. it would help hide the very owner we just collided with.
2547
- */
2548
- private abandonFailedStart;
2549
- private enrichResumedSessionAsync;
2550
- private processJsonlQuestions;
2551
- private cancelPendingQuestion;
2552
- private handleBrowse;
2553
- private handleMkdir;
2554
- /**
2555
- * Retarget a LIVE session's model or effort by typing the corresponding
2556
- * Claude Code slash command into its PTY.
2557
- *
2558
- * There is no CLI or IPC channel for this — `--model`/`--effort` are spawn
2559
- * arguments — so the interactive `/model <x>` / `/effort <y>` commands are the
2560
- * only way to change a session already running. Both accept an argument and
2561
- * apply it without opening the picker (verified against Claude Code v2.1.220).
2562
- *
2563
- * Answers 202, not 200: the value is applied by the TUI on its next render, so
2564
- * there is nothing truthful to echo back synchronously. Clients confirm with
2565
- * `GET /api/sessions/:id`, which scrapes the applied value off the live status
2566
- * line.
2567
- */
2568
- private applyLiveSessionSetting;
2569
- }
2570
-
2571
- export { type AgentClient, type AgentClientOpts, type AgentConfig, type AgentPhase, type AppendArgs, type AskOption, type AskQuestion, CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type CacheAlertResolveAction, type ConversationListResponse, ConversationWatcher, type ConversationWriter, type DbConfig, type DiscoveredProcess, LiveSessionManager, type ManagedSession, PTYManager, type PTYManagerOptions, type PermissionOption, type ProcessLiveness, type ProgressDedupeLRU, type ProviderName, type ServerConfig, type ServerWarmingUpResponse, type ServerWarmupState, type SessionActivity, type SessionCursor, type SessionLifecycle, type SessionListPage, type SessionListQuery, type SessionOwnership, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartForkSessionOptions, type StartFreshSessionOptions, type StartSessionOptions, type StatusConfidence, type StatusSource$1 as StatusSource, StreamerServer, type UserMessage, WSHub, type WSMessage, confidenceForSource, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
1
+ export { type AgentClient, type AgentClientOpts, createAgentClient, } from "./agent/agent-client";
2
+ export { type AgentConfig, readAgentConfig } from "./agent/agent-config";
3
+ export { type AppendArgs, type ConversationWriter, createConversationWriter, } from "./agent/conversation-writer";
4
+ export { createProgressDedupeLRU, type ProgressDedupeLRU } from "./agent/dedupe";
5
+ export { createProgressRoutes } from "./api/routes/progress.routes";
6
+ export { generateApiKey, loadOrCreateApiKey, validateApiKey } from "./auth";
7
+ export type { DbConfig } from "./db";
8
+ export { createPool, getDbConfig, isDbEnabled, maskConnectionString } from "./db";
9
+ export { LiveSessionManager } from "./live-session-manager";
10
+ export { discoverClaudeProcesses } from "./process-discovery";
11
+ export { CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, isProviderName, isProviderResumable, type ProviderName, } from "./providers";
12
+ export { PTYManager } from "./pty-manager";
13
+ export { StreamerServer } from "./server";
14
+ export { ConversationWatcher } from "./services/conversations/conversationWatcher";
15
+ export { SessionStore } from "./session-store";
16
+ export * from "./types";
17
+ export { WSHub } from "./ws-hub";
18
+ //# sourceMappingURL=index.d.ts.map