fuli-context 0.7.4-beta.2 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -7
- package/README.zh-CN.md +84 -7
- package/compose.graphiti.yml +6 -6
- package/dist/web/assets/AboutPage-ByCUor-j.js +1 -0
- package/dist/web/assets/ConnectionsPage-DkdkZ40Q.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-DSe3Gwep.js +1 -0
- package/dist/web/assets/KnowledgeInspector-CYAjAb9J.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-CV4F54R_.js +1 -0
- package/dist/web/assets/KnowledgePage-vYYXipOi.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-z4u5mXOG.js +4 -0
- package/dist/web/assets/OverviewPage-BWOfeS6k.js +1 -0
- package/dist/web/assets/PersonalProfilePage-c3w4-h7a.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-B7AlLEwf.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-CgftpIPB.css +1 -0
- package/dist/web/assets/ProjectAgentsPage-DTHcL_ox.js +11 -0
- package/dist/web/assets/PublicProjectsPage-DdH0oGB3.js +1 -0
- package/dist/web/assets/ReviewPage-BYIpcIHG.js +1 -0
- package/dist/web/assets/SearchableSelect-Dz13ClQb.js +1 -0
- package/dist/web/assets/SettingsPage-2VkbXgZi.js +1 -0
- package/dist/web/assets/SettingsPage-CwZp2ka4.css +1 -0
- package/dist/web/assets/WritingTastePage-BOPQZuva.js +1 -0
- package/dist/web/assets/{console-DMvHxKf9.js → console-BlAhujsH.js} +1 -1
- package/dist/web/assets/{conversation-launcher-settings-Dnlcw7Rj.js → conversation-launcher-settings-DTYxiLJt.js} +1 -1
- package/dist/web/assets/i18n-Ci3heuiL.js +9 -0
- package/dist/web/assets/{identity-C9mLdR8L.js → identity-4_-v6cB7.js} +1 -1
- package/dist/web/assets/index-BnH5mcZx.css +1 -0
- package/dist/web/assets/index-C67Q0JY2.js +2 -0
- package/dist/web/assets/{model-CsYAR8O3.js → model-yG0YqUyZ.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-BLQiPVw0.js → useApi-CROJJdhE-twhdZArA.js} +1 -1
- package/dist/web/assets/vue-router-C-wot_Di.js +1 -0
- package/dist/web/index.html +6 -6
- package/docs/public-personal-architecture.md +73 -0
- package/graph-provider/fuli_graph/app.py +8 -0
- package/graph-provider/fuli_graph/collaboration_context.py +69 -6
- package/graph-provider/fuli_graph/knowledge_search.py +84 -5
- package/graph-provider/fuli_graph/model_validation.py +134 -0
- package/graph-provider/fuli_graph/models.py +24 -5
- package/graph-provider/fuli_graph/project_agent_access.py +54 -0
- package/graph-provider/fuli_graph/project_agent_executor_models.py +635 -0
- package/graph-provider/fuli_graph/project_agent_models.py +278 -0
- package/graph-provider/fuli_graph/project_agent_routes.py +597 -0
- package/graph-provider/fuli_graph/project_agent_task_models.py +542 -0
- package/graph-provider/fuli_graph/runtime.py +259 -0
- package/graph-provider/fuli_graph/search_projection.py +3 -0
- package/graph-provider/fuli_graph/store.py +6 -0
- package/graph-provider/fuli_graph/store_knowledge.py +62 -9
- package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +756 -0
- package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +601 -0
- package/graph-provider/fuli_graph/store_project_agent_executors.py +1087 -0
- package/graph-provider/fuli_graph/store_project_agent_task_reads.py +458 -0
- package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +699 -0
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +409 -0
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +1144 -0
- package/graph-provider/fuli_graph/store_project_agents.py +965 -0
- package/graph-provider/pyproject.toml +2 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
- package/skills/capturing-session-knowledge/SKILL.md +43 -2
- package/src/adaptive-runtime/executor-pool.js +127 -0
- package/src/adaptive-runtime/graph-services.js +166 -0
- package/src/adaptive-runtime/lease-client.js +335 -0
- package/src/adaptive-runtime/runtime-broker.js +318 -0
- package/src/adaptive-runtime/settings.js +111 -0
- package/src/adaptive-runtime/state.js +74 -0
- package/src/agent-tools/graph-definitions.js +396 -0
- package/src/agent-tools/graph-handlers.js +61 -0
- package/src/agent-tools/project-agent-definitions.js +307 -0
- package/src/backup/graph-backup.js +289 -0
- package/src/backup/identity-reconciliation.js +208 -0
- package/src/backup/runtime-adapters.js +269 -0
- package/src/cli/command-registry.js +7 -2
- package/src/cli/graph-data-command.js +112 -0
- package/src/cli/local-runtime-command.js +16 -2
- package/src/cli/setup-command.js +19 -3
- package/src/cli/update-command.js +4 -0
- package/src/cli/workspace-connection-command.js +333 -0
- package/src/cli.js +11 -0
- package/src/graphiti/collaboration-preference-projection.js +29 -1
- package/src/graphiti/collaboration-preference-workflow.js +121 -0
- package/src/graphiti/federated-application.js +74 -112
- package/src/graphiti/knowledge-provider-mapping.js +7 -1
- package/src/graphiti/project-agent-control-plane.js +263 -0
- package/src/graphiti/project-agent-mapping.js +1003 -0
- package/src/graphiti/project-agent-workflows.js +557 -0
- package/src/graphiti/provider-client.js +270 -1
- package/src/graphiti/runtime-config.js +6 -1
- package/src/graphiti/workspace-provider-client.js +403 -0
- package/src/http/api-router.js +63 -9
- package/src/http/graph-api-router.js +278 -0
- package/src/local-runtime/lifecycle.js +46 -10
- package/src/mcp/context-resources.js +15 -6
- package/src/mcp/create-mcp-server.js +51 -16
- package/src/mcp/instructions.js +7 -5
- package/src/mcp/runtime.js +17 -5
- package/src/mcp/session-id.js +38 -1
- package/src/mcp/tool-annotations.js +69 -0
- package/src/mcp-server.js +23 -6
- package/src/native-runtime/runtime.js +717 -0
- package/src/server.js +6 -2
- package/src/setup/agent-installation-status.js +7 -1
- package/src/setup/agents.js +3 -1
- package/src/setup/claude-code-config.js +3 -1
- package/src/setup/codex-bootstrap.js +11 -10
- package/src/setup/codex-config.js +7 -1
- package/src/setup/cursor-config.js +3 -1
- package/src/setup/graph-runtime.js +158 -13
- package/src/setup/neo4j-memory-profile.js +43 -0
- package/src/setup/options.js +38 -2
- package/src/setup/paths.js +14 -0
- package/src/setup/setup.js +37 -1
- package/src/system/resource-monitor.js +181 -16
- package/src/system/runtime-settings.js +10 -1
- package/src/system/system-service.js +27 -2
- package/dist/web/assets/AboutPage-CftRexSY.js +0 -1
- package/dist/web/assets/ConnectionsPage-D5J91x6d.js +0 -1
- package/dist/web/assets/KnowledgeEditDialog-wotVahvr.js +0 -1
- package/dist/web/assets/KnowledgeInspector-BzkW0mVs.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-BebUDiHt.js +0 -1
- package/dist/web/assets/KnowledgePage-OUT07aKH.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-1pHJ0MNZ.js +0 -4
- package/dist/web/assets/OverviewPage-CHE6XhdS.js +0 -1
- package/dist/web/assets/PersonalProfilePage-CXXiabUy.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-d2wYGW8n.js +0 -1
- package/dist/web/assets/PublicProjectsPage-hxLShtnZ.js +0 -1
- package/dist/web/assets/ReviewPage-DUeyu7E8.js +0 -1
- package/dist/web/assets/SearchableSelect-C5RiB5F8.js +0 -1
- package/dist/web/assets/SettingsPage-BMuASSzQ.js +0 -1
- package/dist/web/assets/SettingsPage-CAfz4qiV.css +0 -1
- package/dist/web/assets/WritingTastePage-B9BCYIwQ.js +0 -1
- package/dist/web/assets/i18n-CWfd8Dtt.js +0 -5
- package/dist/web/assets/index-X2j29j8i.css +0 -1
- package/dist/web/assets/index-nP5p5OaY.js +0 -2
- package/dist/web/assets/vue-router-DN8x6iY1.js +0 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fuli-graph-provider"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.5"
|
|
4
4
|
description = "Graphiti-backed personal and shared project provider for Fuli."
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
dependencies = [
|
|
7
7
|
"fastapi==0.116.1",
|
|
8
8
|
"graphiti-core==0.29.2",
|
|
9
|
+
"openai>=1.91,<2",
|
|
9
10
|
"pydantic-settings==2.10.1",
|
|
10
11
|
"uvicorn[standard]==0.35.0",
|
|
11
12
|
]
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fuli-context",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "fuli-context",
|
|
9
|
-
"version": "0.7.
|
|
9
|
+
"version": "0.7.5",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@hono/node-server": "2.0.12",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fuli-context",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"description": "Local-first collaboration relationship graph for AI agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"src/",
|
|
10
10
|
"dist/web/",
|
|
11
11
|
"docs/external-knowledge-architecture.md",
|
|
12
|
+
"docs/public-personal-architecture.md",
|
|
12
13
|
"examples/external-knowledge/markdown-folder.mjs",
|
|
13
14
|
"graph-provider/Dockerfile",
|
|
14
15
|
"graph-provider/pyproject.toml",
|
|
@@ -213,6 +213,45 @@ lowers ranking and marks the item for attention without deleting its history. Ag
|
|
|
213
213
|
must not demote human/source-confirmed knowledge; authoritative contradiction may require review
|
|
214
214
|
or demotion under the Provider policy.
|
|
215
215
|
|
|
216
|
+
## Coordinate Project Agents
|
|
217
|
+
|
|
218
|
+
Use a project Agent team only when the user requests delegation or the host's active delegation
|
|
219
|
+
policy authorizes it and the task has independently verifiable work. Keep one primary coordinator
|
|
220
|
+
responsible for decisions, integration, and final verification. Do not split a small or tightly
|
|
221
|
+
coupled task merely to create more workers.
|
|
222
|
+
|
|
223
|
+
For an eligible task, call `coordinate_project_agent_task` once with the exact current
|
|
224
|
+
`projectPath`, one stable idempotency key, an explicit complexity hint when known, and focused
|
|
225
|
+
context queries. Enable its parallel plan only when there are at least two conflict-free
|
|
226
|
+
workstreams with independent verification; state each workstream boundary. The Provider chooses
|
|
227
|
+
the lead and collaborators from active project assignments, preferring Agents with relevant
|
|
228
|
+
project history. Explicit user-selected Agents and locked policies remain higher priority.
|
|
229
|
+
|
|
230
|
+
Treat the returned durable Agent identity and a host worker as different things. A durable Agent
|
|
231
|
+
keeps its project assignment, preferences, task history, and Agent-scoped graph memory while idle;
|
|
232
|
+
the host creates an execution process only when needed. Start only entries in `worker_plan`, give
|
|
233
|
+
each worker only its returned isolated `context`, role, and workstream boundary, and never expose
|
|
234
|
+
another Agent's private context. Fuli is the control plane and does not itself prove that a native
|
|
235
|
+
worker started.
|
|
236
|
+
|
|
237
|
+
The host adapter must keep an adaptive-runtime graph lease for the whole worker run, and an
|
|
238
|
+
executor lease when Fuli manages that executor. Call `acquire_runtime_lease` before starting
|
|
239
|
+
workers; its MCP process keeps the heartbeat alive. Call `release_runtime_lease` in a `finally`
|
|
240
|
+
path after all results and terminal events are recorded. Record queued/running and terminal state
|
|
241
|
+
with `record_project_agent_task_activity`,
|
|
242
|
+
including concrete worker identity. Call `record_project_agent_executor_actual` only after real
|
|
243
|
+
executor/model use. If the host cannot start a worker, report the task as blocked or failed and do
|
|
244
|
+
not fabricate an execution-summary row. The Provider's `executionSummary` remains authoritative.
|
|
245
|
+
|
|
246
|
+
## Report Actual Worker Execution
|
|
247
|
+
|
|
248
|
+
When a FULI task or task view returns a non-empty Provider `executionSummary`, append one row per worker.
|
|
249
|
+
Include the worker label and occupation emoji, actual executor and `sourceApplication` environment,
|
|
250
|
+
work summary, and `workerStatus`/terminal status. This applies only to Provider-reported actual workers.
|
|
251
|
+
When empty `executionSummary` is returned, omit rows; an absent `executionSummary` also omits rows.
|
|
252
|
+
Configured, allowed, or available clients/executors are not evidence of actual use. Keep this
|
|
253
|
+
Agent-agnostic; client-specific details and work-kind routing remain at adapter boundaries.
|
|
254
|
+
|
|
216
255
|
## Classify Knowledge State
|
|
217
256
|
|
|
218
257
|
Every entity and relationship needs an `originQuadrant`, `confirmationBasis`, and
|
|
@@ -254,8 +293,10 @@ confirmation time. Legacy items without that audit record are `pending`, even if
|
|
|
254
293
|
for new product logic and do not treat them as confirmation evidence.
|
|
255
294
|
|
|
256
295
|
Agent retrieval includes pending items on demand and returns their status so the caller can
|
|
257
|
-
calibrate claims. Only
|
|
258
|
-
|
|
296
|
+
calibrate claims. Only project knowledge whose immutable discovery-time `originQuadrant` is
|
|
297
|
+
`known_known` and whose status is user or authoritative-source `confirmed` may enter public
|
|
298
|
+
review. Knowledge first discovered as `known_unknown`, `unknown_known`, or `unknown_unknown`
|
|
299
|
+
remains in the personal graph even if it is later confirmed; normal personal preview and
|
|
259
300
|
Maintainer review gates still apply.
|
|
260
301
|
|
|
261
302
|
## Grow The Personal Profile Separately
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export function createExecutorPool({
|
|
2
|
+
adapters = new Map(),
|
|
3
|
+
idleMs,
|
|
4
|
+
setTimer = setTimeout,
|
|
5
|
+
clearTimer = clearTimeout
|
|
6
|
+
}) {
|
|
7
|
+
const entries = new Map();
|
|
8
|
+
let closed = false;
|
|
9
|
+
|
|
10
|
+
async function acquire(executorId, leaseId) {
|
|
11
|
+
assertExecutorId(executorId);
|
|
12
|
+
if (closed) throw new Error('Executor pool is closed');
|
|
13
|
+
const entry = entryFor(executorId);
|
|
14
|
+
if (entry.timer) {
|
|
15
|
+
clearTimer(entry.timer);
|
|
16
|
+
entry.timer = null;
|
|
17
|
+
}
|
|
18
|
+
entry.leases.add(leaseId);
|
|
19
|
+
if (!entry.adapter) {
|
|
20
|
+
entry.stage = 'external';
|
|
21
|
+
return snapshotEntry(executorId, entry);
|
|
22
|
+
}
|
|
23
|
+
await exclusive(entry, async () => {
|
|
24
|
+
if (entry.stage !== 'running') {
|
|
25
|
+
entry.stage = 'starting';
|
|
26
|
+
try {
|
|
27
|
+
await entry.adapter.start({ executorId });
|
|
28
|
+
entry.stage = 'running';
|
|
29
|
+
entry.lastError = null;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
entry.leases.delete(leaseId);
|
|
32
|
+
entry.stage = 'failed';
|
|
33
|
+
entry.lastError = errorMessage(error);
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return snapshotEntry(executorId, entry);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function release(executorId, leaseId) {
|
|
42
|
+
const entry = entries.get(executorId);
|
|
43
|
+
if (!entry) return;
|
|
44
|
+
entry.leases.delete(leaseId);
|
|
45
|
+
if (!entry.adapter || entry.leases.size > 0 || entry.timer || closed) return;
|
|
46
|
+
entry.timer = setTimer(() => {
|
|
47
|
+
entry.timer = null;
|
|
48
|
+
if (entry.leases.size > 0 || closed) return;
|
|
49
|
+
void exclusive(entry, async () => {
|
|
50
|
+
if (entry.leases.size > 0 || closed) return;
|
|
51
|
+
await stopEntry(executorId, entry);
|
|
52
|
+
});
|
|
53
|
+
}, idleMs);
|
|
54
|
+
entry.timer?.unref?.();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function stopEntry(executorId, entry) {
|
|
58
|
+
entry.stage = 'stopping';
|
|
59
|
+
try {
|
|
60
|
+
await entry.adapter.stop({ executorId });
|
|
61
|
+
entry.stage = 'idle';
|
|
62
|
+
entry.lastError = null;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
entry.stage = 'failed';
|
|
65
|
+
entry.lastError = errorMessage(error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function status() {
|
|
70
|
+
return [...entries.entries()].map(([executorId, entry]) =>
|
|
71
|
+
snapshotEntry(executorId, entry));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function close() {
|
|
75
|
+
closed = true;
|
|
76
|
+
for (const entry of entries.values()) {
|
|
77
|
+
if (entry.timer) clearTimer(entry.timer);
|
|
78
|
+
entry.timer = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function entryFor(executorId) {
|
|
83
|
+
let entry = entries.get(executorId);
|
|
84
|
+
if (!entry) {
|
|
85
|
+
entry = {
|
|
86
|
+
adapter: adapters.get(executorId) ?? null,
|
|
87
|
+
leases: new Set(),
|
|
88
|
+
timer: null,
|
|
89
|
+
transition: Promise.resolve(),
|
|
90
|
+
stage: adapters.has(executorId) ? 'idle' : 'external',
|
|
91
|
+
lastError: null
|
|
92
|
+
};
|
|
93
|
+
entries.set(executorId, entry);
|
|
94
|
+
}
|
|
95
|
+
return entry;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { acquire, release, status, close };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function exclusive(entry, operation) {
|
|
102
|
+
const result = entry.transition.then(operation, operation);
|
|
103
|
+
entry.transition = result.catch(() => {});
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function snapshotEntry(executorId, entry) {
|
|
108
|
+
return {
|
|
109
|
+
executorId,
|
|
110
|
+
managed: Boolean(entry.adapter),
|
|
111
|
+
stage: entry.stage,
|
|
112
|
+
activeLeaseCount: entry.leases.size,
|
|
113
|
+
lastError: entry.lastError
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function assertExecutorId(value) {
|
|
118
|
+
if (typeof value !== 'string' || !value.trim() || value.length > 200) {
|
|
119
|
+
throw new TypeError('executorId must be non-empty text with at most 200 characters');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function errorMessage(error) {
|
|
124
|
+
return String(error instanceof Error ? error.message : error)
|
|
125
|
+
.replace(/[\r\n\t]+/g, ' ')
|
|
126
|
+
.slice(0, 500);
|
|
127
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { readJsonFile } from '../storage/json-file.js';
|
|
2
|
+
import { createNativeGraphServices } from '../native-runtime/runtime.js';
|
|
3
|
+
import {
|
|
4
|
+
ensureContainerRuntime,
|
|
5
|
+
runDockerCompose
|
|
6
|
+
} from '../setup/container-runtime.js';
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_RUNTIME_SETTINGS,
|
|
9
|
+
managedProviderUrls,
|
|
10
|
+
readRuntimeSettings
|
|
11
|
+
} from '../system/runtime-settings.js';
|
|
12
|
+
|
|
13
|
+
const PERSONAL_SERVICES = Object.freeze({
|
|
14
|
+
providers: ['personal-provider'],
|
|
15
|
+
databases: ['personal-neo4j']
|
|
16
|
+
});
|
|
17
|
+
const WORKSPACE_SERVICES = Object.freeze({
|
|
18
|
+
providers: ['workspace-provider'],
|
|
19
|
+
databases: ['workspace-neo4j']
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export function createGraphServices({
|
|
23
|
+
paths,
|
|
24
|
+
readSettings = readRuntimeSettings,
|
|
25
|
+
createContainerServices = createManagedGraphServices,
|
|
26
|
+
createNativeServices = createNativeGraphServices
|
|
27
|
+
}) {
|
|
28
|
+
const settings = paths.runtimeSettingsPath
|
|
29
|
+
? readSettings(paths.runtimeSettingsPath)
|
|
30
|
+
: DEFAULT_RUNTIME_SETTINGS;
|
|
31
|
+
return settings.graphRuntimeMode === 'native'
|
|
32
|
+
? createNativeServices({ paths })
|
|
33
|
+
: createContainerServices({ paths });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createManagedGraphServices({
|
|
37
|
+
paths,
|
|
38
|
+
readJson = readJsonFile,
|
|
39
|
+
readSettings = readRuntimeSettings,
|
|
40
|
+
ensureRuntime = ensureContainerRuntime,
|
|
41
|
+
runCompose = runDockerCompose,
|
|
42
|
+
fetchImpl = globalThis.fetch,
|
|
43
|
+
wait = delay,
|
|
44
|
+
onProgress = () => {}
|
|
45
|
+
}) {
|
|
46
|
+
let containerRuntime = null;
|
|
47
|
+
|
|
48
|
+
async function runtime() {
|
|
49
|
+
if (containerRuntime?.status === 'ready') return containerRuntime;
|
|
50
|
+
containerRuntime = await ensureRuntime({ onProgress });
|
|
51
|
+
return containerRuntime;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function managedServices() {
|
|
55
|
+
const state = readJson(paths.graphRuntimeStatePath, null);
|
|
56
|
+
const config = readJson(paths.graphRuntimeConfigPath, null);
|
|
57
|
+
const workspace = state?.managedProviders?.includes('development-workspace') ||
|
|
58
|
+
(config?.workspaces ?? []).some(isManagedWorkspace);
|
|
59
|
+
return {
|
|
60
|
+
providers: [
|
|
61
|
+
...PERSONAL_SERVICES.providers,
|
|
62
|
+
...(workspace ? WORKSPACE_SERVICES.providers : [])
|
|
63
|
+
],
|
|
64
|
+
databases: [
|
|
65
|
+
...PERSONAL_SERVICES.databases,
|
|
66
|
+
...(workspace ? WORKSPACE_SERVICES.databases : [])
|
|
67
|
+
],
|
|
68
|
+
providerUrls: providerUrls(config, paths, readSettings, workspace)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function start() {
|
|
73
|
+
const services = managedServices();
|
|
74
|
+
compose([
|
|
75
|
+
'up', '-d', '--no-build',
|
|
76
|
+
...services.databases,
|
|
77
|
+
...services.providers
|
|
78
|
+
], await runtime());
|
|
79
|
+
for (const url of services.providerUrls) await waitForProvider(url, fetchImpl, wait);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function stopProviders() {
|
|
83
|
+
const services = managedServices();
|
|
84
|
+
compose(['stop', '-t', '20', ...services.providers], await runtime());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function stopDatabases() {
|
|
88
|
+
const services = managedServices();
|
|
89
|
+
// Keep shutdown ordering explicit even if an external action restarted a Provider.
|
|
90
|
+
compose(['stop', '-t', '20', ...services.providers], await runtime());
|
|
91
|
+
compose(['stop', '-t', '20', ...services.databases], await runtime());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function ready() {
|
|
95
|
+
const services = managedServices();
|
|
96
|
+
const checks = await Promise.all(services.providerUrls.map((url) =>
|
|
97
|
+
providerReady(url, fetchImpl)));
|
|
98
|
+
return checks.every(Boolean);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function compose(action, selectedRuntime) {
|
|
102
|
+
runCompose([
|
|
103
|
+
'compose',
|
|
104
|
+
'--env-file', paths.graphEnvPath,
|
|
105
|
+
'-f', paths.graphComposePath,
|
|
106
|
+
...action
|
|
107
|
+
], selectedRuntime);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { start, stopProviders, stopDatabases, ready };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function providerUrls(config, paths, readSettings, includesWorkspace) {
|
|
114
|
+
const settings = paths.runtimeSettingsPath
|
|
115
|
+
? readSettings(paths.runtimeSettingsPath)
|
|
116
|
+
: DEFAULT_RUNTIME_SETTINGS;
|
|
117
|
+
const defaults = managedProviderUrls(settings);
|
|
118
|
+
const urls = [config?.personal?.providerUrl ?? defaults.personal];
|
|
119
|
+
if (!includesWorkspace) return urls;
|
|
120
|
+
const managed = (config?.workspaces ?? []).find(isManagedWorkspace);
|
|
121
|
+
return [...urls, managed?.providerUrl ?? defaults.workspace];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isManagedWorkspace(workspace) {
|
|
125
|
+
if (workspace?.managedDevelopment === true) return true;
|
|
126
|
+
try {
|
|
127
|
+
const url = new URL(workspace?.providerUrl);
|
|
128
|
+
return url.protocol === 'http:' && isLoopback(url.hostname) && url.port === '8788';
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function waitForProvider(url, fetchImpl, wait) {
|
|
135
|
+
for (let attempt = 0; attempt < 120; attempt += 1) {
|
|
136
|
+
try {
|
|
137
|
+
const response = await fetchImpl(`${String(url).replace(/\/$/, '')}/health`, {
|
|
138
|
+
signal: AbortSignal.timeout(2500)
|
|
139
|
+
});
|
|
140
|
+
if (response.ok) return;
|
|
141
|
+
} catch {
|
|
142
|
+
// Neo4j recovery and Provider index initialization can take a while after a cold wake.
|
|
143
|
+
}
|
|
144
|
+
if (attempt < 119) await wait(1000);
|
|
145
|
+
}
|
|
146
|
+
throw new Error('The managed graph Provider did not become ready after waking');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function providerReady(url, fetchImpl) {
|
|
150
|
+
try {
|
|
151
|
+
const response = await fetchImpl(`${String(url).replace(/\/$/, '')}/health`, {
|
|
152
|
+
signal: AbortSignal.timeout(2500)
|
|
153
|
+
});
|
|
154
|
+
return response.ok;
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isLoopback(hostname) {
|
|
161
|
+
return ['127.0.0.1', 'localhost', '::1', '[::1]'].includes(hostname);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function delay(ms) {
|
|
165
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
166
|
+
}
|