fuli-context 0.9.0-beta.2 → 0.10.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 (162) hide show
  1. package/README.md +17 -43
  2. package/README.zh-CN.md +14 -40
  3. package/dist/web/assets/AboutPage-D-GyJNv1.js +1 -0
  4. package/dist/web/assets/AboutPage-c4dQpUiP.css +1 -0
  5. package/dist/web/assets/AgentConversations-DOuZgUeA.js +2 -0
  6. package/dist/web/assets/AgentConversations-DS8h3yKn.css +1 -0
  7. package/dist/web/assets/AgentHand-Y83Oo36U.css +1 -0
  8. package/dist/web/assets/AgentHand-eGsXoGSq.js +1 -0
  9. package/dist/web/assets/AgentName-BQTFCWn0.css +1 -0
  10. package/dist/web/assets/AgentName-Dtmu9Ff5.js +1 -0
  11. package/dist/web/assets/AgentProfilePage-D8Tvayn7.js +1 -0
  12. package/dist/web/assets/AgentsDirectoryPage-BTEfKZjF.js +1 -0
  13. package/dist/web/assets/ConnectionsPage-C1w99yN3.css +1 -0
  14. package/dist/web/assets/ConnectionsPage-DwOIcvl9.js +1 -0
  15. package/dist/web/assets/EmployeeRecruitDialog-BrIMRblq.css +1 -0
  16. package/dist/web/assets/EmployeeRecruitDialog-D11blfwU.js +1 -0
  17. package/dist/web/assets/EmployeeWorkbenchPage-BcEzYt65.css +1 -0
  18. package/dist/web/assets/EmployeeWorkbenchPage-CK2BAoGt.js +3 -0
  19. package/dist/web/assets/GrowthLoading-CIuQ1icy.css +1 -0
  20. package/dist/web/assets/GrowthLoading-KTjE2e50.js +1 -0
  21. package/dist/web/assets/KnowledgeEditDialog-B3x8vs3c.css +1 -0
  22. package/dist/web/assets/KnowledgeEditDialog-n9trHQ5d.js +1 -0
  23. package/dist/web/assets/KnowledgeInspector-BcNjgw9H.js +1 -0
  24. package/dist/web/assets/KnowledgeInspector-pgDELOEf.css +1 -0
  25. package/dist/web/assets/KnowledgeOrganizerPage-BzgxP-nG.css +1 -0
  26. package/dist/web/assets/KnowledgeOrganizerPage-DMBpGxLr.js +1 -0
  27. package/dist/web/assets/KnowledgePage-POkCMW8f.js +1 -0
  28. package/dist/web/assets/KnowledgeWorkspace-2FvQadhB.js +4 -0
  29. package/dist/web/assets/NotFoundPage-BCMZbeB8.js +1 -0
  30. package/dist/web/assets/OverviewPage-0CFWCG9-.css +1 -0
  31. package/dist/web/assets/OverviewPage-0mYifpjp.js +1 -0
  32. package/dist/web/assets/PersonalProfilePage-Chv2a2pi.css +1 -0
  33. package/dist/web/assets/PersonalProfilePage-JW6Muix6.js +1 -0
  34. package/dist/web/assets/PersonalProjectsPage-CRbi64JP.js +1 -0
  35. package/dist/web/assets/ProjectAgentFirstTask-DJ0wTbh8.js +1 -0
  36. package/dist/web/assets/ProjectAgentsPage-CPgKVLgg.js +12 -0
  37. package/dist/web/assets/ProjectAgentsPage-DeuRkMp1.css +1 -0
  38. package/dist/web/assets/ProjectScopePicker-BwB7gK-7.js +1 -0
  39. package/dist/web/assets/ProjectScopePicker-DYA7L1Ex.css +1 -0
  40. package/dist/web/assets/PublicProjectsPage-CB272Sq-.js +1 -0
  41. package/dist/web/assets/ReviewPage-CelTZU4C.js +1 -0
  42. package/dist/web/assets/RoundtablesPage-C9dXfWCl.js +2 -0
  43. package/dist/web/assets/{SearchableSelect-Do6X3fXi.js → SearchableSelect-f7EHbyDL.js} +1 -1
  44. package/dist/web/assets/SettingsPage-5g7mRN2F.css +1 -0
  45. package/dist/web/assets/SettingsPage-nAijmm5f.js +1 -0
  46. package/dist/web/assets/SharedJefaReportPage-CjpJ9bkk.js +1 -0
  47. package/dist/web/assets/SharedJefaReportPage-D92FSPru.css +1 -0
  48. package/dist/web/assets/UiDisclosure-CWlC91Zt.js +1 -0
  49. package/dist/web/assets/WritingTastePage-DaHWY61P.js +1 -0
  50. package/dist/web/assets/WritingTastePage-SMGF39mf.css +1 -0
  51. package/dist/web/assets/agent-profile-CBvJHGjt.css +1 -0
  52. package/dist/web/assets/{task-evidence-DTiPCs7r.js → agent-work-summary-LeW-Al_S.js} +1 -1
  53. package/dist/web/assets/attention-store-Dk-_P4Oi.js +1 -0
  54. package/dist/web/assets/{catalog-JmKyJnKL.js → catalog-DN72452f.js} +1 -1
  55. package/dist/web/assets/{client-DTQ_tUr2.js → client-B2cRl-7c.js} +1 -1
  56. package/dist/web/assets/console-BtPqo79j.js +1 -0
  57. package/dist/web/assets/{conversation-launcher-settings-fUWCt3XG.js → conversation-launcher-settings-jkHRmWai.js} +1 -1
  58. package/dist/web/assets/{copy-CX6pyzAG.js → copy-D2LqwtPZ.js} +1 -1
  59. package/dist/web/assets/{i18n-CIIxzhPZ.js → i18n-D2CJkEYK.js} +2 -2
  60. package/dist/web/assets/{identity-CdFITIOu.js → identity-B3zEw-1u.js} +1 -1
  61. package/dist/web/assets/index-CNp0tRNp.css +1 -0
  62. package/dist/web/assets/index-ulISYH02.js +5 -0
  63. package/dist/web/assets/{model-CJsrDUNb.js → model-S7ehAjky.js} +1 -1
  64. package/dist/web/assets/{personal-profile-graph-CQLOait6.js → personal-profile-graph--GG3Mizo.js} +1 -1
  65. package/dist/web/assets/shared-report-CGlCpmNd.css +1 -0
  66. package/dist/web/assets/shared-report-DEK9wvhl.js +1 -0
  67. package/dist/web/assets/useAgentRoster-BAPKWZ9y.js +1 -0
  68. package/dist/web/assets/{useApi-CROJJdhE-fcdVGInY.js → useApi-CROJJdhE-BtFMXFjw.js} +1 -1
  69. package/dist/web/assets/{useModalDialog-B-45idX9.js → useModalDialog-D8tiJfbc.js} +1 -1
  70. package/dist/web/assets/{vue-router-BUX6njIv.js → vue-router-DkaTtPn0.js} +1 -1
  71. package/dist/web/index.html +16 -15
  72. package/docs/project-agent-memory.md +6 -0
  73. package/docs/roundtable.md +46 -0
  74. package/graph-provider/fuli_graph/app.py +3 -1
  75. package/graph-provider/pyproject.toml +1 -1
  76. package/npm-shrinkwrap.json +2 -2
  77. package/package.json +2 -4
  78. package/skills/capturing-session-knowledge/SKILL.md +5 -1
  79. package/skills/operating-fuli/SKILL.md +4 -0
  80. package/src/agent-tools/graph-definitions.js +2 -2
  81. package/src/agents/claude-code/lifecycle-hook.js +6 -19
  82. package/src/agents/codex/lifecycle-hook.js +58 -27
  83. package/src/agents/codex/roundtable-config.js +3 -0
  84. package/src/agents/codex/token-usage.js +235 -0
  85. package/src/agents/cursor/lifecycle-hook.js +4 -9
  86. package/src/agents/identity-receipt.js +11 -4
  87. package/src/agents/token-usage-resolver.js +147 -0
  88. package/src/conversations/current-task-lookup.js +40 -0
  89. package/src/conversations/sync-transcript.js +4 -2
  90. package/src/graphiti/federated-application.js +6 -1
  91. package/src/graphiti/project-agent-mapping.js +1 -1
  92. package/src/graphiti/project-agent-workflows.js +6 -1
  93. package/src/graphiti/provider-client.js +7 -4
  94. package/src/graphiti/task-entry-preferences.js +6 -7
  95. package/src/http/static-handler.js +3 -1
  96. package/src/mcp/instructions.js +8 -7
  97. package/src/mcp/session-id.js +1 -1
  98. package/src/mcp/tool-annotations.js +3 -1
  99. package/src/mcp/tool-result.js +20 -8
  100. package/src/native-runtime/runtime.js +41 -6
  101. package/src/roundtables/domain.js +3 -3
  102. package/src/roundtables/http-router.js +5 -3
  103. package/src/roundtables/mcp-server.js +15 -8
  104. package/src/roundtables/profiles.js +84 -0
  105. package/src/roundtables/service.js +86 -2
  106. package/src/roundtables/store.js +4 -0
  107. package/src/roundtables/tool-contract.js +22 -5
  108. package/src/roundtables/turn-prompt.js +3 -0
  109. package/dist/web/assets/AboutPage-u2Bv6cO-.js +0 -1
  110. package/dist/web/assets/AboutPage-vYIBifKw.css +0 -1
  111. package/dist/web/assets/AgentHand-CsMcNXUm.js +0 -1
  112. package/dist/web/assets/AgentHand-D9sQu2vc.css +0 -1
  113. package/dist/web/assets/AgentName-BboCA-Fd.js +0 -1
  114. package/dist/web/assets/AgentName-Cydd0UI4.css +0 -1
  115. package/dist/web/assets/AgentProfilePage-Dyux6qE3.js +0 -1
  116. package/dist/web/assets/AgentsDirectoryPage-BEJdvmX7.js +0 -1
  117. package/dist/web/assets/ConnectionsPage-CBHAdZXT.css +0 -1
  118. package/dist/web/assets/ConnectionsPage-Dw_hK_EM.js +0 -1
  119. package/dist/web/assets/EmployeeRecruitDialog-B59X3rOi.css +0 -1
  120. package/dist/web/assets/EmployeeRecruitDialog-DP_ktv3w.js +0 -1
  121. package/dist/web/assets/EmployeeWorkbenchPage-CAt8lVnr.js +0 -3
  122. package/dist/web/assets/EmployeeWorkbenchPage-CQwZnTA0.css +0 -1
  123. package/dist/web/assets/GrowthLoading-B_Hh0CRS.css +0 -1
  124. package/dist/web/assets/GrowthLoading-DIwqGFSc.js +0 -1
  125. package/dist/web/assets/KnowledgeEditDialog-D9NuRHvp.css +0 -1
  126. package/dist/web/assets/KnowledgeEditDialog-Degd9AGD.js +0 -1
  127. package/dist/web/assets/KnowledgeInspector--1NujtXW.css +0 -1
  128. package/dist/web/assets/KnowledgeInspector-yms1ysZ4.js +0 -1
  129. package/dist/web/assets/KnowledgeOrganizerPage-C64wNn8a.js +0 -1
  130. package/dist/web/assets/KnowledgeOrganizerPage-h2iKvQpO.css +0 -1
  131. package/dist/web/assets/KnowledgePage-BTFEXzgi.js +0 -1
  132. package/dist/web/assets/KnowledgeWorkspace-BsF1X4Aq.js +0 -4
  133. package/dist/web/assets/NotFoundPage-DfaNTiD1.js +0 -1
  134. package/dist/web/assets/OverviewPage-VXMfcWFI.js +0 -1
  135. package/dist/web/assets/PersonalProfilePage-C1LBhcYu.js +0 -1
  136. package/dist/web/assets/PersonalProfilePage-L8T5xMEC.css +0 -1
  137. package/dist/web/assets/PersonalProjectsPage-B5lV4ZJW.js +0 -1
  138. package/dist/web/assets/ProjectAgentFirstTask-DF-IvEqp.js +0 -1
  139. package/dist/web/assets/ProjectAgentsPage-CgL92gng.js +0 -13
  140. package/dist/web/assets/ProjectAgentsPage-CoJ4qQlA.css +0 -1
  141. package/dist/web/assets/ProjectScopePicker-BvaGTMFx.css +0 -1
  142. package/dist/web/assets/ProjectScopePicker-CZUMdyYz.js +0 -1
  143. package/dist/web/assets/PublicProjectsPage-e4CpxHk9.js +0 -1
  144. package/dist/web/assets/ReviewPage-DugEaFvI.js +0 -1
  145. package/dist/web/assets/RoundtablesPage-642YtZBC.js +0 -2
  146. package/dist/web/assets/SettingsPage-Brl-SL33.css +0 -1
  147. package/dist/web/assets/SettingsPage-wqYxDPyu.js +0 -1
  148. package/dist/web/assets/SharedJefaReportPage-CNQDezJr.js +0 -1
  149. package/dist/web/assets/SharedJefaReportPage-DXbj_JhO.css +0 -1
  150. package/dist/web/assets/WritingTastePage-Bzg6AvnB.css +0 -1
  151. package/dist/web/assets/WritingTastePage-CAUvbDlL.js +0 -1
  152. package/dist/web/assets/agent-profile-CdKOh5xw.css +0 -1
  153. package/dist/web/assets/agent-profile.css_vue_type_style_index_0_src_true_lang-DHOcWSe_.js +0 -1
  154. package/dist/web/assets/console-CrhIK67Y.js +0 -1
  155. package/dist/web/assets/employee-dialog-Bh5ccIbK.css +0 -1
  156. package/dist/web/assets/index-CYZi7E1e.css +0 -1
  157. package/dist/web/assets/index-Dsgn8o4-.js +0 -2
  158. package/dist/web/assets/shared-report-Cll2Yj8W.js +0 -1
  159. package/dist/web/assets/shared-report-ezeb8dW4.css +0 -1
  160. package/docs/roundtable-beta-testing.md +0 -102
  161. package/docs/roundtable-beta-validation.md +0 -133
  162. package/docs/roundtable-research.md +0 -142
