fuli-context 0.9.0 → 0.11.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 (210) hide show
  1. package/README.md +29 -58
  2. package/README.zh-CN.md +24 -54
  3. package/dist/web/assets/AboutPage-CKq4IRHd.js +1 -0
  4. package/dist/web/assets/AgentConversations-DfXiFvBN.js +2 -0
  5. package/dist/web/assets/AgentConversations-XqNEBY_2.css +1 -0
  6. package/dist/web/assets/AgentHand-BBM-sKh2.js +1 -0
  7. package/dist/web/assets/AgentName-C_6R80nP.js +1 -0
  8. package/dist/web/assets/AgentProfilePage-Cz-i1fRD.js +1 -0
  9. package/dist/web/assets/AgentsDirectoryPage-BI0EFz6y.js +1 -0
  10. package/dist/web/assets/AgentsDirectoryPage-yRWktiin.css +1 -0
  11. package/dist/web/assets/ConnectionsPage-BoC6kXgs.js +1 -0
  12. package/dist/web/assets/ConnectionsPage-CM9C-zEI.css +1 -0
  13. package/dist/web/assets/EmployeeRecruitDialog-BVyclQAJ.js +1 -0
  14. package/dist/web/assets/EmployeeRecruitDialog-CVbqCzHb.css +1 -0
  15. package/dist/web/assets/EmployeeWorkbenchPage-D-c_Jm2z.css +1 -0
  16. package/dist/web/assets/EmployeeWorkbenchPage-D2GvYraI.js +3 -0
  17. package/dist/web/assets/KnowledgeEditDialog-a0AKWics.js +1 -0
  18. package/dist/web/assets/KnowledgeEditDialog-hT9znkoe.css +1 -0
  19. package/dist/web/assets/KnowledgeInspector-DMg9yJZ2.js +1 -0
  20. package/dist/web/assets/KnowledgeOrganizerPage-BgsHM3Qw.js +1 -0
  21. package/dist/web/assets/KnowledgeOrganizerPage-DeL7Jp34.css +1 -0
  22. package/dist/web/assets/KnowledgePage-kn8MqUCO.js +1 -0
  23. package/dist/web/assets/KnowledgeWorkspace-BtRq9fvC.js +4 -0
  24. package/dist/web/assets/KnowledgeWorkspace-D4a7FcAb.css +1 -0
  25. package/dist/web/assets/NotFoundPage-D-7jLz3o.js +1 -0
  26. package/dist/web/assets/OverviewPage-BHPc-XxY.js +1 -0
  27. package/dist/web/assets/OverviewPage-By-fUzSG.css +1 -0
  28. package/dist/web/assets/PersonalProfilePage-C00iV0Kx.js +1 -0
  29. package/dist/web/assets/PersonalProfilePage-DrMDAAdy.css +1 -0
  30. package/dist/web/assets/PersonalProjectsPage-CFNkdHSo.js +1 -0
  31. package/dist/web/assets/ProjectAgentFirstTask-Bi1ZsTC1.css +1 -0
  32. package/dist/web/assets/ProjectAgentFirstTask-CrCWEYgn.js +1 -0
  33. package/dist/web/assets/ProjectAgentsPage-BzztLMH-.js +12 -0
  34. package/dist/web/assets/ProjectAgentsPage-DNLNjYp-.css +1 -0
  35. package/dist/web/assets/ProjectScopePicker-CMxn3txv.js +1 -0
  36. package/dist/web/assets/PublicProjectsPage-DKamXDV9.css +1 -0
  37. package/dist/web/assets/PublicProjectsPage-DQVtvhOS.js +1 -0
  38. package/dist/web/assets/ReviewPage-BZhvyDWc.js +1 -0
  39. package/dist/web/assets/RoundtablesPage-CVebTXQa.js +1 -0
  40. package/dist/web/assets/RoundtablesPage-Cb0R4GaM.css +1 -0
  41. package/dist/web/assets/SettingsPage-1knvfOW6.js +1 -0
  42. package/dist/web/assets/SettingsPage-BmYkLul4.css +1 -0
  43. package/dist/web/assets/SharedJefaReportPage-BCVq8u0P.js +1 -0
  44. package/dist/web/assets/UiDialog-Da1P10f4.js +1 -0
  45. package/dist/web/assets/UiDisclosure-8wE8YuE4.js +1 -0
  46. package/dist/web/assets/UiSegmented-BKZXc8QF.js +1 -0
  47. package/dist/web/assets/UiSelect-Bv6R0yr1.css +1 -0
  48. package/dist/web/assets/UiSelect-D5Xj5Ec4.js +1 -0
  49. package/dist/web/assets/WritingTastePage-B8-etOBn.js +1 -0
  50. package/dist/web/assets/WritingTastePage-B_zYD1df.css +1 -0
  51. package/dist/web/assets/agent-profile-BZj3ezWe.css +1 -0
  52. package/dist/web/assets/{attention-store-Dk-_P4Oi.js → attention-store-BfxtL9EV.js} +1 -1
  53. package/dist/web/assets/{catalog-DN72452f.js → catalog-6SN9i3WR.js} +1 -1
  54. package/dist/web/assets/client-CUTkF3t4.js +1 -0
  55. package/dist/web/assets/{console-BtPqo79j.js → console-CG8tdtSs.js} +1 -1
  56. package/dist/web/assets/{conversation-launcher-settings-jkHRmWai.js → conversation-launcher-settings-CyZkLJg7.js} +1 -1
  57. package/dist/web/assets/{i18n-D2CJkEYK.js → i18n-CCMR4gDS.js} +3 -3
  58. package/dist/web/assets/{identity-B3zEw-1u.js → identity-C1eN-FSB.js} +1 -1
  59. package/dist/web/assets/index-kPB0Cf8Q.css +1 -0
  60. package/dist/web/assets/index-sLDKI6R-.js +5 -0
  61. package/dist/web/assets/{model-S7ehAjky.js → model-l-sAqtW_.js} +1 -1
  62. package/dist/web/assets/{personal-profile-graph--GG3Mizo.js → personal-profile-graph-BD1Y6TDS.js} +1 -1
  63. package/dist/web/assets/shared-report-B46dxlCR.js +1 -0
  64. package/dist/web/assets/{useAgentRoster-BAPKWZ9y.js → useAgentRoster-DdxoKDEE.js} +1 -1
  65. package/dist/web/assets/{useApi-CROJJdhE-BtFMXFjw.js → useApi-CROJJdhE-DlOiu8lP.js} +1 -1
  66. package/dist/web/assets/{vue-router-DkaTtPn0.js → vue-router-DjqEU4W2.js} +1 -1
  67. package/dist/web/index.html +14 -16
  68. package/docs/agent-conversations-and-collaboration.md +9 -9
  69. package/docs/agent-roundtable.md +61 -0
  70. package/docs/employee-agents.md +2 -2
  71. package/docs/project-agent-memory.md +1 -1
  72. package/graph-provider/fuli_graph/agent_conversation_models.py +11 -2
  73. package/graph-provider/fuli_graph/conversation_context.py +40 -9
  74. package/graph-provider/fuli_graph/default_project_lead.py +88 -0
  75. package/graph-provider/fuli_graph/project_agent_memory_routes.py +7 -0
  76. package/graph-provider/fuli_graph/project_agent_task_models.py +3 -0
  77. package/graph-provider/fuli_graph/runtime.py +3 -76
  78. package/graph-provider/fuli_graph/store.py +2 -0
  79. package/graph-provider/fuli_graph/store_agent_conversations.py +60 -8
  80. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +3 -1
  81. package/graph-provider/fuli_graph/store_project_agent_tasks.py +12 -8
  82. package/graph-provider/fuli_graph/store_project_agents.py +25 -124
  83. package/graph-provider/fuli_graph/store_task_contexts.py +26 -0
  84. package/graph-provider/fuli_graph/system_agents.py +128 -0
  85. package/graph-provider/fuli_graph/system_hr_identity.py +1 -1
  86. package/graph-provider/fuli_graph/task_context_routes.py +10 -0
  87. package/graph-provider/pyproject.toml +1 -1
  88. package/npm-shrinkwrap.json +2 -2
  89. package/package.json +2 -4
  90. package/skills/capturing-session-knowledge/SKILL.md +1 -1
  91. package/src/agent-roundtable/http.js +14 -0
  92. package/src/agent-roundtable/service.js +218 -0
  93. package/src/agent-roundtable/store.js +86 -0
  94. package/src/agent-roundtable/tools.js +61 -0
  95. package/src/agent-roundtable/wake.js +99 -0
  96. package/src/agent-tools/agent-conversation-definitions.js +3 -3
  97. package/src/agent-tools/graph-definitions.js +3 -2
  98. package/src/agent-tools/interface-catalog.js +0 -5
  99. package/src/agent-tools.js +2 -2
  100. package/src/agents/claude-code/lifecycle-hook.js +6 -19
  101. package/src/agents/claude-code/wake.js +37 -0
  102. package/src/agents/codex/lifecycle-hook.js +51 -27
  103. package/src/agents/codex/token-usage.js +235 -0
  104. package/src/agents/codex/wake.js +47 -0
  105. package/src/agents/cursor/lifecycle-hook.js +3 -9
  106. package/src/agents/identity-receipt.js +16 -6
  107. package/src/agents/session-log.js +34 -0
  108. package/src/agents/token-usage-resolver.js +147 -0
  109. package/src/agents/wake-registry.js +14 -0
  110. package/src/cli/command-registry.js +0 -2
  111. package/src/cli/setup-command.js +2 -5
  112. package/src/cli/update-command.js +13 -2
  113. package/src/cli.js +0 -5
  114. package/src/employees/management-policy.js +4 -2
  115. package/src/employees/manifest.js +3 -0
  116. package/src/employees/recruitment.js +3 -2
  117. package/src/graphiti/agent-conversations.js +11 -9
  118. package/src/graphiti/agent-knowledge-workflows.js +34 -7
  119. package/src/graphiti/federated-application.js +18 -6
  120. package/src/graphiti/project-agent-mapping.js +1 -1
  121. package/src/graphiti/project-agent-task-entry.js +19 -7
  122. package/src/graphiti/project-agent-workflows.js +6 -1
  123. package/src/graphiti/project-path-context.js +7 -2
  124. package/src/graphiti/provider-client.js +8 -0
  125. package/src/graphiti/repository-project-registration.js +17 -0
  126. package/src/graphiti/task-entry-preferences.js +5 -0
  127. package/src/http/api-router.js +5 -3
  128. package/src/http/error-mapping.js +0 -2
  129. package/src/mcp/instructions.js +5 -5
  130. package/src/mcp/session-id.js +1 -1
  131. package/src/mcp/tool-annotations.js +3 -2
  132. package/src/setup/agents.js +1 -41
  133. package/src/setup/graph-runtime.js +15 -2
  134. package/src/setup/session-skill.js +22 -2
  135. package/dist/web/assets/AboutPage-D-GyJNv1.js +0 -1
  136. package/dist/web/assets/AgentConversations-DOuZgUeA.js +0 -2
  137. package/dist/web/assets/AgentConversations-DS8h3yKn.css +0 -1
  138. package/dist/web/assets/AgentHand-eGsXoGSq.js +0 -1
  139. package/dist/web/assets/AgentName-Dtmu9Ff5.js +0 -1
  140. package/dist/web/assets/AgentProfilePage-D8Tvayn7.js +0 -1
  141. package/dist/web/assets/AgentsDirectoryPage-BTEfKZjF.js +0 -1
  142. package/dist/web/assets/ConnectionsPage-C1w99yN3.css +0 -1
  143. package/dist/web/assets/ConnectionsPage-DwOIcvl9.js +0 -1
  144. package/dist/web/assets/EmployeeRecruitDialog-BrIMRblq.css +0 -1
  145. package/dist/web/assets/EmployeeRecruitDialog-D11blfwU.js +0 -1
  146. package/dist/web/assets/EmployeeWorkbenchPage-BcEzYt65.css +0 -1
  147. package/dist/web/assets/EmployeeWorkbenchPage-CK2BAoGt.js +0 -3
  148. package/dist/web/assets/GrowthLoading-KTjE2e50.js +0 -1
  149. package/dist/web/assets/KnowledgeEditDialog-B3x8vs3c.css +0 -1
  150. package/dist/web/assets/KnowledgeEditDialog-n9trHQ5d.js +0 -1
  151. package/dist/web/assets/KnowledgeInspector-BcNjgw9H.js +0 -1
  152. package/dist/web/assets/KnowledgeOrganizerPage-BzgxP-nG.css +0 -1
  153. package/dist/web/assets/KnowledgeOrganizerPage-DMBpGxLr.js +0 -1
  154. package/dist/web/assets/KnowledgePage-POkCMW8f.js +0 -1
  155. package/dist/web/assets/KnowledgeWorkspace-2FvQadhB.js +0 -4
  156. package/dist/web/assets/KnowledgeWorkspace-sb8OJ97I.css +0 -1
  157. package/dist/web/assets/NotFoundPage-BCMZbeB8.js +0 -1
  158. package/dist/web/assets/OverviewPage-0CFWCG9-.css +0 -1
  159. package/dist/web/assets/OverviewPage-0mYifpjp.js +0 -1
  160. package/dist/web/assets/PersonalProfilePage-Chv2a2pi.css +0 -1
  161. package/dist/web/assets/PersonalProfilePage-JW6Muix6.js +0 -1
  162. package/dist/web/assets/PersonalProjectsPage-CRbi64JP.js +0 -1
  163. package/dist/web/assets/ProjectAgentFirstTask-6afJ8HUe.css +0 -1
  164. package/dist/web/assets/ProjectAgentFirstTask-DJ0wTbh8.js +0 -1
  165. package/dist/web/assets/ProjectAgentsPage-BgQLIpxg.css +0 -1
  166. package/dist/web/assets/ProjectAgentsPage-CQemHFxy.js +0 -12
  167. package/dist/web/assets/ProjectScopePicker-BwB7gK-7.js +0 -1
  168. package/dist/web/assets/PublicProjectsPage-CB272Sq-.js +0 -1
  169. package/dist/web/assets/ReviewPage-CelTZU4C.js +0 -1
  170. package/dist/web/assets/RoundtablesPage-ByVeZlgC.js +0 -2
  171. package/dist/web/assets/RoundtablesPage-DCbIBnBq.css +0 -1
  172. package/dist/web/assets/SearchableSelect-f7EHbyDL.js +0 -1
  173. package/dist/web/assets/SettingsPage-5g7mRN2F.css +0 -1
  174. package/dist/web/assets/SettingsPage-nAijmm5f.js +0 -1
  175. package/dist/web/assets/SharedJefaReportPage-CjpJ9bkk.js +0 -1
  176. package/dist/web/assets/UiDisclosure-CWlC91Zt.js +0 -1
  177. package/dist/web/assets/WritingTastePage-DaHWY61P.js +0 -1
  178. package/dist/web/assets/WritingTastePage-SMGF39mf.css +0 -1
  179. package/dist/web/assets/agent-profile-CBvJHGjt.css +0 -1
  180. package/dist/web/assets/client-B2cRl-7c.js +0 -1
  181. package/dist/web/assets/copy-D2LqwtPZ.js +0 -1
  182. package/dist/web/assets/index-CNp0tRNp.css +0 -1
  183. package/dist/web/assets/index-UygXwZDZ.js +0 -5
  184. package/dist/web/assets/shared-report-DEK9wvhl.js +0 -1
  185. package/dist/web/assets/useModalDialog-D8tiJfbc.js +0 -1
  186. package/docs/roundtable-beta-testing.md +0 -102
  187. package/docs/roundtable-beta-validation.md +0 -133
  188. package/docs/roundtable-research.md +0 -142
  189. package/src/agents/a2a/roundtable-participant.js +0 -374
  190. package/src/agents/claude-code/roundtable-participant.js +0 -24
  191. package/src/agents/codex/roundtable-config.js +0 -32
  192. package/src/agents/codex/roundtable-participant.js +0 -34
  193. package/src/agents/grok/roundtable-participant.js +0 -185
  194. package/src/agents/pi/roundtable-participant.js +0 -193
  195. package/src/agents/pi/roundtable-result-format.js +0 -183
  196. package/src/cli/roundtable-command.js +0 -79
  197. package/src/roundtables/application.js +0 -69
  198. package/src/roundtables/domain.js +0 -181
  199. package/src/roundtables/http-router.js +0 -56
  200. package/src/roundtables/mcp-server.js +0 -48
  201. package/src/roundtables/participant-registry.js +0 -12
  202. package/src/roundtables/process.js +0 -70
  203. package/src/roundtables/result-contract.js +0 -47
  204. package/src/roundtables/server.js +0 -52
  205. package/src/roundtables/service.js +0 -396
  206. package/src/roundtables/store.js +0 -59
  207. package/src/roundtables/tool-contract.js +0 -41
  208. package/src/roundtables/turn-prompt.js +0 -24
  209. package/src/roundtables/worker.js +0 -85
  210. /package/dist/web/assets/{GrowthLoading-CIuQ1icy.css → client-CIuQ1icy.css} +0 -0
