@felan-ai/daemon 0.0.0-stage → 0.0.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 (288) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +69 -0
  3. package/README.md +61 -2
  4. package/dist/client.d.ts +5 -0
  5. package/dist/client.d.ts.map +1 -0
  6. package/dist/client.js +4 -0
  7. package/dist/client.js.map +1 -0
  8. package/dist/daemon/admission.d.ts +39 -0
  9. package/dist/daemon/admission.d.ts.map +1 -0
  10. package/dist/daemon/admission.js +175 -0
  11. package/dist/daemon/admission.js.map +1 -0
  12. package/dist/daemon/agent-logs.d.ts +11 -0
  13. package/dist/daemon/agent-logs.d.ts.map +1 -0
  14. package/dist/daemon/agent-logs.js +31 -0
  15. package/dist/daemon/agent-logs.js.map +1 -0
  16. package/dist/daemon/bootstrap.d.ts +7 -0
  17. package/dist/daemon/bootstrap.d.ts.map +1 -0
  18. package/dist/daemon/bootstrap.js +4 -0
  19. package/dist/daemon/bootstrap.js.map +1 -0
  20. package/dist/daemon/broker.d.ts +94 -0
  21. package/dist/daemon/broker.d.ts.map +1 -0
  22. package/dist/daemon/broker.js +374 -0
  23. package/dist/daemon/broker.js.map +1 -0
  24. package/dist/daemon/commands.d.ts +47 -0
  25. package/dist/daemon/commands.d.ts.map +1 -0
  26. package/dist/daemon/commands.js +639 -0
  27. package/dist/daemon/commands.js.map +1 -0
  28. package/dist/daemon/control-recovery.d.ts +9 -0
  29. package/dist/daemon/control-recovery.d.ts.map +1 -0
  30. package/dist/daemon/control-recovery.js +68 -0
  31. package/dist/daemon/control-recovery.js.map +1 -0
  32. package/dist/daemon/control-state.d.ts +1915 -0
  33. package/dist/daemon/control-state.d.ts.map +1 -0
  34. package/dist/daemon/control-state.js +154 -0
  35. package/dist/daemon/control-state.js.map +1 -0
  36. package/dist/daemon/control-store.d.ts +113 -0
  37. package/dist/daemon/control-store.d.ts.map +1 -0
  38. package/dist/daemon/control-store.js +901 -0
  39. package/dist/daemon/control-store.js.map +1 -0
  40. package/dist/daemon/execution-policy.d.ts +15 -0
  41. package/dist/daemon/execution-policy.d.ts.map +1 -0
  42. package/dist/daemon/execution-policy.js +42 -0
  43. package/dist/daemon/execution-policy.js.map +1 -0
  44. package/dist/daemon/execution-services.d.ts +5 -0
  45. package/dist/daemon/execution-services.d.ts.map +1 -0
  46. package/dist/daemon/execution-services.js +73 -0
  47. package/dist/daemon/execution-services.js.map +1 -0
  48. package/dist/daemon/executions.d.ts +134 -0
  49. package/dist/daemon/executions.d.ts.map +1 -0
  50. package/dist/daemon/executions.js +486 -0
  51. package/dist/daemon/executions.js.map +1 -0
  52. package/dist/daemon/extension-ui.d.ts +24 -0
  53. package/dist/daemon/extension-ui.d.ts.map +1 -0
  54. package/dist/daemon/extension-ui.js +161 -0
  55. package/dist/daemon/extension-ui.js.map +1 -0
  56. package/dist/daemon/fanout.d.ts +40 -0
  57. package/dist/daemon/fanout.d.ts.map +1 -0
  58. package/dist/daemon/fanout.js +304 -0
  59. package/dist/daemon/fanout.js.map +1 -0
  60. package/dist/daemon/headless-json.d.ts +3 -0
  61. package/dist/daemon/headless-json.d.ts.map +1 -0
  62. package/dist/daemon/headless-json.js +20 -0
  63. package/dist/daemon/headless-json.js.map +1 -0
  64. package/dist/daemon/host-config.d.ts +61 -0
  65. package/dist/daemon/host-config.d.ts.map +1 -0
  66. package/dist/daemon/host-config.js +167 -0
  67. package/dist/daemon/host-config.js.map +1 -0
  68. package/dist/daemon/host-services.d.ts +61 -0
  69. package/dist/daemon/host-services.d.ts.map +1 -0
  70. package/dist/daemon/host-services.js +2 -0
  71. package/dist/daemon/host-services.js.map +1 -0
  72. package/dist/daemon/ipc.d.ts +14 -0
  73. package/dist/daemon/ipc.d.ts.map +1 -0
  74. package/dist/daemon/ipc.js +233 -0
  75. package/dist/daemon/ipc.js.map +1 -0
  76. package/dist/daemon/legacy-sessions.d.ts +48 -0
  77. package/dist/daemon/legacy-sessions.d.ts.map +1 -0
  78. package/dist/daemon/legacy-sessions.js +328 -0
  79. package/dist/daemon/legacy-sessions.js.map +1 -0
  80. package/dist/daemon/lifecycle.d.ts +44 -0
  81. package/dist/daemon/lifecycle.d.ts.map +1 -0
  82. package/dist/daemon/lifecycle.js +164 -0
  83. package/dist/daemon/lifecycle.js.map +1 -0
  84. package/dist/daemon/main.d.ts +3 -0
  85. package/dist/daemon/main.d.ts.map +1 -0
  86. package/dist/daemon/main.js +83 -0
  87. package/dist/daemon/main.js.map +1 -0
  88. package/dist/daemon/message-display.d.ts +4 -0
  89. package/dist/daemon/message-display.d.ts.map +1 -0
  90. package/dist/daemon/message-display.js +20 -0
  91. package/dist/daemon/message-display.js.map +1 -0
  92. package/dist/daemon/native-ownership.d.ts +13 -0
  93. package/dist/daemon/native-ownership.d.ts.map +1 -0
  94. package/dist/daemon/native-ownership.js +173 -0
  95. package/dist/daemon/native-ownership.js.map +1 -0
  96. package/dist/daemon/operational-log.d.ts +23 -0
  97. package/dist/daemon/operational-log.d.ts.map +1 -0
  98. package/dist/daemon/operational-log.js +79 -0
  99. package/dist/daemon/operational-log.js.map +1 -0
  100. package/dist/daemon/ownership.d.ts +35 -0
  101. package/dist/daemon/ownership.d.ts.map +1 -0
  102. package/dist/daemon/ownership.js +395 -0
  103. package/dist/daemon/ownership.js.map +1 -0
  104. package/dist/daemon/persistence.d.ts +110 -0
  105. package/dist/daemon/persistence.d.ts.map +1 -0
  106. package/dist/daemon/persistence.js +540 -0
  107. package/dist/daemon/persistence.js.map +1 -0
  108. package/dist/daemon/process-adapter.d.ts +23 -0
  109. package/dist/daemon/process-adapter.d.ts.map +1 -0
  110. package/dist/daemon/process-adapter.js +2 -0
  111. package/dist/daemon/process-adapter.js.map +1 -0
  112. package/dist/daemon/processes.d.ts +102 -0
  113. package/dist/daemon/processes.d.ts.map +1 -0
  114. package/dist/daemon/processes.js +691 -0
  115. package/dist/daemon/processes.js.map +1 -0
  116. package/dist/daemon/profile-storage.d.ts +16 -0
  117. package/dist/daemon/profile-storage.d.ts.map +1 -0
  118. package/dist/daemon/profile-storage.js +148 -0
  119. package/dist/daemon/profile-storage.js.map +1 -0
  120. package/dist/daemon/projection.d.ts +110 -0
  121. package/dist/daemon/projection.d.ts.map +1 -0
  122. package/dist/daemon/projection.js +661 -0
  123. package/dist/daemon/projection.js.map +1 -0
  124. package/dist/daemon/provider-loader.d.ts +28 -0
  125. package/dist/daemon/provider-loader.d.ts.map +1 -0
  126. package/dist/daemon/provider-loader.js +376 -0
  127. package/dist/daemon/provider-loader.js.map +1 -0
  128. package/dist/daemon/provider.d.ts +92 -0
  129. package/dist/daemon/provider.d.ts.map +1 -0
  130. package/dist/daemon/provider.js +31 -0
  131. package/dist/daemon/provider.js.map +1 -0
  132. package/dist/daemon/registry.d.ts +209 -0
  133. package/dist/daemon/registry.d.ts.map +1 -0
  134. package/dist/daemon/registry.js +1591 -0
  135. package/dist/daemon/registry.js.map +1 -0
  136. package/dist/daemon/runtime-dependencies.d.ts +5 -0
  137. package/dist/daemon/runtime-dependencies.d.ts.map +1 -0
  138. package/dist/daemon/runtime-dependencies.js +31 -0
  139. package/dist/daemon/runtime-dependencies.js.map +1 -0
  140. package/dist/daemon/server.d.ts +110 -0
  141. package/dist/daemon/server.d.ts.map +1 -0
  142. package/dist/daemon/server.js +556 -0
  143. package/dist/daemon/server.js.map +1 -0
  144. package/dist/daemon/service.d.ts +38 -0
  145. package/dist/daemon/service.d.ts.map +1 -0
  146. package/dist/daemon/service.js +275 -0
  147. package/dist/daemon/service.js.map +1 -0
  148. package/dist/daemon/session-cursor.d.ts +3 -0
  149. package/dist/daemon/session-cursor.d.ts.map +1 -0
  150. package/dist/daemon/session-cursor.js +4 -0
  151. package/dist/daemon/session-cursor.js.map +1 -0
  152. package/dist/daemon/session-execution.d.ts +88 -0
  153. package/dist/daemon/session-execution.d.ts.map +1 -0
  154. package/dist/daemon/session-execution.js +2 -0
  155. package/dist/daemon/session-execution.js.map +1 -0
  156. package/dist/daemon/session-executions.d.ts +5 -0
  157. package/dist/daemon/session-executions.d.ts.map +1 -0
  158. package/dist/daemon/session-executions.js +146 -0
  159. package/dist/daemon/session-executions.js.map +1 -0
  160. package/dist/daemon/session-files.d.ts +28 -0
  161. package/dist/daemon/session-files.d.ts.map +1 -0
  162. package/dist/daemon/session-files.js +189 -0
  163. package/dist/daemon/session-files.js.map +1 -0
  164. package/dist/daemon/session-lifecycle-contracts.d.ts +104 -0
  165. package/dist/daemon/session-lifecycle-contracts.d.ts.map +1 -0
  166. package/dist/daemon/session-lifecycle-contracts.js +2 -0
  167. package/dist/daemon/session-lifecycle-contracts.js.map +1 -0
  168. package/dist/daemon/session-lifecycle.d.ts +26 -0
  169. package/dist/daemon/session-lifecycle.d.ts.map +1 -0
  170. package/dist/daemon/session-lifecycle.js +246 -0
  171. package/dist/daemon/session-lifecycle.js.map +1 -0
  172. package/dist/daemon/session-runtime.d.ts +64 -0
  173. package/dist/daemon/session-runtime.d.ts.map +1 -0
  174. package/dist/daemon/session-runtime.js +3 -0
  175. package/dist/daemon/session-runtime.js.map +1 -0
  176. package/dist/daemon/session-services.d.ts +6 -0
  177. package/dist/daemon/session-services.d.ts.map +1 -0
  178. package/dist/daemon/session-services.js +144 -0
  179. package/dist/daemon/session-services.js.map +1 -0
  180. package/dist/daemon/shutdown-error.d.ts +12 -0
  181. package/dist/daemon/shutdown-error.d.ts.map +1 -0
  182. package/dist/daemon/shutdown-error.js +51 -0
  183. package/dist/daemon/shutdown-error.js.map +1 -0
  184. package/dist/daemon/supervisor.d.ts +28 -0
  185. package/dist/daemon/supervisor.d.ts.map +1 -0
  186. package/dist/daemon/supervisor.js +159 -0
  187. package/dist/daemon/supervisor.js.map +1 -0
  188. package/dist/daemon/target.d.ts +55 -0
  189. package/dist/daemon/target.d.ts.map +1 -0
  190. package/dist/daemon/target.js +191 -0
  191. package/dist/daemon/target.js.map +1 -0
  192. package/dist/daemon/tree.d.ts +7 -0
  193. package/dist/daemon/tree.d.ts.map +1 -0
  194. package/dist/daemon/tree.js +126 -0
  195. package/dist/daemon/tree.js.map +1 -0
  196. package/dist/daemon/ui-vocabulary.d.ts +29 -0
  197. package/dist/daemon/ui-vocabulary.d.ts.map +1 -0
  198. package/dist/daemon/ui-vocabulary.js +323 -0
  199. package/dist/daemon/ui-vocabulary.js.map +1 -0
  200. package/dist/daemon/worker.d.ts +5 -0
  201. package/dist/daemon/worker.d.ts.map +1 -0
  202. package/dist/daemon/worker.js +49 -0
  203. package/dist/daemon/worker.js.map +1 -0
  204. package/dist/index.d.ts +4 -0
  205. package/dist/index.d.ts.map +1 -0
  206. package/dist/index.js +12 -0
  207. package/dist/index.js.map +1 -0
  208. package/dist/lock.d.ts +27 -0
  209. package/dist/lock.d.ts.map +1 -0
  210. package/dist/lock.js +432 -0
  211. package/dist/lock.js.map +1 -0
  212. package/dist/logger.d.ts +9 -0
  213. package/dist/logger.d.ts.map +1 -0
  214. package/dist/logger.js +56 -0
  215. package/dist/logger.js.map +1 -0
  216. package/dist/remote/backoff.d.ts +17 -0
  217. package/dist/remote/backoff.d.ts.map +1 -0
  218. package/dist/remote/backoff.js +18 -0
  219. package/dist/remote/backoff.js.map +1 -0
  220. package/dist/remote/batch.d.ts +3 -0
  221. package/dist/remote/batch.d.ts.map +1 -0
  222. package/dist/remote/batch.js +27 -0
  223. package/dist/remote/batch.js.map +1 -0
  224. package/dist/remote/command-journal.d.ts +30 -0
  225. package/dist/remote/command-journal.d.ts.map +1 -0
  226. package/dist/remote/command-journal.js +72 -0
  227. package/dist/remote/command-journal.js.map +1 -0
  228. package/dist/remote/commands.d.ts +48 -0
  229. package/dist/remote/commands.d.ts.map +1 -0
  230. package/dist/remote/commands.js +145 -0
  231. package/dist/remote/commands.js.map +1 -0
  232. package/dist/remote/connector.d.ts +135 -0
  233. package/dist/remote/connector.d.ts.map +1 -0
  234. package/dist/remote/connector.js +1069 -0
  235. package/dist/remote/connector.js.map +1 -0
  236. package/dist/remote/content.d.ts +17 -0
  237. package/dist/remote/content.d.ts.map +1 -0
  238. package/dist/remote/content.js +116 -0
  239. package/dist/remote/content.js.map +1 -0
  240. package/dist/remote/outbox.d.ts +52 -0
  241. package/dist/remote/outbox.d.ts.map +1 -0
  242. package/dist/remote/outbox.js +321 -0
  243. package/dist/remote/outbox.js.map +1 -0
  244. package/dist/remote/platform.d.ts +53 -0
  245. package/dist/remote/platform.d.ts.map +1 -0
  246. package/dist/remote/platform.js +231 -0
  247. package/dist/remote/platform.js.map +1 -0
  248. package/dist/remote/realtime.d.ts +45 -0
  249. package/dist/remote/realtime.d.ts.map +1 -0
  250. package/dist/remote/realtime.js +265 -0
  251. package/dist/remote/realtime.js.map +1 -0
  252. package/dist/remote/sharing.d.ts +16 -0
  253. package/dist/remote/sharing.d.ts.map +1 -0
  254. package/dist/remote/sharing.js +24 -0
  255. package/dist/remote/sharing.js.map +1 -0
  256. package/dist/remote/state.d.ts +52 -0
  257. package/dist/remote/state.d.ts.map +1 -0
  258. package/dist/remote/state.js +156 -0
  259. package/dist/remote/state.js.map +1 -0
  260. package/dist/remote/text.d.ts +11 -0
  261. package/dist/remote/text.d.ts.map +1 -0
  262. package/dist/remote/text.js +188 -0
  263. package/dist/remote/text.js.map +1 -0
  264. package/dist/remote/token.d.ts +39 -0
  265. package/dist/remote/token.d.ts.map +1 -0
  266. package/dist/remote/token.js +238 -0
  267. package/dist/remote/token.js.map +1 -0
  268. package/dist/runtime-factory.d.ts +12 -0
  269. package/dist/runtime-factory.d.ts.map +1 -0
  270. package/dist/runtime-factory.js +12 -0
  271. package/dist/runtime-factory.js.map +1 -0
  272. package/dist/session-history.d.ts +29 -0
  273. package/dist/session-history.d.ts.map +1 -0
  274. package/dist/session-history.js +374 -0
  275. package/dist/session-history.js.map +1 -0
  276. package/dist/setup/dependencies.d.ts +15 -0
  277. package/dist/setup/dependencies.d.ts.map +1 -0
  278. package/dist/setup/dependencies.js +2 -0
  279. package/dist/setup/dependencies.js.map +1 -0
  280. package/dist/setup/open.d.ts +8 -0
  281. package/dist/setup/open.d.ts.map +1 -0
  282. package/dist/setup/open.js +16 -0
  283. package/dist/setup/open.js.map +1 -0
  284. package/dist/version.d.ts +2 -0
  285. package/dist/version.d.ts.map +1 -0
  286. package/dist/version.js +4 -0
  287. package/dist/version.js.map +1 -0
  288. package/package.json +66 -4
