@noopolis/daimon 0.2.0 → 0.2.2

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 (261) hide show
  1. package/README.md +6 -0
  2. package/dist/contracts/attentionContract.d.ts +25 -0
  3. package/dist/contracts/attentionContract.d.ts.map +1 -1
  4. package/dist/contracts/attentionContract.js +8 -1
  5. package/dist/contracts/grokBrokerTaskClock.d.ts +18 -0
  6. package/dist/contracts/grokBrokerTaskClock.d.ts.map +1 -0
  7. package/dist/contracts/grokBrokerTaskClock.js +16 -0
  8. package/dist/contracts/organizationRuntimeContract.d.ts +76 -0
  9. package/dist/contracts/organizationRuntimeContract.d.ts.map +1 -1
  10. package/dist/contracts/organizationRuntimeContract.js +2 -0
  11. package/dist/contracts/runtimeContractManifest.d.ts +222 -15
  12. package/dist/contracts/runtimeContractManifest.d.ts.map +1 -1
  13. package/dist/contracts/runtimeContractManifest.js +35 -10
  14. package/dist/contracts/shellEnvironmentPolicy.d.ts +3 -0
  15. package/dist/contracts/shellEnvironmentPolicy.d.ts.map +1 -0
  16. package/dist/contracts/shellEnvironmentPolicy.js +5 -0
  17. package/dist/contracts/wakeMomentContract.d.ts +61 -0
  18. package/dist/contracts/wakeMomentContract.d.ts.map +1 -0
  19. package/dist/contracts/wakeMomentContract.js +27 -0
  20. package/dist/core/types.d.ts +2 -0
  21. package/dist/core/types.d.ts.map +1 -1
  22. package/dist/examples/jungianPlayAgent.d.ts +1 -0
  23. package/dist/examples/jungianPlayAgent.d.ts.map +1 -1
  24. package/dist/examples/jungianPlayAgent.js +6 -2
  25. package/dist/observability/causalEvents.d.ts +3 -3
  26. package/dist/observability/causalEvents.d.ts.map +1 -1
  27. package/dist/observability/causalEvents.js +4 -4
  28. package/dist/pi/cliEngineSpawn.d.ts +2 -2
  29. package/dist/pi/cliEngineSpawn.d.ts.map +1 -1
  30. package/dist/pi/cliEngineSpawn.js +7 -2
  31. package/dist/pi/cliEnvironment.d.ts.map +1 -1
  32. package/dist/pi/cliEnvironment.js +2 -0
  33. package/dist/pi/cliMcpRegistration.d.ts +1 -1
  34. package/dist/pi/cliMcpRegistration.d.ts.map +1 -1
  35. package/dist/pi/cliMcpRegistration.js +1 -1
  36. package/dist/pi/cliSession.d.ts +2 -0
  37. package/dist/pi/cliSession.d.ts.map +1 -1
  38. package/dist/pi/cliSession.js +3 -80
  39. package/dist/pi/cliSessionMcp.d.ts +9 -0
  40. package/dist/pi/cliSessionMcp.d.ts.map +1 -0
  41. package/dist/pi/cliSessionMcp.js +80 -0
  42. package/dist/pi/engineTaskClock.d.ts +9 -0
  43. package/dist/pi/engineTaskClock.d.ts.map +1 -0
  44. package/dist/pi/engineTaskClock.js +28 -0
  45. package/dist/pi/grokHomeMcpRegistration.d.ts.map +1 -1
  46. package/dist/pi/grokHomeMcpRegistration.js +2 -1
  47. package/dist/pi/grokSandbox.js +1 -1
  48. package/dist/pi/memoryClock.d.ts +25 -0
  49. package/dist/pi/memoryClock.d.ts.map +1 -0
  50. package/dist/pi/memoryClock.js +18 -0
  51. package/dist/pi/piAgentHandle.d.ts +3 -1
  52. package/dist/pi/piAgentHandle.d.ts.map +1 -1
  53. package/dist/pi/piAgentHandle.js +5 -2
  54. package/dist/pi/piHarness.d.ts +8 -2
  55. package/dist/pi/piHarness.d.ts.map +1 -1
  56. package/dist/pi/piHarness.js +28 -12
  57. package/dist/pi/taskClockProcess.d.ts +6 -0
  58. package/dist/pi/taskClockProcess.d.ts.map +1 -0
  59. package/dist/pi/taskClockProcess.js +8 -0
  60. package/dist/pi/turnCausal.d.ts +4 -2
  61. package/dist/pi/turnCausal.d.ts.map +1 -1
  62. package/dist/pi/turnCausal.js +4 -4
  63. package/dist/pi/wakeAcceptance.d.ts +1 -1
  64. package/dist/pi/wakeAcceptance.d.ts.map +1 -1
  65. package/dist/pi/wakeAcceptance.js +2 -2
  66. package/dist/runtime/agentSkillsDirectory.d.ts +25 -0
  67. package/dist/runtime/agentSkillsDirectory.d.ts.map +1 -0
  68. package/dist/runtime/agentSkillsDirectory.js +101 -0
  69. package/dist/runtime/agySubscriptionBootstrap.js +1 -1
  70. package/dist/runtime/agySubscriptionRealm.js +1 -1
  71. package/dist/runtime/attention.d.ts +5 -0
  72. package/dist/runtime/attention.d.ts.map +1 -1
  73. package/dist/runtime/attention.js +14 -1
  74. package/dist/runtime/attentionDispatcher.d.ts +14 -2
  75. package/dist/runtime/attentionDispatcher.d.ts.map +1 -1
  76. package/dist/runtime/attentionDispatcher.js +127 -33
  77. package/dist/runtime/cli.js +5 -0
  78. package/dist/runtime/contract-manifest.json +1 -1
  79. package/dist/runtime/contract-manifest.sha256 +1 -1
  80. package/dist/runtime/engineBrokerCapabilities.d.ts +10 -0
  81. package/dist/runtime/engineBrokerCapabilities.d.ts.map +1 -1
  82. package/dist/runtime/engineBrokerCapabilities.js +10 -0
  83. package/dist/runtime/engineBrokerControlClient.d.ts +12 -1
  84. package/dist/runtime/engineBrokerControlClient.d.ts.map +1 -1
  85. package/dist/runtime/engineBrokerControlClient.js +139 -43
  86. package/dist/runtime/engineBrokerCredentialJournal.d.ts +2 -0
  87. package/dist/runtime/engineBrokerCredentialJournal.d.ts.map +1 -1
  88. package/dist/runtime/engineBrokerCredentialJournal.js +9 -0
  89. package/dist/runtime/engineBrokerInferenceProtocol.d.ts +3 -2
  90. package/dist/runtime/engineBrokerInferenceProtocol.d.ts.map +1 -1
  91. package/dist/runtime/engineBrokerInferenceProtocol.js +4 -4
  92. package/dist/runtime/engineBrokerMcpCallLog.d.ts +11 -0
  93. package/dist/runtime/engineBrokerMcpCallLog.d.ts.map +1 -1
  94. package/dist/runtime/engineBrokerMcpCallLog.js +13 -1
  95. package/dist/runtime/engineBrokerMcpFacade.d.ts +5 -2
  96. package/dist/runtime/engineBrokerMcpFacade.d.ts.map +1 -1
  97. package/dist/runtime/engineBrokerMcpFacade.js +5 -2
  98. package/dist/runtime/engineBrokerNativeClient.js +2 -2
  99. package/dist/runtime/engineBrokerProtocol.d.ts +42 -10
  100. package/dist/runtime/engineBrokerProtocol.d.ts.map +1 -1
  101. package/dist/runtime/engineBrokerProtocol.js +97 -14
  102. package/dist/runtime/engineBrokerSealLedger.d.ts.map +1 -1
  103. package/dist/runtime/engineBrokerSealLedger.js +9 -0
  104. package/dist/runtime/engineBrokerService.d.ts +8 -2
  105. package/dist/runtime/engineBrokerService.d.ts.map +1 -1
  106. package/dist/runtime/engineBrokerService.js +95 -37
  107. package/dist/runtime/engineBrokerServiceCli.js +1 -1
  108. package/dist/runtime/engineBrokerServiceConfig.d.ts +15 -2
  109. package/dist/runtime/engineBrokerServiceConfig.d.ts.map +1 -1
  110. package/dist/runtime/engineBrokerServiceConfig.js +53 -11
  111. package/dist/runtime/engineBrokerTurnRegistry.d.ts +1 -1
  112. package/dist/runtime/engineBrokerTurnRegistry.d.ts.map +1 -1
  113. package/dist/runtime/engineBrokerTurnRegistry.js +7 -6
  114. package/dist/runtime/engineDispatcher.d.ts +2 -0
  115. package/dist/runtime/engineDispatcher.d.ts.map +1 -1
  116. package/dist/runtime/engineDispatcher.js +14 -2
  117. package/dist/runtime/engineReadiness.js +1 -1
  118. package/dist/runtime/grokBrokerCredentialAuthority.d.ts +1 -0
  119. package/dist/runtime/grokBrokerCredentialAuthority.d.ts.map +1 -1
  120. package/dist/runtime/grokBrokerCredentialAuthority.js +48 -22
  121. package/dist/runtime/grokBrokerCredentialReader.d.ts +1 -0
  122. package/dist/runtime/grokBrokerCredentialReader.d.ts.map +1 -1
  123. package/dist/runtime/grokBrokerCredentialReader.js +2 -1
  124. package/dist/runtime/grokBrokerProjection.d.ts +26 -2
  125. package/dist/runtime/grokBrokerProjection.d.ts.map +1 -1
  126. package/dist/runtime/grokBrokerProjection.js +18 -7
  127. package/dist/runtime/grokBrokerProviderClock.d.ts +26 -0
  128. package/dist/runtime/grokBrokerProviderClock.d.ts.map +1 -0
  129. package/dist/runtime/grokBrokerProviderClock.js +133 -0
  130. package/dist/runtime/grokBrokerProxy.d.ts +17 -3
  131. package/dist/runtime/grokBrokerProxy.d.ts.map +1 -1
  132. package/dist/runtime/grokBrokerProxy.js +73 -9
  133. package/dist/runtime/grokBrokerProxyJson.d.ts +10 -0
  134. package/dist/runtime/grokBrokerProxyJson.d.ts.map +1 -0
  135. package/dist/runtime/grokBrokerProxyJson.js +31 -0
  136. package/dist/runtime/grokBrokerProxyRequest.d.ts.map +1 -1
  137. package/dist/runtime/grokBrokerProxyRequest.js +17 -6
  138. package/dist/runtime/grokBrokerRealmLease.js +1 -1
  139. package/dist/runtime/grokBrokerRefresh.js +1 -1
  140. package/dist/runtime/grokBrokerRunBudget.d.ts +20 -0
  141. package/dist/runtime/grokBrokerRunBudget.d.ts.map +1 -0
  142. package/dist/runtime/grokBrokerRunBudget.js +85 -0
  143. package/dist/runtime/grokBrokerSlot.d.ts +6 -0
  144. package/dist/runtime/grokBrokerSlot.d.ts.map +1 -0
  145. package/dist/runtime/grokBrokerSlot.js +21 -0
  146. package/dist/runtime/grokBrokerTaskClock.d.ts +32 -0
  147. package/dist/runtime/grokBrokerTaskClock.d.ts.map +1 -0
  148. package/dist/runtime/grokBrokerTaskClock.js +120 -0
  149. package/dist/runtime/grokBrokerTurnMeter.d.ts +55 -3
  150. package/dist/runtime/grokBrokerTurnMeter.d.ts.map +1 -1
  151. package/dist/runtime/grokBrokerTurnMeter.js +90 -4
  152. package/dist/runtime/grokBrokerWorkerConfig.d.ts +5 -2
  153. package/dist/runtime/grokBrokerWorkerConfig.d.ts.map +1 -1
  154. package/dist/runtime/grokBrokerWorkerConfig.js +22 -3
  155. package/dist/runtime/grokEngineBroker.d.ts +43 -3
  156. package/dist/runtime/grokEngineBroker.d.ts.map +1 -1
  157. package/dist/runtime/grokEngineBroker.js +159 -71
  158. package/dist/runtime/grokEngineBrokerTurn.d.ts +32 -8
  159. package/dist/runtime/grokEngineBrokerTurn.d.ts.map +1 -1
  160. package/dist/runtime/grokEngineBrokerTurn.js +204 -21
  161. package/dist/runtime/grokEngineBrokerTurnEnd.d.ts +88 -0
  162. package/dist/runtime/grokEngineBrokerTurnEnd.d.ts.map +1 -0
  163. package/dist/runtime/grokEngineBrokerTurnEnd.js +148 -0
  164. package/dist/runtime/grokInferenceProxy.d.ts.map +1 -1
  165. package/dist/runtime/grokInferenceProxy.js +3 -0
  166. package/dist/runtime/grokSubscriptionRealm.js +1 -1
  167. package/dist/runtime/grokWorkerAttestation.d.ts +12 -5
  168. package/dist/runtime/grokWorkerAttestation.d.ts.map +1 -1
  169. package/dist/runtime/grokWorkerAttestation.js +5 -3
  170. package/dist/runtime/grokWorkerHomeAttestation.d.ts +51 -5
  171. package/dist/runtime/grokWorkerHomeAttestation.d.ts.map +1 -1
  172. package/dist/runtime/grokWorkerHomeAttestation.js +103 -8
  173. package/dist/runtime/grokWorkerSkills.d.ts +86 -0
  174. package/dist/runtime/grokWorkerSkills.d.ts.map +1 -0
  175. package/dist/runtime/grokWorkerSkills.js +135 -0
  176. package/dist/runtime/index.d.ts +10 -2
  177. package/dist/runtime/index.d.ts.map +1 -1
  178. package/dist/runtime/index.js +8 -1
  179. package/dist/runtime/native/daimon-engine-broker +0 -0
  180. package/dist/runtime/organizationRuntime.d.ts +3 -0
  181. package/dist/runtime/organizationRuntime.d.ts.map +1 -1
  182. package/dist/runtime/organizationRuntimeAdmission.d.ts +6 -0
  183. package/dist/runtime/organizationRuntimeAdmission.d.ts.map +1 -0
  184. package/dist/runtime/organizationRuntimeAdmission.js +22 -0
  185. package/dist/runtime/organizationRuntimeControl.d.ts +9 -0
  186. package/dist/runtime/organizationRuntimeControl.d.ts.map +1 -1
  187. package/dist/runtime/organizationRuntimeControl.js +72 -24
  188. package/dist/runtime/organizationRuntimeHost.d.ts.map +1 -1
  189. package/dist/runtime/organizationRuntimeHost.js +13 -4
  190. package/dist/runtime/organizationRuntimeParsing.d.ts.map +1 -1
  191. package/dist/runtime/organizationRuntimeParsing.js +4 -1
  192. package/dist/runtime/organizationRuntimeReadiness.d.ts.map +1 -1
  193. package/dist/runtime/organizationRuntimeReadiness.js +8 -0
  194. package/dist/runtime/organizationRuntimeRecording.d.ts +7 -0
  195. package/dist/runtime/organizationRuntimeRecording.d.ts.map +1 -0
  196. package/dist/runtime/organizationRuntimeRecording.js +58 -0
  197. package/dist/runtime/physicalReadiness.d.ts +12 -0
  198. package/dist/runtime/physicalReadiness.d.ts.map +1 -1
  199. package/dist/runtime/physicalReadiness.js +43 -9
  200. package/dist/runtime/productionAgentTools.d.ts.map +1 -1
  201. package/dist/runtime/productionAgentTools.js +13 -5
  202. package/dist/runtime/relocateRuntimeConfig.d.ts +14 -0
  203. package/dist/runtime/relocateRuntimeConfig.d.ts.map +1 -0
  204. package/dist/runtime/relocateRuntimeConfig.js +38 -0
  205. package/dist/runtime/schedule.d.ts.map +1 -1
  206. package/dist/runtime/schedule.js +12 -7
  207. package/dist/runtime/storeCoordination.d.ts +10 -0
  208. package/dist/runtime/storeCoordination.d.ts.map +1 -1
  209. package/dist/runtime/storeCoordination.js +23 -0
  210. package/dist/runtime/taskClock.d.ts +42 -0
  211. package/dist/runtime/taskClock.d.ts.map +1 -0
  212. package/dist/runtime/taskClock.js +102 -0
  213. package/dist/runtime/taskClockProcess.d.ts +17 -0
  214. package/dist/runtime/taskClockProcess.d.ts.map +1 -0
  215. package/dist/runtime/taskClockProcess.js +58 -0
  216. package/dist/runtime/turnRequestLedger.d.ts +0 -5
  217. package/dist/runtime/turnRequestLedger.d.ts.map +1 -1
  218. package/dist/runtime/turnRequestLedger.js +23 -21
  219. package/dist/runtime/wakeAcceptanceClaimDiagnostics.d.ts +34 -0
  220. package/dist/runtime/wakeAcceptanceClaimDiagnostics.d.ts.map +1 -0
  221. package/dist/runtime/wakeAcceptanceClaimDiagnostics.js +79 -0
  222. package/dist/runtime/wakeAcceptanceIndex.d.ts +55 -0
  223. package/dist/runtime/wakeAcceptanceIndex.d.ts.map +1 -0
  224. package/dist/runtime/wakeAcceptanceIndex.js +158 -0
  225. package/dist/runtime/wakeAcceptanceProcessIdentity.d.ts +9 -1
  226. package/dist/runtime/wakeAcceptanceProcessIdentity.d.ts.map +1 -1
  227. package/dist/runtime/wakeAcceptanceProcessIdentity.js +5 -1
  228. package/dist/runtime/wakeAcceptanceRecord.d.ts +2 -0
  229. package/dist/runtime/wakeAcceptanceRecord.d.ts.map +1 -1
  230. package/dist/runtime/wakeAcceptanceRecord.js +4 -2
  231. package/dist/runtime/wakeAcceptanceRetention.d.ts +25 -1
  232. package/dist/runtime/wakeAcceptanceRetention.d.ts.map +1 -1
  233. package/dist/runtime/wakeAcceptanceRetention.js +28 -2
  234. package/dist/runtime/wakeAcceptanceStore.d.ts +8 -8
  235. package/dist/runtime/wakeAcceptanceStore.d.ts.map +1 -1
  236. package/dist/runtime/wakeAcceptanceStore.js +66 -63
  237. package/dist/runtime/wakeFuse.d.ts +58 -3
  238. package/dist/runtime/wakeFuse.d.ts.map +1 -1
  239. package/dist/runtime/wakeFuse.js +106 -7
  240. package/dist/runtime/wakeMomentFs.d.ts +27 -0
  241. package/dist/runtime/wakeMomentFs.d.ts.map +1 -0
  242. package/dist/runtime/wakeMomentFs.js +223 -0
  243. package/dist/runtime/wakeMomentHistory.d.ts +26 -0
  244. package/dist/runtime/wakeMomentHistory.d.ts.map +1 -0
  245. package/dist/runtime/wakeMomentHistory.js +109 -0
  246. package/dist/runtime/wakeMomentIo.d.ts +22 -0
  247. package/dist/runtime/wakeMomentIo.d.ts.map +1 -0
  248. package/dist/runtime/wakeMomentIo.js +114 -0
  249. package/dist/runtime/wakeMomentRecorder.d.ts +33 -0
  250. package/dist/runtime/wakeMomentRecorder.d.ts.map +1 -0
  251. package/dist/runtime/wakeMomentRecorder.js +81 -0
  252. package/dist/runtime/wakeMomentRetention.d.ts +7 -0
  253. package/dist/runtime/wakeMomentRetention.d.ts.map +1 -0
  254. package/dist/runtime/wakeMomentRetention.js +143 -0
  255. package/dist/runtime/wakeMomentRows.d.ts +8 -0
  256. package/dist/runtime/wakeMomentRows.d.ts.map +1 -0
  257. package/dist/runtime/wakeMomentRows.js +66 -0
  258. package/dist/runtime/wakeMomentSnapshot.d.ts +56 -0
  259. package/dist/runtime/wakeMomentSnapshot.d.ts.map +1 -0
  260. package/dist/runtime/wakeMomentSnapshot.js +248 -0
  261. package/package.json +4 -4