@@ -26,20 +26,9 @@ export async function codexStopLifecycleOutput(input, invoke) {
26
26
  if (check?.decision !== 'block' || typeof check.reason !== 'string' || !check.reason.trim()) {
27
27
  return {};
28
28
  }
29
- if (input.stop_hook_active === true) {
30
- if (typeof check.task_context_token === 'string' && check.task_context_token.trim()) {
31
- await invoke('checkpoint_task_knowledge', {
32
- taskContextToken: check.task_context_token,
33
- disposition: 'retain_nothing',
34
- reason: 'Codex Stop hook fallback after one checkpoint continuation.',
35
- workLog: { status: 'incomplete', summary: 'The host stopped before the employee submitted a work summary. Review the prior task before resuming.' },
36
- sourceApplication: 'codex',
37
- sourceSessionId: sessionId
38
- });
39
- }
40
- return {};
41
- }
42
- return { decision: 'block', reason: boundedHookMessage(check.reason) };
29
+ // Checkpoints are advisory to the host. Never force another model turn or
30
+ // fabricate a work log merely because the user is ending the conversation.
31
+ return { systemMessage: boundedHookMessage(check.reason) };
43
32
  }
44
33
 
45
34
  export async function codexLifecycleOutput(event, input, invoke) {
@@ -73,6 +62,9 @@ export async function runCodexLifecycleHook(args, dependencies = {}) {
73
62
  );
74
63
  let app;
75
64
  let leases;
65
+ let closing;
66
+ const cleanup = () => closing ??= Promise.allSettled([closeQuietly(leases), closeQuietly(app)]);
67
+ dependencies.onCleanup?.(cleanup);
76
68
  try {
77
69
  app = (dependencies.openApplication ?? openFederatedGraphApplication)({
78
70
  runtimeConfigPath
@@ -94,32 +86,31 @@ export async function runCodexLifecycleHook(args, dependencies = {}) {
94
86
  // Existing guards and all later failures remain fail-closed.
95
87
  return { systemMessage: `Fuli: ${sync.reason}. Message submission is allowed; no saved Agent context was loaded or changed.` };
96
88
  }
97
- if (event === 'UserPromptSubmit' && sync.entryBlocked) return { decision: 'block', reason: sync.reason };
89
+ if (event === 'UserPromptSubmit' && sync.entryBlocked) return { systemMessage: boundedHookMessage(sync.reason) };
98
90
  const entryGuard = sync.status === 'capture_disabled' ? null : guard;
99
91
  if (event === 'UserPromptSubmit' && entryGuard?.value) await entryGuard.write({ ...entryGuard.value, phase: 'beginning' });
100
92
  const result = await codexLifecycleOutput(event, input, async (name, parameters) => {
101
- const context = await invokeTool(app, name, parameters);
93
+ const context = await invokeTool(app, name, parameters, { signal: dependencies.signal });
102
94
  if (name === 'begin_task_context') await recordTranscriptTaskEntry(entryGuard, context);
103
95
  return context;
104
96
  });
105
97
  if (event === 'UserPromptSubmit') sync = await claimTranscriptBoundary(app, input, 'codex', sync, {
106
98
  normalize: normalizeCodexRecord, verify: verifyCodexTranscript
107
99
  }, entryGuard);
108
- if (event === 'UserPromptSubmit' && sync.entryBlocked) return { decision: 'block', reason: sync.reason };
100
+ if (event === 'UserPromptSubmit' && sync.entryBlocked) return { systemMessage: boundedHookMessage(sync.reason) };
109
101
  return withTranscriptNotice(result, sync);
110
102
  });
111
103
  } catch (error) {
112
104
  if (event !== 'UserPromptSubmit' || !app.getCapturePolicy?.().enabled || !input.transcript_path) throw error;
113
- return { decision: 'block', reason: error.code === 'EADDRINUSE'
105
+ return { systemMessage: error.code === 'EADDRINUSE'
114
106
  ? 'The session handoff lock is occupied. Retry after the current hook finishes; if it persists, start a new host session and resume the intended Agent.'
115
107
  : 'Fuli transcript handoff could not be completed safely. Retry task entry; no previous Agent context was supplied.' };
116
108
  }
117
- });
109
+ }, { signal: dependencies.signal });
118
110
  write(`${JSON.stringify(output)}\n`);
