fuli-context 0.2.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 (220) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +178 -0
  3. package/compose.graphiti.yml +86 -0
  4. package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +1 -0
  5. package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +1 -0
  6. package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +1 -0
  7. package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +1 -0
  8. package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +1 -0
  9. package/dist/web/assets/KnowledgePage-Khuo3cNs.js +1 -0
  10. package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +4 -0
  11. package/dist/web/assets/OverviewPage-DC4C4Fhn.js +1 -0
  12. package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +1 -0
  13. package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +1 -0
  14. package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +1 -0
  15. package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +1 -0
  16. package/dist/web/assets/ReviewPage-Dztk2jDG.js +1 -0
  17. package/dist/web/assets/console-tbvTUi6p.js +1 -0
  18. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  19. package/dist/web/assets/identity-B3s5CXhv.js +1 -0
  20. package/dist/web/assets/index-Devf7glZ.css +1 -0
  21. package/dist/web/assets/index-DwUDhQKc.js +6 -0
  22. package/dist/web/assets/model-DVT8sjy9.js +1 -0
  23. package/dist/web/assets/paths-B790iniO.js +1 -0
  24. package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +1 -0
  25. package/dist/web/assets/vue-router-BYTAoWT3.js +1 -0
  26. package/dist/web/index.html +22 -0
  27. package/graph-provider/Dockerfile +16 -0
  28. package/graph-provider/fuli_graph/__init__.py +5 -0
  29. package/graph-provider/fuli_graph/app.py +547 -0
  30. package/graph-provider/fuli_graph/auth.py +17 -0
  31. package/graph-provider/fuli_graph/collaboration_context.py +290 -0
  32. package/graph-provider/fuli_graph/config.py +34 -0
  33. package/graph-provider/fuli_graph/graph_models.py +123 -0
  34. package/graph-provider/fuli_graph/graph_projection.py +405 -0
  35. package/graph-provider/fuli_graph/graph_query.py +561 -0
  36. package/graph-provider/fuli_graph/knowledge_audit.py +765 -0
  37. package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +246 -0
  38. package/graph-provider/fuli_graph/knowledge_management.py +816 -0
  39. package/graph-provider/fuli_graph/knowledge_records.py +65 -0
  40. package/graph-provider/fuli_graph/knowledge_search.py +723 -0
  41. package/graph-provider/fuli_graph/knowledge_usage_models.py +52 -0
  42. package/graph-provider/fuli_graph/model_validation.py +58 -0
  43. package/graph-provider/fuli_graph/models.py +1179 -0
  44. package/graph-provider/fuli_graph/personal_project_access.py +29 -0
  45. package/graph-provider/fuli_graph/preference_conflicts.py +420 -0
  46. package/graph-provider/fuli_graph/project_action_models.py +142 -0
  47. package/graph-provider/fuli_graph/project_knowledge.py +707 -0
  48. package/graph-provider/fuli_graph/project_management.py +251 -0
  49. package/graph-provider/fuli_graph/provider_values.py +40 -0
  50. package/graph-provider/fuli_graph/runtime.py +350 -0
  51. package/graph-provider/fuli_graph/search_projection.py +156 -0
  52. package/graph-provider/fuli_graph/store.py +24 -0
  53. package/graph-provider/fuli_graph/store_identity.py +237 -0
  54. package/graph-provider/fuli_graph/store_knowledge.py +447 -0
  55. package/graph-provider/fuli_graph/store_projects.py +242 -0
  56. package/graph-provider/fuli_graph/store_publication.py +370 -0
  57. package/graph-provider/fuli_graph/store_records.py +134 -0
  58. package/graph-provider/pyproject.toml +35 -0
  59. package/npm-shrinkwrap.json +6045 -0
  60. package/package.json +99 -0
  61. package/skills/capturing-session-knowledge/SKILL.md +226 -0
  62. package/skills/capturing-session-knowledge/agents/openai.yaml +13 -0
  63. package/skills/grilling-project/SKILL.md +129 -0
  64. package/skills/grilling-project/agents/openai.yaml +4 -0
  65. package/skills/grilling-project/references/project-profile.md +105 -0
  66. package/src/agent/candidate-read-service.js +53 -0
  67. package/src/agent/context-pack-service.js +83 -0
  68. package/src/agent/public-read-policy.js +39 -0
  69. package/src/agent/public-read-service.js +69 -0
  70. package/src/agent/read-projection.js +107 -0
  71. package/src/agent/search-service.js +61 -0
  72. package/src/agent/space-boundary.js +33 -0
  73. package/src/agent-tools/graph-definitions.js +479 -0
  74. package/src/agent-tools/graph-handlers.js +96 -0
  75. package/src/agent-tools/schema.js +37 -0
  76. package/src/agent-tools.js +16 -0
  77. package/src/app/application-error.js +13 -0
  78. package/src/app/bootstrap.js +8 -0
  79. package/src/app/create-application.js +143 -0
  80. package/src/app/state-service.js +38 -0
  81. package/src/candidates.js +78 -0
  82. package/src/classifier.js +63 -0
  83. package/src/cli/command-arguments.js +14 -0
  84. package/src/cli/command-registry.js +78 -0
  85. package/src/cli/commands/candidates.js +17 -0
  86. package/src/cli/commands/ingestion.js +32 -0
  87. package/src/cli/commands/reads.js +50 -0
  88. package/src/cli/commands/spaces.js +20 -0
  89. package/src/cli/invocation.js +47 -0
  90. package/src/cli/local-runtime-command.js +94 -0
  91. package/src/cli/local-runtime-options.js +64 -0
  92. package/src/cli/migrate-command.js +71 -0
  93. package/src/cli/migration-destination.js +117 -0
  94. package/src/cli/setup-agent-selection.js +195 -0
  95. package/src/cli/setup-command.js +85 -0
  96. package/src/cli/uninstall-command.js +67 -0
  97. package/src/cli.js +90 -0
  98. package/src/context-pack.js +206 -0
  99. package/src/defaults.js +1 -0
  100. package/src/extractor.js +77 -0
  101. package/src/fact-history.js +39 -0
  102. package/src/facts/fact-service.js +89 -0
  103. package/src/graphiti/agent-access-policy.js +88 -0
  104. package/src/graphiti/capture-policy.js +84 -0
  105. package/src/graphiti/federated-application.js +1292 -0
  106. package/src/graphiti/personal-context.js +29 -0
  107. package/src/graphiti/project-path-context.js +159 -0
  108. package/src/graphiti/provider-client.js +288 -0
  109. package/src/graphiti/runtime-config.js +85 -0
  110. package/src/graphiti/setup.js +43 -0
  111. package/src/graphiti/source-marker.js +142 -0
  112. package/src/http/api-router.js +133 -0
  113. package/src/http/bounded-json-array.js +19 -0
  114. package/src/http/error-mapping.js +36 -0
  115. package/src/http/graph-api-router.js +257 -0
  116. package/src/http/lens-route.js +50 -0
  117. package/src/http/request-policy.js +41 -0
  118. package/src/http/response.js +29 -0
  119. package/src/http/static-handler.js +55 -0
  120. package/src/ingestion.js +161 -0
  121. package/src/lens/lens-correction-service.js +81 -0
  122. package/src/lens/lens-fact-writer.js +85 -0
  123. package/src/lens/lens-input.js +62 -0
  124. package/src/lens/lens-query-projection.js +87 -0
  125. package/src/lens/lens-query-ranking.js +62 -0
  126. package/src/lens/lens-query.js +143 -0
  127. package/src/lens/lens-resource-projection.js +93 -0
  128. package/src/lens/lens-resource-selection.js +45 -0
  129. package/src/lens/lens-resource-service.js +103 -0
  130. package/src/lens/lens-search-query.js +122 -0
  131. package/src/lens/lens-service.js +183 -0
  132. package/src/local-runtime/lifecycle.js +254 -0
  133. package/src/mcp/create-mcp-server.js +85 -0
  134. package/src/mcp/instructions.js +10 -0
  135. package/src/mcp/project-action-preview-tokens.js +72 -0
  136. package/src/mcp/runtime.js +71 -0
  137. package/src/mcp/tool-annotations.js +63 -0
  138. package/src/mcp/tool-result.js +167 -0
  139. package/src/mcp/tool-schema.js +60 -0
  140. package/src/mcp-server.js +76 -0
  141. package/src/models.js +44 -0
  142. package/src/observer.js +83 -0
  143. package/src/package-metadata.js +6 -0
  144. package/src/project-rules.js +33 -0
  145. package/src/publication/canonical-json.js +39 -0
  146. package/src/publication/outbox-service.js +46 -0
  147. package/src/publication/publication-envelope-schema.js +75 -0
  148. package/src/publication/publication-envelope.js +37 -0
  149. package/src/publication/publication-policy.js +82 -0
  150. package/src/publication/publication-service.js +48 -0
  151. package/src/router.js +58 -0
  152. package/src/runtime-options.js +139 -0
  153. package/src/security/episode-visibility.js +14 -0
  154. package/src/security/fact-visibility.js +15 -0
  155. package/src/security/sensitive-content.js +18 -0
  156. package/src/security/source-metadata-policy.js +8 -0
  157. package/src/server/application-lifecycle.js +34 -0
  158. package/src/server/listen.js +24 -0
  159. package/src/server.js +135 -0
  160. package/src/setup/agent-installation-status.js +97 -0
  161. package/src/setup/agents.js +123 -0
  162. package/src/setup/codex-bootstrap.js +118 -0
  163. package/src/setup/codex-config.js +122 -0
  164. package/src/setup/config-backup.js +22 -0
  165. package/src/setup/container-runtime.js +388 -0
  166. package/src/setup/cursor-config.js +61 -0
  167. package/src/setup/graph-runtime.js +360 -0
  168. package/src/setup/node-runtime.js +24 -0
  169. package/src/setup/options.js +91 -0
  170. package/src/setup/paths.js +47 -0
  171. package/src/setup/session-skill.js +91 -0
  172. package/src/setup/setup.js +85 -0
  173. package/src/setup/uninstall.js +90 -0
  174. package/src/storage/episode-evidence-preview.js +100 -0
  175. package/src/storage/fact-search-order.js +18 -0
  176. package/src/storage/file/episode-evidence.js +37 -0
  177. package/src/storage/file/episode-index.js +59 -0
  178. package/src/storage/file/fact-search.js +60 -0
  179. package/src/storage/file-store.js +437 -0
  180. package/src/storage/import-json.js +70 -0
  181. package/src/storage/json-file.js +41 -0
  182. package/src/storage/legacy-record-validation.js +152 -0
  183. package/src/storage/legacy-snapshot-validation.js +32 -0
  184. package/src/storage/migrate.js +128 -0
  185. package/src/storage/migrations/001-initial.sql +83 -0
  186. package/src/storage/migrations/002-storage-integrity.sql +113 -0
  187. package/src/storage/record-validation.js +46 -0
  188. package/src/storage/snapshot-defaults.js +54 -0
  189. package/src/storage/sqlite/candidates-repository.js +84 -0
  190. package/src/storage/sqlite/episode-evidence-repository.js +154 -0
  191. package/src/storage/sqlite/episode-privacy-sql.js +33 -0
  192. package/src/storage/sqlite/episodes-repository.js +52 -0
  193. package/src/storage/sqlite/fact-search-repository.js +162 -0
  194. package/src/storage/sqlite/facts-repository.js +146 -0
  195. package/src/storage/sqlite/imports-repository.js +39 -0
  196. package/src/storage/sqlite/mapper.js +84 -0
  197. package/src/storage/sqlite/open-database.js +15 -0
  198. package/src/storage/sqlite/outbox-repository.js +103 -0
  199. package/src/storage/sqlite/snapshot-repository.js +69 -0
  200. package/src/storage/sqlite/spaces-repository.js +52 -0
  201. package/src/storage/sqlite/subscriptions-repository.js +54 -0
  202. package/src/storage/sqlite-store.js +159 -0
  203. package/src/storage/stable-json.js +24 -0
  204. package/src/storage/store-port.js +20 -0
  205. package/src/store.js +2 -0
  206. package/src/workspace-protocol/array-validation.js +19 -0
  207. package/src/workspace-protocol/base64url-contract.js +27 -0
  208. package/src/workspace-protocol/constants.js +37 -0
  209. package/src/workspace-protocol/error-contract.js +24 -0
  210. package/src/workspace-protocol/fact-contract.js +110 -0
  211. package/src/workspace-protocol/governance-contract.js +47 -0
  212. package/src/workspace-protocol/index.js +53 -0
  213. package/src/workspace-protocol/json-budget.js +19 -0
  214. package/src/workspace-protocol/manifest-contract.js +186 -0
  215. package/src/workspace-protocol/publication-contract.js +212 -0
  216. package/src/workspace-protocol/query-contract.js +149 -0
  217. package/src/workspace-protocol/scalar-contract.js +192 -0
  218. package/src/workspace-protocol/sync-contract.js +184 -0
  219. package/src/workspace-protocol/text-contract.js +39 -0
  220. package/src/workspace-protocol/validation.js +66 -0