@@ -1,25 +1,36 @@
1
+ import { brokerTaskClockFileFor } from "./grokBrokerSlot.js";
2
+ import { brokerClockObservation, parseBrokerProcessClock, prepareBrokerTaskClock, writeBrokerTaskClock } from "./grokBrokerTaskClock.js";
3
+ import { GrokBrokerProviderClock } from "./grokBrokerProviderClock.js";
1
4
  import { createHash, randomUUID } from "node:crypto";
5
+ import { engineBrokerCapabilityTtlMs } from "./engineBrokerCapabilities.js";
2
6
  import { decodeGrokHeadlessTurn } from "../pi/grokHeadlessResult.js";
3
7
  import { decodeGrokStreamUsage } from "../pi/grokStreamUsage.js";
4
- import { ENGINE_BROKER_VERSION } from "./engineBrokerProtocol.js";
8
+ import { ENGINE_BROKER_VERSION, ENGINE_BROKER_TASK_CLOCK_RESET_FAILED } from "./engineBrokerProtocol.js";
5
9
  import { lowerEngineBrokerTurnLimits, mapGrokReportedModel } from "./engineBrokerTurnAccounting.js";
6
10
  import { NativeBrokerTurnFailure } from "./engineBrokerNativeClient.js";
7
11
  import { engineBrokerRequestLedgerPathFor, engineBrokerSealLedgerPathFor } from "./engineBrokerServiceConfig.js";