119
111
  return output;
120
112
  } finally {
121
- await closeQuietly(leases);
122
- await closeQuietly(app);
113
+ await cleanup();
123
114
  }
124
115
  }
125
116
 
@@ -142,13 +133,46 @@ async function readHookInput() {
142
133
  return JSON.parse(Buffer.concat(chunks).toString('utf8'));
143
134
  }
144
135
 
145
- async function main() {
136
+ export async function runCodexLifecycleCommand(args, dependencies = {}) {
137
+ const configured = Number(args[args.indexOf('--timeout-ms') + 1]);
138
+ const timeoutMs = args.includes('--timeout-ms') && Number.isSafeInteger(configured) && configured > 0
139
+ ? Math.min(configured, 5000) : 5000;
140
+ const controller = new AbortController();
141
+ let emitted = false;
142
+ const write = value => {
143
+ if (emitted) return;
144
+ emitted = true;
145
+ (dependencies.write ?? (value => process.stdout.write(value)))(value);
146
+ };
147
+ const unavailable = JSON.stringify({ systemMessage: 'Fuli context is unavailable. Continue with the user request; saved context and checkpoint were not verified.' }) + '\n';
148
+ let cleanup = async () => {};
149
+ let deadline;
150
+ const expired = new Promise(resolve => {
151
+ deadline = setTimeout(async () => {
152
+ controller.abort();
153
+ write(unavailable);
154
+ // A broken dependency must not hold the host open. Give opened leases
155
+ // and connections a bounded chance to close before the process exits.
156
+ let grace;
157
+ await Promise.race([cleanup(), new Promise(done => { grace = setTimeout(done, 250); })]);
158
+ clearTimeout(grace);
159
+ resolve({ timedOut: true });
160
+ }, timeoutMs);
161
+ });
146
162
  try {
147
- await runCodexLifecycleHook(process.argv.slice(2));
148
- } catch {
149
- // Fail open without printing prompts, transcripts, paths or credentials.
150
- process.stdout.write('{}\n');
163
+ const running = runCodexLifecycleHook(args, { ...dependencies, signal: controller.signal, write,
164
+ onCleanup: close => { cleanup = close; } }).then(() => ({ timedOut: false }), () => {
165
+ // Never print prompts, transcripts, paths or credentials.
166
+ write(unavailable);
167
+ return { timedOut: false };
168
+ });
169
+ return await Promise.race([running, expired]);
170
+ } finally {
171
+ clearTimeout(deadline);
151
172
  }
152
173
  }
153
174
 
154
- if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main();
175
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
176
+ const result = await runCodexLifecycleCommand(process.argv.slice(2));
177
+ if (result.timedOut) process.exit(0);
178
+ }
@@ -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
+ }
@@ -0,0 +1,47 @@
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ import { findFile, firstField, listDirectory } from '../session-log.js';
6
+
7
+ // Wakes Codex headlessly with a read-only sandbox. Resuming records the exchange in
8
+ // that conversation; Codex refuses while the conversation is open in the app.
9
+ export const codexWake = Object.freeze({
10
+ id: 'codex',
11
+
12
+ resolveCommand({ env, which, platform, fileExists = existsSync }) {
13
+ if (env.FULI_CODEX_BIN) return env.FULI_CODEX_BIN;
14
+ const found = which('codex', platform);
15
+ if (found) return found;
16
+ if (platform === 'win32') return newestBundledCodex(join(env.LOCALAPPDATA ?? join(homedir(), 'AppData', 'Local'), 'OpenAI', 'Codex', 'bin'));
17
+ const bundled = platform === 'darwin' ? '/Applications/Codex.app/Contents/Resources/codex' : null;
18
+ return bundled && fileExists(bundled) ? bundled : null;
19
+ },
20
+
21
+ args({ sessionId, cwd }) {
22
+ return sessionId
23
+ ? ['exec', 'resume', '--json', '--skip-git-repo-check', '-c', 'sandbox_mode="read-only"', sessionId, '-']
24
+ : ['exec', '--json', '--skip-git-repo-check', '--sandbox', 'read-only', '-C', cwd, '-'];
25
+ },
26
+
27
+ // A session resumes only from the directory it started in.
28
+ sessionDirectory(sessionId, { home, env }) {
29
+ const file = findFile(join(env.CODEX_HOME || join(home, '.codex'), 'sessions'), `${sessionId}.jsonl`, 4);
30
+ return file ? firstField(file, (line) => line.type === 'session_meta' ? line.payload?.cwd : null) : null;
31
+ },
32
+
33
+ // `codex exec --json` streams items; the last agent message before turn.completed is the answer.
34
+ initialState: () => ({ body: '', completed: false, failed: false }),
35
+ readEvent(event, state) {
36
+ if (event.type === 'item.completed' && event.item?.type === 'agent_message') state.body = event.item.text ?? '';
37
+ if (event.type === 'turn.completed') state.completed = true;
38
+ if (event.type === 'turn.failed' || event.type === 'error') state.failed = true;
39
+ },
40
+ });
41
+
42
+ // The Codex app keeps each CLI version in its own folder; the newest is current.
43
+ function newestBundledCodex(root) {
44
+ const candidates = [join(root, 'codex.exe'), ...listDirectory(root).map((entry) => join(root, entry, 'codex.exe'))]
45
+ .filter((path) => existsSync(path));
46
+ return candidates.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0] ?? null;
47
+ }
@@ -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') {
@@ -12,20 +12,30 @@ export function agentContinuation(projectId, agentId, conversationId = null) {
12
12
  guidance: 'Use a connected client and the same authorized project/Agent. Obtain a fresh task context; never reuse the previous client’s token. This is a FULI prompt, not a native client @ picker registration.' };
