fuli-context 0.7.4-beta.3 → 0.7.6

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 (130) hide show
  1. package/README.md +77 -6
  2. package/README.zh-CN.md +69 -6
  3. package/compose.graphiti.yml +6 -6
  4. package/dist/web/assets/AboutPage-sLV9WChi.js +1 -0
  5. package/dist/web/assets/ConnectionsPage-BWn4ywGJ.js +1 -0
  6. package/dist/web/assets/KnowledgeEditDialog-CZ5ziPrD.js +1 -0
  7. package/dist/web/assets/KnowledgeInspector-CRQWOaos.js +1 -0
  8. package/dist/web/assets/KnowledgeOrganizerPage-Dfj3vIpU.js +1 -0
  9. package/dist/web/assets/KnowledgePage-BRYC9S5Y.js +1 -0
  10. package/dist/web/assets/KnowledgeWorkspace-ChEB5Bwk.js +4 -0
  11. package/dist/web/assets/OverviewPage-Cd66M0RT.js +1 -0
  12. package/dist/web/assets/PersonalProfilePage-RCFHL0Wz.js +1 -0
  13. package/dist/web/assets/PersonalProjectsPage-DEh8XWby.js +1 -0
  14. package/dist/web/assets/ProjectAgentsPage-Crh7PrXa.css +1 -0
  15. package/dist/web/assets/ProjectAgentsPage-DPnaH-TR.js +11 -0
  16. package/dist/web/assets/PublicProjectsPage-mstPEjHe.js +1 -0
  17. package/dist/web/assets/ReviewPage-6Y1SmfZy.js +1 -0
  18. package/dist/web/assets/SearchableSelect-D3HM8YF8.js +1 -0
  19. package/dist/web/assets/SettingsPage--LdTMUm7.js +1 -0
  20. package/dist/web/assets/SettingsPage-CwZp2ka4.css +1 -0
  21. package/dist/web/assets/WritingTastePage-CIpKswvs.js +1 -0
  22. package/dist/web/assets/{console-D_2zC4lm.js → console-BT1P2Db4.js} +1 -1
  23. package/dist/web/assets/{conversation-launcher-settings-DQmMH2j7.js → conversation-launcher-settings-DrJEotdf.js} +1 -1
  24. package/dist/web/assets/i18n-js6gfc7j.js +9 -0
  25. package/dist/web/assets/{identity-DmGcY3Y6.js → identity-CfDfanpa.js} +1 -1
  26. package/dist/web/assets/index-BnH5mcZx.css +1 -0
  27. package/dist/web/assets/index-GqJf1xqg.js +2 -0
  28. package/dist/web/assets/{model-CVoOdRsL.js → model-DJkLAJsg.js} +1 -1
  29. package/dist/web/assets/{useApi-CROJJdhE-CBNg4CdH.js → useApi-CROJJdhE-BdSti7Y2.js} +1 -1
  30. package/dist/web/assets/vue-router-Kk67EaCP.js +1 -0
  31. package/dist/web/index.html +6 -6
  32. package/graph-provider/fuli_graph/app.py +8 -0
  33. package/graph-provider/fuli_graph/collaboration_context.py +69 -6
  34. package/graph-provider/fuli_graph/knowledge_search.py +84 -5
  35. package/graph-provider/fuli_graph/model_validation.py +130 -0
  36. package/graph-provider/fuli_graph/models.py +24 -5
  37. package/graph-provider/fuli_graph/project_agent_access.py +54 -0
  38. package/graph-provider/fuli_graph/project_agent_coordination_models.py +22 -0
  39. package/graph-provider/fuli_graph/project_agent_executor_models.py +635 -0
  40. package/graph-provider/fuli_graph/project_agent_models.py +278 -0
  41. package/graph-provider/fuli_graph/project_agent_routes.py +626 -0
  42. package/graph-provider/fuli_graph/project_agent_task_models.py +545 -0
  43. package/graph-provider/fuli_graph/runtime.py +259 -0
  44. package/graph-provider/fuli_graph/search_projection.py +3 -0
  45. package/graph-provider/fuli_graph/store.py +6 -0
  46. package/graph-provider/fuli_graph/store_knowledge.py +62 -9
  47. package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +122 -0
  48. package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +756 -0
  49. package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +601 -0
  50. package/graph-provider/fuli_graph/store_project_agent_executors.py +1087 -0
  51. package/graph-provider/fuli_graph/store_project_agent_task_reads.py +458 -0
  52. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +705 -0
  53. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +586 -0
  54. package/graph-provider/fuli_graph/store_project_agent_tasks.py +1151 -0
  55. package/graph-provider/fuli_graph/store_project_agents.py +965 -0
  56. package/graph-provider/pyproject.toml +2 -1
  57. package/npm-shrinkwrap.json +2 -2
  58. package/package.json +1 -1
  59. package/skills/capturing-session-knowledge/SKILL.md +39 -0
  60. package/src/adaptive-runtime/executor-pool.js +127 -0
  61. package/src/adaptive-runtime/graph-services.js +166 -0
  62. package/src/adaptive-runtime/lease-client.js +335 -0
  63. package/src/adaptive-runtime/runtime-broker.js +318 -0
  64. package/src/adaptive-runtime/settings.js +111 -0
  65. package/src/adaptive-runtime/state.js +74 -0
  66. package/src/agent-tools/graph-definitions.js +419 -0
  67. package/src/agent-tools/graph-handlers.js +65 -0
  68. package/src/agent-tools/project-agent-definitions.js +307 -0
  69. package/src/backup/graph-backup.js +289 -0
  70. package/src/backup/identity-reconciliation.js +208 -0
  71. package/src/backup/runtime-adapters.js +269 -0
  72. package/src/cli/command-registry.js +6 -2
  73. package/src/cli/graph-data-command.js +112 -0
  74. package/src/cli/local-runtime-command.js +16 -2
  75. package/src/cli/setup-command.js +19 -3
  76. package/src/cli/update-command.js +4 -0
  77. package/src/cli.js +6 -0
  78. package/src/graphiti/collaboration-preference-projection.js +29 -1
  79. package/src/graphiti/collaboration-preference-workflow.js +121 -0
  80. package/src/graphiti/federated-application.js +53 -90
  81. package/src/graphiti/project-agent-control-plane.js +275 -0
  82. package/src/graphiti/project-agent-mapping.js +1024 -0
  83. package/src/graphiti/project-agent-workflows.js +576 -0
  84. package/src/graphiti/provider-client.js +282 -1
  85. package/src/http/api-router.js +63 -9
  86. package/src/http/graph-api-router.js +290 -0
  87. package/src/local-runtime/lifecycle.js +46 -10
  88. package/src/mcp/context-resources.js +15 -6
  89. package/src/mcp/create-mcp-server.js +51 -16
  90. package/src/mcp/instructions.js +7 -5
  91. package/src/mcp/runtime.js +17 -5
  92. package/src/mcp/session-id.js +38 -1
  93. package/src/mcp/tool-annotations.js +71 -0
  94. package/src/mcp-server.js +23 -6
  95. package/src/native-runtime/runtime.js +717 -0
  96. package/src/server.js +6 -2
  97. package/src/setup/agent-installation-status.js +7 -1
  98. package/src/setup/agents.js +3 -1
  99. package/src/setup/claude-code-config.js +3 -1
  100. package/src/setup/codex-bootstrap.js +11 -10
  101. package/src/setup/codex-config.js +7 -1
  102. package/src/setup/cursor-config.js +3 -1
  103. package/src/setup/graph-runtime.js +158 -13
  104. package/src/setup/neo4j-memory-profile.js +43 -0
  105. package/src/setup/options.js +38 -2
  106. package/src/setup/paths.js +14 -0
  107. package/src/setup/setup.js +37 -1
  108. package/src/system/resource-monitor.js +181 -16
  109. package/src/system/runtime-settings.js +10 -1
  110. package/src/system/system-service.js +27 -2
  111. package/dist/web/assets/AboutPage-Dpf5fRWv.js +0 -1
  112. package/dist/web/assets/ConnectionsPage-D21aUDCP.js +0 -1
  113. package/dist/web/assets/KnowledgeEditDialog-u7lpfN3j.js +0 -1
  114. package/dist/web/assets/KnowledgeInspector-k4xnHtXd.js +0 -1
  115. package/dist/web/assets/KnowledgeOrganizerPage-Bdj7Rn84.js +0 -1
  116. package/dist/web/assets/KnowledgePage-UHVAbC1W.js +0 -1
  117. package/dist/web/assets/KnowledgeWorkspace-Lq3d-MeC.js +0 -4
  118. package/dist/web/assets/OverviewPage-CycAgA1V.js +0 -1
  119. package/dist/web/assets/PersonalProfilePage-U9oIV5FT.js +0 -1
  120. package/dist/web/assets/PersonalProjectsPage-C5RpXKcA.js +0 -1
  121. package/dist/web/assets/PublicProjectsPage-DiWxm35E.js +0 -1
  122. package/dist/web/assets/ReviewPage-jxRfGSJK.js +0 -1
  123. package/dist/web/assets/SearchableSelect-B3-5IPup.js +0 -1
  124. package/dist/web/assets/SettingsPage-BqfGDOho.js +0 -1
  125. package/dist/web/assets/SettingsPage-CAfz4qiV.css +0 -1
  126. package/dist/web/assets/WritingTastePage-BJxhMqJR.js +0 -1
  127. package/dist/web/assets/i18n-CwqXDI1C.js +0 -5
  128. package/dist/web/assets/index-IUGLt4lG.js +0 -2
  129. package/dist/web/assets/index-X2j29j8i.css +0 -1
  130. package/dist/web/assets/vue-router-CWSsUUio.js +0 -1