@@ -0,0 +1,235 @@
1
+ import { constants } from 'node:fs';
2
+ import { lstat, opendir, open } from 'node:fs/promises';
3
+ import { basename, extname, join } from 'node:path';
4
+
5
+ const MAX_HEADER_BYTES = 64 * 1024;
6
+ const MAX_USAGE_TAIL_BYTES = 4 * 1024 * 1024;
7
+ const MAX_CANDIDATE_FILES = 128;
8
+ const MAX_DIRECTORY_ENTRIES = 2048;
9
+ const MAX_DIRECTORY_DEPTH = 8;
10
+
11
+ /**
12
+ * Codex's native JSONL session adapter. It returns only the latest cumulative
13
+ * token_count snapshot and never returns transcript records or text.
14
+ */
15
+ export function createCodexTokenUsageResolver({
16
+ sessionsDirectory = undefined,
17
+ archivedSessionsDirectory = undefined,
18
+ codexSessionsDirectory = undefined,
19
+ codexArchivedSessionsDirectory = undefined,
20
+ codexHomeDirectory = undefined,
21
+ homeDirectory = undefined
22
+ } = {}) {
23
+ const explicitSessionsDirectory = sessionsDirectory ?? codexSessionsDirectory;
24
+ const explicitArchivedSessionsDirectory =
25
+ archivedSessionsDirectory ?? codexArchivedSessionsDirectory;
26
+ const home = typeof homeDirectory === 'string' && homeDirectory
27
+ ? homeDirectory : defaultHomeDirectory();
28
+ const codexHome = typeof codexHomeDirectory === 'string' && codexHomeDirectory
29
+ ? codexHomeDirectory
30
+ : homeDirectory === undefined ? (defaultCodexHomeDirectory() ?? (home ? join(home, '.codex') : null))
31
+ : (home ? join(home, '.codex') : null);
32
+ const useDefaultRoots = explicitSessionsDirectory === undefined &&
33
+ explicitArchivedSessionsDirectory === undefined;
34
+ const roots = [
35
+ explicitSessionsDirectory ?? (useDefaultRoots && codexHome ? join(codexHome, 'sessions') : null),
36
+ explicitArchivedSessionsDirectory ??
37
+ (useDefaultRoots && codexHome ? join(codexHome, 'archived_sessions') : null)
38
+ ]
39
+ .filter((root, index, values) => typeof root === 'string' && root && values.indexOf(root) === index);
40
+ return {
41
+ resolve: (request) => resolveCodexTokenUsage(request, { roots })
42
+ };
43
+ }
44
+
45
+ export async function resolveCodexTokenUsage(request = {}, { roots } = {}) {
46
+ const sessionId = safeSessionId(request.sessionId ?? request.session_id);
47
+ if (!sessionId) return null;
48
+ const configuredRoots = Array.isArray(roots) ? roots : roots ? [roots] : null;
49
+ const directories = (configuredRoots ?? [
50
+ defaultCodexSessionsDirectory(),
51
+ defaultCodexArchivedSessionsDirectory()
52
+ ]).filter((root, index, values) => typeof root === 'string' && root && values.indexOf(root) === index);
53
+
54
+ let inspectedEntries = 0;
55
+ let candidates = 0;
56
+ for (const root of directories) {
57
+ const files = await matchingSessionFiles(root, sessionId, {
58
+ onEntry: () => { inspectedEntries += 1; },
59
+ get entriesInspected() { return inspectedEntries; },
60
+ get candidatesFound() { return candidates; },
61
+ addCandidate: () => { candidates += 1; }
62
+ });
63
+ for (const file of files) {
64
+ const usage = await readSessionUsage(file, sessionId);
65
+ if (usage) return usage;
66
+ }
67
+ if (inspectedEntries >= MAX_DIRECTORY_ENTRIES || candidates >= MAX_CANDIDATE_FILES) break;
68
+ }
69
+ return null;
70
+ }
71
+
72
+ async function matchingSessionFiles(root, sessionId, counters, depth = 0) {
73
+ if (depth > MAX_DIRECTORY_DEPTH || counters.entriesInspected >= MAX_DIRECTORY_ENTRIES ||
74
+ counters.candidatesFound >= MAX_CANDIDATE_FILES) return [];
75
+ let directory;
76
+ try {
77
+ const rootStat = await lstat(root);
78
+ if (!rootStat.isDirectory()) return [];
79
+ directory = await opendir(root);
80
+ } catch {
81
+ return [];
82
+ }
83
+
84
+ const files = [];
85
+ try {
86
+ for await (const entry of directory) {
87
+ counters.onEntry();
88
+ if (counters.entriesInspected > MAX_DIRECTORY_ENTRIES ||
89
+ counters.candidatesFound >= MAX_CANDIDATE_FILES) break;
90
+ const path = join(root, entry.name);
91
+ if (entry.isDirectory()) {
92
+ files.push(...await matchingSessionFiles(path, sessionId, counters, depth + 1));
93
+ continue;
94
+ }
95
+ if (!entry.isFile() || extname(entry.name) !== '.jsonl' ||
96
+ !basename(entry.name).includes(sessionId)) continue;
97
+ counters.addCandidate();
98
+ files.push(path);
99
+ }
100
+ } catch {
101
+ // A session directory can rotate while the host is running. An incomplete
102
+ // scan is an unavailable snapshot, never a reason to guess another file.
103
+ } finally {
104
+ await directory.close().catch(() => {});
105
+ }
106
+ return files;
107
+ }
108
+
109
+ async function readSessionUsage(filePath, sessionId) {
110
+ let file;
111
+ try {
112
+ file = await open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
113
+ const stat = await file.stat();
114
+ if (!stat.isFile() || stat.size <= 0) return null;
115
+
116
+ const header = await readBytes(file, Math.min(MAX_HEADER_BYTES, stat.size), 0);
117
+ const firstLineEnd = header.indexOf(10);
118
+ if (firstLineEnd < 0 || !isSessionHeader(header.subarray(0, firstLineEnd), sessionId)) {
119
+ return null;
120
+ }
121
+
122
+ const start = Math.max(0, stat.size - MAX_USAGE_TAIL_BYTES);
123
+ const tail = await readBytes(file, stat.size - start, start);
124
+ let firstLine = 0;
125
+ if (start > 0) {
126
+ const previous = await readBytes(file, 1, start - 1);
127
+ if (previous[0] !== 10) {
128
+ firstLine = tail.indexOf(10) + 1;
129
+ if (firstLine <= 0) return null;
130
+ }
131
+ }
132
+ return latestTokenUsage(tail.subarray(firstLine));
133
+ } catch {
134
+ return null;
135
+ } finally {
136
+ await file?.close().catch(() => {});
137
+ }
138
+ }
139
+
140
+ async function readBytes(file, length, position) {
141
+ const buffer = Buffer.allocUnsafe(length);
142
+ let offset = 0;
143
+ while (offset < length) {
144
+ const result = await file.read(buffer, offset, length - offset, position + offset);
145
+ if (!result.bytesRead) break;
146
+ offset += result.bytesRead;
147
+ }
148
+ return offset === length ? buffer : buffer.subarray(0, offset);
149
+ }
150
+
151
+ function isSessionHeader(bytes, sessionId) {
152
+ try {
153
+ const record = JSON.parse(bytes.toString('utf8'));
154
+ return record?.type === 'session_meta' && record.payload?.id === sessionId;
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
159
+
160
+ function latestTokenUsage(bytes) {
161
+ let latest = null;
162
+ const text = bytes.toString('utf8');
163
+ let offset = 0;
164
+ while (offset < text.length) {
165
+ const end = text.indexOf('\n', offset);
166
+ const line = text.slice(offset, end < 0 ? text.length : end).trim();
167
+ if (line.includes('"token_count"') && line.includes('"total_token_usage"')) {
168
+ try {
169
+ const record = JSON.parse(line);
170
+ const total = record?.payload?.info?.total_token_usage;
171
+ if (record?.type === 'event_msg' && record.payload?.type === 'token_count') {
172
+ const totalTokens = nonNegativeInteger(total?.total_tokens);
173
+ if (totalTokens !== null) {
174
+ latest = {
175
+ source: 'host',
176
+ totalTokens,
177
+ inputTokens: optionalNonNegativeInteger(total?.input_tokens),
178
+ outputTokens: optionalNonNegativeInteger(total?.output_tokens),
179
+ cachedInputTokens: optionalNonNegativeInteger(total?.cached_input_tokens),
180
+ cacheWriteInputTokens: optionalNonNegativeInteger(total?.cache_write_input_tokens),
181
+ reasoningOutputTokens: optionalNonNegativeInteger(total?.reasoning_output_tokens)
182
+ };
183
+ }
184
+ }
185
+ } catch {
186
+ // Ignore malformed native records and continue to later snapshots.
187
+ }
188
+ }
189
+ if (end < 0) break;
190
+ offset = end + 1;
191
+ }
192
+ return latest;
193
+ }
194
+
195
+ function optionalNonNegativeInteger(value) {
196
+ return value === null || value === undefined ? null : nonNegativeInteger(value);
197
+ }
198
+
199
+ function nonNegativeInteger(value) {
200
+ return Number.isSafeInteger(value) && value >= 0 ? value : null;
201
+ }
202
+
203
+ function safeSessionId(value) {
204
+ if (typeof value !== 'string') return null;
205
+ const normalized = value.trim();
206
+ if (!normalized || normalized.length > 256 || normalized.includes('..') ||
207
+ !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(normalized)) return null;
208
+ return normalized;
209
+ }
210
+
211
+ function defaultCodexSessionsDirectory() {
212
+ const codexHome = defaultCodexRootDirectory();
213
+ return codexHome ? join(codexHome, 'sessions') : null;
214
+ }
215
+
216
+ function defaultCodexArchivedSessionsDirectory() {
217
+ const codexHome = defaultCodexRootDirectory();
218
+ return codexHome ? join(codexHome, 'archived_sessions') : null;
219
+ }
220
+
221
+ function defaultCodexRootDirectory() {
222
+ const configured = defaultCodexHomeDirectory();
223
+ if (configured) return configured;
224
+ const home = defaultHomeDirectory();
225
+ return home ? join(home, '.codex') : null;
226
+ }
227
+
228
+ function defaultCodexHomeDirectory() {
229
+ const value = process.env.CODEX_HOME;
230
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
231
+ }
232
+
233
+ function defaultHomeDirectory() {
234
+ return process.env.HOME ?? process.env.USERPROFILE ?? null;
235
+ }
@@ -6,7 +6,6 @@ import { callAgentTool } from '../../agent-tools.js';
6
6
  import { createRuntimeLeaseClient } from '../../adaptive-runtime/lease-client.js';
7
7
  import { openFederatedGraphApplication } from '../../graphiti/federated-application.js';
8
8
  import { resolveGraphRuntimeOptions } from '../../graphiti/runtime-config.js';
9
- import { boundedHookMessage } from '../lifecycle-message.js';
10
9
 
11
10
  const EVENTS = new Set(['sessionStart', 'beforeSubmitPrompt', 'stop']);
12
11
 
@@ -18,14 +17,9 @@ export async function cursorLifecycleOutput(event, input, invoke) {
18
17
  }
19
18
  const source = { sourceApplication: 'cursor', sourceSessionId: sessionId };
20
19
  if (event === 'stop') {
21
- // One host-counted continuation at most; never restart cancelled/error turns.
22
- if (input.status !== 'completed' || (input.loop_count ?? 0) >= 1) return {};
23
- const check = await invoke('verify_task_checkpoint', { sessionId, ...source });
24
- const notice = '\nIf this mode is read-only or checkpoint writes are denied, state that the checkpoint was not saved and do not retry. This is the only checkpoint continuation.';
25
- const reason = boundedHookMessage(check.reason, { maxBytes: 8_000 - Buffer.byteLength(notice, 'utf8') });
26
- return check.status === 'checkpoint_required' && reason.trim()
27
- ? { followup_message: reason + notice }
28
- : {};
20
+ // Cursor has no advisory Stop output. A followup_message would start
21
+ // another model turn, so pending checkpoints remain available through MCP.
22
+ return {};
29
23
  }
30
24
  const roots = input.workspace_roots;
31
25
  if (!Array.isArray(roots) || roots.length !== 1 || typeof roots[0] !== 'string') {
@@ -50,6 +44,7 @@ export async function cursorLifecycleOutput(event, input, invoke) {
50
44
  + `get_collaboration_preferences with sessionId=${JSON.stringify(sessionId)}, `
51
45
  + 'projectPath=current working directory and taskPrompt=current request '
52
46
  + 'to restore the role selected for that exact prompt and its latest project memory. '
47
+ + 'Every final reply MUST include this task’s agent_receipt.markdown unchanged exactly once, including greetings and capture-disabled tasks. '
53
48
  + 'Before finishing, call '
54
49
  + `verify_task_checkpoint with sessionId=${JSON.stringify(sessionId)}; use its task_context_token `
55
50
  + 'to checkpoint_task_knowledge, including agentMemory if the role context changed. '
@@ -14,11 +14,18 @@ export function agentContinuation(projectId, agentId, conversationId = null) {
14
14
 
15
15
  export function taskAgentReceipt(application, { projectId, agentId, name = null,
16
16
  sourceApplication = null, workStatus = null, conversationId = null, persistence = null }) {
17
- return { owner: { agent_id: agentId, name,
18
- profile_url: agentProfileUrl(application, agentId), source_application: sourceApplication },
17
+ const owner = agentId ? { agent_id: agentId, name,
18
+ profile_url: agentProfileUrl(application, agentId), source_application: sourceApplication } : null;
19
+ const label = String(name?.trim() || agentId).replace(/[\r\n\t]+/g, ' ')
20
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
21
+ .replace(/[\\`*_{}\[\]()!|]/g, '\\$&');
22
+ const markdown = owner
23
+ ? `FULI Agent:[${label}](<${owner.profile_url.replace(/[<>\s]/g, encodeURIComponent)}>)`
24
+ : 'FULI Agent:未选定';
25
+ return { required: true, markdown, owner,
19
26
  personal_project_id: projectId, work_status: workStatus, persistence,
20
- continuation: agentContinuation(projectId, agentId, conversationId),
21
- guidance: 'Finish with the named owner and profile link, truthful work/memory status and continuation. Add only actually used collaboration_receipt participants (for example Jefa); tools and configured peers are not independent model workers.' };
27
+ continuation: owner ? agentContinuation(projectId, agentId, conversationId) : null,
28
+ guidance: 'MUST include agent_receipt.markdown unchanged exactly once in every final user-visible reply, including greetings, status and errors, even when capture is disabled or nothing is retained. Use this task’s receipt; never invent an owner. Report only truthful work/memory status and actually used collaboration_receipt participants; tools and configured peers are not independent model workers.' };
22
29
  }
23
30
 
24
31
  export function employeeCollaborationReceipt(application, context, { tool, permission, sourceApplication }) {
@@ -0,0 +1,147 @@
1
+ import { createCodexTokenUsageResolver } from './codex/token-usage.js';
2
+
3
+ const TOKEN_USAGE_SOURCES = new Set(['executor', 'host', 'dingdong']);
4
+ const DEFAULT_RESOLVER = createAgentTokenUsageResolver();
5
+
6
+ /**
7
+ * Resolve one exact worker/session identity through the runtime adapter port.
8
+ *
9
+ * A worker runtime is authoritative when present. Its session is never
10
+ * replaced with the reporting host session. A reporting host session is used
11
+ * only when no worker runtime was supplied, which covers direct host activity.
12
+ */
13
+ export function resolveAgentSession(input = {}) {
14
+ const workerRuntime = input.workerRuntime ?? input.worker_runtime;
15
+ if (workerRuntime !== undefined && workerRuntime !== null) {
16
+ if (!workerRuntime || typeof workerRuntime !== 'object' || Array.isArray(workerRuntime)) {
17
+ return null;
18
+ }
19
+ const application = workerRuntime.application;
20
+ const sessionId = safeSessionId(workerRuntime.sessionId ?? workerRuntime.session_id);
21
+ if (!validApplication(application) || !sessionId) return null;
22
+ return {
23
+ application,
24
+ sessionId,
25
+ sourceApplication: input.sourceApplication ?? input.source_application ?? null,
26
+ sourceSessionId: input.sourceSessionId ?? input.source_session_id ?? null,
27
+ scope: 'worker'
28
+ };
29
+ }
30
+
31
+ if (hasWorkerMetadata(input)) return null;
32
+ const application = input.sourceApplication ?? input.source_application;
33
+ const sessionId = safeSessionId(input.sourceSessionId ?? input.source_session_id);
34
+ if (!validApplication(application) || !sessionId) return null;
35
+ return {
36
+ application,
37
+ sessionId,
38
+ sourceApplication: application,
39
+ sourceSessionId: sessionId,
40
+ scope: 'host'
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Shared adapter port for exact session token usage.
46
+ *
47
+ * Adapters receive only the selected session identity and return a cumulative
48
+ * snapshot or null. They must not return transcript content.
49
+ */
50
+ export function createAgentTokenUsageResolver({ adapters } = {}) {
51
+ const registry = adapters instanceof Map
52
+ ? adapters
53
+ : new Map(Object.entries(adapters ?? {
54
+ codex: createCodexTokenUsageResolver()
55
+ }));
56
+ return {
57
+ async resolve(input = {}) {
58
+ const session = resolveAgentSession(input);
59
+ if (!session) return null;
60
+ const adapter = registry.get(session.application);
61
+ const resolve = typeof adapter === 'function' ? adapter : adapter?.resolve;
62
+ if (typeof resolve !== 'function') return null;
63
+ try {
64
+ return normalizeTokenUsage(await resolve.call(adapter, session));
65
+ } catch {
66
+ // Usage collection is best-effort. It must not prevent the host from
67
+ // recording otherwise valid worker activity.
68
+ return null;
69
+ }
70
+ }
71
+ };
72
+ }
73
+
74
+ export async function enrichProjectAgentTaskActivity(input = {}, resolver = DEFAULT_RESOLVER) {
75
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return input;
76
+ const actorKind = input.actorKind ?? input.actor_kind;
77
+ if (actorKind === 'human') return input;
78
+ // Null is an explicit report (including an explicit zero inside the object),
79
+ // so only an omitted value is eligible for automatic collection.
80
+ const explicitTokenUsage = (Object.hasOwn(input, 'tokenUsage') && input.tokenUsage !== undefined) ||
81
+ (Object.hasOwn(input, 'token_usage') && input.token_usage !== undefined);
82
+ if (explicitTokenUsage) return input;
83
+
84
+ const resolve = typeof resolver === 'function' ? resolver : resolver?.resolve;
85
+ if (typeof resolve !== 'function') return input;
86
+ try {
87
+ const tokenUsage = normalizeTokenUsage(await resolve.call(resolver, input));
88
+ return tokenUsage ? { ...input, tokenUsage } : input;
89
+ } catch {
90
+ return input;
91
+ }
92
+ }
93
+
94
+ export async function resolveProjectAgentTaskActivityTokenUsage(input, resolver = DEFAULT_RESOLVER) {
95
+ const enriched = await enrichProjectAgentTaskActivity(input, resolver);
96
+ if (!enriched || typeof enriched !== 'object') return null;
97
+ return enriched.tokenUsage !== undefined ? enriched.tokenUsage : enriched.token_usage ?? null;
98
+ }
99
+
100
+ function normalizeTokenUsage(value) {
101
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
102
+ const source = value.source;
103
+ const totalTokens = nonNegativeInteger(value.totalTokens ?? value.total_tokens);
104
+ if (!TOKEN_USAGE_SOURCES.has(source) || totalTokens === null) return null;
105
+ return {
106
+ source,
107
+ totalTokens,
108
+ inputTokens: optionalNonNegativeInteger(value.inputTokens ?? value.input_tokens),
109
+ outputTokens: optionalNonNegativeInteger(value.outputTokens ?? value.output_tokens),
110
+ cachedInputTokens: optionalNonNegativeInteger(value.cachedInputTokens ?? value.cached_input_tokens),
111
+ cacheWriteInputTokens: optionalNonNegativeInteger(
112
+ value.cacheWriteInputTokens ?? value.cache_write_input_tokens
113
+ ),
114
+ reasoningOutputTokens: optionalNonNegativeInteger(
115
+ value.reasoningOutputTokens ?? value.reasoning_output_tokens
116
+ )
117
+ };
118
+ }
119
+
120
+ function optionalNonNegativeInteger(value) {
121
+ return value === null || value === undefined ? null : nonNegativeInteger(value);
122
+ }
123
+
124
+ function nonNegativeInteger(value) {
125
+ return Number.isSafeInteger(value) && value >= 0 ? value : null;
126
+ }
127
+
128
+ function validApplication(value) {
129
+ return typeof value === 'string' && value.length > 0 && value.length <= 64;
130
+ }
131
+
132
+ function hasWorkerMetadata(input) {
133
+ return [
134
+ input.workerId ?? input.worker_id,
135
+ input.workerLabel ?? input.worker_label,
136
+ input.workerStatus ?? input.worker_status,
137
+ input.workerOccupationEmoji ?? input.worker_occupation_emoji
138
+ ].some((value) => value !== undefined && value !== null && value !== '');
139
+ }
140
+
141
+ function safeSessionId(value) {
142
+ if (typeof value !== 'string') return null;
143
+ const normalized = value.trim();
144
+ if (!normalized || normalized.length > 256 || normalized.includes('..') ||
145
+ !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(normalized)) return null;
146
+ return normalized;
147
+ }
@@ -0,0 +1,40 @@
1
+ import { ProviderRequestError } from '../graphiti/provider-client.js';
2
+
3
+ // Only this first, read-only lookup can be deferred before a scope is known.
4
+ // Each real HTTP attempt is bounded; writes and later handoff operations are
5
+ // never retried here or mistaken for a compatibility failure.
6
+ export async function lookupTranscriptTask(application, input, sourceApplication) {
7
+ for (let attempt = 0; attempt < 2; attempt++) {
8
+ try {
9
+ const task = await application.personal.currentTaskContext({
10
+ personal_space_id: application.config.personal.spaceId,
11
+ session_id: input.session_id, source_application: sourceApplication
12
+ }, { timeoutMs: 1500 });
13
+ if (task !== null && (!task || typeof task !== 'object' || Array.isArray(task)
14
+ || typeof task.token !== 'string' || !task.token
15
+ || !Object.hasOwn(task, 'project_agent_id')
16
+ || (task.project_agent_id !== null && (typeof task.project_agent_id !== 'string'
17
+ || !task.project_agent_id || typeof task.personal_project_id !== 'string' || !task.personal_project_id))
18
+ || (task.session_id !== undefined && task.session_id !== input.session_id)
19
+ || (task.source_application !== undefined && task.source_application !== sourceApplication)
20
+ || (task.personal_space_id !== undefined && task.personal_space_id !== application.config.personal.spaceId))) {
21
+ throw new ProviderRequestError('Current task response does not match its requested scope', { code: 'provider_invalid_response' });
22
+ }
23
+ return { task };
24
+ } catch (error) {
25
+ if (!(error instanceof ProviderRequestError)) throw error;
26
+ if (error.status === 404 && error.code === 'provider_error'
27
+ && error.diagnostic?.status === 404 && error.diagnostic.detail === 'Not Found') {
28
+ return { failure: { status: 'unsupported', entryBlocked: true,
29
+ code: 'current_task_endpoint_unavailable',
30
+ reason: 'Visible transcript synchronization is not available on this Fuli provider version' } };
31
+ }
32
+ const transient = ['provider_unavailable', 'provider_timeout'].includes(error.code)
33
+ || (error.code === 'provider_http_5xx' && [502, 503, 504].includes(error.status));
34
+ if (!transient) throw error;
35
+ if (attempt === 1) return { failure: { status: 'unsaved', entryBlocked: true,
36
+ code: 'current_task_lookup_unavailable',
37
+ reason: 'Visible transcript synchronization is temporarily unavailable after bounded retries; native history remains available for a later verified task entry' } };
38
+ }
39
+ }
40
+ }
@@ -1,5 +1,6 @@
1
1
  import { initialTranscriptBoundary, readTranscriptBatch } from './transcript-reader.js';
2
2
  import { transcriptDigest } from './transcript-guard.js';
3
+ import { lookupTranscriptTask } from './current-task-lookup.js';
3
4
 
4
5
  const pending = reason => ({ status: 'partial', entryBlocked: true, reason });
5
6
  const retryReason = 'Transcript handoff is pending. Retry task entry to continue its saved progress; no new Agent context was supplied';
@@ -9,8 +10,9 @@ export async function syncConversationTranscript(application, input, sourceAppli
9
10
  if (!input?.transcript_path) return { status: 'partial', reason: 'Host did not supply a visible transcript' };
10
11
  let committedPages = 0;
11
12
  try {
12
- const task = await application.personal.currentTaskContext({ personal_space_id: application.config.personal.spaceId,
13
- session_id: input.session_id, source_application: sourceApplication });
13
+ const lookup = await lookupTranscriptTask(application, input, sourceApplication);
14
+ if (lookup.failure) return lookup.failure;
15
+ const task = lookup.task;
14
16
  if (!task?.project_agent_id) return guard?.value ? pending(retryReason) : { status: 'unassigned' };
15
17
  const scope = { personal_space_id: application.config.personal.spaceId, personal_project_id: task.personal_project_id,
16
18
  agent_id: task.project_agent_id, source_application: sourceApplication };
@@ -92,6 +92,7 @@ export function openFederatedGraphApplication({
92
92
  config,
93
93
  capturePolicyStore,
94
94
  agentAccessPolicyStore,
95
+ tokenUsageResolver,
95
96
  fetchImpl = globalThis.fetch,
96
97
  env = process.env
97
98
  }) {
@@ -105,6 +106,7 @@ export function openFederatedGraphApplication({
105
106
  agentAccessPolicyStore: agentAccessPolicyStore ?? new AgentAccessPolicyStore(
106
107
  agentAccessPolicyPathForRuntime(runtimeConfigPath)
107
108
  ),
109
+ tokenUsageResolver,
108
110
  consoleUrl: sourceConsoleUrl(runtimeConfigPath),
109
111
  providerRequestTimeoutMs: providerRequestTimeoutFromEnv(env)
110
112
  });
@@ -140,7 +142,8 @@ export class FederatedGraphApplication extends ProjectAgentControlPlaneApplicati
140
142
  consoleUrl = sourceConsoleUrl(null),
141
143
  projectPathResolver = resolvePersonalProjectPath,
142
144
  taskContextRegistry = null,
143
- providerRequestTimeoutMs = undefined
145
+ providerRequestTimeoutMs = undefined,
146
+ tokenUsageResolver = undefined
144
147
  } = {}) {
145
148
  super();
146
149
  this.graphiti = true;
@@ -149,6 +152,7 @@ export class FederatedGraphApplication extends ProjectAgentControlPlaneApplicati
149
152
  this.agentAccessPolicyStore = agentAccessPolicyStore;
150
153
  this.consoleUrl = consoleUrl;
151
154
  this.projectPathResolver = projectPathResolver;
155
+ this.tokenUsageResolver = tokenUsageResolver;
152
156
  this.personal = new GraphitiProviderClient({
153
157
  baseUrl: config.personal.providerUrl,
154
158
  accessToken: config.personal.accessToken,
@@ -285,6 +289,7 @@ export class FederatedGraphApplication extends ProjectAgentControlPlaneApplicati
285
289
  return {
286
290
  status: 'generated',
287
291
  ...skill,
292
+ agent_receipt: context.agent_receipt,
288
293
  ...(context.task_knowledge_recall
289
294
  ? { task_knowledge_recall: context.task_knowledge_recall }
290
295
  : {}),
@@ -380,7 +380,7 @@ export function providerProjectAgentTaskActivity(input) {
380
380
  }
381
381
  if (hasAny(input, ['tokenUsage', 'token_usage'])) {
382
382
  result.token_usage = providerProjectAgentTokenUsage(
383
- input.tokenUsage ?? input.token_usage
383
+ input.tokenUsage !== undefined ? input.tokenUsage : input.token_usage
384
384
  );
385
385
  }
386
386
  if (hasAny(input, ['workerRuntime', 'worker_runtime'])) {
@@ -1,5 +1,6 @@
1
1
  import { loadProjectAgentContinuity } from './project-agent-task-entry.js';
2
2
  import { resolveCoordinationScope, scopedTaskIdempotencyKey } from './temporary-project-scope.js';
3
+ import { enrichProjectAgentTaskActivity } from '../agents/token-usage-resolver.js';
3
4
  import {
4
5
  executorActualReportRecord,
5
6
  executorRecord,
@@ -306,8 +307,12 @@ export async function viewProjectAgentTask(application, input) {
306
307
  }
307
308
 
308
309
  export async function recordProjectAgentTaskActivity(application, input) {
310
+ const activity = await enrichProjectAgentTaskActivity(
311
+ input,
312
+ application.tokenUsageResolver
313
+ );
309
314
  const value = await application.personal.recordProjectAgentTaskActivity(
310
- providerProjectAgentTaskActivity(input)
315
+ providerProjectAgentTaskActivity(activity)
311
316
  );
312
317
  return value?.task ? projectAgentTaskRecord(value.task) : projectAgentTaskRecord(value);
313
318
  }
@@ -72,8 +72,10 @@ export class GraphitiProviderClient {
72
72
  method: operation === 'policy' ? 'PUT' : 'POST', body: input
73
73
  });
74
74
  }
75
- currentTaskContext(input) {
76
- return this.#request(`/v1/task-context-sessions/current?${new URLSearchParams(input)}`);
75
+ currentTaskContext(input, { timeoutMs = this.requestTimeoutMs } = {}) {
76
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) throw new TypeError('Current task timeout must be a positive safe integer');
77
+ return this.#request(`/v1/task-context-sessions/current?${new URLSearchParams(input)}`,
78
+ { timeoutMs: Math.min(timeoutMs, this.requestTimeoutMs) });
77
79
  }
78
80
  health() { return this.#request('/health', { authenticated: false }); }
79
81
  listSpaces() { return this.#request('/v1/spaces'); }
@@ -695,7 +697,8 @@ export class GraphitiProviderClient {
695
697
  method = 'GET',
696
698
  body,
697
699
  authenticated = true,
698
- workflowObservation = false
700
+ workflowObservation = false,
701
+ timeoutMs = this.requestTimeoutMs
699
702
  } = {}) {
700
703
  const headers = { accept: 'application/json' };
701
704
  if (authenticated) headers.authorization = `Bearer ${this.accessToken}`;
@@ -708,7 +711,7 @@ export class GraphitiProviderClient {
708
711
  const timeout = setTimeout(() => {
709
712
  timedOut = true;
710
713
  controller.abort(new Error('Graphiti provider request timed out'));
711
- }, this.requestTimeoutMs);
714
+ }, timeoutMs);
712
715
  const callerSignal = activeAgentRequestSignal();
713
716
  const signal = callerSignal
714
717
  ? AbortSignal.any([controller.signal, callerSignal])
@@ -36,15 +36,14 @@ export async function taskEntryPreferences(application, projectResolution, {
36
36
  sourceApplication, sourceSessionId, sessionId })
37
37
  : null;
38
38
  const managedPreferences = management ? { ...preferences, project_management_context: management } : preferences;
39
- if (!selection) return managedPreferences;
40
- const context = selection.agent ? await loadProjectAgentContinuity(application, {
39
+ const context = selection?.agent ? await loadProjectAgentContinuity(application, {
41
40
  projectId: projectResolution.personalProjectId, agent: selection.agent,
42
41
  sourceApplication, taskPrompt, selectionReason: selection.reason,
43
42
  matchBasis: selection.match_basis
44
43
  }) : selection;
45
- return { ...managedPreferences, project_agent_context: context,
46
- ...(selection.agent ? { agent_receipt: taskAgentReceipt(application, {
47
- projectId: projectResolution.personalProjectId, agentId: selection.agent.agentId,
48
- name: context.role?.name, sourceApplication
49
- }) } : {}) };
44
+ return { ...managedPreferences, ...(context ? { project_agent_context: context } : {}),
45
+ agent_receipt: taskAgentReceipt(application, {
46
+ projectId: projectResolution.personalProjectId, agentId: selection?.agent?.agentId ?? null,
47
+ name: context?.role?.name, sourceApplication
48
+ }) };
50
49
  }
@@ -78,5 +78,7 @@ function faviconFilePath() {
78
78
  }
79
79
 
80
80
  function shouldServeApplication(pathname) {
81
- return extname(pathname) === '' && !pathname.startsWith('/api/');
81
+ // Agent identities (for example employee.jefa) are route parameters, not assets.
82
+ const agentProfile = /^\/agents\/[^/]+\/[^/]+\/?$/.test(pathname);
83
+ return !pathname.startsWith('/api/') && (agentProfile || extname(pathname) === '');
82
84
  }