@@ -0,0 +1,1069 @@
1
+ import { hostname } from 'node:os';
2
+ import { INTERACTION_KINDS, ROW_SCHEMA_VERSION, isPublishedChangeType, parseRemoteConfigureParams, parseRemoteStatus, } from '@felan-ai/session-protocol';
3
+ import { commandError } from '../daemon/commands.js';
4
+ import { redact } from '../logger.js';
5
+ import { DAEMON_VERSION } from '../version.js';
6
+ import { retryDelay, schedule, systemClock } from './backoff.js';
7
+ import { publicationBatches } from './batch.js';
8
+ import { RemoteCommandJournal } from './command-journal.js';
9
+ import { RemoteCommandExecutor } from './commands.js';
10
+ import { publishableChange, publishedEvent, publishedSession } from './content.js';
11
+ import { RemoteOutbox, RemoteOutboxCapacityError } from './outbox.js';
12
+ import { PlatformClient, PlatformError } from './platform.js';
13
+ import { sessionShared, setSessionSharing, webCreatedSession } from './sharing.js';
14
+ import { RemoteTextSanitizer } from './text.js';
15
+ import { RemoteIntegrityError, RemoteStorageError, RemoteStore } from './state.js';
16
+ import { HostTokenSource, HostTokenExchangeError } from './token.js';
17
+ export const REMOTE_CAPABILITIES = {
18
+ operations: ['sessions.create', 'prompt', 'steer', 'cancel', 'interaction.answer'],
19
+ interactionKinds: [...INTERACTION_KINDS],
20
+ };
21
+ const stoppedMessages = {
22
+ invalid_pat: 'Run /remote login with a new personal access token to resume sharing.',
23
+ not_a_member: 'Publishing stopped: the host owner is no longer a team member.',
24
+ host_revoked: 'Publishing stopped: this host was revoked. Run /remote login to pair it again.',
25
+ unsupported_schema: 'Update Felan (the daemon) to resume sharing with a supported schema.',
26
+ };
27
+ export class RemoteConnector {
28
+ options;
29
+ store;
30
+ outbox;
31
+ executor;
32
+ clock;
33
+ capabilities;
34
+ lastStatus;
35
+ state = 'off';
36
+ reason;
37
+ message;
38
+ supported;
39
+ configured = false;
40
+ host;
41
+ token;
42
+ platform;
43
+ generation = 0;
44
+ started = false;
45
+ stopped = false;
46
+ draining = false;
47
+ attempt = 0;
48
+ retryTimer;
49
+ heartbeatTimer;
50
+ publishTimer;
51
+ unobserve = () => { };
52
+ unsubscribeCommands = () => { };
53
+ publishWork;
54
+ claimWork;
55
+ claimAgain = false;
56
+ publishAgain = false;
57
+ configureWork = Promise.resolve();
58
+ registrationWork = Promise.resolve();
59
+ work = new Set();
60
+ text = new RemoteTextSanitizer();
61
+ indexed = new Set();
62
+ backfilling = new Set();
63
+ backfillScheduled = new Set();
64
+ backfillRetryAt = new Map();
65
+ backfillIssues = new Map();
66
+ buffered = new Map();
67
+ reauthorizedPublications = new Set();
68
+ publicationGaps = new Map();
69
+ outboxErrorsWired = false;
70
+ blocked = new Map();
71
+ recordingIssues = new Map();
72
+ recordingBlocked = new Set();
73
+ rootIds = new Map();
74
+ constructor(options) {
75
+ this.options = options;
76
+ this.clock = options.clock ?? systemClock;
77
+ this.capabilities = structuredClone(options.capabilities ?? REMOTE_CAPABILITIES);
78
+ this.store = new RemoteStore(options.database);
79
+ this.outbox = new RemoteOutbox(options.database);
80
+ this.lastStatus = { configured: false, share: this.store.settings().share, state: 'off', apiUrl: this.apiOrigin(), projects: [], pending: 0, sessions: 0 };
81
+ options.owner.assertCurrent();
82
+ const journal = new RemoteCommandJournal(options.database);
83
+ this.executor = new RemoteCommandExecutor({
84
+ submit: (request, remote) => options.server.submitRemote(request, remote),
85
+ lookup: id => options.server.hasRemoteRequest(id),
86
+ owner: () => this.token?.current()?.host.userId,
87
+ clock: this.clock,
88
+ projects: () => this.store.settings().projects,
89
+ isShared: id => this.isShared(id),
90
+ markWebCreated: id => {
91
+ options.owner.assertCurrent();
92
+ this.store.setSession(id, { ...webCreatedSession(), ...this.store.session(id), origin: 'web', shared: true, unsharedPending: false });
93
+ this.backfill(id);
94
+ this.wakePublish();
95
+ },
96
+ capabilities: this.capabilities,
97
+ unrecordedIsIndeterminate: () => options.database.rebuilt && options.database.record('command_recovery', this.host ?? '') === undefined,
98
+ journal: {
99
+ get: id => { options.owner.assertCurrent(); return journal.get(id); },
100
+ write: entry => { options.owner.assertCurrent(); journal.write(entry); },
101
+ forget: id => { options.owner.assertCurrent(); journal.forget(id); },
102
+ },
103
+ });
104
+ }
105
+ start() {
106
+ if (this.started || this.stopped)
107
+ return;
108
+ this.started = true;
109
+ this.unobserve = this.options.registry.observe(event => this.observe(event), id => this.safelyBackfill(id));
110
+ const generation = this.generation;
111
+ this.track((async () => {
112
+ try {
113
+ const pat = await this.bounded(this.options.secretStorage.read('remote', 'pat'));
114
+ if (!this.current(generation))
115
+ return;
116
+ if (pat)
117
+ this.login(pat);
118
+ }
119
+ catch {
120
+ if (this.current(generation))
121
+ this.message = 'Remote credentials are unavailable. Run /remote login to retry.';
122
+ }
123
+ })());
124
+ }
125
+ beginDrain() { this.draining = true; }
126
+ endDrain() { this.draining = false; this.wakeClaims(); }
127
+ async stop() {
128
+ if (this.stopped)
129
+ return;
130
+ this.stopped = true;
131
+ this.generation++;
132
+ this.unobserve();
133
+ this.clearTimers();
134
+ this.token?.stop();
135
+ this.token = undefined;
136
+ const close = this.disconnect();
137
+ await this.bounded(Promise.allSettled([close, ...this.work]), this.options.shutdownTimeoutMs ?? 2_000).catch(() => { });
138
+ try {
139
+ this.outbox.flush();
140
+ }
141
+ catch (error) {
142
+ this.recordingFailure('outbox', error);
143
+ }
144
+ }
145
+ status() {
146
+ try {
147
+ return this.readStatus();
148
+ }
149
+ catch {
150
+ return { ...structuredClone(this.lastStatus), configured: this.configured, state: this.state === 'stopped' ? 'stopped' : 'offline',
151
+ reason: this.state === 'stopped' ? this.reason ?? 'unavailable' : 'unavailable',
152
+ message: 'Remote persistence is unavailable; counts may be stale. Restart the daemon after checking local storage.' };
153
+ }
154
+ }
155
+ readStatus() {
156
+ try {
157
+ return this.collectStatus();
158
+ }
159
+ catch (error) {
160
+ if (!this.recordingIssues.size)
161
+ throw error;
162
+ const last = structuredClone(this.lastStatus);
163
+ delete last.reason;
164
+ return { ...last, configured: this.configured, state: this.state, ...(this.reason ? { reason: this.reason } : {}),
165
+ message: `Remote persistence is unavailable; counts may be stale. ${[...this.recordingIssues].slice(0, 3).map(([id, reason]) => `${id}: ${reason}`).join('; ')}` };
166
+ }
167
+ }
168
+ collectStatus() {
169
+ const settings = this.store.settings();
170
+ const issues = [];
171
+ const omissions = [];
172
+ for (const id of this.store.sessions()) {
173
+ const record = this.store.session(id);
174
+ if (record?.terminal)
175
+ issues.push(`${id}: ${record.terminal}`);
176
+ if (record?.skippedChanges)
177
+ omissions.push(`${id}: ${record.skippedChanges} changes could not be published`);
178
+ }
179
+ for (const [id, reason] of this.blocked)
180
+ issues.push(`${id}: ${reason}`);
181
+ for (const [id, reason] of this.recordingIssues)
182
+ issues.push(`${id}: ${reason}`);
183
+ for (const [id, reason] of this.backfillIssues)
184
+ issues.push(`${id}: ${reason}`);
185
+ let sessions = 0;
186
+ for (const row of this.options.registry.list()) {
187
+ this.rootIds.set(row.id, row.root);
188
+ if (this.configured && sessionShared(this.sharing(row.id, row.root, settings.share)))
189
+ sessions++;
190
+ }
191
+ const message = [this.message, issues.length ? `${issues.length} session issue(s). ${issues.slice(0, 3).join('; ')}` : undefined,
192
+ ...omissions.slice(0, 3)].filter(Boolean).join(' ');
193
+ this.lastStatus = parseRemoteStatus({ configured: this.configured, share: settings.share, state: this.state,
194
+ ...(this.reason ? { reason: this.reason } : {}), ...(message ? { message: redact(message) } : {}),
195
+ ...(this.host ? { host: { id: this.host } } : {}), apiUrl: this.apiOrigin(),
196
+ projects: settings.projects.map(project => ({ id: project.id, name: redact(project.name), path: redact(project.path) })),
197
+ pending: this.outbox.pendingCount(), sessions,
198
+ ...(this.supported ? { supported: this.supported } : {}) });
199
+ return structuredClone(this.lastStatus);
200
+ }
201
+ setupNeeds() {
202
+ return this.state === 'stopped' ? [{ code: 'remote', message: this.message ?? 'Publishing stopped. Run /remote login to retry.' }] : [];
203
+ }
204
+ configure(value) {
205
+ const work = this.configureWork.catch(() => { }).then(async () => {
206
+ if (this.stopped)
207
+ throw commandError('unavailable', 'Remote connector is stopped.');
208
+ let params;
209
+ try {
210
+ params = parseRemoteConfigureParams(value);
211
+ }
212
+ catch {
213
+ const message = value?.action === 'login' ? 'Invalid remote login token or API URL.'
214
+ : value?.action === 'open_project' ? 'Remote project path must be absolute and its name must be 1–256 bytes.' : 'Invalid remote configuration.';
215
+ throw commandError('invalid_request', message);
216
+ }
217
+ if (params.action === 'login' && params.apiUrl !== undefined)
218
+ this.validateApiUrl(params.apiUrl);
219
+ try {
220
+ if (params.action === 'login') {
221
+ await this.bounded(this.options.secretStorage.write('remote', 'pat', params.token));
222
+ if (this.stopped)
223
+ throw commandError('unavailable');
224
+ if (params.apiUrl !== undefined)
225
+ this.store.configure({ apiUrl: params.apiUrl });
226
+ this.generation++;
227
+ this.token?.stop();
228
+ this.token = undefined;
229
+ await this.bounded(this.disconnect(), this.options.shutdownTimeoutMs ?? 2_000).catch(() => { });
230
+ this.clearTimers();
231
+ this.login(params.token);
232
+ }
233
+ else if (params.action === 'logout') {
234
+ await this.bounded(this.options.secretStorage.remove('remote', 'pat'));
235
+ if (this.stopped)
236
+ throw commandError('unavailable');
237
+ this.generation++;
238
+ this.configured = false;
239
+ this.token?.stop();
240
+ this.token = undefined;
241
+ this.clearTimers();
242
+ await this.bounded(this.disconnect(), this.options.shutdownTimeoutMs ?? 2_000).catch(() => { });
243
+ this.state = 'off';
244
+ this.reason = undefined;
245
+ this.message = undefined;
246
+ this.supported = undefined;
247
+ }
248
+ else {
249
+ if (params.action === 'share') {
250
+ const rows = this.options.registry.list();
251
+ const share = this.store.settings().share;
252
+ const before = new Map(rows.map(row => [row.id, sessionShared(this.sharing(row.id, row.root, share))]));
253
+ this.store.configure({ share: params.share });
254
+ for (const row of rows) {
255
+ const shared = sessionShared(this.sharing(row.id, row.root, params.share));
256
+ if (before.get(row.id) && !shared)
257
+ this.markUnshared(row.id);
258
+ else if (shared) {
259
+ const record = this.store.session(row.id);
260
+ if (!before.get(row.id) && record?.backfilled)
261
+ this.store.setSession(row.id, { ...record, sharedPending: true, unsharedPending: false });
262
+ this.safelyBackfill(row.id);
263
+ }
264
+ }
265
+ this.wakePublish();
266
+ }
267
+ else {
268
+ if (params.action === 'open_project')
269
+ this.store.addProject(params.path, params.name);
270
+ else
271
+ this.store.removeProject(params.id);
272
+ const platform = this.platform;
273
+ if (platform)
274
+ this.track(this.register(platform).catch(error => this.failed(error, platform)));
275
+ }
276
+ }
277
+ return this.status();
278
+ }
279
+ catch (error) {
280
+ const cause = error && typeof error === 'object' ? Reflect.get(error, 'cause') ?? error : error;
281
+ if (params.action === 'open_project' && cause && typeof cause === 'object'
282
+ && (['ENOENT', 'ENOTDIR', 'invalid_request'].includes(String(Reflect.get(cause, 'code')))
283
+ || ['Remote project path must be absolute.', 'Remote project path must be a directory.'].includes(String(Reflect.get(cause, 'message'))))) {
284
+ throw commandError('invalid_request', 'Remote project path must name an existing directory and its name must be 1–256 bytes.');
285
+ }
286
+ throw commandError('unavailable', 'Remote configuration could not be saved. Check the directory and credential storage, then retry.');
287
+ }
288
+ });
289
+ this.configureWork = work;
290
+ this.track(work);
291
+ return work;
292
+ }
293
+ setShared(session, shared) {
294
+ const root = this.currentRootId(session);
295
+ if (!root)
296
+ throw commandError('not_found');
297
+ const children = this.options.registry.list().filter(row => row.root === root);
298
+ const share = this.store.settings().share;
299
+ const before = new Map(children.map(row => [row.id, sessionShared(this.sharing(row.id, root, share))]));
300
+ let transition;
301
+ try {
302
+ transition = setSessionSharing(this.sharing(session, root, share), shared);
303
+ }
304
+ catch {
305
+ throw commandError('forbidden', 'Web-created or terminal remote sessions cannot change sharing.');
306
+ }
307
+ this.store.setSession(root, transition.record);
308
+ for (const child of children) {
309
+ if (!shared && before.get(child.id))
310
+ this.markUnshared(child.id);
311
+ if (shared) {
312
+ const record = this.store.session(child.id);
313
+ if (record?.backfilled)
314
+ this.store.setSession(child.id, { ...record, unsharedPending: false, sharedPending: true });
315
+ this.safelyBackfill(child.id);
316
+ }
317
+ }
318
+ this.wakePublish();
319
+ }
320
+ isShared(session) {
321
+ if (!this.configured)
322
+ return false;
323
+ const root = this.currentRootId(session);
324
+ return root !== undefined && sessionShared(this.sharing(session, root));
325
+ }
326
+ rootId(session) {
327
+ return this.rootIds.get(session) ?? this.currentRootId(session);
328
+ }
329
+ currentRootId(session) {
330
+ const row = this.options.registry.options.store.findSessionMetadata(session);
331
+ if (!row || row.deletedAt !== undefined) {
332
+ this.rootIds.delete(session);
333
+ return;
334
+ }
335
+ this.rootIds.set(session, row.root);
336
+ return row.root;
337
+ }
338
+ sharing(session, root = this.rootId(session), share = this.store.settings().share) {
339
+ const record = this.store.session(session);
340
+ const rootRecord = root && root !== session ? this.store.session(root) : undefined;
341
+ return { configured: this.configured, share, ...(record ? { record } : {}), ...(rootRecord ? { rootRecord } : {}) };
342
+ }
343
+ record(session, share) {
344
+ return this.store.session(session) ?? { shared: (share ?? this.store.settings().share) === 'all', explicit: false, origin: 'local', backfilled: false, unsharedPending: false };
345
+ }
346
+ markUnshared(session) {
347
+ this.text.discard(session);
348
+ this.buffered.delete(session);
349
+ this.backfillIssues.delete(session);
350
+ this.backfillRetryAt.delete(session);
351
+ const record = this.record(session);
352
+ if (record.terminal)
353
+ return;
354
+ if (!record.hostId && !record.uploadAttempted && this.outbox.acked(session) === 0) {
355
+ this.outbox.discard(session);
356
+ this.store.setSession(session, { ...record, backfilled: false, unsharedPending: false, sharedPending: false });
357
+ this.recordingBlocked.delete(session);
358
+ this.recordingIssues.delete(session);
359
+ this.blocked.delete(session);
360
+ }
361
+ else
362
+ this.store.setSession(session, { ...record, unsharedPending: true, sharedPending: false });
363
+ }
364
+ apiUrl() { return this.store.settings().apiUrl ?? this.options.apiUrl ?? process.env.FELAN_API_URL ?? 'https://app.felan.ai'; }
365
+ apiOrigin() {
366
+ try {
367
+ this.validateApiUrl(this.apiUrl());
368
+ return new URL(this.apiUrl()).origin;
369
+ }
370
+ catch {
371
+ this.state = 'offline';
372
+ this.reason = 'unavailable';
373
+ this.message = 'Remote API URL is invalid. Run /remote login with a valid HTTPS URL (HTTP is allowed only on loopback).';
374
+ // Status requires an HTTP origin even when the configured endpoint cannot be parsed.
375
+ return 'https://invalid.invalid';
376
+ }
377
+ }
378
+ validateApiUrl(value) {
379
+ try {
380
+ const url = new URL(value);
381
+ if (url.username || url.password || url.search || url.hash || !['https:', 'http:'].includes(url.protocol)
382
+ || /[\\\u0000-\u0020\u007f-\u009f]/u.test(value)
383
+ || url.protocol === 'http:' && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))
384
+ throw new Error();
385
+ }
386
+ catch {
387
+ throw commandError('invalid_request', 'Remote API URL must use HTTPS (HTTP is allowed only on loopback), without credentials, query or fragment.');
388
+ }
389
+ }
390
+ registration() {
391
+ return { name: Array.from(this.options.name ?? hostname()).reduce((name, char) => Buffer.byteLength(name + char) <= 128 ? name + char : name, ''),
392
+ daemonVersion: this.options.daemonVersion ?? DAEMON_VERSION, rowSchemaVersion: ROW_SCHEMA_VERSION,
393
+ capabilities: this.capabilities, projects: this.store.settings().projects };
394
+ }
395
+ register(platform) {
396
+ const generation = this.generation;
397
+ const work = this.registrationWork.catch(() => { }).then(async () => {
398
+ while (this.current(generation)) {
399
+ const registration = this.registration();
400
+ await this.bounded(platform.register(registration));
401
+ if (JSON.stringify(registration) === JSON.stringify(this.registration()))
402
+ return;
403
+ }
404
+ });
405
+ this.registrationWork = work;
406
+ return work;
407
+ }
408
+ login(pat) {
409
+ if (this.stopped)
410
+ return;
411
+ this.configured = true;
412
+ this.state = 'connecting';
413
+ this.reason = undefined;
414
+ this.message = undefined;
415
+ this.supported = undefined;
416
+ this.attempt = 0;
417
+ this.reauthorizedPublications.clear();
418
+ try {
419
+ try {
420
+ this.validateApiUrl(this.apiUrl());
421
+ }
422
+ catch {
423
+ this.apiOrigin();
424
+ return;
425
+ }
426
+ this.token = (this.options.tokenSource ?? (options => new HostTokenSource(options)))({
427
+ pat, apiUrl: this.apiUrl(), installId: this.store.settings().installId,
428
+ daemonVersion: this.options.daemonVersion ?? DAEMON_VERSION, name: this.options.name ?? hostname(), clock: this.clock,
429
+ });
430
+ for (const row of this.options.registry.list())
431
+ this.safelyBackfill(row.id);
432
+ this.connect();
433
+ }
434
+ catch {
435
+ this.state = 'offline';
436
+ this.reason = 'unavailable';
437
+ this.message = 'Remote connection settings are unavailable. Run /remote login to retry.';
438
+ }
439
+ }
440
+ connect() {
441
+ const token = this.token;
442
+ const generation = this.generation;
443
+ if (!token || this.stopped || this.state === 'stopped')
444
+ return;
445
+ try {
446
+ this.validateApiUrl(this.apiUrl());
447
+ }
448
+ catch {
449
+ this.apiOrigin();
450
+ return;
451
+ }
452
+ const work = (async () => {
453
+ let platform;
454
+ try {
455
+ let abandoned = false;
456
+ const opening = (this.options.connect ?? (source => PlatformClient.connect(source)))(token);
457
+ void opening.then(opened => { if (abandoned)
458
+ void this.bounded(opened.close()).catch(() => { }); }, () => { });
459
+ try {
460
+ platform = await this.bounded(opening);
461
+ }
462
+ catch (error) {
463
+ abandoned = true;
464
+ throw error;
465
+ }
466
+ if (!this.current(generation)) {
467
+ await this.bounded(platform.close()).catch(() => { });
468
+ return;
469
+ }
470
+ const host = token.current()?.host.id;
471
+ if (!host)
472
+ throw new PlatformError('invalid', 'Host metadata is unavailable.');
473
+ this.host = host;
474
+ this.fenceRepairedSessions(host);
475
+ await this.register(platform);
476
+ if (!this.current(generation)) {
477
+ await this.bounded(platform.close()).catch(() => { });
478
+ return;
479
+ }
480
+ for (const id of this.store.sessions()) {
481
+ const record = this.store.session(id);
482
+ if (!record.terminal && record.indexedHostId === host)
483
+ this.indexed.add(id);
484
+ }
485
+ this.platform = platform;
486
+ this.unsubscribeCommands = platform.realtime.subscribeCommands(host, () => this.wakeClaims(), () => this.wakeClaims());
487
+ this.state = 'connected';
488
+ this.reason = undefined;
489
+ this.message = undefined;
490
+ this.attempt = 0;
491
+ this.clearStoppedPresentations();
492
+ this.wakeClaims();
493
+ this.wakePublish();
494
+ this.heartbeat(platform);
495
+ }
496
+ catch (error) {
497
+ if (platform && platform !== this.platform)
498
+ void this.bounded(platform.close()).catch(() => { });
499
+ if (this.current(generation))
500
+ await this.failed(error);
501
+ }
502
+ })();
503
+ this.track(work);
504
+ }
505
+ fenceRepairedSessions(host) {
506
+ for (const id of this.store.sessions()) {
507
+ const record = this.store.session(id);
508
+ if (!record.terminal && record.hostId && record.hostId !== host)
509
+ this.terminate(id, 'forbidden');
510
+ }
511
+ }
512
+ heartbeat(platform) {
513
+ this.heartbeatTimer = schedule(this.clock, () => {
514
+ this.heartbeatTimer = undefined;
515
+ this.track((async () => {
516
+ try {
517
+ await this.bounded(platform.heartbeat());
518
+ if (this.platform !== platform || this.stopped)
519
+ return;
520
+ this.clearStoppedPresentations();
521
+ this.wakeClaims();
522
+ this.wakePublish();
523
+ this.heartbeat(platform);
524
+ }
525
+ catch (error) {
526
+ await this.failed(error, platform);
527
+ }
528
+ })());
529
+ }, this.options.heartbeatMs ?? 30_000);
530
+ }
531
+ observe(event) {
532
+ try {
533
+ if (this.stopped || !this.configured || !('change' in event) && (!this.platform || this.state !== 'connected'))
534
+ return;
535
+ if ('change' in event && !isPublishedChangeType(event.change.type))
536
+ return;
537
+ if (this.recordingBlocked.has(event.session))
538
+ return;
539
+ const root = this.rootId(event.session);
540
+ if (!root)
541
+ return;
542
+ const sharing = this.sharing(event.session, root);
543
+ if (!sessionShared(sharing))
544
+ return;
545
+ if ('change' in event) {
546
+ if (event.change.type === 'message.completed' && 'message' in event.change) {
547
+ for (const text of this.text.finish(event.session, event.change.message.id))
548
+ if (this.indexed.has(event.session))
549
+ this.platform?.realtime.sendText(text);
550
+ }
551
+ if (!sharing.record?.backfilled || this.backfilling.has(event.session)) {
552
+ const buffer = this.buffered.get(event.session) ?? { items: [], bytes: 0 };
553
+ const item = { rev: Math.max(1, event.rev), occurredAt: this.clock.now(), change: event.change };
554
+ const bytes = Buffer.byteLength(JSON.stringify(item));
555
+ // A future snapshot subsumes this bounded pre-snapshot window.
556
+ if (buffer.bytes + bytes > 4 * 1024 * 1024) {
557
+ buffer.items = [];
558
+ buffer.bytes = 0;
559
+ }
560
+ if (bytes <= 4 * 1024 * 1024) {
561
+ buffer.items.push(item);
562
+ buffer.bytes += bytes;
563
+ }
564
+ this.buffered.set(event.session, buffer);
565
+ this.safelyBackfill(event.session);
566
+ }
567
+ else {
568
+ const change = this.publicationChange(event.session, event.change);
569
+ if (change) {
570
+ this.outbox.append(event.session, [{ rev: Math.max(1, event.rev), occurredAt: this.clock.now(), change }]);
571
+ this.wakePublish();
572
+ }
573
+ }
574
+ }
575
+ else if (this.platform && this.state === 'connected') {
576
+ const safe = this.text.push(event);
577
+ if (safe && this.indexed.has(event.session))
578
+ this.platform.realtime.sendText(safe);
579
+ }
580
+ }
581
+ catch (error) {
582
+ this.recordingFailure(event.session, error);
583
+ }
584
+ }
585
+ safelyBackfill(session) {
586
+ try {
587
+ if (!this.outboxErrorsWired) {
588
+ this.outbox.setErrorHandler((id, error) => this.recordingFailure(id, error));
589
+ this.outboxErrorsWired = true;
590
+ }
591
+ if (!this.currentRootId(session)) {
592
+ this.removeLocalSession(session);
593
+ return;
594
+ }
595
+ this.backfill(session);
596
+ }
597
+ catch (error) {
598
+ this.recordingFailure(session, error);
599
+ }
600
+ }
601
+ blockRecording(session, reason) {
602
+ this.recordingBlocked.add(session);
603
+ this.recordingIssues.set(session, reason);
604
+ this.buffered.delete(session);
605
+ }
606
+ recordingFailure(session, error) {
607
+ if (error instanceof RemoteOutboxCapacityError) {
608
+ this.blockRecording(session, `Recording stopped: pending outbox reached the ${error.scope === 'total' ? '256 MiB total' : '64 MiB session'} limit. Changes during this block will not be uploaded; restart after draining it.`);
609
+ }
610
+ else if (error instanceof RemoteStorageError || error instanceof RemoteIntegrityError) {
611
+ this.blockRecording(session, 'Recording failed: local storage is unavailable. Changes during this block will not be uploaded; restart after checking local storage.');
612
+ }
613
+ else {
614
+ this.backfillIssues.set(session, 'Backfill unavailable; retrying on a later publishable change or residency.');
615
+ this.backfillRetryAt.set(session, this.clock.now() + 1_000);
616
+ }
617
+ }
618
+ publicationChange(session, change) {
619
+ try {
620
+ return publishableChange(change);
621
+ }
622
+ catch {
623
+ const record = this.record(session);
624
+ this.store.setSession(session, { ...record, skippedChanges: (record.skippedChanges ?? 0) + 1 });
625
+ return;
626
+ }
627
+ }
628
+ backfill(session) {
629
+ if (this.stopped || !this.configured || this.recordingBlocked.has(session) || this.backfilling.has(session)
630
+ || this.backfillScheduled.has(session) || (this.backfillRetryAt.get(session) ?? 0) > this.clock.now()
631
+ || !this.options.registry.binding(session))
632
+ return;
633
+ const sharing = this.sharing(session);
634
+ if (!sessionShared(sharing) || sharing.record?.backfilled)
635
+ return;
636
+ this.backfillScheduled.add(session);
637
+ // Projection observers run mid-refresh; snapshot() must not re-enter that refresh.
638
+ queueMicrotask(() => {
639
+ this.backfillScheduled.delete(session);
640
+ try {
641
+ this.performBackfill(session);
642
+ }
643
+ catch (error) {
644
+ this.recordingFailure(session, error);
645
+ }
646
+ });
647
+ }
648
+ performBackfill(session) {
649
+ if (this.stopped || !this.configured || this.recordingBlocked.has(session) || !this.currentRootId(session)
650
+ || !this.options.registry.binding(session))
651
+ return;
652
+ const sharing = this.sharing(session);
653
+ if (!sessionShared(sharing) || sharing.record?.backfilled)
654
+ return;
655
+ const record = sharing.record ?? this.record(session, sharing.share);
656
+ if (this.outbox.nextSeq(session) > 1) {
657
+ this.store.setSession(session, { ...record, backfilled: true });
658
+ this.finishBackfill(session, 0);
659
+ return;
660
+ }
661
+ this.backfilling.add(session);
662
+ try {
663
+ const projection = this.options.registry.projection(session);
664
+ const snapshot = projection.snapshot();
665
+ const revision = Math.max(1, snapshot.revision);
666
+ const pages = [snapshot.history];
667
+ while (pages.at(-1).cursor)
668
+ pages.push(projection.history(pages.at(-1).cursor, snapshot.revision));
669
+ const entries = [];
670
+ const append = (change, occurredAt = this.clock.now()) => {
671
+ const value = this.publicationChange(session, change);
672
+ if (value)
673
+ entries.push({ rev: revision, occurredAt, change: value });
674
+ };
675
+ for (const page of pages.reverse())
676
+ for (const message of page.entries)
677
+ append({ type: 'message.completed', message }, message.timestamp);
678
+ append({ type: 'model.updated', model: snapshot.model, thinking: snapshot.thinking });
679
+ append({ type: 'metadata.updated', title: snapshot.title, state: snapshot.state, residency: snapshot.residency });
680
+ append({ type: 'run.updated', run: snapshot.run });
681
+ append({ type: 'queue.updated', queue: snapshot.queue });
682
+ for (const interaction of snapshot.interactions)
683
+ append({ type: 'interaction.requested', interaction });
684
+ for (const child of snapshot.children)
685
+ append({ type: 'child.updated', child });
686
+ for (const tool of snapshot.tools)
687
+ append({ type: 'tool.updated', tool });
688
+ this.outbox.initialize(session, entries);
689
+ this.store.setSession(session, { ...this.record(session), backfilled: true });
690
+ this.finishBackfill(session, snapshot.revision);
691
+ }
692
+ finally {
693
+ this.backfilling.delete(session);
694
+ }
695
+ }
696
+ finishBackfill(session, revision) {
697
+ const items = this.buffered.get(session)?.items.filter(item => item.rev > revision).flatMap(item => {
698
+ const change = this.publicationChange(session, item.change);
699
+ return change ? [{ ...item, change }] : [];
700
+ }) ?? [];
701
+ if (items.length)
702
+ this.outbox.append(session, items);
703
+ this.buffered.delete(session);
704
+ this.backfillIssues.delete(session);
705
+ this.backfillRetryAt.delete(session);
706
+ this.wakePublish();
707
+ }
708
+ removeLocalSession(session, unshared = false) {
709
+ const record = this.store.session(session);
710
+ const attempted = record?.hostId || record?.uploadAttempted || this.outbox.acked(session) > 0;
711
+ if (attempted && !record?.terminal && !unshared) {
712
+ this.store.setSession(session, { ...this.record(session), shared: false, unsharedPending: true, sharedPending: false });
713
+ this.wakePublish();
714
+ }
715
+ else {
716
+ if (attempted || record?.terminal)
717
+ this.outbox.drop(session);
718
+ else
719
+ this.outbox.discard(session);
720
+ this.store.removeSession(session);
721
+ }
722
+ this.text.discard(session);
723
+ this.indexed.delete(session);
724
+ this.rootIds.delete(session);
725
+ this.buffered.delete(session);
726
+ this.backfillIssues.delete(session);
727
+ this.backfillRetryAt.delete(session);
728
+ this.blocked.delete(session);
729
+ this.recordingBlocked.delete(session);
730
+ this.recordingIssues.delete(session);
731
+ this.publicationGaps.delete(session);
732
+ }
733
+ wakePublish() {
734
+ if (!this.platform || this.stopped)
735
+ return;
736
+ if (this.publishWork) {
737
+ this.publishAgain = true;
738
+ return;
739
+ }
740
+ if (this.publishTimer)
741
+ return;
742
+ this.publishTimer = schedule(this.clock, () => {
743
+ this.publishTimer = undefined;
744
+ const platform = this.platform;
745
+ if (!platform)
746
+ return;
747
+ this.publishAgain = false;
748
+ const work = Promise.resolve().then(() => this.publish(platform));
749
+ this.publishWork = work;
750
+ this.track(work);
751
+ void work.finally(() => { if (this.publishWork === work) {
752
+ this.publishWork = undefined;
753
+ if (this.publishAgain)
754
+ this.wakePublish();
755
+ } }).catch(() => { });
756
+ }, 0);
757
+ }
758
+ async publish(platform) {
759
+ try {
760
+ do {
761
+ this.publishAgain = false;
762
+ let pending = [];
763
+ try {
764
+ pending = this.outbox.sessions();
765
+ }
766
+ catch (error) {
767
+ if (!(error instanceof RemoteStorageError || error instanceof RemoteIntegrityError))
768
+ throw error;
769
+ this.recordingFailure('outbox', error);
770
+ }
771
+ const ids = new Set([...pending, ...this.store.sessions().filter(id => {
772
+ const record = this.store.session(id);
773
+ return record?.unsharedPending || record?.sharedPending || record?.backfilled && !this.indexed.has(id) && !record.terminal;
774
+ })]);
775
+ for (const id of ids) {
776
+ if (this.platform !== platform || this.stopped)
777
+ return;
778
+ const record = this.store.session(id);
779
+ if (this.blocked.has(id) && !record?.unsharedPending || record?.terminal)
780
+ continue;
781
+ try {
782
+ if (await this.publishSession(platform, id))
783
+ this.publishAgain = true;
784
+ }
785
+ catch (error) {
786
+ if (error instanceof RemoteStorageError || error instanceof RemoteIntegrityError) {
787
+ this.recordingFailure(id, error);
788
+ continue;
789
+ }
790
+ if (error instanceof PlatformError && error.kind === 'revoked' && error.httpStatus === 403) {
791
+ if (!this.reauthorizedPublications.has(id)) {
792
+ await this.failed(error, platform, id);
793
+ return;
794
+ }
795
+ this.blocked.set(id, 'Publication rejected again after token exchange; check the session payload before restarting the daemon.');
796
+ continue;
797
+ }
798
+ if (error instanceof HostTokenExchangeError || error instanceof PlatformError && ['retry', 'revoked', 'unsupported_schema'].includes(error.kind))
799
+ throw error;
800
+ this.blocked.set(id, 'Publication failed; restart the daemon after checking the session and local storage.');
801
+ }
802
+ }
803
+ } while (this.publishAgain && this.platform === platform && !this.stopped);
804
+ }
805
+ catch (error) {
806
+ if (error instanceof RemoteStorageError || error instanceof RemoteIntegrityError)
807
+ this.recordingFailure('outbox', error);
808
+ else
809
+ await this.failed(error, platform);
810
+ }
811
+ }
812
+ async publishSession(platform, id) {
813
+ try {
814
+ return await this.publishSessionNow(platform, id);
815
+ }
816
+ catch (error) {
817
+ if (error instanceof RemoteStorageError || error instanceof RemoteIntegrityError || error instanceof RemoteOutboxCapacityError)
818
+ this.recordingFailure(id, error);
819
+ throw error;
820
+ }
821
+ }
822
+ async publishSessionNow(platform, id) {
823
+ const row = this.options.registry.options.store.findSessionMetadata(id);
824
+ if (!row || row.deletedAt !== undefined) {
825
+ this.removeLocalSession(id);
826
+ if (!row || !this.store.session(id)?.unsharedPending)
827
+ return false;
828
+ }
829
+ if (this.platform === platform && !this.stopped) {
830
+ const record = this.record(id);
831
+ if (record.hostId && this.host && record.hostId !== this.host) {
832
+ this.terminate(id, 'forbidden');
833
+ return false;
834
+ }
835
+ const shared = this.isShared(id);
836
+ if (!shared && !record.unsharedPending)
837
+ return false;
838
+ const pending = shared ? this.outbox.pending(id) : [];
839
+ if (shared && !pending.length && !record.sharedPending && this.indexed.has(id)) {
840
+ this.publicationGaps.delete(id);
841
+ return false;
842
+ }
843
+ const currentRow = this.options.registry.list().find(row => row.id === id) ?? row;
844
+ const session = publishedSession({ ...currentRow, session: id, shared });
845
+ const batch = publicationBatches(session, pending.map(entry => publishedEvent(id, entry)))[0];
846
+ const host = this.host;
847
+ const generation = this.generation;
848
+ try {
849
+ if (!record.uploadAttempted)
850
+ this.store.setSession(id, { ...record, uploadAttempted: true, ...(host ? { hostId: host } : {}) });
851
+ const acknowledged = await this.bounded(platform.publish(session, batch));
852
+ if (this.platform !== platform || !this.current(generation) || host !== this.host)
853
+ return false;
854
+ this.reauthorizedPublications.delete(id);
855
+ if (!this.currentRootId(id)) {
856
+ this.removeLocalSession(id, !shared);
857
+ return false;
858
+ }
859
+ if (batch.length && (acknowledged < batch.at(-1).seq || acknowledged >= this.outbox.nextSeq(id))) {
860
+ throw new PlatformError('sequence_gap', 'Invalid publication acknowledgement.', acknowledged);
861
+ }
862
+ this.options.owner.assertCurrent();
863
+ const current = this.record(id);
864
+ const next = { ...current, ...(host ? { hostId: host, indexedHostId: host } : {}), ...(!shared && !this.isShared(id) ? { unsharedPending: false } : {}), ...(shared && this.isShared(id) ? { sharedPending: false } : {}) };
865
+ if (next.hostId !== current.hostId || next.indexedHostId !== current.indexedHostId || next.unsharedPending !== current.unsharedPending || next.sharedPending !== current.sharedPending)
866
+ this.store.setSession(id, next);
867
+ if (host && this.host && host !== this.host) {
868
+ this.terminate(id, 'forbidden');
869
+ return false;
870
+ }
871
+ this.indexed.add(id);
872
+ if (batch.length)
873
+ this.outbox.acknowledge(id, acknowledged);
874
+ this.publicationGaps.delete(id);
875
+ return true;
876
+ }
877
+ catch (error) {
878
+ if (this.platform !== platform || this.stopped)
879
+ return false;
880
+ if (error instanceof PlatformError && ['session_deleted', 'forbidden'].includes(error.kind)) {
881
+ this.terminate(id, error.kind === 'session_deleted' ? 'deleted' : 'forbidden');
882
+ if (!this.currentRootId(id))
883
+ this.removeLocalSession(id, true);
884
+ return false;
885
+ }
886
+ if (error instanceof PlatformError && error.kind === 'sequence_gap') {
887
+ const seq = error.lastSeq;
888
+ const previous = this.publicationGaps.get(id);
889
+ if (seq !== undefined && Number.isSafeInteger(seq) && seq >= this.outbox.acked(id) && seq !== previous?.lastSeq) {
890
+ const fastForward = seq >= this.outbox.nextSeq(id);
891
+ if (!fastForward || !previous?.fastForwarded) {
892
+ if (fastForward)
893
+ this.outbox.fastForward(id, seq);
894
+ else
895
+ this.outbox.acknowledge(id, seq);
896
+ this.publicationGaps.set(id, { lastSeq: seq, fastForwarded: fastForward || previous?.fastForwarded === true });
897
+ return true;
898
+ }
899
+ }
900
+ this.blocked.set(id, 'Publication sequence cannot be recovered from the local outbox; restart the daemon after checking local storage.');
901
+ return false;
902
+ }
903
+ throw error;
904
+ }
905
+ }
906
+ return false;
907
+ }
908
+ terminate(session, terminal) {
909
+ this.store.setSession(session, { ...this.record(session), terminal, unsharedPending: false });
910
+ this.outbox.drop(session);
911
+ this.indexed.delete(session);
912
+ this.publicationGaps.delete(session);
913
+ }
914
+ wakeClaims() {
915
+ if (!this.platform || this.stopped || this.draining)
916
+ return;
917
+ if (this.claimWork) {
918
+ this.claimAgain = true;
919
+ return;
920
+ }
921
+ const platform = this.platform;
922
+ const work = (async () => {
923
+ try {
924
+ do {
925
+ this.claimAgain = false;
926
+ const startedAt = this.clock.now();
927
+ const rows = await this.bounded(platform.claim());
928
+ const receivedAt = this.clock.now();
929
+ // An empty claim proves this host's old commands have drained after journal loss.
930
+ if (!rows.length && this.platform === platform && !this.stopped && this.host && this.options.database.rebuilt) {
931
+ const database = this.options.database;
932
+ if (database.record('command_recovery', this.host) === undefined)
933
+ database.transaction(() => database.putRecord('command_recovery', this.host, 'drained'));
934
+ }
935
+ for (const row of rows.sort((a, b) => a.id.localeCompare(b.id))) {
936
+ if (this.platform !== platform || this.stopped || this.draining)
937
+ return;
938
+ if (!row.id)
939
+ continue;
940
+ const outcome = await this.executor.execute(row, { startedAt, receivedAt });
941
+ if (this.platform !== platform || this.stopped)
942
+ return;
943
+ try {
944
+ await this.bounded(platform.complete(row.id, outcome));
945
+ }
946
+ catch (error) {
947
+ if (!(error instanceof PlatformError))
948
+ throw error;
949
+ if (error.kind === 'invalid') {
950
+ try {
951
+ await this.bounded(platform.complete(row.id, { v: 1, re: row.id, outcome: 'rejected',
952
+ error: { code: 'unavailable', message: 'Remote command outcome could not be published.', retryable: false } }));
953
+ }
954
+ catch (fallback) {
955
+ if (!(fallback instanceof PlatformError) || fallback.kind !== 'conflict') {
956
+ continue;
957
+ }
958
+ }
959
+ }
960
+ else if (!['conflict', 'forbidden', 'session_deleted'].includes(error.kind))
961
+ throw error;
962
+ }
963
+ this.executor.forget(row.id);
964
+ }
965
+ } while (this.claimAgain && !this.draining && this.platform === platform && !this.stopped);
966
+ }
967
+ catch (error) {
968
+ await this.failed(error, platform);
969
+ }
970
+ })();
971
+ this.claimWork = work;
972
+ this.track(work);
973
+ void work.finally(() => { if (this.claimWork === work) {
974
+ this.claimWork = undefined;
975
+ if (this.claimAgain)
976
+ this.wakeClaims();
977
+ } }).catch(() => { });
978
+ }
979
+ async failed(error, expected, reauthorizeSession) {
980
+ if (this.stopped || expected && this.platform !== expected || this.state === 'stopped')
981
+ return;
982
+ const generation = this.generation;
983
+ this.clearTimers();
984
+ const close = this.disconnect();
985
+ let failure = error;
986
+ if (failure instanceof PlatformError && failure.kind === 'unsupported_schema') {
987
+ failure = new HostTokenExchangeError('unsupported_schema', 'Publishing stopped.', failure.supported ? { protocol: [], rowSchemaVersion: failure.supported } : undefined);
988
+ }
989
+ else if (failure instanceof PlatformError && failure.kind === 'revoked') {
990
+ try {
991
+ this.token?.invalidate();
992
+ await this.bounded(this.token.token());
993
+ if (reauthorizeSession && this.current(generation))
994
+ this.reauthorizedPublications.add(reauthorizeSession);
995
+ }
996
+ catch (exchange) {
997
+ failure = exchange;
998
+ }
999
+ }
1000
+ if (!this.current(generation))
1001
+ return;
1002
+ if (failure instanceof HostTokenExchangeError && failure.kind in stoppedMessages) {
1003
+ this.state = 'stopped';
1004
+ this.reason = failure.kind;
1005
+ this.message = stoppedMessages[this.reason];
1006
+ this.supported = failure.supported;
1007
+ this.token?.stop();
1008
+ this.notifyStopped();
1009
+ }
1010
+ else {
1011
+ this.state = 'offline';
1012
+ this.reason = (failure instanceof PlatformError || failure instanceof HostTokenExchangeError) && failure.rateLimited ? 'rate_limited' : 'unavailable';
1013
+ this.message = 'Remote connection is unavailable; local work is unaffected. Retrying.';
1014
+ this.retryTimer = schedule(this.clock, () => { this.retryTimer = undefined; this.connect(); }, retryDelay(this.attempt++));
1015
+ }
1016
+ await this.bounded(close, this.options.shutdownTimeoutMs ?? 2_000).catch(() => { });
1017
+ }
1018
+ notifyStopped() {
1019
+ for (const row of this.options.registry.list()) {
1020
+ try {
1021
+ if (this.isShared(row.id) && this.options.registry.binding(row.id) && this.options.registry.attachmentCount(row.id)) {
1022
+ this.options.registry.projection(row.id).setPresentation({ id: 'remote-stopped', kind: 'notification', level: 'warning', text: this.message });
1023
+ }
1024
+ }
1025
+ catch { }
1026
+ }
1027
+ }
1028
+ clearStoppedPresentations() {
1029
+ for (const row of this.options.registry.list()) {
1030
+ if (this.options.registry.binding(row.id) && this.options.registry.projection(row.id).presentations().some(item => item.id === 'remote-stopped')) {
1031
+ this.options.registry.projection(row.id).setPresentation({ id: 'remote-stopped', remove: true });
1032
+ }
1033
+ }
1034
+ }
1035
+ disconnect() {
1036
+ this.text.clear();
1037
+ this.indexed.clear();
1038
+ this.unsubscribeCommands();
1039
+ this.unsubscribeCommands = () => { };
1040
+ const platform = this.platform;
1041
+ this.platform = undefined;
1042
+ return platform ? platform.close().catch(() => { }) : Promise.resolve();
1043
+ }
1044
+ clearTimers() {
1045
+ for (const timer of [this.retryTimer, this.heartbeatTimer, this.publishTimer])
1046
+ if (timer)
1047
+ this.clock.clearTimeout(timer);
1048
+ this.retryTimer = undefined;
1049
+ this.heartbeatTimer = undefined;
1050
+ this.publishTimer = undefined;
1051
+ }
1052
+ current(generation) { return !this.stopped && generation === this.generation; }
1053
+ track(work) {
1054
+ this.work.add(work);
1055
+ void work.finally(() => this.work.delete(work)).catch(() => { });
1056
+ }
1057
+ async bounded(work, timeout = this.options.requestTimeoutMs ?? 15_000) {
1058
+ let timer;
1059
+ const deadline = new Promise((_, reject) => { timer = schedule(this.clock, () => reject(new PlatformError('retry', 'Remote request timed out.')), timeout); });
1060
+ try {
1061
+ return await Promise.race([work, deadline]);
1062
+ }
1063
+ finally {
1064
+ if (timer)
1065
+ this.clock.clearTimeout(timer);
1066
+ }
1067
+ }
1068
+ }
1069
+ //# sourceMappingURL=connector.js.map