8
12
  import { ensureBrokerTurnLedgered } from "./grokEngineBrokerLedger.js";
9
13
  import { finishBrokerTurnWithUsage } from "./grokEngineBrokerMetering.js";
10
14
  import { GrokBrokerTurnMeter } from "./grokBrokerTurnMeter.js";
15
+ import { awaitGrokWorkerIdentityEmpty, findGrokWorkerProcess, GROK_WORKER_REAP_WAIT_MS, grokWorkerIdentityEmpty, watchGrokBrokerTurnEnd } from "./grokEngineBrokerTurnEnd.js";
11
16
  import { GrokWorkerAttestationFailure } from "./grokWorkerAttestation.js";
12
17
  export class EngineBrokerTurnFailure extends Error {
13
18
  code;
14
19
  diagnostic;
15
20
  accounting;
16
21
  mcpCalls;
17
- constructor(code, diagnostic, accounting, mcpCalls) {
22
+ shellClock;
23
+ providerClock;
24
+ cleanupDiagnostic;
25
+ constructor(code, diagnostic, accounting, mcpCalls, shellClock, providerClock, cleanupDiagnostic) {
18
26
  super("engine broker turn failed");
19
27
  this.code = code;
20
28
  this.diagnostic = diagnostic;
21
29
  this.accounting = accounting;
22
30
  this.mcpCalls = mcpCalls;
31
+ this.shellClock = shellClock;
32
+ this.providerClock = providerClock;
33
+ this.cleanupDiagnostic = cleanupDiagnostic;
23
34
  }
24
35
  }
25
36
  const limitReasonFor = { tokens: "token_ceiling", requests: "request_ceiling", timeout: "wake_timeout" };
@@ -38,37 +49,91 @@ const usageOf = (usage) => ({ input: usage.input, cacheRead: usage.cacheRead, ca
38
49
  * metered through {@link finishBrokerTurnWithUsage}; a replayed turn returns its
39
50
  * sealed accounting before any of this runs and never meters again.
40
51
  */
41
- export async function runGrokEngineBrokerTurn(deps, registration, wakeId, prompt, mcpEndpoint, signal, overrides) {
52
+ export async function runGrokEngineBrokerTurn(deps, registration, wakeId, prompt, mcpEndpoint, signal, overrides, processClock) {
42
53
  const { agentId } = registration, declared = registration.model.model;
54
+ const writeClock = deps.writeTaskClock ?? ((value) => writeBrokerTaskClock(value, brokerTaskClockFileFor(registration.slot)));
43
55
  let limits;
44
56
  try {
45
57
  limits = lowerEngineBrokerTurnLimits(registration.limits, overrides);
58
+ if (processClock !== undefined)
59
+ parseBrokerProcessClock(processClock);
46
60
  }
47
61
  catch {
48
62
  throw new EngineBrokerTurnFailure("invalid_request", undefined, { outcome: "failed", usage: null, model: declared, requests: 0, limitReason: "none" });
49
63
  }
50
- const turnId = createHash("sha256").update(`${agentId}\0${wakeId}`).digest("hex");
51
- const request = { version: ENGINE_BROKER_VERSION, kind: "start_turn", requestId: randomUUID(), turnId, agentId, wakeId, prompt, mcpEndpoint, ...(overrides === undefined ? {} : { limits: overrides }) };
64
+ const turnId = createHash("sha256").update(`${registration.slot}\0${agentId}\0${wakeId}`).digest("hex");
65
+ const request = { version: ENGINE_BROKER_VERSION, kind: "start_turn", requestId: randomUUID(), turnId, slot: registration.slot, agentId, wakeId, prompt, mcpEndpoint, ...(overrides === undefined ? {} : { limits: overrides }), ...(processClock === undefined ? {} : { processClock }) };
52
66
  const begun = await deps.turns.begin(request, declared);
53
67
  const metering = { usageLedgerPath: registration.usageLedgerPath, requestLedgerPath: engineBrokerRequestLedgerPathFor(registration.usageLedgerPath), sealLedgerPath: engineBrokerSealLedgerPathFor(registration.usageLedgerPath), agentId, wakeId };
54
68
  if (begun !== "start") {
55
69
  await ensureBrokerTurnLedgered(begun.ledger, turnId, metering);
56
70
  return replay(begun.replay);
57
71
  }
72
+ const clock = processClock === undefined ? undefined : new GrokBrokerProviderClock(processClock);
58
73
  const controller = new AbortController();
74
+ let worker;
59
75
  const meter = new GrokBrokerTurnMeter(limits, () => controller.abort());
60
- const onAbort = () => controller.abort();
76
+ let cancelledByCaller = false;
77
+ const onAbort = () => { cancelledByCaller = true; controller.abort(); };
61
78
  signal?.addEventListener("abort", onAbort, { once: true });
62
- if (signal?.aborted)
79
+ if (signal?.aborted) {
80
+ cancelledByCaller = true;
63
81
  controller.abort();
82
+ }
64
83
  const timer = setTimeout(() => meter.trip("timeout"), limits.timeoutMs);
65
84
  timer.unref?.();
85
+ // What was in flight, and which worker held the final reply, at the instant the
86
+ // turn ended — read before killing the worker can close its calls or empty its
87
+ // identity. A final reply the 1 s watchdog poll never saw is caught here too.
88
+ let inFlightAtEnd = false;
89
+ controller.signal.addEventListener("abort", () => {
90
+ let mcp = 0;
91
+ try {
92
+ mcp = deps.mcp.activity?.(turnId)?.inFlight ?? 0;
93
+ }
94
+ catch {
95
+ mcp = 0;
96
+ }
97
+ inFlightAtEnd = mcp > 0 || meter.busy();
98
+ if (meter.finalReply() !== undefined)
99
+ worker ??= (deps.observeWorker ?? findGrokWorkerProcess)(registration.workerUid).catch(() => undefined);
100
+ }, { once: true });
101
+ let shellClock;
102
+ let resetClock = deps.clockedSlot === true;
103
+ let nativeStarted = false;
104
+ let workerReaped = false;
105
+ let sealedFailure;
66
106
  let nativeDiagnostic, attested = false, output, rejected = false, sealed;
107
+ // Set when the broker itself ended the turn (`grokEngineBrokerTurnEnd.ts`); the worker seen at the final reply names the turn it ended.
108
+ let endedBy;
109
+ let watchdog, isolationGuard;
67
110
  try {
68
- const isolationGuard = await deps.prepareIsolation(registration);
111
+ const isolation = await deps.prepareIsolation(registration);
112
+ isolationGuard = isolation.guard;
113
+ // Publication may throw after rename. Claim cleanup before attempting it.
114
+ resetClock ||= isolation.configVariant === "clocked";
115
+ const coverage = await prepareBrokerTaskClock(isolation.configVariant, processClock, writeClock);
116
+ if (isolation.configVariant === "clocked" || processClock !== undefined)
117
+ shellClock = coverage;
69
118
  deps.proxy.registerIsolationGuard(turnId, isolationGuard);
70
- deps.proxy.registerTurn(turnId, { policy: registration.model, meter });
71
- const providerCapability = deps.proxy.capabilities.issue(agentId, turnId), mcpCapability = deps.mcp.register(agentId, turnId, mcpEndpoint);
119
+ deps.proxy.registerTurn(turnId, { policy: registration.model, meter, clock });
120
+ const capabilityTtlMs = engineBrokerCapabilityTtlMs(limits.timeoutMs);
121
+ const providerCapability = deps.proxy.capabilities.issue(agentId, turnId, capabilityTtlMs), mcpCapability = deps.mcp.register(agentId, turnId, mcpEndpoint, capabilityTtlMs);
122
+ watchdog = watchGrokBrokerTurnEnd({
123
+ meter, activity: () => deps.mcp.activity?.(turnId), signal: controller.signal, ...(deps.turnEnd === undefined ? {} : { timing: deps.turnEnd }),
124
+ // The worker is read once, as soon as it has made a request — long before
125
+ // anything can end the turn and reap it — so a final reply is never left
126
+ // without the worker that gave it.
127
+ onPoll: () => { if (worker === undefined && meter.snapshot().requests > 0)
128
+ worker = (deps.observeWorker ?? findGrokWorkerProcess)(registration.workerUid).catch(() => undefined); },
129
+ onFinalReply: () => { worker ??= (deps.observeWorker ?? findGrokWorkerProcess)(registration.workerUid).catch(() => undefined); },
130
+ // An idle stall is a deadline of its own: it trips the meter as a timeout, so it seals like one.
131
+ onEnd: (reason) => { endedBy = reason; if (reason === "idle")
132
+ meter.trip("timeout");
133
+ else
134
+ controller.abort(); }
135
+ });
136
+ nativeStarted = true;
72
137
  const result = await deps.runNative({ slot: registration.slot, requestId: request.requestId, turnId, agentId, wakeId, prompt, providerCapability, mcpCapability }, controller.signal);
73
138
  nativeDiagnostic = result.diagnostic;
74
139
  output = result.text;
@@ -86,8 +151,16 @@ export async function runGrokEngineBrokerTurn(deps, registration, wakeId, prompt
86
151
  throw new Error("engine broker turn limit reached");
87
152
  const usage = decoded.usage === undefined ? streamOrMeterUsage(stream, snapshot) : usageOf(decoded.usage);
88
153
  const accounting = { outcome: "completed", usage, model: declared, requests: requestCount(stream, snapshot), limitReason: "none" };
89
- const completed = { version: request.version, kind: "completed", requestId: request.requestId, turnId, text: decoded.text, workerPid: result.workerPid, workerUid: result.workerUid, workerStartTime: result.startTicks.toString(), ...accounting };
90
- const result_ = { text: completed.text, workerPid: completed.workerPid, workerUid: completed.workerUid, workerStartTime: completed.workerStartTime, ...accounting };
154
+ const observation = brokerClockObservation(shellClock, clock?.snapshot());
155
+ const completed = {
156
+ version: request.version, kind: "completed", requestId: request.requestId, turnId,
157
+ text: decoded.text, workerPid: result.workerPid, workerUid: result.workerUid,
158
+ workerStartTime: result.startTicks.toString(), ...observation, ...accounting
159
+ };
160
+ const result_ = {
161
+ text: completed.text, workerPid: completed.workerPid, workerUid: completed.workerUid,
162
+ workerStartTime: completed.workerStartTime, ...observation, ...accounting
163
+ };
91
164
  await finishBrokerTurnWithUsage(deps.turns, request, completed, metering, { notionalUsd: decoded.usage?.notionalUsd ?? 0, complete: decoded.usage?.complete ?? false, estimatedRequests: snapshot.estimatedRequests, requests: requestRows(stream, snapshot), ...(stream.sessionId === undefined ? {} : { session: stream.sessionId }) }, () => { sealed = result_; });
92
165
  return result_;
93
166
  }
@@ -95,6 +168,34 @@ export async function runGrokEngineBrokerTurn(deps, registration, wakeId, prompt
95
168
  // Once the completed record is published it is the durable truth: anything
96
169
  // failing after that (metering) must neither re-seal the turn as failed nor
97
170
  // append a second row.
171
+ if (sealed !== undefined)
172
+ return sealed;
173
+ watchdog?.stop();
174
+ // A turn this broker aborted is sealed only once its worker identity is
175
+ // empty (bounded): the agent's next turn must not start a worker the
176
+ // previous launcher handler's identity-wide reap would kill.
177
+ if (controller.signal.aborted) {
178
+ workerReaped = await awaitGrokWorkerIdentityEmpty(registration.workerUid, deps.workerReapWaitMs ?? GROK_WORKER_REAP_WAIT_MS, deps.workerIdentityEmpty ?? ((uid) => grokWorkerIdentityEmpty(uid, undefined, resetClock)));
179
+ }
180
+ // Not aborted (an output-limit failure, say): nothing was killed, so the live view is the true one.
181
+ if (!controller.signal.aborted) {
182
+ let mcp = 0;
183
+ try {
184
+ mcp = deps.mcp.activity?.(turnId)?.inFlight ?? 0;
185
+ }
186
+ catch {
187
+ mcp = 0;
188
+ }
189
+ inFlightAtEnd = mcp > 0 || meter.busy();
190
+ }
191
+ const vetoed = cancelledByCaller || rejected || (output !== undefined && !attested) || inFlightAtEnd;
192
+ // Finished work is completed work. A worker that gave its final reply and was
193
+ // then ended by the broker, refused its stdout by the launcher's output bound,
194
+ // or left idle into the deadline did its job; only its exit went wrong.
195
+ const finished = vetoed ? undefined : await completedFromFinalReply(deps, registration, request, meter, error, endedBy, worker, isolationGuard, metering, (value) => { sealed = value; }, shellClock, clock).catch(() => undefined);
196
+ if (finished !== undefined)
197
+ return finished;
198
+ // Sealed completed but its ledger append failed: the published record is the truth, exactly as on the success path.
98
199
  if (sealed !== undefined)
99
200
  return sealed;
100
201
  const snapshot = meter.snapshot();
@@ -112,27 +213,109 @@ export async function runGrokEngineBrokerTurn(deps, registration, wakeId, prompt
112
213
  catch {
113
214
  mcpCalls = undefined;
114
215
  }
115
- const failed = { version: request.version, kind: "failed", requestId: request.requestId, turnId, code, ...(diagnostic ? { diagnostic } : {}), ...(mcpCalls === undefined ? {} : { mcpCalls }), ...accounting };
216
+ const observation = brokerClockObservation(shellClock, clock?.snapshot());
217
+ const failed = {
218
+ version: request.version, kind: "failed", requestId: request.requestId, turnId, code,
219
+ ...observation, ...(diagnostic ? { diagnostic } : {}),
220
+ ...(mcpCalls === undefined ? {} : { mcpCalls }), ...accounting
221
+ };
116
222
  const reason = snapshot.limitReason !== "none" ? limitReasonFor[snapshot.limitReason] : rejected ? "turn_rejected" : "unknown";
117
- await finishBrokerTurnWithUsage(deps.turns, request, failed, metering, { notionalUsd: 0, complete: false, reason, estimatedRequests: snapshot.estimatedRequests, requests: requestRows(stream, snapshot), ...(stream?.sessionId === undefined ? {} : { session: stream.sessionId }) });
118
- throw new EngineBrokerTurnFailure(code, diagnostic, accounting, mcpCalls);
223
+ const failure = new EngineBrokerTurnFailure(code, diagnostic, accounting, mcpCalls, shellClock, observation.providerClock);
224
+ await finishBrokerTurnWithUsage(deps.turns, request, failed, metering, {
225
+ notionalUsd: 0, complete: false, reason, estimatedRequests: snapshot.estimatedRequests,
226
+ requests: requestRows(stream, snapshot), ...(stream?.sessionId === undefined ? {} : { session: stream.sessionId })
227
+ }, () => { sealedFailure = failure; });
228
+ throw failure;
119
229
  }
120
230
  finally {
231
+ watchdog?.stop();
121
232
  clearTimeout(timer);
122
233
  signal?.removeEventListener("abort", onAbort);
123
234
  meter.abortInFlight();
124
- deps.proxy.revokeTurn(turnId);
125
- deps.proxy.revokeIsolationGuard(turnId);
126
- deps.proxy.capabilities.revoke(turnId);
127
- deps.mcp.revoke(turnId);
235
+ try {
236
+ deps.proxy.revokeTurn(turnId);
237
+ deps.proxy.revokeIsolationGuard(turnId);
238
+ deps.proxy.capabilities.revoke(turnId);
239
+ deps.mcp.revoke(turnId);
240
+ }
241
+ finally {
242
+ if (resetClock) {
243
+ try {
244
+ // A disconnected launcher client can settle before its worker is reaped.
245
+ // Never change the clock of a surviving shell, or release the slot as healthy.
246
+ if (nativeStarted && !workerReaped) {
247
+ workerReaped = await awaitGrokWorkerIdentityEmpty(registration.workerUid, deps.workerReapWaitMs ?? GROK_WORKER_REAP_WAIT_MS, deps.workerIdentityEmpty ?? ((uid) => grokWorkerIdentityEmpty(uid, undefined, true)));
248
+ if (!workerReaped)
249
+ throw new Error("Grok broker task clock worker did not terminate");
250
+ }
251
+ await writeClock(undefined);
252
+ }
253
+ catch (error) {
254
+ deps.taskClockFailed?.();
255
+ // Cleanup cannot replace a published outcome. Report the live fence
256
+ // on the wire without rewriting the seal or its accounting.
257
+ if (sealed !== undefined)
258
+ return { ...sealed, cleanupDiagnostic: ENGINE_BROKER_TASK_CLOCK_RESET_FAILED };
259
+ if (sealedFailure !== undefined)
260
+ throw new EngineBrokerTurnFailure(sealedFailure.code, sealedFailure.diagnostic, sealedFailure.accounting, sealedFailure.mcpCalls, sealedFailure.shellClock, sealedFailure.providerClock, ENGINE_BROKER_TASK_CLOCK_RESET_FAILED);
261
+ throw new Error("Grok broker task clock cleanup failed; restart required", { cause: error });
262
+ }
263
+ }
264
+ }
128
265
  }
129
266
  }
130
267
  function replay(response) {
131
268
  const accounting = { outcome: response.outcome, usage: response.usage, model: response.model, requests: response.requests, limitReason: response.limitReason };
132
269
  if (response.kind === "completed")
133
- return { text: response.text, workerPid: response.workerPid, workerUid: response.workerUid, workerStartTime: response.workerStartTime, ...accounting, outcome: "completed" };
270
+ return {
271
+ text: response.text, workerPid: response.workerPid, workerUid: response.workerUid,
272
+ workerStartTime: response.workerStartTime,
273
+ ...brokerClockObservation(response.shellClock, response.providerClock), ...accounting, outcome: "completed"
274
+ };
134
275
  const code = response.code === "turn_conflict" || response.code === "unavailable" ? "engine_failed" : response.code;
135
- throw new EngineBrokerTurnFailure(code, response.diagnostic, accounting, response.mcpCalls);
276
+ throw new EngineBrokerTurnFailure(code, response.diagnostic, accounting, response.mcpCalls, response.shellClock, response.providerClock);
277
+ }
278
+ /**
279
+ * The completed seal for a turn whose model gave its final reply but whose
280
+ * worker never delivered stdout: the broker ended it (`final_reply`), the
281
+ * launcher's output bound refused its stream (`output_limit`, which publishes
282
+ * nothing), or it idled into the wall-clock deadline. The reply is the one the
283
+ * proxy saw, already bounded with a marker (`boundGrokFinalReply`), the usage is
284
+ * what the proxy metered, and the worker is the one the launcher reported or
285
+ * the broker observed while it was alive. `undefined` — and the ordinary
286
+ * failure seal — whenever any of that is missing, the isolation attestation
287
+ * fails, or the failure was anything else (a token or request limit, a client
288
+ * cancellation, an engine fault before the reply). The caller vetoes it outright
289
+ * for a caller cancellation, a rejected or unattested stdout, and a model
290
+ * request or MCP tool call still in flight.
291
+ */
292
+ async function completedFromFinalReply(deps, registration, request, meter, error, endedBy, worker, isolationGuard, metering, onSealed, shellClock, clock) {
293
+ const final = meter.finalReply(), snapshot = meter.snapshot();
294
+ if (final === undefined || isolationGuard === undefined || snapshot.usage === null)
295
+ return undefined;
296
+ // A token or request limit is never finished work, whatever else failed beside it.
297
+ if (snapshot.limitReason === "tokens" || snapshot.limitReason === "requests")
298
+ return undefined;
299
+ const outputLimited = error instanceof NativeBrokerTurnFailure && error.diagnostic.failureClass === "output_limit" && error.diagnostic.workerPid > 0;
300
+ if (!(endedBy === "final_reply" || outputLimited || (snapshot.limitReason === "timeout" && endedBy !== "idle")))
301
+ return undefined;
302
+ const identity = outputLimited ? { pid: error.diagnostic.workerPid, uid: error.diagnostic.workerUid, startTicks: error.diagnostic.startTicks } : await (async () => { const seen = (await worker) ?? await (deps.observeWorker ?? findGrokWorkerProcess)(registration.workerUid).catch(() => undefined); return seen === undefined ? undefined : { ...seen, uid: registration.workerUid }; })();
303
+ if (identity === undefined || identity.uid !== registration.workerUid)
304
+ return undefined;
305
+ await isolationGuard();
306
+ const accounting = { outcome: "completed", usage: snapshot.usage, model: registration.model.model, requests: snapshot.requests, limitReason: "none" };
307
+ const observation = brokerClockObservation(shellClock, clock?.snapshot());
308
+ const completed = {
309
+ version: request.version, kind: "completed", requestId: request.requestId, turnId: request.turnId,
310
+ text: final.text, workerPid: identity.pid, workerUid: identity.uid, workerStartTime: identity.startTicks,
311
+ ...observation, ...accounting
312
+ };
313
+ const result = {
314
+ text: completed.text, workerPid: completed.workerPid, workerUid: completed.workerUid,
315
+ workerStartTime: completed.workerStartTime, ...observation, ...accounting
316
+ };
317
+ await finishBrokerTurnWithUsage(deps.turns, request, completed, metering, { notionalUsd: 0, complete: false, estimatedRequests: snapshot.estimatedRequests, requests: requestRows(undefined, snapshot) }, () => onSealed(result));
318
+ return result;
136
319
  }
137
320
  /** The proxy saw every forwarded request; the stream is the fallback when no request crossed this proxy. */
138
321
  const requestCount = (stream, snapshot) => snapshot.requests > 0 ? snapshot.requests : stream?.requests.length ?? 0;
@@ -0,0 +1,88 @@
1
+ import type { EngineBrokerMcpActivity } from "./engineBrokerMcpCallLog.js";
2
+ import type { GrokBrokerTurnMeter } from "./grokBrokerTurnMeter.js";
3
+ /**
4
+ * When a brokered turn is over, decided by the broker rather than by the
5
+ * worker's exit.
6
+ *
7
+ * Grok 1.0.34 answers, logs `handle_prompt.done ok:true`, ends its session
8
+ * `turn_ended completed` — and then does not exit. Production measured it: 61
9
+ * of 128 turns over two days sat 8–27 minutes after their last model request
10
+ * until the wall-clock limit killed them, and each was then sealed
11
+ * `failed/timeout` with its work already done. Every one still had its MCP GET
12
+ * tunnel open, but that is a symptom: against the real 1.0.34 binary the tunnel
13
+ * held open, closed, refused (405) or absent all exit ~55 ms after `result`,
14
+ * and Grok reopens a closed tunnel within milliseconds, so closing it cannot
15
+ * end a worker. The launcher only publishes a worker's output when the worker
16
+ * exits, so waiting for the exit made the deadline the only way a turn could
17
+ * end.
18
+ *
19
+ * The proxy sees every model answer, so the broker knows when the model has
20
+ * given its final reply: a successful response that called no tool and
21
+ * stopped. From that point:
22
+ *
23
+ * 1. `onFinalReply` runs once — the broker notes the worker it will end while
24
+ * that worker is certainly still alive.
25
+ * 2. After {@link GROK_BROKER_TURN_END}`.finalGraceMs` with no new model request
26
+ * and no tool call in flight, `onEnd("final_reply")` runs: the broker ends
27
+ * the worker and seals the turn completed with that reply.
28
+ *
29
+ * A new model request clears the final reply (the meter forgets it on admit),
30
+ * so a model that keeps going is never cut off. Independently, a turn with no
31
+ * model request and no tool call in flight and no activity of either kind for
32
+ * `idleMs` ends as `onEnd("idle")` — the backstop for a worker that stalls
33
+ * without ever answering. Shell tools Grok runs itself are invisible here, so
34
+ * the idle bound is minutes, not seconds.
35
+ */
36
+ export type GrokBrokerTurnEndTiming = Readonly<{
37
+ finalGraceMs: number;
38
+ idleMs: number;
39
+ pollMs: number;
40
+ }>;
41
+ export declare const GROK_BROKER_TURN_END: GrokBrokerTurnEndTiming;
42
+ /** How long a turn the broker aborted waits for its worker identity to empty before it is sealed. */
43
+ export declare const GROK_WORKER_REAP_WAIT_MS = 10000;
44
+ export type GrokBrokerTurnEndReason = "final_reply" | "idle";
45
+ export declare function watchGrokBrokerTurnEnd(input: Readonly<{
46
+ meter: Pick<GrokBrokerTurnMeter, "finalReply" | "busy" | "lastActivityAt">;
47
+ activity: () => EngineBrokerMcpActivity | undefined;
48
+ onFinalReply: () => void;
49
+ /** Every poll, before any decision: the broker records the worker while it is certainly alive. */
50
+ onPoll?: () => void;
51
+ onEnd: (reason: GrokBrokerTurnEndReason) => void;
52
+ timing?: GrokBrokerTurnEndTiming;
53
+ now?: () => number;
54
+ /** The turn's own abort: once anything else ended the turn, the watchdog never claims it. */
55
+ signal?: AbortSignal;
56
+ }>): Readonly<{
57
+ stop(): void;
58
+ }>;
59
+ /** The launcher-forked root of a worker identity's process tree: its pid and `/proc/<pid>/stat` start time, as the launcher itself reports them. */
60
+ export type GrokWorkerProcess = Readonly<{
61
+ pid: number;
62
+ startTicks: string;
63
+ }>;
64
+ /**
65
+ * Finds the worker the launcher started for `uid`, read-only from `/proc`.
66
+ *
67
+ * A turn the broker ends early never receives the launcher's result frame (a
68
+ * cancelled client is not written to), yet a completed response must name the
69
+ * worker it came from. One identity carries at most one turn at a time
70
+ * (`native/AGENTS.md`), so a process owned by `uid` whose parent is *not* owned
71
+ * by `uid` is a worker the launcher forked. A previous turn's worker can still
72
+ * be alive beside it — a hung worker is exactly what this module exists for, and
73
+ * its kill is the launcher's — so the newest such root (the latest start time)
74
+ * is this turn's: turns of one identity are sequential.
75
+ */
76
+ export declare function findGrokWorkerProcess(uid: number, procRoot?: string): Promise<GrokWorkerProcess | undefined>;
77
+ /** No process at all, zombies included, owned by `uid`. */
78
+ export declare function grokWorkerIdentityEmpty(uid: number, procRoot?: string, requireReadable?: boolean): Promise<boolean>;
79
+ /**
80
+ * Waits, bounded, for the worker identity to empty after the broker ended a
81
+ * turn. Ending a turn kills the launcher's client; the launcher handler then
82
+ * kills and reaps the identity on its own schedule. Sealing first would let
83
+ * the agent's next turn start a worker on the same identity that the previous
84
+ * handler's identity-wide reap then kills. The bound keeps a wedged launcher
85
+ * from holding the agent: past it the turn is sealed anyway, as before.
86
+ */
87
+ export declare function awaitGrokWorkerIdentityEmpty(uid: number, timeoutMs: number, empty?: (uid: number) => Promise<boolean>, pollMs?: number, stableMs?: number): Promise<boolean>;
88
+ //# sourceMappingURL=grokEngineBrokerTurnEnd.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grokEngineBrokerTurnEnd.d.ts","sourceRoot":"","sources":["../../src/runtime/grokEngineBrokerTurnEnd.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAC3E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAEpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AACzG,eAAO,MAAM,oBAAoB,EAAE,uBAAqG,CAAC;AACzI,qGAAqG;AACrG,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAC/C,MAAM,MAAM,uBAAuB,GAAG,aAAa,GAAG,MAAM,CAAC;AAE7D,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,QAAQ,CAAC;IACrD,KAAK,EAAE,IAAI,CAAC,mBAAmB,EAAE,YAAY,GAAG,MAAM,GAAG,gBAAgB,CAAC,CAAC;IAC3E,QAAQ,EAAE,MAAM,uBAAuB,GAAG,SAAS,CAAC;IACpD,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,kGAAkG;IAClG,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,KAAK,EAAE,CAAC,MAAM,EAAE,uBAAuB,KAAK,IAAI,CAAC;IACjD,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,6FAA6F;IAC7F,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC,GAAG,QAAQ,CAAC;IAAE,IAAI,IAAI,IAAI,CAAA;CAAE,CAAC,CAyB9B;AAED,oJAAoJ;AACpJ,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE9E;;;;;;;;;;;GAWG;AACH,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,SAAU,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAqBnH;AAED,2DAA2D;AAC3D,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,MAAM,EAAE,QAAQ,SAAU,EAAE,eAAe,UAAQ,GACvD,OAAO,CAAC,OAAO,CAAC,CAgBlB;AAED;;;;;;;GAOG;AACH,wBAAsB,4BAA4B,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAA2B,EAAE,MAAM,SAAM,EAAE,QAAQ,SAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAarM"}
@@ -0,0 +1,148 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { performance } from "node:perf_hooks";
3
+ export const GROK_BROKER_TURN_END = Object.freeze({ finalGraceMs: 15_000, idleMs: 10 * 60_000, pollMs: 1_000 });
4
+ /** How long a turn the broker aborted waits for its worker identity to empty before it is sealed. */
5
+ export const GROK_WORKER_REAP_WAIT_MS = 10_000;
6
+ export function watchGrokBrokerTurnEnd(input) {
7
+ const timing = input.timing ?? GROK_BROKER_TURN_END, now = input.now ?? Date.now;
8
+ let notified, done = false;
9
+ const check = () => {
10
+ if (done)
11
+ return;
12
+ if (input.signal?.aborted === true) {
13
+ stop();
14
+ return;
15
+ }
16
+ try {
17
+ input.onPoll?.();
18
+ }
19
+ catch { /* observation only */ }
20
+ const final = input.meter.finalReply();
21
+ if (final !== undefined && notified !== final.index) {
22
+ notified = final.index;
23
+ try {
24
+ input.onFinalReply();
25
+ }
26
+ catch { /* closing streams is best effort; the grace below still ends the turn */ }
27
+ }
28
+ let activity;
29
+ try {
30
+ activity = input.activity();
31
+ }
32
+ catch {
33
+ activity = undefined;
34
+ }
35
+ if (input.meter.busy() || (activity?.inFlight ?? 0) > 0)
36
+ return;
37
+ const quietSince = Math.max(input.meter.lastActivityAt(), activity?.lastActivityAt ?? 0);
38
+ const reason = final !== undefined && now() - Math.max(final.at, quietSince) >= timing.finalGraceMs ? "final_reply"
39
+ : now() - quietSince >= timing.idleMs ? "idle" : undefined;
40
+ if (reason === undefined)
41
+ return;
42
+ stop();
43
+ input.onEnd(reason);
44
+ };
45
+ const timer = setInterval(check, timing.pollMs);
46
+ timer.unref?.();
47
+ const stop = () => { done = true; clearInterval(timer); };
48
+ return { stop };
49
+ }
50
+ /**
51
+ * Finds the worker the launcher started for `uid`, read-only from `/proc`.
52
+ *
53
+ * A turn the broker ends early never receives the launcher's result frame (a
54
+ * cancelled client is not written to), yet a completed response must name the
55
+ * worker it came from. One identity carries at most one turn at a time
56
+ * (`native/AGENTS.md`), so a process owned by `uid` whose parent is *not* owned
57
+ * by `uid` is a worker the launcher forked. A previous turn's worker can still
58
+ * be alive beside it — a hung worker is exactly what this module exists for, and
59
+ * its kill is the launcher's — so the newest such root (the latest start time)
60
+ * is this turn's: turns of one identity are sequential.
61
+ */
62
+ export async function findGrokWorkerProcess(uid, procRoot = "/proc") {
63
+ let entries;
64
+ try {
65
+ entries = await readdir(procRoot);
66
+ }
67
+ catch {
68
+ return undefined;
69
+ }
70
+ const owned = new Map();
71
+ for (const entry of entries) {
72
+ if (!/^\d+$/u.test(entry))
73
+ continue;
74
+ try {
75
+ const status = await readFile(`${procRoot}/${entry}/status`, "utf8");
76
+ const real = /^Uid:\s+(\d+)/mu.exec(status)?.[1];
77
+ if (real === undefined || Number(real) !== uid)
78
+ continue;
79
+ const stat = await readFile(`${procRoot}/${entry}/stat`, "utf8");
80
+ const fields = stat.slice(stat.lastIndexOf(")") + 1).trim().split(/\s+/u);
81
+ // After the command: state(0) ppid(1) … starttime is field 22 of stat, index 19 here.
82
+ const parent = Number(fields[1]), startTicks = fields[19];
83
+ if (fields[0] === "Z" || !Number.isSafeInteger(parent) || startTicks === undefined || !/^[1-9][0-9]*$/u.test(startTicks))
84
+ continue;
85
+ owned.set(Number(entry), { parent, startTicks });
86
+ }
87
+ catch { /* a process exiting mid-scan is not part of the answer */ }
88
+ }
89
+ const roots = [...owned].filter(([, value]) => !owned.has(value.parent)).sort(([, left], [, right]) => BigInt(right.startTicks) > BigInt(left.startTicks) ? 1 : BigInt(right.startTicks) < BigInt(left.startTicks) ? -1 : 0);
90
+ const newest = roots[0];
91
+ return newest === undefined ? undefined : { pid: newest[0], startTicks: newest[1].startTicks };
92
+ }
93
+ /** No process at all, zombies included, owned by `uid`. */
94
+ export async function grokWorkerIdentityEmpty(uid, procRoot = "/proc", requireReadable = false) {
95
+ let entries;
96
+ try {
97
+ entries = await readdir(procRoot);
98
+ }
99
+ catch {
100
+ return !requireReadable;
101
+ }
102
+ for (const entry of entries) {
103
+ if (!/^\d+$/u.test(entry))
104
+ continue;
105
+ try {
106
+ const status = await readFile(`${procRoot}/${entry}/status`, "utf8");
107
+ // Zombies count, exactly as the launcher's own reap counts them.
108
+ if (Number(/^Uid:\s+(\d+)/mu.exec(status)?.[1]) !== uid)
109
+ continue;
110
+ return false;
111
+ }
112
+ catch (error) {
113
+ // ENOENT is a process exiting mid-scan; an unreadable identity is not empty.
114
+ if (requireReadable && error.code !== "ENOENT")
115
+ return false;
116
+ }
117
+ }
118
+ return true;
119
+ }
120
+ /**
121
+ * Waits, bounded, for the worker identity to empty after the broker ended a
122
+ * turn. Ending a turn kills the launcher's client; the launcher handler then
123
+ * kills and reaps the identity on its own schedule. Sealing first would let
124
+ * the agent's next turn start a worker on the same identity that the previous
125
+ * handler's identity-wide reap then kills. The bound keeps a wedged launcher
126
+ * from holding the agent: past it the turn is sealed anyway, as before.
127
+ */
128
+ export async function awaitGrokWorkerIdentityEmpty(uid, timeoutMs, empty = grokWorkerIdentityEmpty, pollMs = 100, stableMs = 300) {
129
+ // Empty must hold across a window longer than the launcher's own reap pass
130
+ // (10 ms between scans): one empty glimpse can fall between its kill and its
131
+ // re-scan, and that re-scan would kill the agent's next worker.
132
+ // Cleanup must measure elapsed real time even if the wall clock changes.
133
+ const deadline = performance.now() + timeoutMs;
134
+ let emptySince;
135
+ for (;;) {
136
+ const now = performance.now();
137
+ if (await empty(uid).catch(() => false)) {
138
+ emptySince ??= now;
139
+ if (now - emptySince >= stableMs)
140
+ return true;
141
+ }
142
+ else
143
+ emptySince = undefined;
144
+ if (now >= deadline)
145
+ return false;
146
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
147
+ }
148
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"grokInferenceProxy.d.ts","sourceRoot":"","sources":["../../src/runtime/grokInferenceProxy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGhD,OAAO,KAAK,EAAE,6BAA6B,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE9F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAGpE,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEzK,yGAAyG;AACzG,eAAO,MAAM,8BAA8B,+BAA2B,CAAC;AAEvE;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,uBAAuB,CAAC,KAAK,EAAE,uBAAuB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,EAAE,mBAAmB,EAAE,SAAS,EAAE,6BAA6B,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiC1N"}
1
+ {"version":3,"file":"grokInferenceProxy.d.ts","sourceRoot":"","sources":["../../src/runtime/grokInferenceProxy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGhD,OAAO,KAAK,EAAE,6BAA6B,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE9F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAGpE,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEzK,yGAAyG;AACzG,eAAO,MAAM,8BAA8B,+BAA2B,CAAC;AAEvE;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,uBAAuB,CAAC,KAAK,EAAE,uBAAuB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,EAAE,mBAAmB,EAAE,SAAS,EAAE,6BAA6B,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkC1N"}
@@ -1,3 +1,4 @@
1
+ import { GrokBrokerRunLimit } from "./grokBrokerRunBudget.js";
1
2
  import { createHash } from "node:crypto";
2
3
  import { GrokBrokerProxyRefusal } from "./grokBrokerProxy.js";
3
4
  import { parseGrokUpstreamUsage } from "./grokBrokerTurnMeter.js";
@@ -57,6 +58,8 @@ export async function serveGrokInferenceGrant(input, response, grants, authority
57
58
  }
58
59
  catch (error) {
59
60
  settle?.(undefined);
61
+ if (error instanceof GrokBrokerRunLimit)
62
+ return json(response, 429, '{"error":"run_request_limit"}');
60
63
  // Same rule as the subject path: a policy miss is non-retryable (400), because a
61
64
  // retryable 503 makes the client re-send a request the broker will never accept,
62
65
  // charging estimated usage for every attempt. 503 stays for transient faults only.
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { constants } from "node:fs";
3
- import { spawn } from "node:child_process";
3
+ import { spawnRealTime as spawn } from "./taskClockProcess.js";
4
4
  import { lstat, open, rename, unlink } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { terminateChild, trackCliChild } from "../pi/cliProcess.js";
@@ -1,3 +1,5 @@
1
+ import type { GrokWorkerConfigVariant } from "../contracts/grokBrokerTaskClock.js";
2
+ import { type GrokWorkerConfigPins } from "./grokWorkerHomeAttestation.js";
1
3
  import { type GrokWorkerTmpOptions } from "./grokWorkerTmpAttestation.js";
2
4
  /**
3
5
  * The per-turn freshness watermark taken before the worker is launched: the
@@ -10,6 +12,7 @@ export type GrokWorkerAttestationSnapshot = Readonly<{
10
12
  ino: number;
11
13
  size: number;
12
14
  denyPaths: readonly string[];
15
+ configVariant?: GrokWorkerConfigVariant;
13
16
  }>;
14
17
  type Snapshot = GrokWorkerAttestationSnapshot;
15
18
  export declare class GrokWorkerAttestationFailure extends Error {
@@ -50,7 +53,8 @@ export declare function parseGrokWorkerSandboxProfile(bytes: Uint8Array, profile
50
53
  * the events watermark it requires the 1.0.34 layout: events under
51
54
  * `$GROK_HOME/sessions/` (the root `sandbox-events.jsonl` stays empty on
52
55
  * 1.0.34), and a root-owned read-only worker home whose `config.toml` hashes to
53
- * the declared renderer output (`grokWorkerHomeAttestation.ts`).
56
+ * the declared renderer output and whose `skills/` is exactly the declared
57
+ * skills (`skillsSha256`; absent for none) (`grokWorkerHomeAttestation.ts`).
54
58
  *
55
59
  * It also refuses a deny list bubblewrap could not materialize
56
60
  * (`grokWorkerDenyPlacement.ts`), naming the entry and the ancestor that stops
@@ -69,7 +73,8 @@ export declare function prepareGrokWorkerAttestation(input: Readonly<{
69
73
  workerUid: number;
70
74
  workerGid?: number;
71
75
  brokerGid: number;
72
- configSha256: string;
76
+ configSha256: GrokWorkerConfigPins;
77
+ skillsSha256?: string;
73
78
  registeredWorkers?: readonly Readonly<{
74
79
  profilePath: string;
75
80
  workerUid: number;
@@ -78,7 +83,9 @@ export declare function prepareGrokWorkerAttestation(input: Readonly<{
78
83
  uid: number;
79
84
  gid: number;
80
85
  tmp?: GrokWorkerTmpOptions;
81
- }>): Promise<Snapshot>;
86
+ }>): Promise<Snapshot & {
87
+ configVariant: GrokWorkerConfigVariant;
88
+ }>;
82
89
  /** The per-turn attestation input for one registration, carrying every registered worker so sibling temp is attested too. */
83
90
  export declare const grokBrokerAttestationInput: <T extends Readonly<{
84
91
  profilePath: string;
@@ -88,9 +95,9 @@ export declare const grokBrokerAttestationInput: <T extends Readonly<{
88
95
  }>>(registration: T, registrations: readonly Readonly<{
89
96
  profilePath: string;
90
97
  workerUid: number;
91
- }>[], configSha256: string) => T & {
98
+ }>[], configSha256: GrokWorkerConfigPins) => T & {
92
99
  brokerGid: number;
93
- configSha256: string;
100
+ configSha256: GrokWorkerConfigPins;
94
101
  registeredWorkers: {
95
102
  profilePath: string;
96
103
  workerUid: number;