@@ -0,0 +1,1292 @@
1
+ import { GraphitiProviderClient } from './provider-client.js';
2
+ import {
3
+ canonicalProviderUrl,
4
+ readGraphRuntimeConfig
5
+ } from './runtime-config.js';
6
+ import { ApplicationError } from '../app/application-error.js';
7
+ import { detectSensitiveContent } from '../security/sensitive-content.js';
8
+ import {
9
+ CapturePolicyStore,
10
+ capturePolicyPathForRuntime
11
+ } from './capture-policy.js';
12
+ import {
13
+ AgentAccessPolicyStore,
14
+ agentAccessPolicyPathForRuntime
15
+ } from './agent-access-policy.js';
16
+ import {
17
+ personalContextProjectIds,
18
+ personalProjectSearchBatches,
19
+ scopedSearchItems
20
+ } from './personal-context.js';
21
+ import {
22
+ createFuliSourceMarker,
23
+ sourceConsoleUrl
24
+ } from './source-marker.js';
25
+ import { resolvePersonalProjectPath } from './project-path-context.js';
26
+
27
+ export function openFederatedGraphApplication({
28
+ runtimeConfigPath,
29
+ config,
30
+ capturePolicyStore,
31
+ agentAccessPolicyStore,
32
+ fetchImpl = globalThis.fetch
33
+ }) {
34
+ const resolved = config ?? readGraphRuntimeConfig(runtimeConfigPath);
35
+ const policyStore = capturePolicyStore ?? new CapturePolicyStore(
36
+ capturePolicyPathForRuntime(runtimeConfigPath)
37
+ );
38
+ return new FederatedGraphApplication(resolved, {
39
+ fetchImpl,
40
+ capturePolicyStore: policyStore,
41
+ agentAccessPolicyStore: agentAccessPolicyStore ?? new AgentAccessPolicyStore(
42
+ agentAccessPolicyPathForRuntime(runtimeConfigPath)
43
+ ),
44
+ consoleUrl: sourceConsoleUrl(runtimeConfigPath)
45
+ });
46
+ }
47
+
48
+ export class FederatedGraphApplication {
49
+ constructor(config, {
50
+ fetchImpl = globalThis.fetch,
51
+ capturePolicyStore = new CapturePolicyStore(),
52
+ agentAccessPolicyStore = new AgentAccessPolicyStore(),
53
+ consoleUrl = sourceConsoleUrl(null),
54
+ projectPathResolver = resolvePersonalProjectPath
55
+ } = {}) {
56
+ this.graphiti = true;
57
+ this.config = config;
58
+ this.capturePolicyStore = capturePolicyStore;
59
+ this.agentAccessPolicyStore = agentAccessPolicyStore;
60
+ this.consoleUrl = consoleUrl;
61
+ this.projectPathResolver = projectPathResolver;
62
+ this.personal = new GraphitiProviderClient({
63
+ baseUrl: config.personal.providerUrl,
64
+ accessToken: config.personal.accessToken,
65
+ fetchImpl
66
+ });
67
+ this.workspaces = new Map(
68
+ config.workspaces.map((workspace) => {
69
+ const url = canonicalProviderUrl(workspace.providerUrl);
70
+ return [url, {
71
+ ...workspace,
72
+ client: new GraphitiProviderClient({
73
+ baseUrl: url,
74
+ accessToken: workspace.accessToken,
75
+ fetchImpl
76
+ })
77
+ }];
78
+ })
79
+ );
80
+ }
81
+
82
+ async captureSessionKnowledge(input) {
83
+ const capturePolicy = this.getCapturePolicy();
84
+ if (!capturePolicy.enabled) {
85
+ return {
86
+ route: 'disabled',
87
+ status: 'capture_disabled',
88
+ capturePolicy
89
+ };
90
+ }
91
+ assertNoCredentials(input);
92
+ const episode = providerEpisode(input);
93
+ if (input.targetKind === 'personal') {
94
+ if (input.spaceId !== this.config.personal.spaceId) {
95
+ throw new TypeError('Personal capture target must be the active personal space');
96
+ }
97
+ const result = await this.personal.commit({
98
+ space_id: input.spaceId,
99
+ personal_project_id: input.personalProjectId ?? null,
100
+ episode
101
+ });
102
+ return { route: 'personal', ...result };
103
+ }
104
+ if (episode.sensitivity !== 'normal') {
105
+ throw new TypeError('Private or restricted knowledge cannot enter a team-shared project queue');
106
+ }
107
+ assertPublicKnowledgeEligible(episode);
108
+ const workspace = this.#workspace(input.providerUrl);
109
+ const draft = await this.personal.createPublicationDraft({
110
+ personal_space_id: this.config.personal.spaceId,
111
+ target_project_id: input.spaceId,
112
+ provider_url: workspace.providerUrl,
113
+ episode
114
+ });
115
+ return {
116
+ route: 'personal_review',
117
+ status: draft.status,
118
+ draftId: draft.id,
119
+ projectId: input.spaceId,
120
+ providerUrl: workspace.providerUrl
121
+ };
122
+ }
123
+
124
+ async getCollaborationPreferences({
125
+ personalProjectId = null,
126
+ projectPath = null,
127
+ limit = 100,
128
+ agentInvocation = false,
129
+ agentToolName = 'get_collaboration_preferences'
130
+ } = {}) {
131
+ const projectResolution = await this.#resolvePreferenceProject({
132
+ personalProjectId,
133
+ projectPath
134
+ });
135
+ const resolvedProjectId = projectResolution.personalProjectId;
136
+ const [result, queuedConflicts] = await Promise.all([
137
+ this.personal.collaborationPreferences(
138
+ this.config.personal.spaceId,
139
+ resolvedProjectId,
140
+ limit
141
+ ),
142
+ this.personal.listPreferenceConflicts(
143
+ this.config.personal.spaceId,
144
+ 'ai_pending',
145
+ limit
146
+ )
147
+ ]);
148
+ const deferredConflicts = deferredPreferenceConflicts(
149
+ result,
150
+ queuedConflicts
151
+ );
152
+ if (agentInvocation) {
153
+ const items = [
154
+ ...(result.global_preferences ?? []),
155
+ ...(result.project_preferences ?? [])
156
+ ];
157
+ await this.#recordAgentViews(items.map(({ id, item_kind: itemKind }) => ({
158
+ item_id: id,
159
+ item_kind: itemKind
160
+ })), agentToolName);
161
+ }
162
+ const applicationGuidance = {
163
+ apply: 'effective_preferences',
164
+ global_scope: 'Apply personal-global preferences in every user task.',
165
+ project_scope: resolvedProjectId
166
+ ? `Also apply preferences scoped to the matched personal project ${resolvedProjectId}.`
167
+ : 'No exact personal project matched; do not apply project-scoped preferences.',
168
+ conflicts: 'Do not apply entries listed in conflicts until the user resolves them.',
169
+ deferred_conflicts: 'If the current task would use a deferred_conflict, call resolve_deferred_preference_conflict before applying either side. Ignore unrelated deferred conflicts. The resolution must preserve the AI audit marker.',
170
+ authority: 'Human or authoritative-source confirmed preferences outrank agent-confirmed preferences. Agent-confirmed preferences are usable but lower priority and remain explicitly marked.',
171
+ pending: 'Pending preferences are available only through on-demand knowledge search; invalid and unrelated-project preferences are excluded. Automatic preference injection never counts as usage evidence.'
172
+ };
173
+ if (!agentInvocation) {
174
+ return {
175
+ ...result,
176
+ deferred_conflicts: deferredConflicts,
177
+ application_guidance: applicationGuidance,
178
+ project_resolution: agentProjectResolution(projectResolution)
179
+ };
180
+ }
181
+ return {
182
+ effective_preferences: (result.effective_preferences ?? [])
183
+ .map(agentCollaborationPreference),
184
+ deferred_conflicts: deferredConflicts.map(agentDeferredPreferenceConflict),
185
+ application_guidance: applicationGuidance,
186
+ context: {
187
+ personal_space_id: result.personal_space_id,
188
+ personal_project_id: result.personal_project_id,
189
+ global_preference_count: (result.global_preferences ?? []).length,
190
+ project_preference_count: (result.project_preferences ?? []).length,
191
+ conflict_count: (result.conflicts ?? []).length,
192
+ ai_deferred_conflict_count: deferredConflicts.length,
193
+ overridden_global_count: (result.overridden_global_ids ?? []).length,
194
+ source_truncated: result.truncated === true,
195
+ project_resolution: agentProjectResolution(projectResolution)
196
+ }
197
+ };
198
+ }
199
+
200
+ async #resolvePreferenceProject({ personalProjectId, projectPath }) {
201
+ if (projectPath !== undefined && projectPath !== null) {
202
+ const projects = await this.personal.listPersonalProjects(
203
+ this.config.personal.spaceId
204
+ );
205
+ const resolution = this.projectPathResolver(projectPath, projects);
206
+ if (
207
+ personalProjectId
208
+ && resolution.personalProjectId
209
+ && personalProjectId !== resolution.personalProjectId
210
+ ) {
211
+ throw new TypeError(
212
+ 'personalProjectId conflicts with the exact projectPath match'
213
+ );
214
+ }
215
+ if (personalProjectId) {
216
+ return {
217
+ status: 'matched',
218
+ basis: 'explicit_personal_project_id',
219
+ personalProjectId
220
+ };
221
+ }
222
+ return resolution;
223
+ }
224
+ if (personalProjectId) {
225
+ return {
226
+ status: 'matched',
227
+ basis: 'explicit_personal_project_id',
228
+ personalProjectId
229
+ };
230
+ }
231
+ return {
232
+ status: 'not_provided',
233
+ basis: null,
234
+ personalProjectId: null
235
+ };
236
+ }
237
+
238
+ async searchKnowledge({
239
+ personalSpaceId,
240
+ query,
241
+ projectIds = [],
242
+ personalProjectId = null,
243
+ contextPersonalProjectIds = [],
244
+ personalProjectScope = 'bounded',
245
+ limit = 12,
246
+ includeHistorical = false,
247
+ includePending = true,
248
+ agentInvocation = false,
249
+ agentToolName = 'search_knowledge_graph'
250
+ }) {
251
+ if (personalSpaceId !== this.config.personal.spaceId) {
252
+ throw new TypeError('Search must use the active personal space');
253
+ }
254
+ if (!['bounded', 'all_local_confirmed'].includes(personalProjectScope)) {
255
+ throw new TypeError('Unknown personal project search scope');
256
+ }
257
+ const subscriptions = await this.personal.listSubscriptions(personalSpaceId);
258
+ const requestedProjects = new Set(projectIds);
259
+ const selectedSubscriptions = subscriptions.filter(({ project_id: projectId }) =>
260
+ requestedProjects.has(projectId)
261
+ );
262
+ if (selectedSubscriptions.length !== requestedProjects.size) {
263
+ throw new TypeError('Every selected project must be an active subscription');
264
+ }
265
+ const selectedPersonalProjectIds = personalProjectScope === 'all_local_confirmed'
266
+ ? personalContextProjectIds(
267
+ null,
268
+ (await this.personal.listPersonalProjects(personalSpaceId))
269
+ .map(({ project_id: projectId }) => projectId)
270
+ )
271
+ : personalContextProjectIds(personalProjectId, contextPersonalProjectIds);
272
+ const personalSearches = personalProjectSearchBatches(selectedPersonalProjectIds)
273
+ .map((personalProjectIds, index) => this.personal.search({
274
+ space_ids: [personalSpaceId],
275
+ query,
276
+ limit,
277
+ include_historical: includeHistorical,
278
+ include_exploratory: includePending,
279
+ personal_project_ids: personalProjectIds,
280
+ active_personal_project_id: (
281
+ personalProjectScope === 'bounded' &&
282
+ personalProjectId &&
283
+ personalProjectIds.includes(personalProjectId)
284
+ ) ? personalProjectId : null,
285
+ inherit_project_knowledge: personalProjectScope === 'bounded',
286
+ include_personal_global: index === 0
287
+ }));
288
+ const grouped = groupSubscriptions(selectedSubscriptions);
289
+ const workspaceSearches = [...grouped.entries()].map(([providerUrl, items]) => {
290
+ const projectIdsForProvider = items.map(({ project_id: projectId }) => projectId);
291
+ return {
292
+ projectIds: projectIdsForProvider,
293
+ operation: (async () => {
294
+ const workspace = this.#workspace(providerUrl);
295
+ const result = await workspace.client.search({
296
+ space_ids: projectIdsForProvider,
297
+ query,
298
+ limit,
299
+ include_historical: includeHistorical,
300
+ include_exploratory: includePending
301
+ });
302
+ return scopedSearchItems(result, 'project', workspace.providerUrl);
303
+ })()
304
+ };
305
+ });
306
+ const searchResults = await Promise.allSettled([
307
+ ...personalSearches,
308
+ ...workspaceSearches.map(({ operation }) => operation)
309
+ ]);
310
+ const personalSettlements = searchResults.slice(0, personalSearches.length);
311
+ const personalFailure = personalSettlements.find(({ status }) => status === 'rejected');
312
+ if (personalFailure) throw personalFailure.reason;
313
+ const personalItems = personalSettlements
314
+ .map(({ value }) => scopedSearchItems(
315
+ value,
316
+ 'personal',
317
+ null,
318
+ this.config.personal.spaceId
319
+ ));
320
+ const successfulProjectIds = new Set();
321
+ const failedProjectIds = new Set();
322
+ const projects = [];
323
+ searchResults.slice(personalSearches.length).forEach((settlement, index) => {
324
+ const { projectIds: providerProjectIds } = workspaceSearches[index];
325
+ if (settlement.status === 'fulfilled') {
326
+ projects.push(settlement.value);
327
+ providerProjectIds.forEach((projectId) => successfulProjectIds.add(projectId));
328
+ return;
329
+ }
330
+ providerProjectIds.forEach((projectId) => failedProjectIds.add(projectId));
331
+ });
332
+ const requestedProjectIds = [...requestedProjects];
333
+ const searchedProjectIds = requestedProjectIds.filter((projectId) =>
334
+ successfulProjectIds.has(projectId)
335
+ );
336
+ const unavailableProjectIds = requestedProjectIds.filter((projectId) =>
337
+ failedProjectIds.has(projectId)
338
+ );
339
+ const rankedSources = [...personalItems, ...projects];
340
+ const facts = rankedSearchItems(rankedSources, 'facts', limit);
341
+ const entities = rankedSearchItems(rankedSources, 'entities', limit);
342
+ const sourceMarker = createFuliSourceMarker({
343
+ consoleUrl: this.consoleUrl,
344
+ facts,
345
+ entities
346
+ });
347
+ const noMatchSourceMarker = createFuliSourceMarker({
348
+ consoleUrl: this.consoleUrl,
349
+ facts: [],
350
+ entities: []
351
+ });
352
+ if (agentInvocation) {
353
+ await this.#recordAgentViews([
354
+ ...facts
355
+ .filter(({ scope }) => scope === 'personal')
356
+ .map(({ id }) => ({ item_id: id, item_kind: 'relationship' })),
357
+ ...entities
358
+ .filter(({ scope }) => scope === 'personal')
359
+ .map(({ id }) => ({ item_id: id, item_kind: 'entity' }))
360
+ ], agentToolName);
361
+ }
362
+ return {
363
+ query,
364
+ sourceMarker,
365
+ noMatchSourceMarker,
366
+ retrievalGuidance: retrievalGuidanceForScope(personalProjectScope),
367
+ facts,
368
+ entities,
369
+ personalGlobalIncluded: true,
370
+ personalProjectScope,
371
+ searchedPersonalProjectIds: selectedPersonalProjectIds,
372
+ requestedProjectIds,
373
+ searchedProjectIds,
374
+ failedProjectIds: unavailableProjectIds,
375
+ partial: unavailableProjectIds.length > 0,
376
+ subscriptions: selectedSubscriptions
377
+ };
378
+ }
379
+
380
+ async getKnowledgeGraph({
381
+ spaceId,
382
+ providerUrl = null,
383
+ personalProjectId = null,
384
+ limit = 500,
385
+ agentInvocation = false,
386
+ agentToolName = 'get_knowledge_graph'
387
+ }) {
388
+ if (!providerUrl) {
389
+ if (spaceId !== this.config.personal.spaceId) {
390
+ throw new TypeError('A configured providerUrl is required for a team-shared project graph');
391
+ }
392
+ const result = await this.personal.graph(spaceId, limit, personalProjectId);
393
+ if (agentInvocation) {
394
+ await this.#recordAgentViews([
395
+ ...result.nodes.map(({ id }) => ({ item_id: id, item_kind: 'entity' })),
396
+ ...result.edges.map(({ id }) => ({ item_id: id, item_kind: 'relationship' }))
397
+ ], agentToolName);
398
+ }
399
+ return result;
400
+ }
401
+ if (personalProjectId) {
402
+ throw new TypeError('Personal project scope cannot be used with a team-shared provider');
403
+ }
404
+ return this.#workspace(providerUrl).client.graph(spaceId, limit);
405
+ }
406
+
407
+ async recordKnowledgeUsage({
408
+ personalSpaceId,
409
+ taskId,
410
+ sessionId = null,
411
+ toolName = null,
412
+ items
413
+ }) {
414
+ this.#assertActivePersonalSpace(personalSpaceId);
415
+ return this.personal.recordKnowledgeUsage({
416
+ personal_space_id: personalSpaceId,
417
+ task_id: taskId,
418
+ session_id: sessionId,
419
+ tool_name: toolName,
420
+ items: items.map((item) => ({
421
+ item_id: item.itemId,
422
+ item_kind: item.itemKind,
423
+ use_kind: item.useKind
424
+ }))
425
+ });
426
+ }
427
+
428
+ async reviseKnowledgeItem(input) {
429
+ this.#assertActivePersonalSpace(input.personalSpaceId);
430
+ assertSafeKnowledgeMutation(input);
431
+ const body = {
432
+ personal_space_id: input.personalSpaceId,
433
+ personal_project_id: input.personalProjectId ?? null,
434
+ item_kind: input.itemKind,
435
+ action: input.action,
436
+ reason: input.reason,
437
+ name: input.name ?? null,
438
+ summary: input.summary ?? null,
439
+ fact: input.fact ?? null,
440
+ operation_actor: input.operationActor ?? 'agent'
441
+ };
442
+ if (input.currentQuadrant !== undefined) {
443
+ body.current_quadrant = input.currentQuadrant;
444
+ }
445
+ if (input.originQuadrant !== undefined) {
446
+ body.origin_quadrant = input.originQuadrant;
447
+ }
448
+ if (input.epistemicStatus !== undefined) {
449
+ body.epistemic_status = input.epistemicStatus;
450
+ }
451
+ if (input.confirmationStatus !== undefined) {
452
+ body.confirmation_status = input.confirmationStatus;
453
+ }
454
+ if (input.confirmationBasis !== undefined) {
455
+ body.confirmation_basis = providerConfirmationBasis(input.confirmationBasis);
456
+ }
457
+ if (input.reasoningSummary !== undefined) {
458
+ body.reasoning_summary = input.reasoningSummary;
459
+ }
460
+ if (input.profileAspect !== undefined) {
461
+ body.profile_aspect = input.profileAspect;
462
+ }
463
+ if (input.inheritanceMode !== undefined) {
464
+ body.inheritance_mode = input.inheritanceMode;
465
+ }
466
+ if (input.inheritedProjectIds !== undefined) {
467
+ body.inherited_project_ids = input.inheritedProjectIds;
468
+ }
469
+ if (input.replacementItemId !== undefined) {
470
+ body.replacement_item_id = input.replacementItemId;
471
+ }
472
+ if (input.replacementItemKind !== undefined) {
473
+ body.replacement_item_kind = input.replacementItemKind;
474
+ }
475
+ return this.personal.reviseKnowledgeItem(input.itemId, body);
476
+ }
477
+
478
+ async listPreferenceConflicts({
479
+ personalSpaceId,
480
+ status = null,
481
+ limit = 500
482
+ }) {
483
+ this.#assertActivePersonalSpace(personalSpaceId);
484
+ return this.personal.listPreferenceConflicts(
485
+ personalSpaceId,
486
+ status,
487
+ limit
488
+ );
489
+ }
490
+
491
+ async deferPreferenceConflict(input) {
492
+ this.#assertActivePersonalSpace(input.personalSpaceId);
493
+ return this.personal.deferPreferenceConflict({
494
+ personal_space_id: input.personalSpaceId,
495
+ conflict_id: input.conflictId,
496
+ preference_key: input.preferenceKey,
497
+ preference_scope: input.preferenceScope,
498
+ preference_project_id: input.preferenceProjectId ?? null,
499
+ left_item_id: input.leftItemId,
500
+ left_item_kind: input.leftItemKind,
501
+ right_item_id: input.rightItemId,
502
+ right_item_kind: input.rightItemKind,
503
+ reason: input.reason,
504
+ operation_actor: input.operationActor ?? 'human'
505
+ });
506
+ }
507
+
508
+ async resolveDeferredPreferenceConflict(input) {
509
+ this.#assertActivePersonalSpace(input.personalSpaceId);
510
+ assertSafeKnowledgeMutation(input);
511
+ return this.personal.resolvePreferenceConflict(input.conflictId, {
512
+ personal_space_id: input.personalSpaceId,
513
+ resolution: input.resolution,
514
+ reason: input.reason,
515
+ canonical_item_id: input.canonicalItemId ?? null,
516
+ merged_instruction: input.mergedInstruction ?? null,
517
+ split_item_id: input.splitItemId ?? null,
518
+ split_project_id: input.splitProjectId ?? null,
519
+ operation_actor: input.operationActor ?? 'agent'
520
+ });
521
+ }
522
+
523
+ async completePreferenceConflict(input) {
524
+ this.#assertActivePersonalSpace(input.personalSpaceId);
525
+ return this.personal.completePreferenceConflict(input.conflictId, {
526
+ personal_space_id: input.personalSpaceId,
527
+ resolution: input.resolution,
528
+ reason: input.reason,
529
+ operation_actor: input.operationActor ?? 'human'
530
+ });
531
+ }
532
+
533
+ async confirmKnowledgeBatch(input) {
534
+ this.#assertActivePersonalSpace(input.personalSpaceId);
535
+ assertSafeKnowledgeMutation(input);
536
+ return this.personal.confirmKnowledgeBatch({
537
+ personal_space_id: input.personalSpaceId,
538
+ group_kind: input.groupKind,
539
+ group_value: input.groupValue,
540
+ reason: input.reason,
541
+ confirmer: providerConfirmationActor(input.confirmer),
542
+ operation_actor: input.operationActor ?? 'agent',
543
+ items: input.items.map((item) => ({
544
+ item_id: item.itemId,
545
+ item_kind: item.itemKind,
546
+ existence_reason: item.existenceReason,
547
+ quadrant_reason: item.quadrantReason,
548
+ proposed_by: providerConfirmationActor(item.proposedBy)
549
+ }))
550
+ });
551
+ }
552
+
553
+ async reassignKnowledgeItem(input) {
554
+ this.#assertActivePersonalSpace(input.personalSpaceId);
555
+ return this.personal.reassignKnowledgeItem(input.itemId, {
556
+ personal_space_id: input.personalSpaceId,
557
+ item_kind: input.itemKind,
558
+ target_project_id: input.targetProjectId,
559
+ reason: input.reason,
560
+ operation_actor: input.operationActor ?? 'agent'
561
+ });
562
+ }
563
+
564
+ async setPersonalPreferenceScope(input) {
565
+ this.#assertActivePersonalSpace(input.personalSpaceId);
566
+ assertSafeKnowledgeMutation(input);
567
+ return this.personal.setPreferenceScope(input.itemId, {
568
+ personal_space_id: input.personalSpaceId,
569
+ item_kind: input.itemKind,
570
+ scope: input.scope,
571
+ project_id: input.scope === 'project' ? input.projectId : null,
572
+ reason: input.reason,
573
+ operation_actor: input.operationActor ?? 'agent'
574
+ });
575
+ }
576
+
577
+ async previewKnowledgeProjectAction(input) {
578
+ this.#assertActivePersonalSpace(input.personalSpaceId);
579
+ return this.personal.previewKnowledgeProjectAction(input.itemId, {
580
+ personal_space_id: input.personalSpaceId,
581
+ item_kind: input.itemKind,
582
+ mode: input.mode ?? 'existing',
583
+ target_project_id: input.targetProjectId ?? null,
584
+ new_project_id: input.newProjectId ?? null,
585
+ new_project_name: input.newProjectName ?? null,
586
+ new_project_purpose: input.newProjectPurpose ?? null,
587
+ keep_source_relation: input.keepSourceRelation ?? true,
588
+ relation_type: input.relationType ?? 'RELATED_TO',
589
+ conflict_resolution: input.conflictResolution ?? 'defer',
590
+ reason: input.reason ?? null
591
+ });
592
+ }
593
+
594
+ async applyKnowledgeProjectAction(input) {
595
+ this.#assertActivePersonalSpace(input.personalSpaceId);
596
+ assertSafeKnowledgeMutation(input);
597
+ return this.personal.applyKnowledgeProjectAction(input.itemId, {
598
+ personal_space_id: input.personalSpaceId,
599
+ item_kind: input.itemKind,
600
+ mode: input.mode,
601
+ target_project_id: input.targetProjectId ?? null,
602
+ new_project_id: input.newProjectId ?? null,
603
+ new_project_name: input.newProjectName ?? null,
604
+ new_project_purpose: input.newProjectPurpose ?? null,
605
+ keep_source_relation: input.keepSourceRelation ?? true,
606
+ relation_type: input.relationType ?? 'RELATED_TO',
607
+ conflict_resolution: input.conflictResolution ?? 'defer',
608
+ reason: input.reason,
609
+ operation_actor: input.operationActor ?? 'agent'
610
+ });
611
+ }
612
+
613
+ async searchHumanChanges({
614
+ personalSpaceId,
615
+ query = '',
616
+ status = 'all',
617
+ limit = 50,
618
+ agentInvocation = false,
619
+ agentToolName = 'search_human_knowledge_changes'
620
+ }) {
621
+ this.#assertActivePersonalSpace(personalSpaceId);
622
+ const result = await this.personal.searchHumanChanges({
623
+ personal_space_id: personalSpaceId,
624
+ query,
625
+ status,
626
+ limit
627
+ });
628
+ if (agentInvocation) {
629
+ await this.#recordAgentViews(
630
+ result.items.map(({ item_id: itemId, item_kind: itemKind }) => ({
631
+ item_id: itemId,
632
+ item_kind: itemKind
633
+ })),
634
+ agentToolName
635
+ );
636
+ }
637
+ return result;
638
+ }
639
+
640
+ async reviewHumanChange(input) {
641
+ this.#assertActivePersonalSpace(input.personalSpaceId);
642
+ assertSafeKnowledgeMutation(input);
643
+ return this.personal.reviewHumanChange(input.itemId, {
644
+ personal_space_id: input.personalSpaceId,
645
+ item_kind: input.itemKind,
646
+ human_change_version: input.humanChangeVersion,
647
+ conflict_check: input.conflictCheck,
648
+ classification_check: input.classificationCheck,
649
+ note: input.note
650
+ });
651
+ }
652
+
653
+ async listKnowledgeSpaces() {
654
+ const [personalSpaces, personalProjects, subscriptions, ...workspaceSpaces] = await Promise.all([
655
+ this.personal.listSpaces(),
656
+ this.personal.listPersonalProjects(this.config.personal.spaceId),
657
+ this.personal.listSubscriptions(this.config.personal.spaceId),
658
+ ...[...this.workspaces.values()].map(async (workspace) => ({
659
+ providerUrl: workspace.providerUrl,
660
+ principalId: workspace.principalId,
661
+ spaces: await safely(() => workspace.client.listSpaces(), [])
662
+ }))
663
+ ]);
664
+ return {
665
+ activePersonalSpaceId: this.config.personal.spaceId,
666
+ personalSpaces,
667
+ personalProjects,
668
+ projects: workspaceSpaces.flatMap(({ providerUrl, principalId, spaces }) =>
669
+ spaces.map((space) => ({
670
+ ...space,
671
+ providerUrl,
672
+ isOwner: space.owner_id === principalId
673
+ }))
674
+ ),
675
+ subscriptions
676
+ };
677
+ }
678
+
679
+ async subscribePublicProject(input) {
680
+ if (input.personalSpaceId !== this.config.personal.spaceId) {
681
+ throw new TypeError('Subscription must belong to the active personal space');
682
+ }
683
+ const workspace = this.#workspace(input.providerUrl);
684
+ const accessible = await workspace.client.listSpaces();
685
+ if (!accessible.some(({ id }) => id === input.projectId)) {
686
+ throw new TypeError('Public project is not accessible with the configured identity');
687
+ }
688
+ return this.personal.subscribe({
689
+ personal_space_id: input.personalSpaceId,
690
+ project_id: input.projectId,
691
+ provider_url: workspace.providerUrl,
692
+ project_name: input.projectName
693
+ });
694
+ }
695
+
696
+ async unsubscribePublicProject(input) {
697
+ if (input.personalSpaceId !== this.config.personal.spaceId) {
698
+ throw new TypeError('Subscription must belong to the active personal space');
699
+ }
700
+ const workspace = this.#workspace(input.providerUrl);
701
+ return this.personal.unsubscribe(
702
+ input.personalSpaceId,
703
+ input.projectId,
704
+ workspace.providerUrl
705
+ );
706
+ }
707
+
708
+ async publishPersonalProject(input) {
709
+ if (input.personalSpaceId !== this.config.personal.spaceId) {
710
+ throw new TypeError('Published project must belong to the active personal space');
711
+ }
712
+ const localProjectId = String(input.localProjectId ?? '').trim();
713
+ if (!localProjectId) throw new TypeError('Local project ID is required');
714
+ const releaseVersion = String(input.releaseVersion ?? '').trim();
715
+ const updateSummary = String(input.updateSummary ?? '').trim();
716
+ if (!/^[0-9A-Za-z][0-9A-Za-z._-]{0,63}$/.test(releaseVersion)) {
717
+ throw new TypeError('Release version must use letters, numbers, dots, underscores, or hyphens');
718
+ }
719
+ if (!updateSummary || updateSummary.length > 4096) {
720
+ throw new TypeError('Release update summary is required and must be at most 4096 characters');
721
+ }
722
+ const localProject = await this.personal.getPersonalProject(
723
+ input.personalSpaceId,
724
+ localProjectId
725
+ );
726
+ const workspace = this.#workspace(input.providerUrl);
727
+ const project = await workspace.client.createSpace({
728
+ name: localProject.profile.name,
729
+ kind: 'project',
730
+ description: localProject.profile.purpose?.slice(0, 2000) ?? null,
731
+ publication_key: localProject.publication_key,
732
+ profile: providerProjectProfile(localProject.profile),
733
+ release: {
734
+ version: releaseVersion,
735
+ summary: updateSummary
736
+ }
737
+ });
738
+ const subscription = await this.personal.subscribe({
739
+ personal_space_id: input.personalSpaceId,
740
+ project_id: project.id,
741
+ provider_url: workspace.providerUrl,
742
+ project_name: project.name
743
+ });
744
+ return { project: { ...project, providerUrl: workspace.providerUrl }, subscription };
745
+ }
746
+
747
+ async listProjectReleases(input) {
748
+ const workspace = this.#workspace(input.providerUrl);
749
+ return {
750
+ projectId: input.projectId,
751
+ providerUrl: workspace.providerUrl,
752
+ releases: await workspace.client.listProjectReleases(input.projectId)
753
+ };
754
+ }
755
+
756
+ async deletePublicProject(input) {
757
+ const workspace = this.#workspace(input.providerUrl);
758
+ const result = await workspace.client.deleteProject(input.projectId);
759
+ await this.personal.unsubscribe(
760
+ this.config.personal.spaceId,
761
+ input.projectId,
762
+ workspace.providerUrl
763
+ );
764
+ return { ...result, providerUrl: workspace.providerUrl };
765
+ }
766
+
767
+ async upsertPersonalProject(input) {
768
+ if (input.personalSpaceId !== this.config.personal.spaceId) {
769
+ throw new TypeError('Personal project must belong to the active personal space');
770
+ }
771
+ return this.personal.upsertPersonalProject({
772
+ personal_space_id: input.personalSpaceId,
773
+ project_id: input.projectId,
774
+ profile: providerProjectProfile(input.profile)
775
+ });
776
+ }
777
+
778
+ async listPersonalProjects(input = {}) {
779
+ const personalSpaceId = input.personalSpaceId ?? this.config.personal.spaceId;
780
+ if (personalSpaceId !== this.config.personal.spaceId) {
781
+ throw new TypeError('Personal projects must use the active personal space');
782
+ }
783
+ return this.personal.listPersonalProjects(personalSpaceId);
784
+ }
785
+
786
+ async createProjectRelation(input) {
787
+ const workspace = this.#workspace(input.providerUrl);
788
+ return workspace.client.createProjectRelation(input.sourceProjectId, {
789
+ target_project_id: input.targetProjectId,
790
+ relation_type: input.relationType,
791
+ note: input.note ?? null
792
+ });
793
+ }
794
+
795
+ async listProjectRelations(input) {
796
+ const workspace = this.#workspace(input.providerUrl);
797
+ return {
798
+ projectId: input.projectId,
799
+ providerUrl: workspace.providerUrl,
800
+ relations: await workspace.client.listProjectRelations(input.projectId)
801
+ };
802
+ }
803
+
804
+ async reviewProjectRelation(input) {
805
+ const workspace = this.#workspace(input.providerUrl);
806
+ return workspace.client.decideProjectRelation(
807
+ input.targetProjectId,
808
+ input.relationId,
809
+ { decision: input.decision, note: input.note ?? null }
810
+ );
811
+ }
812
+
813
+ async listPersonalReviewQueue(input = {}) {
814
+ const personalSpaceId = input.personalSpaceId ?? this.config.personal.spaceId;
815
+ if (personalSpaceId !== this.config.personal.spaceId) {
816
+ throw new TypeError('Personal review must use the active personal space');
817
+ }
818
+ return {
819
+ personalSpaceId,
820
+ drafts: await this.personal.listPublicationDrafts(
821
+ personalSpaceId,
822
+ input.status ?? 'pending'
823
+ )
824
+ };
825
+ }
826
+
827
+ async reviewPersonalDraft(input) {
828
+ const personalSpaceId = this.config.personal.spaceId;
829
+ if (!['submit_public', 'keep_personal', 'ignore'].includes(input.decision)) {
830
+ throw new TypeError('Unknown personal draft decision');
831
+ }
832
+ if (input.decision !== 'submit_public') {
833
+ return this.personal.decidePublicationDraft(personalSpaceId, input.draftId, {
834
+ decision: input.decision,
835
+ shared_proposal_id: null
836
+ });
837
+ }
838
+ const draft = await this.personal.getPublicationDraft(personalSpaceId, input.draftId);
839
+ if (draft.status !== 'pending') throw new TypeError('Publication draft is not pending');
840
+ if (draft.episode.sensitivity !== 'normal') {
841
+ throw new TypeError('Private or restricted knowledge cannot be submitted publicly');
842
+ }
843
+ assertPublicKnowledgeEligible(draft.episode);
844
+ const workspace = this.#workspace(draft.provider_url);
845
+ const proposal = await workspace.client.createProposal(draft.target_project_id, {
846
+ episode: draft.episode
847
+ });
848
+ return this.personal.decidePublicationDraft(personalSpaceId, input.draftId, {
849
+ decision: 'submit_public',
850
+ shared_proposal_id: proposal.id
851
+ });
852
+ }
853
+
854
+ async listReviewQueue({ projectId, providerUrl, status = 'pending' }) {
855
+ const workspace = this.#workspace(providerUrl);
856
+ return {
857
+ projectId,
858
+ providerUrl: workspace.providerUrl,
859
+ proposals: await workspace.client.listProposals(projectId, status)
860
+ };
861
+ }
862
+
863
+ async reviewProposal({ projectId, providerUrl, proposalId, decision, note = null }) {
864
+ const workspace = this.#workspace(providerUrl);
865
+ return workspace.client.decideProposal(projectId, proposalId, { decision, note });
866
+ }
867
+
868
+ async getGraphitiStatus() {
869
+ const [personal, ...workspaces] = await Promise.all([
870
+ this.personal.health(),
871
+ ...[...this.workspaces.values()].map(async (workspace) => {
872
+ try {
873
+ return {
874
+ providerUrl: workspace.providerUrl,
875
+ ...(await workspace.client.health())
876
+ };
877
+ } catch {
878
+ return { providerUrl: workspace.providerUrl, status: 'unavailable' };
879
+ }
880
+ })
881
+ ]);
882
+ return { personal, workspaces };
883
+ }
884
+
885
+ getCapturePolicy() {
886
+ return this.capturePolicyStore.read();
887
+ }
888
+
889
+ updateCapturePolicy(input) {
890
+ return this.capturePolicyStore.update(input);
891
+ }
892
+
893
+ getAgentAccessPolicy() {
894
+ return this.agentAccessPolicyStore.read();
895
+ }
896
+
897
+ updateAgentAccessPolicy(input) {
898
+ return this.agentAccessPolicyStore.update(input);
899
+ }
900
+
901
+ async state() {
902
+ const [spaces, providers] = await Promise.all([
903
+ this.listKnowledgeSpaces(),
904
+ this.getGraphitiStatus()
905
+ ]);
906
+ const configured = this.workspaces.size > 0;
907
+ const connected = providers.workspaces.some(({ status }) => status === 'ready');
908
+ const publicStatus = configured ? (connected ? 'ready' : 'unavailable') : 'not_connected';
909
+ return {
910
+ mode: configured ? (connected ? 'connected' : 'degraded') : 'personal_only',
911
+ ...spaces,
912
+ providers,
913
+ capturePolicy: this.getCapturePolicy(),
914
+ agentAccessPolicy: this.getAgentAccessPolicy(),
915
+ publicProvider: { configured, status: publicStatus },
916
+ capabilities: {
917
+ browsePublicProjects: connected,
918
+ publishProject: connected,
919
+ submitKnowledge: connected,
920
+ subscribeProject: connected,
921
+ reviewProposals: connected && spaces.projects.some(({ role }) => role === 'maintainer')
922
+ }
923
+ };
924
+ }
925
+
926
+ close() {}
927
+
928
+ async #recordAgentViews(items, toolName) {
929
+ const unique = new Map(items.map((item) => [
930
+ `${item.item_kind}:${item.item_id}`,
931
+ item
932
+ ]));
933
+ const values = [...unique.values()];
934
+ for (let index = 0; index < values.length; index += 200) {
935
+ await this.personal.recordAgentViews({
936
+ personal_space_id: this.config.personal.spaceId,
937
+ tool_name: toolName,
938
+ items: values.slice(index, index + 200)
939
+ });
940
+ }
941
+ }
942
+
943
+ #assertActivePersonalSpace(personalSpaceId) {
944
+ if (personalSpaceId !== this.config.personal.spaceId) {
945
+ throw new TypeError('Knowledge changes must use the active personal space');
946
+ }
947
+ }
948
+
949
+ #workspace(providerUrl) {
950
+ const requested = providerUrl
951
+ ? canonicalProviderUrl(providerUrl)
952
+ : (this.workspaces.size === 1 ? [...this.workspaces.keys()][0] : null);
953
+ const workspace = requested ? this.workspaces.get(requested) : null;
954
+ if (!workspace) throw new TypeError('Public project provider is not configured');
955
+ return { ...workspace, providerUrl: requested };
956
+ }
957
+ }
958
+
959
+ function agentCollaborationPreference(item) {
960
+ const preference = {
961
+ instruction: item.instruction ?? '',
962
+ preference_key: item.preference_key ?? item.key ?? item.id,
963
+ title: item.title ?? item.preference_key ?? item.key ?? item.id,
964
+ profile_aspect: item.profile_aspect ?? null,
965
+ preference_scope: item.preference_scope ?? 'global',
966
+ confirmation_status: item.confirmation_status ?? 'confirmed'
967
+ };
968
+ if (item.preference_project_id) {
969
+ preference.preference_project_id = item.preference_project_id;
970
+ }
971
+ return preference;
972
+ }
973
+
974
+ function deferredPreferenceConflicts(result, queuedConflicts) {
975
+ const items = [
976
+ ...(result.global_preferences ?? []),
977
+ ...(result.project_preferences ?? [])
978
+ ];
979
+ const itemById = new Map(items.map((item) => [item.id, item]));
980
+ const activeConflictPairs = new Set(
981
+ (result.conflicts ?? []).flatMap((conflict) => {
982
+ const ids = conflict.item_ids ?? [];
983
+ const pairs = [];
984
+ for (let left = 0; left < ids.length; left += 1) {
985
+ for (let right = left + 1; right < ids.length; right += 1) {
986
+ pairs.push(preferenceConflictPairKey(ids[left], ids[right]));
987
+ }
988
+ }
989
+ return pairs;
990
+ })
991
+ );
992
+ return (queuedConflicts ?? [])
993
+ .filter((conflict) =>
994
+ conflict.status === 'ai_pending' &&
995
+ activeConflictPairs.has(preferenceConflictPairKey(
996
+ conflict.left_item_id,
997
+ conflict.right_item_id
998
+ )) &&
999
+ itemById.has(conflict.left_item_id) &&
1000
+ itemById.has(conflict.right_item_id)
1001
+ )
1002
+ .map((conflict) => ({
1003
+ ...conflict,
1004
+ left: itemById.get(conflict.left_item_id),
1005
+ right: itemById.get(conflict.right_item_id)
1006
+ }));
1007
+ }
1008
+
1009
+ function agentDeferredPreferenceConflict(conflict) {
1010
+ return {
1011
+ id: conflict.id,
1012
+ preference_key: conflict.preference_key,
1013
+ preference_scope: conflict.preference_scope,
1014
+ ...(conflict.preference_project_id
1015
+ ? { preference_project_id: conflict.preference_project_id }
1016
+ : {}),
1017
+ status: conflict.status,
1018
+ deferred_at: conflict.deferred_at,
1019
+ reason: conflict.reason,
1020
+ left: agentConflictPreference(conflict.left),
1021
+ right: agentConflictPreference(conflict.right),
1022
+ required_action: 'Resolve this conflict before using either side when it is relevant to the current task.',
1023
+ resolution_options: ['merge', 'keep_left', 'keep_right', 'split_scope']
1024
+ };
1025
+ }
1026
+
1027
+ function agentConflictPreference(item) {
1028
+ return {
1029
+ item_id: item.id,
1030
+ item_kind: item.item_kind,
1031
+ title: item.title,
1032
+ instruction: item.instruction,
1033
+ confirmed_at: item.confirmed_at,
1034
+ attributes: item.attributes ?? {}
1035
+ };
1036
+ }
1037
+
1038
+ function preferenceConflictPairKey(leftId, rightId) {
1039
+ return [leftId, rightId].sort().join('\u0000');
1040
+ }
1041
+
1042
+ function agentProjectResolution(resolution) {
1043
+ const result = {
1044
+ status: resolution.status,
1045
+ basis: resolution.basis,
1046
+ personal_project_id: resolution.personalProjectId
1047
+ };
1048
+ if (resolution.candidateCount !== undefined) {
1049
+ result.candidate_count = resolution.candidateCount;
1050
+ }
1051
+ return result;
1052
+ }
1053
+
1054
+ function assertNoCredentials(input) {
1055
+ const content = JSON.stringify({
1056
+ name: input.name,
1057
+ sourceDescription: input.sourceDescription,
1058
+ sourceExcerpt: input.sourceExcerpt,
1059
+ summary: input.summary,
1060
+ entities: input.entities,
1061
+ relationships: input.relationships
1062
+ });
1063
+ if (detectSensitiveContent(content).restricted) {
1064
+ throw new ApplicationError(
1065
+ 'validation',
1066
+ 'Structured knowledge contains credentials and cannot be stored'
1067
+ );
1068
+ }
1069
+ }
1070
+
1071
+ function retrievalGuidanceForScope(personalProjectScope) {
1072
+ if (personalProjectScope === 'all_local_confirmed') {
1073
+ return {
1074
+ currentPersonalProjectScope: 'all_local_confirmed',
1075
+ markerToUseIfNoSupportingEvidence: 'noMatchSourceMarker',
1076
+ requiredNextActionIfNoSupportingEvidence:
1077
+ 'search_current_workspace_files_or_ask_for_safe_root',
1078
+ workspaceFileSearch: {
1079
+ available: true,
1080
+ consentSource: 'bounded_expansion_confirmation',
1081
+ rootBoundary: 'current_working_directory',
1082
+ requiresSafeProjectOrWorkspaceRoot: true,
1083
+ forbiddenBroadRoots: ['user_home', 'filesystem_root'],
1084
+ readOnly: true,
1085
+ includesPublicProjects: false
1086
+ },
1087
+ instruction: 'Use read-only local file search in the current repository or explicit ' +
1088
+ 'workspace root, preserving exact names first. If the working directory is the user ' +
1089
+ 'home, filesystem root, or otherwise too broad, ask for a safe root. Never search ' +
1090
+ 'outside that root or inspect credential stores; if no evidence supports the answer, ' +
1091
+ 'ask for a source clue.'
1092
+ };
1093
+ }
1094
+ return {
1095
+ currentPersonalProjectScope: 'bounded',
1096
+ markerToUseIfNoSupportingEvidence: 'noMatchSourceMarker',
1097
+ requiredNextActionIfNoSupportingEvidence:
1098
+ 'ask_user_to_confirm_all_local_and_workspace_search',
1099
+ expansion: {
1100
+ available: true,
1101
+ requiresExplicitUserConfirmation: true,
1102
+ input: { personalProjectScope: 'all_local_confirmed' },
1103
+ readOnly: true,
1104
+ oneQueryOnly: true,
1105
+ includesPublicProjects: false,
1106
+ includesCurrentWorkspaceFiles: true
1107
+ },
1108
+ instruction: 'Ask whether to widen this one read-only lookup to all registered local ' +
1109
+ 'personal projects and, if still unresolved, current repository or workspace files. ' +
1110
+ 'Exclude public projects and paths outside the current workspace; then stop and wait.'
1111
+ };
1112
+ }
1113
+
1114
+ function assertSafeKnowledgeMutation(input) {
1115
+ const content = JSON.stringify({
1116
+ reason: input.reason,
1117
+ name: input.name,
1118
+ summary: input.summary,
1119
+ fact: input.fact,
1120
+ note: input.note,
1121
+ newProjectName: input.newProjectName,
1122
+ newProjectPurpose: input.newProjectPurpose,
1123
+ batchItems: input.items?.map((item) => ({
1124
+ existenceReason: item.existenceReason,
1125
+ quadrantReason: item.quadrantReason
1126
+ }))
1127
+ });
1128
+ if (detectSensitiveContent(content).restricted) {
1129
+ throw new ApplicationError(
1130
+ 'validation',
1131
+ 'Knowledge revision contains credentials and cannot be stored'
1132
+ );
1133
+ }
1134
+ }
1135
+
1136
+ function providerEpisode(input) {
1137
+ return {
1138
+ idempotency_key: input.idempotencyKey,
1139
+ session_id: input.sessionId,
1140
+ name: input.name,
1141
+ source_kind: input.sourceKind,
1142
+ source_description: input.sourceDescription,
1143
+ source_application: input.sourceApplication ?? null,
1144
+ source_turn_id: input.sourceTurnId ?? null,
1145
+ source_excerpt: input.sourceExcerpt ?? null,
1146
+ reference_time: input.referenceTime,
1147
+ summary: input.summary ?? '',
1148
+ sensitivity: input.sensitivity ?? 'normal',
1149
+ entities: input.entities.map((entity) => ({
1150
+ key: entity.key,
1151
+ name: entity.name,
1152
+ type: entity.type,
1153
+ summary: entity.summary ?? '',
1154
+ origin_quadrant: entity.originQuadrant ?? 'known_known',
1155
+ current_quadrant: entity.currentQuadrant ?? entity.originQuadrant ?? 'known_known',
1156
+ epistemic_status: entity.epistemicStatus ?? 'confirmed',
1157
+ confirmation_status: entity.confirmationStatus,
1158
+ confirmation_basis: providerConfirmationBasis(entity.confirmationBasis),
1159
+ reasoning_summary: entity.reasoningSummary ?? null,
1160
+ profile_aspect: entity.profileAspect ?? null,
1161
+ inheritance_mode: entity.inheritanceMode ?? 'local_only',
1162
+ inherited_project_ids: entity.inheritedProjectIds ?? [],
1163
+ attributes: entity.attributes ?? {}
1164
+ })),
1165
+ relationships: input.relationships.map((relationship) => ({
1166
+ key: relationship.key,
1167
+ source: relationship.source,
1168
+ target: relationship.target,
1169
+ type: relationship.type,
1170
+ fact: relationship.fact,
1171
+ valid_at: relationship.validAt ?? null,
1172
+ invalid_at: relationship.invalidAt ?? null,
1173
+ supersedes: relationship.supersedes ?? [],
1174
+ confidence: relationship.confidence ?? 1,
1175
+ origin_quadrant: relationship.originQuadrant ?? 'known_known',
1176
+ current_quadrant: relationship.currentQuadrant ??
1177
+ relationship.originQuadrant ?? 'known_known',
1178
+ epistemic_status: relationship.epistemicStatus ?? 'confirmed',
1179
+ confirmation_status: relationship.confirmationStatus,
1180
+ confirmation_basis: providerConfirmationBasis(relationship.confirmationBasis),
1181
+ reasoning_summary: relationship.reasoningSummary ?? null,
1182
+ profile_aspect: relationship.profileAspect ?? null,
1183
+ inheritance_mode: relationship.inheritanceMode ?? 'local_only',
1184
+ inherited_project_ids: relationship.inheritedProjectIds ?? [],
1185
+ attributes: relationship.attributes ?? {}
1186
+ }))
1187
+ };
1188
+ }
1189
+
1190
+ function assertPublicKnowledgeEligible(episode) {
1191
+ const blocked = [...episode.entities, ...episode.relationships].find((item) =>
1192
+ item.profile_aspect ||
1193
+ item.confirmation_status !== 'confirmed' ||
1194
+ !item.confirmation_basis?.confirmed_by ||
1195
+ !item.confirmation_basis?.confirmed_at ||
1196
+ !['user', 'authoritative_source'].includes(item.confirmation_basis.confirmed_by.kind)
1197
+ );
1198
+ if (blocked) {
1199
+ throw new TypeError(
1200
+ 'Only knowledge with an auditable confirmation can enter public review'
1201
+ );
1202
+ }
1203
+ }
1204
+
1205
+ function providerConfirmationBasis(basis) {
1206
+ if (!basis) return undefined;
1207
+ return {
1208
+ existence_reason: basis.existenceReason,
1209
+ quadrant_reason: basis.quadrantReason,
1210
+ proposed_by: providerConfirmationActor(basis.proposedBy),
1211
+ confirmed_by: providerConfirmationActor(basis.confirmedBy),
1212
+ confirmed_at: basis.confirmedAt ?? null,
1213
+ agent_policy_version: basis.agentPolicyVersion ?? null
1214
+ };
1215
+ }
1216
+
1217
+ function providerConfirmationActor(actor) {
1218
+ if (!actor) return null;
1219
+ return {
1220
+ kind: actor.kind,
1221
+ label: actor.label ?? null
1222
+ };
1223
+ }
1224
+
1225
+ function providerProjectProfile(profile) {
1226
+ if (!profile || typeof profile !== 'object') throw new TypeError('Project profile is required');
1227
+ const assessment = profile.assessment ? sanitizeProjectAssessment(profile.assessment) : null;
1228
+ return {
1229
+ name: profile.name,
1230
+ purpose: profile.purpose ?? null,
1231
+ scope: profile.scope ?? null,
1232
+ technical_summary: profile.technical_summary ?? profile.technicalSummary ?? null,
1233
+ lifecycle: profile.lifecycle ?? 'planned',
1234
+ sources: profile.sources ?? [],
1235
+ boundaries: profile.boundaries ?? [],
1236
+ assessment
1237
+ };
1238
+ }
1239
+
1240
+ function sanitizeProjectAssessment(input) {
1241
+ const assessment = {
1242
+ score: input.score,
1243
+ label: input.label,
1244
+ confirmed: input.confirmed ?? [],
1245
+ inferred: input.inferred ?? [],
1246
+ dimensions: (input.dimensions ?? []).map((dimension) => ({
1247
+ key: dimension.key,
1248
+ label: dimension.label,
1249
+ score: dimension.score,
1250
+ state: dimension.state === 'missing' ? 'inferred' : dimension.state,
1251
+ evidence: dimension.evidence ?? []
1252
+ })),
1253
+ analyzed_at: input.analyzed_at ?? input.analyzedAt
1254
+ };
1255
+ return assessment;
1256
+ }
1257
+
1258
+ function groupSubscriptions(subscriptions) {
1259
+ const grouped = new Map();
1260
+ for (const subscription of subscriptions) {
1261
+ const url = canonicalProviderUrl(subscription.provider_url);
1262
+ if (!grouped.has(url)) grouped.set(url, []);
1263
+ grouped.get(url).push(subscription);
1264
+ }
1265
+ return grouped;
1266
+ }
1267
+
1268
+ function rankedSearchItems(searchResults, key, limit) {
1269
+ return searchResults
1270
+ .flatMap((result) => result[key])
1271
+ .map((item, index) => ({ item, index }))
1272
+ .sort((left, right) =>
1273
+ searchItemScore(right.item) - searchItemScore(left.item) ||
1274
+ left.index - right.index
1275
+ )
1276
+ .slice(0, limit)
1277
+ .map(({ item }) => item);
1278
+ }
1279
+
1280
+ function searchItemScore(item) {
1281
+ return typeof item?.score === 'number' && Number.isFinite(item.score)
1282
+ ? item.score
1283
+ : 0;
1284
+ }
1285
+
1286
+ async function safely(operation, fallback) {
1287
+ try {
1288
+ return await operation();
1289
+ } catch {
1290
+ return fallback;
1291
+ }
1292
+ }