@@ -0,0 +1,335 @@
1
+ import { dirname } from 'node:path';
2
+
3
+ import { resolveSetupPaths } from '../setup/paths.js';
4
+ import { readJsonFile } from '../storage/json-file.js';
5
+ import { readAdaptiveRuntimeSettings } from './settings.js';
6
+
7
+ export function createRuntimeLeaseClient({
8
+ runtimeConfigPath,
9
+ paths = resolveSetupPaths({ dataDir: dirname(runtimeConfigPath) }),
10
+ readJson = readJsonFile,
11
+ readSettings = readAdaptiveRuntimeSettings,
12
+ fetchImpl = globalThis.fetch,
13
+ setHeartbeat = setInterval,
14
+ clearHeartbeat = clearInterval
15
+ }) {
16
+ const leases = new Map();
17
+
18
+ async function withGraphLease(owner, operation) {
19
+ return withLease({ kind: 'graph', owner }, operation);
20
+ }
21
+
22
+ async function withExecutorLease(executorId, owner, operation) {
23
+ return withLease({ kind: 'executor', executorId, owner }, operation);
24
+ }
25
+
26
+ async function acquireGraphLease(owner) {
27
+ return acquireLease({ kind: 'graph', owner });
28
+ }
29
+
30
+ async function acquireExecutorLease(executorId, owner) {
31
+ return acquireLease({ kind: 'executor', executorId, owner });
32
+ }
33
+
34
+ async function acquireLease(input) {
35
+ const settings = readSettings(paths.adaptiveRuntimeSettingsPath);
36
+ if (!settings.enabled) return disabledLease();
37
+
38
+ const consoleUrl = runtimeConsoleUrl(readJson(paths.graphRuntimeStatePath, null));
39
+ const lease = await requestJson(`${consoleUrl}/api/system/runtime/leases`, {
40
+ method: 'POST',
41
+ body: input,
42
+ timeoutMs: 180_000,
43
+ fetchImpl
44
+ });
45
+ const handle = publicLease(lease);
46
+ if (!handle.enabled) return handle;
47
+
48
+ const entry = {
49
+ handle,
50
+ consoleUrl,
51
+ heartbeat: null,
52
+ heartbeatActive: false,
53
+ refreshPromise: null,
54
+ released: false
55
+ };
56
+ leases.set(handle.leaseId, entry);
57
+ try {
58
+ startHeartbeat(entry, settings);
59
+ return { ...entry.handle };
60
+ } catch (error) {
61
+ removeLease(entry);
62
+ await releaseRemote(entry).catch(() => {});
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ async function refreshLease(reference) {
68
+ const entry = findLease(reference);
69
+ if (!entry) return { ...leaseReference(reference), refreshed: false };
70
+ return refreshEntry(entry);
71
+ }
72
+
73
+ async function releaseLease(reference) {
74
+ const entry = findLease(reference);
75
+ if (!entry) return { ...leaseReference(reference), released: false };
76
+
77
+ const pendingRefresh = entry.refreshPromise;
78
+ removeLease(entry);
79
+ if (pendingRefresh) await pendingRefresh.catch(() => {});
80
+
81
+ try {
82
+ const result = await releaseRemote(entry);
83
+ return {
84
+ ...entry.handle,
85
+ released: result?.released !== false
86
+ };
87
+ } catch {
88
+ return { ...entry.handle, released: false };
89
+ }
90
+ }
91
+
92
+ async function withLease(input, operation) {
93
+ if (typeof operation !== 'function') throw new TypeError('Runtime operation is required');
94
+ const handle = await acquireLease(input);
95
+ try {
96
+ return await operation();
97
+ } finally {
98
+ await releaseLease(handle);
99
+ }
100
+ }
101
+
102
+ async function close() {
103
+ const handles = [...leases.values()].map(({ handle }) => ({ ...handle }));
104
+ await Promise.allSettled(handles.map((handle) => releaseLease(handle)));
105
+ }
106
+
107
+ function startHeartbeat(entry, settings) {
108
+ if (entry.heartbeatActive || entry.released) return;
109
+ const ttlSeconds = Number(settings.leaseTtlSeconds);
110
+ const heartbeatMs = Number.isFinite(ttlSeconds) && ttlSeconds > 0
111
+ ? Math.max(1000, Math.floor(ttlSeconds * 1000 / 3))
112
+ : 1000;
113
+ let heartbeat;
114
+ try {
115
+ heartbeat = setHeartbeat(() => {
116
+ if (!entry.heartbeatActive || entry.released) return;
117
+ void refreshEntry(entry).catch(() => {});
118
+ }, heartbeatMs);
119
+ entry.heartbeat = heartbeat;
120
+ entry.heartbeatActive = true;
121
+ heartbeat?.unref?.();
122
+ } catch (error) {
123
+ entry.heartbeat = null;
124
+ entry.heartbeatActive = false;
125
+ if (heartbeat !== undefined) {
126
+ try {
127
+ clearHeartbeat(heartbeat);
128
+ } catch {
129
+ // A failed cleanup must not hide the heartbeat setup error.
130
+ }
131
+ }
132
+ throw error;
133
+ }
134
+ }
135
+
136
+ function stopHeartbeat(entry) {
137
+ if (!entry.heartbeatActive && entry.heartbeat === null) return;
138
+ const heartbeat = entry.heartbeat;
139
+ entry.heartbeat = null;
140
+ entry.heartbeatActive = false;
141
+ if (heartbeat !== undefined && heartbeat !== null) {
142
+ try {
143
+ clearHeartbeat(heartbeat);
144
+ } catch {
145
+ // Releasing a lease remains best effort even if timer cleanup fails.
146
+ }
147
+ }
148
+ }
149
+
150
+ function removeLease(entry) {
151
+ if (entry.released) return;
152
+ entry.released = true;
153
+ stopHeartbeat(entry);
154
+ if (leases.get(entry.handle.leaseId) === entry) leases.delete(entry.handle.leaseId);
155
+ }
156
+
157
+ async function refreshEntry(entry) {
158
+ if (entry.released || leases.get(entry.handle.leaseId) !== entry) {
159
+ return { ...entry.handle, refreshed: false };
160
+ }
161
+ if (entry.refreshPromise) return entry.refreshPromise;
162
+
163
+ const promise = (async () => {
164
+ try {
165
+ const result = await requestJson(
166
+ `${entry.consoleUrl}/api/system/runtime/leases/${encodeURIComponent(entry.handle.leaseId)}`,
167
+ { method: 'PATCH', body: {}, timeoutMs: 10_000, fetchImpl }
168
+ );
169
+ if (entry.released || leases.get(entry.handle.leaseId) !== entry) {
170
+ return { ...entry.handle, refreshed: false };
171
+ }
172
+ const refreshed = result?.refreshed !== false;
173
+ const nextHandle = publicLease({ ...entry.handle, ...result });
174
+ if (nextHandle.enabled) entry.handle = nextHandle;
175
+ if (!refreshed) {
176
+ removeLease(entry);
177
+ } else if (!entry.heartbeatActive) {
178
+ try {
179
+ startHeartbeat(entry, readSettings(paths.adaptiveRuntimeSettingsPath));
180
+ } catch {
181
+ // The successful manual refresh still extends the remote lease.
182
+ // The next explicit refresh can retry heartbeat setup.
183
+ }
184
+ }
185
+ return { ...entry.handle, refreshed };
186
+ } catch (error) {
187
+ stopHeartbeat(entry);
188
+ throw error;
189
+ }
190
+ })();
191
+ entry.refreshPromise = promise;
192
+ try {
193
+ return await promise;
194
+ } finally {
195
+ if (entry.refreshPromise === promise) entry.refreshPromise = null;
196
+ }
197
+ }
198
+
199
+ function findLease(reference) {
200
+ const leaseId = leaseReference(reference).leaseId;
201
+ return leaseId ? leases.get(leaseId) ?? null : null;
202
+ }
203
+
204
+ async function releaseRemote(entry) {
205
+ return requestJson(
206
+ `${entry.consoleUrl}/api/system/runtime/leases/${encodeURIComponent(entry.handle.leaseId)}`,
207
+ { method: 'DELETE', timeoutMs: 10_000, fetchImpl }
208
+ );
209
+ }
210
+
211
+ return {
212
+ acquireGraphLease,
213
+ acquireExecutorLease,
214
+ refreshLease,
215
+ releaseLease,
216
+ withGraphLease,
217
+ withExecutorLease,
218
+ close
219
+ };
220
+ }
221
+
222
+ export function bindRuntimeLeaseAgentTools(application, leaseClient) {
223
+ if (!application || typeof application !== 'object') {
224
+ throw new TypeError('Runtime lease application is required');
225
+ }
226
+ for (const method of [
227
+ 'acquireGraphLease',
228
+ 'acquireExecutorLease',
229
+ 'refreshLease',
230
+ 'releaseLease'
231
+ ]) {
232
+ if (typeof leaseClient?.[method] !== 'function') {
233
+ throw new TypeError(`Runtime lease client is missing ${method}`);
234
+ }
235
+ }
236
+ application.acquireRuntimeLease = ({ kind, executorId = null, owner }) => {
237
+ if (kind === 'graph') {
238
+ if (executorId !== null && executorId !== undefined) {
239
+ throw new TypeError('Graph runtime lease cannot select an executor');
240
+ }
241
+ return leaseClient.acquireGraphLease(owner);
242
+ }
243
+ if (kind === 'executor') {
244
+ if (typeof executorId !== 'string' || !executorId.trim()) {
245
+ throw new TypeError('Executor runtime lease requires executorId');
246
+ }
247
+ return leaseClient.acquireExecutorLease(executorId, owner);
248
+ }
249
+ throw new TypeError('Runtime lease kind must be graph or executor');
250
+ };
251
+ application.refreshRuntimeLease = ({ leaseId }) =>
252
+ leaseClient.refreshLease(leaseId);
253
+ application.releaseRuntimeLease = ({ leaseId }) =>
254
+ leaseClient.releaseLease(leaseId);
255
+ return application;
256
+ }
257
+
258
+ function disabledLease() {
259
+ return { enabled: false, leaseId: null };
260
+ }
261
+
262
+ function leaseReference(value) {
263
+ if (typeof value === 'string' && value) return { enabled: true, leaseId: value };
264
+ return publicLease(value);
265
+ }
266
+
267
+ function publicLease(value, fallback = null) {
268
+ const source = {
269
+ ...(fallback && typeof fallback === 'object' ? fallback : {}),
270
+ ...(value && typeof value === 'object' ? value : {})
271
+ };
272
+ if (source.enabled !== true || typeof source.leaseId !== 'string' || !source.leaseId) {
273
+ return disabledLease();
274
+ }
275
+ const handle = {
276
+ enabled: true,
277
+ leaseId: source.leaseId
278
+ };
279
+ if (typeof source.kind === 'string') handle.kind = source.kind;
280
+ if (source.executorId === null || typeof source.executorId === 'string') {
281
+ handle.executorId = source.executorId;
282
+ }
283
+ for (const key of ['acquiredAt', 'expiresAt']) {
284
+ if (source[key] === null || typeof source[key] === 'string') handle[key] = source[key];
285
+ }
286
+ return handle;
287
+ }
288
+
289
+ function runtimeConsoleUrl(state) {
290
+ if (!state || typeof state.url !== 'string') {
291
+ throw new Error('Adaptive memory needs the Fuli management UI; run `fuli start` first');
292
+ }
293
+ let url;
294
+ try {
295
+ url = new URL(state.url);
296
+ } catch {
297
+ throw new Error('The recorded Fuli management UI address is invalid');
298
+ }
299
+ if (
300
+ url.protocol !== 'http:' ||
301
+ !['127.0.0.1', 'localhost', '::1', '[::1]'].includes(url.hostname) ||
302
+ url.username || url.password ||
303
+ (url.pathname !== '/' && url.pathname !== '') || url.search || url.hash
304
+ ) {
305
+ throw new Error('Adaptive memory only accepts the recorded loopback Fuli management UI');
306
+ }
307
+ return url.origin;
308
+ }
309
+
310
+ async function requestJson(url, {
311
+ method,
312
+ body,
313
+ timeoutMs,
314
+ fetchImpl
315
+ }) {
316
+ const response = await fetchImpl(url, {
317
+ method,
318
+ headers: body === undefined ? undefined : { 'content-type': 'application/json' },
319
+ body: body === undefined ? undefined : JSON.stringify(body),
320
+ signal: AbortSignal.timeout(timeoutMs)
321
+ });
322
+ if (!response.ok) {
323
+ let detail = null;
324
+ try {
325
+ const value = await response.json();
326
+ detail = typeof value?.message === 'string'
327
+ ? value.message
328
+ : typeof value?.error === 'string' ? value.error : null;
329
+ } catch {
330
+ // The status is enough when the local coordinator did not return JSON.
331
+ }
332
+ throw new Error(detail ?? `Adaptive runtime request failed (${response.status})`);
333
+ }
334
+ return response.json();
335
+ }
@@ -0,0 +1,318 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import { createExecutorPool } from './executor-pool.js';
4
+ import { createGraphServices } from './graph-services.js';
5
+ import { normalizeAdaptiveRuntimeSettings } from './settings.js';
6
+ import {
7
+ initialAdaptiveRuntimeState,
8
+ readAdaptiveRuntimeState,
9
+ writeAdaptiveRuntimeState
10
+ } from './state.js';
11
+
12
+ export function createAdaptiveRuntimeBroker({
13
+ paths,
14
+ settings,
15
+ services = createGraphServices({ paths }),
16
+ executorAdapters = new Map(),
17
+ readState = readAdaptiveRuntimeState,
18
+ writeState = writeAdaptiveRuntimeState,
19
+ now = () => Date.now(),
20
+ setTimer = setTimeout,
21
+ clearTimer = clearTimeout
22
+ }) {
23
+ const configured = normalizeAdaptiveRuntimeSettings(settings);
24
+ const leases = new Map();
25
+ const executorPool = createExecutorPool({
26
+ adapters: executorAdapters,
27
+ idleMs: configured.executorIdleSeconds * 1000,
28
+ setTimer,
29
+ clearTimer
30
+ });
31
+ let state = initializeState();
32
+ let providerTimer = null;
33
+ let databaseTimer = null;
34
+ let transition = Promise.resolve();
35
+ let closed = false;
36
+
37
+ scheduleIdle();
38
+
39
+ async function acquire(input = {}) {
40
+ if (!configured.enabled) return disabledLease();
41
+ if (closed) throw new Error('Adaptive runtime coordinator is closed');
42
+ const kind = input.kind ?? 'graph';
43
+ if (!['graph', 'executor'].includes(kind)) {
44
+ throw new TypeError('Runtime lease kind must be graph or executor');
45
+ }
46
+ const executorId = kind === 'executor' ? requiredExecutorId(input.executorId) : null;
47
+ const leaseId = randomUUID();
48
+ const lease = {
49
+ leaseId,
50
+ kind,
51
+ executorId,
52
+ owner: ownerText(input.owner),
53
+ acquiredAt: isoNow(),
54
+ expiresAt: null,
55
+ timer: null
56
+ };
57
+ leases.set(leaseId, lease);
58
+ clearIdleTimers();
59
+ armExpiry(lease);
60
+ try {
61
+ await exclusive(async () => {
62
+ await ensureAwake();
63
+ if (executorId) await executorPool.acquire(executorId, leaseId);
64
+ });
65
+ return publicLease(lease);
66
+ } catch (error) {
67
+ leases.delete(leaseId);
68
+ if (lease.timer) clearTimer(lease.timer);
69
+ if (executorId) executorPool.release(executorId, leaseId);
70
+ touch();
71
+ scheduleIdle();
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ function refresh(leaseId) {
77
+ const lease = leases.get(validLeaseId(leaseId));
78
+ if (!lease) return { refreshed: false };
79
+ armExpiry(lease);
80
+ return { refreshed: true, ...publicLease(lease) };
81
+ }
82
+
83
+ function release(leaseId) {
84
+ const normalized = validLeaseId(leaseId);
85
+ const lease = leases.get(normalized);
86
+ if (!lease) return { released: false };
87
+ leases.delete(normalized);
88
+ if (lease.timer) clearTimer(lease.timer);
89
+ if (lease.executorId) executorPool.release(lease.executorId, normalized);
90
+ touch();
91
+ scheduleIdle();
92
+ return { released: true };
93
+ }
94
+
95
+ async function withLease(input, operation) {
96
+ if (typeof operation !== 'function') throw new TypeError('Runtime operation is required');
97
+ if (!configured.enabled) return operation();
98
+ const lease = await acquire(input);
99
+ const stopHeartbeat = scheduleHeartbeat(lease.leaseId);
100
+ try {
101
+ return await operation();
102
+ } finally {
103
+ stopHeartbeat();
104
+ release(lease.leaseId);
105
+ }
106
+ }
107
+
108
+ function status() {
109
+ const lastActivityMs = Date.parse(state.lastActivityAt);
110
+ return {
111
+ enabled: configured.enabled,
112
+ stage: state.stage,
113
+ activeLeaseCount: leases.size,
114
+ lastActivityAt: state.lastActivityAt,
115
+ updatedAt: state.updatedAt,
116
+ nextProviderSleepAt: nextSleepAt(lastActivityMs, configured.providerIdleSeconds, [
117
+ 'awake', 'degraded'
118
+ ]),
119
+ nextDatabaseSleepAt: nextSleepAt(lastActivityMs, configured.databaseIdleSeconds, [
120
+ 'awake', 'provider-sleeping', 'degraded'
121
+ ]),
122
+ lastError: state.lastError,
123
+ settings: configured,
124
+ executors: executorPool.status()
125
+ };
126
+ }
127
+
128
+ function close() {
129
+ if (closed) return;
130
+ closed = true;
131
+ clearIdleTimers();
132
+ for (const lease of leases.values()) {
133
+ if (lease.timer) clearTimer(lease.timer);
134
+ }
135
+ leases.clear();
136
+ executorPool.close();
137
+ }
138
+
139
+ async function ensureAwake() {
140
+ const alreadyReady = state.stage === 'awake' &&
141
+ (typeof services.ready !== 'function' || await services.ready());
142
+ if (alreadyReady) return;
143
+ persist('waking');
144
+ try {
145
+ await services.start();
146
+ persist('awake');
147
+ } catch (error) {
148
+ persist('degraded', errorMessage(error));
149
+ throw error;
150
+ }
151
+ }
152
+
153
+ function scheduleIdle() {
154
+ clearIdleTimers();
155
+ if (!configured.enabled || closed || leases.size > 0) return;
156
+ const lastActivityMs = Date.parse(state.lastActivityAt);
157
+ if (['awake', 'degraded'].includes(state.stage)) {
158
+ providerTimer = scheduleAt(
159
+ lastActivityMs + configured.providerIdleSeconds * 1000,
160
+ sleepProvider
161
+ );
162
+ }
163
+ if (['awake', 'provider-sleeping', 'degraded'].includes(state.stage)) {
164
+ databaseTimer = scheduleAt(
165
+ lastActivityMs + configured.databaseIdleSeconds * 1000,
166
+ sleepDatabase
167
+ );
168
+ }
169
+ }
170
+
171
+ function scheduleAt(timestamp, operation) {
172
+ const timer = setTimer(() => {
173
+ void exclusive(async () => {
174
+ if (closed || leases.size > 0) return;
175
+ await operation();
176
+ }).catch((error) => {
177
+ persist('degraded', errorMessage(error));
178
+ scheduleIdle();
179
+ });
180
+ }, Math.max(0, timestamp - now()));
181
+ timer?.unref?.();
182
+ return timer;
183
+ }
184
+
185
+ async function sleepProvider() {
186
+ if (!['awake', 'degraded'].includes(state.stage)) return;
187
+ await services.stopProviders();
188
+ persist('provider-sleeping');
189
+ scheduleIdle();
190
+ }
191
+
192
+ async function sleepDatabase() {
193
+ if (state.stage === 'sleeping') return;
194
+ await services.stopDatabases();
195
+ persist('sleeping');
196
+ clearIdleTimers();
197
+ }
198
+
199
+ function exclusive(operation) {
200
+ const result = transition.then(operation, operation);
201
+ transition = result.catch(() => {});
202
+ return result;
203
+ }
204
+
205
+ function scheduleHeartbeat(leaseId) {
206
+ const delayMs = Math.max(1000, Math.floor(configured.leaseTtlSeconds * 1000 / 3));
207
+ const token = { cancelled: false, timer: null };
208
+ const beat = () => {
209
+ if (token.cancelled) return;
210
+ if (!refresh(leaseId).refreshed) return;
211
+ token.timer = setTimer(beat, delayMs);
212
+ token.timer?.unref?.();
213
+ };
214
+ token.timer = setTimer(beat, delayMs);
215
+ token.timer?.unref?.();
216
+ return () => {
217
+ token.cancelled = true;
218
+ if (token.timer) clearTimer(token.timer);
219
+ token.timer = null;
220
+ };
221
+ }
222
+
223
+ function armExpiry(lease) {
224
+ if (lease.timer) clearTimer(lease.timer);
225
+ const expiresAtMs = now() + configured.leaseTtlSeconds * 1000;
226
+ lease.expiresAt = new Date(expiresAtMs).toISOString();
227
+ lease.timer = setTimer(() => release(lease.leaseId), configured.leaseTtlSeconds * 1000);
228
+ lease.timer?.unref?.();
229
+ }
230
+
231
+ function initializeState() {
232
+ const saved = readState(paths.adaptiveRuntimeStatePath);
233
+ const initial = saved ?? initialAdaptiveRuntimeState({
234
+ enabled: configured.enabled,
235
+ now: new Date(now())
236
+ });
237
+ if (!configured.enabled) return persistValue({ ...initial, stage: 'always-on' });
238
+ if (initial.stage === 'always-on' || initial.stage === 'waking') {
239
+ return persistValue({ ...initial, stage: 'awake' });
240
+ }
241
+ return initial;
242
+ }
243
+
244
+ function touch() {
245
+ state = persistValue({ ...state, lastActivityAt: isoNow(), lastError: null });
246
+ }
247
+
248
+ function persist(stage, lastError = null) {
249
+ state = persistValue({ ...state, stage, lastError });
250
+ }
251
+
252
+ function persistValue(value) {
253
+ const next = {
254
+ ...value,
255
+ version: 1,
256
+ updatedAt: isoNow()
257
+ };
258
+ return writeState(paths.adaptiveRuntimeStatePath, next) ?? next;
259
+ }
260
+
261
+ function nextSleepAt(lastActivityMs, idleSeconds, stages) {
262
+ if (!configured.enabled || leases.size > 0 || !stages.includes(state.stage)) return null;
263
+ return new Date(lastActivityMs + idleSeconds * 1000).toISOString();
264
+ }
265
+
266
+ function clearIdleTimers() {
267
+ if (providerTimer) clearTimer(providerTimer);
268
+ if (databaseTimer) clearTimer(databaseTimer);
269
+ providerTimer = null;
270
+ databaseTimer = null;
271
+ }
272
+
273
+ function isoNow() {
274
+ return new Date(now()).toISOString();
275
+ }
276
+
277
+ return { acquire, refresh, release, withLease, status, close };
278
+ }
279
+
280
+ function publicLease(lease) {
281
+ return {
282
+ enabled: true,
283
+ leaseId: lease.leaseId,
284
+ kind: lease.kind,
285
+ executorId: lease.executorId,
286
+ acquiredAt: lease.acquiredAt,
287
+ expiresAt: lease.expiresAt
288
+ };
289
+ }
290
+
291
+ function disabledLease() {
292
+ return { enabled: false, leaseId: null };
293
+ }
294
+
295
+ function validLeaseId(value) {
296
+ if (typeof value !== 'string' || !/^[0-9a-f-]{36}$/i.test(value)) {
297
+ throw new TypeError('Runtime lease ID is invalid');
298
+ }
299
+ return value;
300
+ }
301
+
302
+ function requiredExecutorId(value) {
303
+ if (typeof value !== 'string' || !value.trim() || value.length > 200) {
304
+ throw new TypeError('executorId must be non-empty text with at most 200 characters');
305
+ }
306
+ return value.trim();
307
+ }
308
+
309
+ function ownerText(value) {
310
+ const owner = String(value ?? 'unknown').replace(/[\r\n\t]+/g, ' ').trim();
311
+ return owner.slice(0, 200) || 'unknown';
312
+ }
313
+
314
+ function errorMessage(error) {
315
+ return String(error instanceof Error ? error.message : error)
316
+ .replace(/[\r\n\t]+/g, ' ')
317
+ .slice(0, 500);
318
+ }