13
13
  }
14
14
 
15
- export function taskAgentReceipt(application, { projectId, agentId, name = null,
16
- sourceApplication = null, workStatus = null, conversationId = null, persistence = null }) {
17
- const owner = agentId ? { agent_id: agentId, name,
18
- profile_url: agentProfileUrl(application, agentId), source_application: sourceApplication } : null;
15
+ function agentLink(application, agentId, name) {
19
16
  const label = String(name?.trim() || agentId).replace(/[\r\n\t]+/g, ' ')
20
17
  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
21
18
  .replace(/[\\`*_{}\[\]()!|]/g, '\\$&');
19
+ return `[${label}](<${agentProfileUrl(application, agentId).replace(/[<>\s]/g, encodeURIComponent)}>)`;
20
+ }
21
+
22
+ // The reply's attribution line: who led the task and who actually helped.
23
+ export function taskAgentReceipt(application, { projectId, agentId, name = null,
24
+ sourceApplication = null, workStatus = null, conversationId = null, persistence = null,
25
+ collaborators = [] }) {
26
+ const owner = agentId ? { agent_id: agentId, name,
27
+ profile_url: agentProfileUrl(application, agentId), source_application: sourceApplication } : null;
28
+ const helpers = owner ? collaborators.filter(item => item.agentId !== agentId) : [];
22
29
  const markdown = owner
23
- ? `FULI Agent:[${label}](<${owner.profile_url.replace(/[<>\s]/g, encodeURIComponent)}>)`
30
+ ? `FULI Agent:负责人 ${agentLink(application, agentId, name)}`
31
+ + (helpers.length ? ` · 协作 ${helpers.map(item => agentLink(application, item.agentId, item.name)).join('、')}` : '')
24
32
  : 'FULI Agent:未选定';
25
33
  return { required: true, markdown, owner,
34
+ collaborators: helpers.map(item => ({ agent_id: item.agentId, name: item.name,
35
+ profile_url: agentProfileUrl(application, item.agentId) })),
26
36
  personal_project_id: projectId, work_status: workStatus, persistence,
27
37
  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.' };
38
+ guidance: 'MUST end every final user-visible reply with agent_receipt.markdown unchanged, exactly once, including greetings, status and errors, even when capture is disabled or nothing is retained. The receipt returned by checkpoint_task_knowledge supersedes the entry receipt because it names actual collaborators. Never invent an owner or a collaborator.' };
29
39
  }
30
40
 
31
41
  export function employeeCollaborationReceipt(application, context, { tool, permission, sourceApplication }) {
@@ -0,0 +1,34 @@
1
+ import { closeSync, openSync, readSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ // Read-only helpers for locating a client's JSONL session log on disk.
5
+
6
+ export function listDirectory(directory) {
7
+ try { return readdirSync(directory); } catch { return []; }
8
+ }
9
+
10
+ // Newest-first search for a file ending in `suffix`, at most `depth` levels down.
11
+ export function findFile(directory, suffix, depth) {
12
+ for (const entry of listDirectory(directory).sort().reverse()) {
13
+ const path = join(directory, entry);
14
+ if (entry.endsWith(suffix)) return path;
15
+ if (depth > 0 && !entry.includes('.')) {
16
+ const found = findFile(path, suffix, depth - 1);
17
+ if (found) return found;
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+
23
+ // The first non-empty string `pick` returns for a JSON line near the start of the log.
24
+ export function firstField(file, pick) {
25
+ const handle = openSync(file, 'r');
26
+ try {
27
+ const buffer = Buffer.alloc(256 * 1024);
28
+ const length = readSync(handle, buffer, 0, buffer.length, 0);
29
+ for (const line of buffer.subarray(0, length).toString('utf8').split('\n')) {
30
+ try { const value = pick(JSON.parse(line)); if (typeof value === 'string' && value) return value; } catch { /* partial line */ }
31
+ }
32
+ return null;
33
+ } finally { closeSync(handle); }
34
+ }
@@ -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,14 @@
1
+ import { claudeCodeWake } from './claude-code/wake.js';
2
+ import { codexWake } from './codex/wake.js';
3
+
4
+ // Clients that can be woken headlessly to answer another Agent, in preference order.
5
+ export const WAKE_ADAPTERS = Object.freeze({
6
+ [claudeCodeWake.id]: claudeCodeWake,
7
+ [codexWake.id]: codexWake,
8
+ });
9
+
10
+ export const WAKE_CLIENTS = Object.freeze(Object.keys(WAKE_ADAPTERS));
11
+
12
+ export function wakeAdapter(client) {
13
+ return Object.hasOwn(WAKE_ADAPTERS, client) ? WAKE_ADAPTERS[client] : null;
14
+ }