lingxios 1.0.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 (298) hide show
  1. package/README.md +54 -0
  2. package/db/schema.sql +306 -0
  3. package/dist/src/app/artifacts.d.ts +8 -0
  4. package/dist/src/app/artifacts.js +114 -0
  5. package/dist/src/app/artifacts.js.map +1 -0
  6. package/dist/src/app/doctor.d.ts +22 -0
  7. package/dist/src/app/doctor.js +44 -0
  8. package/dist/src/app/doctor.js.map +1 -0
  9. package/dist/src/app/index.d.ts +123 -0
  10. package/dist/src/app/index.js +291 -0
  11. package/dist/src/app/index.js.map +1 -0
  12. package/dist/src/app/input.d.ts +22 -0
  13. package/dist/src/app/input.js +60 -0
  14. package/dist/src/app/input.js.map +1 -0
  15. package/dist/src/app/recover-wait.d.ts +5 -0
  16. package/dist/src/app/recover-wait.js +59 -0
  17. package/dist/src/app/recover-wait.js.map +1 -0
  18. package/dist/src/app/resources.d.ts +5 -0
  19. package/dist/src/app/resources.js +9 -0
  20. package/dist/src/app/resources.js.map +1 -0
  21. package/dist/src/app/storage.d.ts +2 -0
  22. package/dist/src/app/storage.js +41 -0
  23. package/dist/src/app/storage.js.map +1 -0
  24. package/dist/src/cli/doctor.d.ts +2 -0
  25. package/dist/src/cli/doctor.js +20 -0
  26. package/dist/src/cli/doctor.js.map +1 -0
  27. package/dist/src/cli/eval.d.ts +2 -0
  28. package/dist/src/cli/eval.js +44 -0
  29. package/dist/src/cli/eval.js.map +1 -0
  30. package/dist/src/config.d.ts +22 -0
  31. package/dist/src/config.js +56 -0
  32. package/dist/src/config.js.map +1 -0
  33. package/dist/src/context/attachments.d.ts +10 -0
  34. package/dist/src/context/attachments.js +18 -0
  35. package/dist/src/context/attachments.js.map +1 -0
  36. package/dist/src/context/document-text.d.ts +1 -0
  37. package/dist/src/context/document-text.js +29 -0
  38. package/dist/src/context/document-text.js.map +1 -0
  39. package/dist/src/context/document-worker.d.ts +1 -0
  40. package/dist/src/context/document-worker.js +62 -0
  41. package/dist/src/context/document-worker.js.map +1 -0
  42. package/dist/src/context/evidence.d.ts +19 -0
  43. package/dist/src/context/evidence.js +37 -0
  44. package/dist/src/context/evidence.js.map +1 -0
  45. package/dist/src/context/request.d.ts +21 -0
  46. package/dist/src/context/request.js +32 -0
  47. package/dist/src/context/request.js.map +1 -0
  48. package/dist/src/context/research-evidence.d.ts +4 -0
  49. package/dist/src/context/research-evidence.js +24 -0
  50. package/dist/src/context/research-evidence.js.map +1 -0
  51. package/dist/src/context/resource-checks.d.ts +7 -0
  52. package/dist/src/context/resource-checks.js +15 -0
  53. package/dist/src/context/resource-checks.js.map +1 -0
  54. package/dist/src/context/task-contract.d.ts +11 -0
  55. package/dist/src/context/task-contract.js +23 -0
  56. package/dist/src/context/task-contract.js.map +1 -0
  57. package/dist/src/context/text.d.ts +2 -0
  58. package/dist/src/context/text.js +7 -0
  59. package/dist/src/context/text.js.map +1 -0
  60. package/dist/src/control-plane/http-server.d.ts +22 -0
  61. package/dist/src/control-plane/http-server.js +196 -0
  62. package/dist/src/control-plane/http-server.js.map +1 -0
  63. package/dist/src/control-plane/memory-store.d.ts +71 -0
  64. package/dist/src/control-plane/memory-store.js +389 -0
  65. package/dist/src/control-plane/memory-store.js.map +1 -0
  66. package/dist/src/control-plane/pg-store.d.ts +62 -0
  67. package/dist/src/control-plane/pg-store.js +386 -0
  68. package/dist/src/control-plane/pg-store.js.map +1 -0
  69. package/dist/src/control-plane/service.d.ts +52 -0
  70. package/dist/src/control-plane/service.js +630 -0
  71. package/dist/src/control-plane/service.js.map +1 -0
  72. package/dist/src/control-plane/stores.d.ts +184 -0
  73. package/dist/src/control-plane/stores.js +19 -0
  74. package/dist/src/control-plane/stores.js.map +1 -0
  75. package/dist/src/errors.d.ts +66 -0
  76. package/dist/src/errors.js +102 -0
  77. package/dist/src/errors.js.map +1 -0
  78. package/dist/src/eval/calibration.d.ts +16 -0
  79. package/dist/src/eval/calibration.js +28 -0
  80. package/dist/src/eval/calibration.js.map +1 -0
  81. package/dist/src/eval/index.d.ts +40 -0
  82. package/dist/src/eval/index.js +40 -0
  83. package/dist/src/eval/index.js.map +1 -0
  84. package/dist/src/eval/review.d.ts +20 -0
  85. package/dist/src/eval/review.js +49 -0
  86. package/dist/src/eval/review.js.map +1 -0
  87. package/dist/src/host/http-client.d.ts +48 -0
  88. package/dist/src/host/http-client.js +127 -0
  89. package/dist/src/host/http-client.js.map +1 -0
  90. package/dist/src/host/port.d.ts +26 -0
  91. package/dist/src/host/port.js +2 -0
  92. package/dist/src/host/port.js.map +1 -0
  93. package/dist/src/index.d.ts +17 -0
  94. package/dist/src/index.js +9 -0
  95. package/dist/src/index.js.map +1 -0
  96. package/dist/src/integrations/lingxiloop/actions.d.ts +25 -0
  97. package/dist/src/integrations/lingxiloop/actions.js +77 -0
  98. package/dist/src/integrations/lingxiloop/actions.js.map +1 -0
  99. package/dist/src/integrations/lingxiloop/app.d.ts +167 -0
  100. package/dist/src/integrations/lingxiloop/app.js +434 -0
  101. package/dist/src/integrations/lingxiloop/app.js.map +1 -0
  102. package/dist/src/integrations/lingxiloop/approvals.d.ts +148 -0
  103. package/dist/src/integrations/lingxiloop/approvals.js +540 -0
  104. package/dist/src/integrations/lingxiloop/approvals.js.map +1 -0
  105. package/dist/src/integrations/lingxiloop/attachments.d.ts +8 -0
  106. package/dist/src/integrations/lingxiloop/attachments.js +38 -0
  107. package/dist/src/integrations/lingxiloop/attachments.js.map +1 -0
  108. package/dist/src/integrations/lingxiloop/calendar-approvals.d.ts +21 -0
  109. package/dist/src/integrations/lingxiloop/calendar-approvals.js +126 -0
  110. package/dist/src/integrations/lingxiloop/calendar-approvals.js.map +1 -0
  111. package/dist/src/integrations/lingxiloop/calendar-writes.d.ts +15 -0
  112. package/dist/src/integrations/lingxiloop/calendar-writes.js +79 -0
  113. package/dist/src/integrations/lingxiloop/calendar-writes.js.map +1 -0
  114. package/dist/src/integrations/lingxiloop/calendar.d.ts +19 -0
  115. package/dist/src/integrations/lingxiloop/calendar.js +43 -0
  116. package/dist/src/integrations/lingxiloop/calendar.js.map +1 -0
  117. package/dist/src/integrations/lingxiloop/canvas-events.d.ts +7 -0
  118. package/dist/src/integrations/lingxiloop/canvas-events.js +53 -0
  119. package/dist/src/integrations/lingxiloop/canvas-events.js.map +1 -0
  120. package/dist/src/integrations/lingxiloop/canvas-summary.d.ts +5 -0
  121. package/dist/src/integrations/lingxiloop/canvas-summary.js +52 -0
  122. package/dist/src/integrations/lingxiloop/canvas-summary.js.map +1 -0
  123. package/dist/src/integrations/lingxiloop/canvas-work-report.d.ts +12 -0
  124. package/dist/src/integrations/lingxiloop/canvas-work-report.js +120 -0
  125. package/dist/src/integrations/lingxiloop/canvas-work-report.js.map +1 -0
  126. package/dist/src/integrations/lingxiloop/canvas-work.d.ts +11 -0
  127. package/dist/src/integrations/lingxiloop/canvas-work.js +306 -0
  128. package/dist/src/integrations/lingxiloop/canvas-work.js.map +1 -0
  129. package/dist/src/integrations/lingxiloop/canvas.d.ts +12 -0
  130. package/dist/src/integrations/lingxiloop/canvas.js +79 -0
  131. package/dist/src/integrations/lingxiloop/canvas.js.map +1 -0
  132. package/dist/src/integrations/lingxiloop/chat.d.ts +11 -0
  133. package/dist/src/integrations/lingxiloop/chat.js +46 -0
  134. package/dist/src/integrations/lingxiloop/chat.js.map +1 -0
  135. package/dist/src/integrations/lingxiloop/document-approvals.d.ts +21 -0
  136. package/dist/src/integrations/lingxiloop/document-approvals.js +75 -0
  137. package/dist/src/integrations/lingxiloop/document-approvals.js.map +1 -0
  138. package/dist/src/integrations/lingxiloop/document-content.d.ts +80 -0
  139. package/dist/src/integrations/lingxiloop/document-content.js +195 -0
  140. package/dist/src/integrations/lingxiloop/document-content.js.map +1 -0
  141. package/dist/src/integrations/lingxiloop/document-writes.d.ts +8 -0
  142. package/dist/src/integrations/lingxiloop/document-writes.js +49 -0
  143. package/dist/src/integrations/lingxiloop/document-writes.js.map +1 -0
  144. package/dist/src/integrations/lingxiloop/documents.d.ts +22 -0
  145. package/dist/src/integrations/lingxiloop/documents.js +38 -0
  146. package/dist/src/integrations/lingxiloop/documents.js.map +1 -0
  147. package/dist/src/integrations/lingxiloop/event-outbox.d.ts +5 -0
  148. package/dist/src/integrations/lingxiloop/event-outbox.js +47 -0
  149. package/dist/src/integrations/lingxiloop/event-outbox.js.map +1 -0
  150. package/dist/src/integrations/lingxiloop/index.d.ts +3 -0
  151. package/dist/src/integrations/lingxiloop/index.js +3 -0
  152. package/dist/src/integrations/lingxiloop/index.js.map +1 -0
  153. package/dist/src/integrations/lingxiloop/learning-attempts.d.ts +13 -0
  154. package/dist/src/integrations/lingxiloop/learning-attempts.js +58 -0
  155. package/dist/src/integrations/lingxiloop/learning-attempts.js.map +1 -0
  156. package/dist/src/integrations/lingxiloop/learning-evaluation.d.ts +8 -0
  157. package/dist/src/integrations/lingxiloop/learning-evaluation.js +64 -0
  158. package/dist/src/integrations/lingxiloop/learning-evaluation.js.map +1 -0
  159. package/dist/src/integrations/lingxiloop/learning-evidence.d.ts +7 -0
  160. package/dist/src/integrations/lingxiloop/learning-evidence.js +88 -0
  161. package/dist/src/integrations/lingxiloop/learning-evidence.js.map +1 -0
  162. package/dist/src/integrations/lingxiloop/learning-missions.d.ts +18 -0
  163. package/dist/src/integrations/lingxiloop/learning-missions.js +95 -0
  164. package/dist/src/integrations/lingxiloop/learning-missions.js.map +1 -0
  165. package/dist/src/integrations/lingxiloop/learning.d.ts +22 -0
  166. package/dist/src/integrations/lingxiloop/learning.js +150 -0
  167. package/dist/src/integrations/lingxiloop/learning.js.map +1 -0
  168. package/dist/src/integrations/lingxiloop/memory.d.ts +14 -0
  169. package/dist/src/integrations/lingxiloop/memory.js +133 -0
  170. package/dist/src/integrations/lingxiloop/memory.js.map +1 -0
  171. package/dist/src/integrations/lingxiloop/polls.d.ts +9 -0
  172. package/dist/src/integrations/lingxiloop/polls.js +47 -0
  173. package/dist/src/integrations/lingxiloop/polls.js.map +1 -0
  174. package/dist/src/integrations/lingxiloop/presentations.d.ts +11 -0
  175. package/dist/src/integrations/lingxiloop/presentations.js +34 -0
  176. package/dist/src/integrations/lingxiloop/presentations.js.map +1 -0
  177. package/dist/src/integrations/lingxiloop/questionnaire.d.ts +21 -0
  178. package/dist/src/integrations/lingxiloop/questionnaire.js +57 -0
  179. package/dist/src/integrations/lingxiloop/questionnaire.js.map +1 -0
  180. package/dist/src/integrations/lingxiloop/research.d.ts +19 -0
  181. package/dist/src/integrations/lingxiloop/research.js +20 -0
  182. package/dist/src/integrations/lingxiloop/research.js.map +1 -0
  183. package/dist/src/integrations/lingxiloop/routines.d.ts +91 -0
  184. package/dist/src/integrations/lingxiloop/routines.js +198 -0
  185. package/dist/src/integrations/lingxiloop/routines.js.map +1 -0
  186. package/dist/src/integrations/lingxiloop/service-contracts.d.ts +1045 -0
  187. package/dist/src/integrations/lingxiloop/service-contracts.js +2 -0
  188. package/dist/src/integrations/lingxiloop/service-contracts.js.map +1 -0
  189. package/dist/src/integrations/lingxiloop/teacher-context.d.ts +32 -0
  190. package/dist/src/integrations/lingxiloop/teacher-context.js +25 -0
  191. package/dist/src/integrations/lingxiloop/teacher-context.js.map +1 -0
  192. package/dist/src/integrations/lingxiloop/teacher-digest.d.ts +13 -0
  193. package/dist/src/integrations/lingxiloop/teacher-digest.js +152 -0
  194. package/dist/src/integrations/lingxiloop/teacher-digest.js.map +1 -0
  195. package/dist/src/integrations/lingxiloop/teacher-preview.d.ts +22 -0
  196. package/dist/src/integrations/lingxiloop/teacher-preview.js +41 -0
  197. package/dist/src/integrations/lingxiloop/teacher-preview.js.map +1 -0
  198. package/dist/src/integrations/lingxiloop/teacher.d.ts +24 -0
  199. package/dist/src/integrations/lingxiloop/teacher.js +224 -0
  200. package/dist/src/integrations/lingxiloop/teacher.js.map +1 -0
  201. package/dist/src/kernel/manager.d.ts +52 -0
  202. package/dist/src/kernel/manager.js +407 -0
  203. package/dist/src/kernel/manager.js.map +1 -0
  204. package/dist/src/logging.d.ts +21 -0
  205. package/dist/src/logging.js +54 -0
  206. package/dist/src/logging.js.map +1 -0
  207. package/dist/src/memory/evidence.d.ts +6 -0
  208. package/dist/src/memory/evidence.js +43 -0
  209. package/dist/src/memory/evidence.js.map +1 -0
  210. package/dist/src/memory/processor.d.ts +3 -0
  211. package/dist/src/memory/processor.js +66 -0
  212. package/dist/src/memory/processor.js.map +1 -0
  213. package/dist/src/memory/semantic.d.ts +19 -0
  214. package/dist/src/memory/semantic.js +93 -0
  215. package/dist/src/memory/semantic.js.map +1 -0
  216. package/dist/src/memory/store.d.ts +20 -0
  217. package/dist/src/memory/store.js +34 -0
  218. package/dist/src/memory/store.js.map +1 -0
  219. package/dist/src/memory/synthesis.d.ts +24 -0
  220. package/dist/src/memory/synthesis.js +138 -0
  221. package/dist/src/memory/synthesis.js.map +1 -0
  222. package/dist/src/metrics.d.ts +43 -0
  223. package/dist/src/metrics.js +130 -0
  224. package/dist/src/metrics.js.map +1 -0
  225. package/dist/src/model/driver.d.ts +60 -0
  226. package/dist/src/model/driver.js +2 -0
  227. package/dist/src/model/driver.js.map +1 -0
  228. package/dist/src/model/embeddings.d.ts +24 -0
  229. package/dist/src/model/embeddings.js +97 -0
  230. package/dist/src/model/embeddings.js.map +1 -0
  231. package/dist/src/model/openai.d.ts +45 -0
  232. package/dist/src/model/openai.js +340 -0
  233. package/dist/src/model/openai.js.map +1 -0
  234. package/dist/src/outcome/assessment.d.ts +16 -0
  235. package/dist/src/outcome/assessment.js +45 -0
  236. package/dist/src/outcome/assessment.js.map +1 -0
  237. package/dist/src/outcome/content-check.d.ts +19 -0
  238. package/dist/src/outcome/content-check.js +44 -0
  239. package/dist/src/outcome/content-check.js.map +1 -0
  240. package/dist/src/outcome/envelope.d.ts +34 -0
  241. package/dist/src/outcome/envelope.js +42 -0
  242. package/dist/src/outcome/envelope.js.map +1 -0
  243. package/dist/src/protocol/constants.d.ts +25 -0
  244. package/dist/src/protocol/constants.js +26 -0
  245. package/dist/src/protocol/constants.js.map +1 -0
  246. package/dist/src/protocol/kernel-wire.d.ts +74 -0
  247. package/dist/src/protocol/kernel-wire.js +2 -0
  248. package/dist/src/protocol/kernel-wire.js.map +1 -0
  249. package/dist/src/protocol/outcome.d.ts +16 -0
  250. package/dist/src/protocol/outcome.js +14 -0
  251. package/dist/src/protocol/outcome.js.map +1 -0
  252. package/dist/src/protocol/types.d.ts +266 -0
  253. package/dist/src/protocol/types.js +19 -0
  254. package/dist/src/protocol/types.js.map +1 -0
  255. package/dist/src/research/address.d.ts +2 -0
  256. package/dist/src/research/address.js +32 -0
  257. package/dist/src/research/address.js.map +1 -0
  258. package/dist/src/research/fetch.d.ts +7 -0
  259. package/dist/src/research/fetch.js +83 -0
  260. package/dist/src/research/fetch.js.map +1 -0
  261. package/dist/src/research/index.d.ts +26 -0
  262. package/dist/src/research/index.js +109 -0
  263. package/dist/src/research/index.js.map +1 -0
  264. package/dist/src/runtime/compaction.d.ts +41 -0
  265. package/dist/src/runtime/compaction.js +86 -0
  266. package/dist/src/runtime/compaction.js.map +1 -0
  267. package/dist/src/runtime/corrections.d.ts +20 -0
  268. package/dist/src/runtime/corrections.js +27 -0
  269. package/dist/src/runtime/corrections.js.map +1 -0
  270. package/dist/src/runtime/policy.d.ts +42 -0
  271. package/dist/src/runtime/policy.js +91 -0
  272. package/dist/src/runtime/policy.js.map +1 -0
  273. package/dist/src/runtime/resource-refresh.d.ts +8 -0
  274. package/dist/src/runtime/resource-refresh.js +37 -0
  275. package/dist/src/runtime/resource-refresh.js.map +1 -0
  276. package/dist/src/runtime/runtime.d.ts +51 -0
  277. package/dist/src/runtime/runtime.js +804 -0
  278. package/dist/src/runtime/runtime.js.map +1 -0
  279. package/dist/src/runtime/tool.d.ts +13 -0
  280. package/dist/src/runtime/tool.js +42 -0
  281. package/dist/src/runtime/tool.js.map +1 -0
  282. package/dist/src/ui/index.d.ts +27 -0
  283. package/dist/src/ui/index.js +54 -0
  284. package/dist/src/ui/index.js.map +1 -0
  285. package/dist/src/versions.d.ts +9 -0
  286. package/dist/src/versions.js +11 -0
  287. package/dist/src/versions.js.map +1 -0
  288. package/dist/src/worker/index.d.ts +3 -0
  289. package/dist/src/worker/index.js +57 -0
  290. package/dist/src/worker/index.js.map +1 -0
  291. package/dist/src/worker/main.d.ts +2 -0
  292. package/dist/src/worker/main.js +9 -0
  293. package/dist/src/worker/main.js.map +1 -0
  294. package/dist/src/worker/worker.d.ts +45 -0
  295. package/dist/src/worker/worker.js +154 -0
  296. package/dist/src/worker/worker.js.map +1 -0
  297. package/kernel/runner.py +507 -0
  298. package/package.json +83 -0
@@ -0,0 +1,630 @@
1
+ import { appendResearchEvidence } from '../context/research-evidence.js';
2
+ import { appendResourceCheck } from '../context/resource-checks.js';
3
+ import { createTaskContract } from '../context/task-contract.js';
4
+ import { snapshotAttachments } from '../context/attachments.js';
5
+ /**
6
+ * ControlPlaneService — transport-independent control-plane logic.
7
+ *
8
+ * The HTTP layer (`http-server.ts`) is a thin adapter over this class, so the
9
+ * full behavior is unit-testable without sockets. Responsibilities:
10
+ *
11
+ * - lease validation on every worker call;
12
+ * - authoritative capability enforcement on host actions (the kernel-side
13
+ * allowlist is advisory; this check is the security boundary);
14
+ * - the host-action idempotency ledger;
15
+ * - run-event envelope validation, attempt-range fencing, and dedupe;
16
+ * - stream-integrity verification of final assistant messages.
17
+ */
18
+ import { createHash } from 'node:crypto';
19
+ import { isDeepStrictEqual } from 'node:util';
20
+ import { snapshotEvidence } from '../context/evidence.js';
21
+ import { createResponseEnvelope, snapshotArtifacts } from '../outcome/envelope.js';
22
+ import { parseFinalCandidate } from '../outcome/assessment.js';
23
+ import { errorMessage } from '../errors.js';
24
+ import { nullLogger } from '../logging.js';
25
+ import { RUN_SEQUENCE_SPAN } from '../protocol/constants.js';
26
+ import { sessionKeyOf, actionKeyOf } from '../protocol/types.js';
27
+ import { isGoalOutcome } from '../protocol/outcome.js';
28
+ import { isModelItem } from './stores.js';
29
+ export class ControlPlaneError extends Error {
30
+ status;
31
+ constructor(status, message) {
32
+ super(message);
33
+ this.status = status;
34
+ this.name = 'ControlPlaneError';
35
+ }
36
+ }
37
+ function hashToken(token) {
38
+ return createHash('sha256').update(token).digest('hex');
39
+ }
40
+ const RUN_STAGES = new Set(['started', 'delta', 'completed', 'failed', 'cancelled']);
41
+ function canonicalJson(value) {
42
+ if (Array.isArray(value))
43
+ return `[${value.map(canonicalJson).join(',')}]`;
44
+ if (value && typeof value === 'object') {
45
+ const record = value;
46
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
47
+ }
48
+ return JSON.stringify(value) ?? 'null';
49
+ }
50
+ export function actionFingerprint(work, action) {
51
+ return createHash('sha256').update(canonicalJson({
52
+ tenantId: work.tenantId, principalId: work.principalId ?? null, agentId: work.agentId,
53
+ sessionId: work.sessionId, action: action.action, args: action.args,
54
+ })).digest('hex');
55
+ }
56
+ export class ControlPlaneService {
57
+ deps;
58
+ logger;
59
+ constructor(deps) {
60
+ this.deps = deps;
61
+ this.logger = deps.logger ?? nullLogger;
62
+ }
63
+ // -------------------------------------------------------------------------
64
+ // Work lifecycle
65
+ // -------------------------------------------------------------------------
66
+ async enqueue(input) {
67
+ for (const [field, value] of Object.entries({
68
+ tenantId: input.tenantId, agentId: input.agentId, sessionId: input.sessionId,
69
+ kind: input.kind, lane: input.lane, triggerRef: input.triggerRef,
70
+ })) {
71
+ if (typeof value !== 'string' || !value.trim()) {
72
+ throw new ControlPlaneError(400, `enqueue requires a non-empty ${field}`);
73
+ }
74
+ }
75
+ const result = await this.deps.work.enqueue(input);
76
+ this.deps.metrics?.counter('agentos_work_enqueued_total', 'Work items enqueued').inc({ kind: input.kind, lane: input.lane });
77
+ return result;
78
+ }
79
+ async claim(workerId) {
80
+ if (typeof workerId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(workerId)) {
81
+ throw new ControlPlaneError(400, 'workerId must be 1-128 safe identifier characters');
82
+ }
83
+ const work = await this.deps.work.claim(workerId);
84
+ if (work) {
85
+ this.deps.metrics?.counter('agentos_work_claimed_total', 'Work items claimed').inc({ lane: work.lane });
86
+ }
87
+ return work;
88
+ }
89
+ async requireLease(proof, options = {}) {
90
+ if (!proof.id || !Number.isSafeInteger(proof.fence) || !proof.leaseToken) {
91
+ throw new ControlPlaneError(400, 'work lease proof required');
92
+ }
93
+ const leased = await this.deps.work.getLeased(proof.id, proof.fence, hashToken(proof.leaseToken));
94
+ if (!leased)
95
+ throw new ControlPlaneError(409, 'work lease lost or expired');
96
+ if (options.rejectCancelled && leased.cancelRequested) {
97
+ throw new ControlPlaneError(409, 'work is cancelled; no further actions are permitted');
98
+ }
99
+ return leased.work;
100
+ }
101
+ async heartbeat(proof) {
102
+ const row = await this.deps.work.heartbeat(proof.id, proof.fence, hashToken(proof.leaseToken));
103
+ if (!row)
104
+ return { ok: false };
105
+ return {
106
+ ok: true,
107
+ cancelRequested: row.cancelRequested,
108
+ preemptRequested: row.preemptRequested,
109
+ steer: row.steer,
110
+ };
111
+ }
112
+ async yieldWork(proof) {
113
+ const ok = await this.deps.work.yieldWork(proof.id, proof.fence, hashToken(proof.leaseToken));
114
+ if (!ok)
115
+ throw new ControlPlaneError(409, 'work is no longer yieldable');
116
+ }
117
+ async complete(proof, completion) {
118
+ if (completion.goalOutcome !== undefined && !isGoalOutcome(completion.goalOutcome)) {
119
+ throw new ControlPlaneError(400, 'invalid goal outcome');
120
+ }
121
+ if (completion.goalOutcome?.verification === 'passed') {
122
+ throw new ControlPlaneError(409, 'goal verification requires authoritative acceptance evidence');
123
+ }
124
+ if (!['completed', 'failed', 'cancelled'].includes(completion.status)) {
125
+ throw new ControlPlaneError(400, 'invalid completion status');
126
+ }
127
+ const work = await this.requireLease(proof);
128
+ if (completion.status === 'completed' && typeof work.meta?.['text'] === 'string' && !completion.goalOutcome) {
129
+ throw new ControlPlaneError(400, 'goal outcome is required for a captured request');
130
+ }
131
+ if (completion.goalOutcome) {
132
+ const request = (await this.getSession(proof, sessionKeyOf(work)))?.request;
133
+ const version = request?.workId === work.id ? request.revisions.length + 1 : 1;
134
+ if (completion.goalOutcome.requestVersion !== version)
135
+ throw new ControlPlaneError(409, 'goal outcome request version mismatch');
136
+ await this.validateWait(work, completion.goalOutcome);
137
+ if (completion.goalOutcome.status === 'satisfied' || completion.goalOutcome.status === 'delegated') {
138
+ const committed = await this.deps.delivery.getMessage?.(work);
139
+ if (!committed?.envelope.assessment || !isDeepStrictEqual(committed.envelope.goalOutcome, completion.goalOutcome)
140
+ || committed.body !== completion.resultText)
141
+ throw new ControlPlaneError(409, 'goal outcome requires the committed assessed response');
142
+ }
143
+ }
144
+ const ok = await this.deps.work.complete(proof.id, proof.fence, hashToken(proof.leaseToken), completion);
145
+ if (!ok)
146
+ throw new ControlPlaneError(409, 'work lease lost before completion');
147
+ this.deps.metrics?.counter('agentos_work_completed_total', 'Work attempts finished').inc({ status: completion.status });
148
+ }
149
+ async requestCancel(id) { return this.deps.work.requestCancel(id); }
150
+ async validateWait(work, outcome) {
151
+ if (outcome.status === 'awaiting_approval') {
152
+ if (!await this.deps.actions.hasWait(work.id, outcome.requestVersion, { approvalId: outcome.approvalId })) {
153
+ throw new ControlPlaneError(409, 'approval wait requires its current durable pending receipt');
154
+ }
155
+ }
156
+ else if (outcome.status === 'awaiting_input') {
157
+ if (!outcome.question || !await this.deps.actions.hasWait(work.id, outcome.requestVersion, { question: outcome.question })) {
158
+ throw new ControlPlaneError(409, 'input wait requires its current durable question receipt');
159
+ }
160
+ }
161
+ }
162
+ async requestPreempt(id) { return this.deps.work.requestPreempt(id); }
163
+ async addSteer(id, text) {
164
+ if (typeof text !== 'string' || !text.trim() || text.length > 8_000) {
165
+ throw new ControlPlaneError(400, 'steer text must be a non-empty string of at most 8000 characters');
166
+ }
167
+ return this.deps.work.addSteer(id, text);
168
+ }
169
+ // -------------------------------------------------------------------------
170
+ // Context
171
+ // -------------------------------------------------------------------------
172
+ async loadContext(proof) {
173
+ const work = await this.requireLease(proof);
174
+ const context = await this.deps.contextProvider.loadContext(work);
175
+ if (typeof work.meta?.['text'] === 'string') {
176
+ context.capabilities = [...new Set([...context.capabilities, 'task'])];
177
+ if (context.promptContextCandidate)
178
+ context.promptContextCandidate = { ...context.promptContextCandidate, capabilities: context.capabilities };
179
+ }
180
+ const priorArtifacts = new Map();
181
+ if (work.fence > 1) {
182
+ const events = await this.deps.events.listRange(work.id, 0, (work.fence - 1) * RUN_SEQUENCE_SPAN, ['ipython.completed']);
183
+ for (const event of events) {
184
+ for (const artifact of snapshotArtifacts(event.data['artifacts'])) {
185
+ priorArtifacts.set(artifact.path, artifact);
186
+ if (priorArtifacts.size > 512)
187
+ throw new ControlPlaneError(409, 'prior artifact inventory exceeds the per-run limit');
188
+ }
189
+ }
190
+ }
191
+ return { work: { ...work, leaseToken: proof.leaseToken }, ...context, priorArtifacts: [...priorArtifacts.values()] };
192
+ }
193
+ // -------------------------------------------------------------------------
194
+ // Host actions: grant enforcement + idempotency ledger
195
+ // -------------------------------------------------------------------------
196
+ async executeAction(proof, action) {
197
+ const work = await this.requireLease(proof, { rejectCancelled: true });
198
+ if (!action || typeof action !== 'object'
199
+ || typeof action.action !== 'string'
200
+ || typeof action.idempotencyKey !== 'string'
201
+ || !action.idempotencyKey
202
+ || action.runId !== work.id
203
+ || typeof action.cellId !== 'string' || !action.cellId
204
+ || !Number.isSafeInteger(action.callIndex) || action.callIndex < 0
205
+ || action.idempotencyKey !== actionKeyOf(action)
206
+ || !action.args || typeof action.args !== 'object' || Array.isArray(action.args)) {
207
+ throw new ControlPlaneError(400, 'invalid host action envelope');
208
+ }
209
+ const [namespace, method, ...rest] = action.action.split('.');
210
+ if (!namespace || !method || rest.length > 0) {
211
+ throw new ControlPlaneError(400, 'host action must be <namespace>.<method>');
212
+ }
213
+ // Authoritative capability check. The kernel-side allowlist only shapes
214
+ // what the model can conveniently express; this is the boundary.
215
+ const grants = await this.deps.capabilityResolver.resolve(work);
216
+ const grant = namespace === 'task' && typeof work.meta?.['text'] === 'string' ? grants.find(candidate => candidate.name === 'task') ?? { name: 'task', methods: ['contract', 'ask', 'check_receipt', 'check_resource', 'inspect'] } : grants.find((candidate) => candidate.name === namespace);
217
+ if (!grant || (grant.methods && !grant.methods.includes(method))) {
218
+ this.deps.metrics?.counter('agentos_actions_denied_total', 'Host actions denied by grant').inc({ namespace });
219
+ return { ok: false, error: `capability denied: ${action.action} is not granted to this work item` };
220
+ }
221
+ if (action.action === 'task.check_receipt' || action.action === 'task.check_resource') {
222
+ const target = action.args['action'];
223
+ const [targetNamespace, targetMethod, ...extra] = typeof target === 'string' ? target.split('.') : [];
224
+ const targetGrant = grants.find(candidate => candidate.name === targetNamespace);
225
+ if (!targetMethod || extra.length || targetNamespace === 'task' || !targetGrant
226
+ || (targetGrant.methods && !targetGrant.methods.includes(targetMethod))) {
227
+ return { ok: false, error: 'capability denied: receipt action is not granted to this work item' };
228
+ }
229
+ }
230
+ let requestVersion = null;
231
+ if (typeof work.meta?.['text'] === 'string') {
232
+ const request = (await this.getSession(proof, sessionKeyOf(work)))?.request;
233
+ const current = await this.heartbeat(proof);
234
+ if (!current.ok || current.cancelRequested)
235
+ throw new ControlPlaneError(409, 'work lease lost or cancelled before action');
236
+ if (!request || request.workId !== work.id || !isDeepStrictEqual(request.revisions, current.steer ?? [])) {
237
+ return { ok: false, error: 'request snapshot is stale or missing; process the latest user revisions before acting' };
238
+ }
239
+ requestVersion = request.revisions.length + 1;
240
+ }
241
+ const fingerprint = actionFingerprint(work, action);
242
+ const reservation = await this.deps.actions.reserve(action.idempotencyKey, fingerprint, {
243
+ workId: work.id, tenantId: work.tenantId, principalId: work.principalId ?? null, agentId: work.agentId,
244
+ sessionId: work.sessionId, threadId: work.threadId ?? null, requestVersion, action: structuredClone(action),
245
+ });
246
+ const replayed = await this.deps.actions.find(action.idempotencyKey);
247
+ if (replayed) {
248
+ this.deps.metrics?.counter('agentos_actions_replayed_total', 'Host actions served from the ledger').inc({ namespace });
249
+ return replayed;
250
+ }
251
+ if (reservation === 'existing') {
252
+ return { ok: false, executionState: 'unknown', error: 'action intent exists without a receipt; reconcile before retrying' };
253
+ }
254
+ let result;
255
+ try {
256
+ if (action.action === 'task.inspect') {
257
+ if (Object.keys(action.args).length)
258
+ throw new Error('task.inspect accepts no arguments');
259
+ const pending = await this.deps.actions.unsettled(work.id);
260
+ result = { ok: true, value: { requestVersion, pending: pending.slice(0, 64), truncated: pending.length > 64 } };
261
+ }
262
+ else if (action.action === 'task.contract') {
263
+ const request = (await this.getSession(proof, sessionKeyOf(work)))?.request;
264
+ if (!request || request.workId !== work.id)
265
+ throw new Error('task contract requires the current request snapshot');
266
+ const contract = createTaskContract(request.originalText, request.revisions.length + 1, action.args);
267
+ result = { ok: true, value: { status: 'draft_validated', requestVersion: contract.requestVersion }, directive: { type: 'task_contract', data: { ...contract } } };
268
+ }
269
+ else if (action.action === 'task.check_receipt') {
270
+ const { idempotencyKey, action: expectedAction, expected } = action.args;
271
+ if (Object.keys(action.args).length !== 3 || typeof idempotencyKey !== 'string' || !idempotencyKey
272
+ || typeof expectedAction !== 'string' || !expectedAction || expectedAction.startsWith('task.')
273
+ || expected === undefined)
274
+ throw new Error('task.check_receipt requires idempotencyKey, a business action and its complete expected value');
275
+ const intent = await this.deps.actions.findIntent(idempotencyKey);
276
+ if (!intent || intent.workId !== work.id || intent.tenantId !== work.tenantId
277
+ || intent.principalId !== (work.principalId ?? null) || intent.agentId !== work.agentId
278
+ || intent.sessionId !== work.sessionId || intent.threadId !== (work.threadId ?? null)
279
+ || intent.requestVersion !== requestVersion || intent.action.action !== expectedAction) {
280
+ throw new Error('receipt is unavailable for this request version and action');
281
+ }
282
+ const receipt = await this.deps.actions.find(idempotencyKey);
283
+ const observed = receipt?.ok === true && receipt.executionState !== 'unknown'
284
+ && receipt.directive === undefined && receipt.value !== undefined;
285
+ result = { ok: true, value: { scope: 'recorded_action_result', requestVersion, idempotencyKey,
286
+ action: expectedAction, status: !observed ? 'not_observed' : isDeepStrictEqual(receipt.value, expected) ? 'pass' : 'fail',
287
+ ...(observed ? { observed: receipt.value } : {}),
288
+ limitation: 'This checks a recorded action result, not current resource state or overall goal completion.' } };
289
+ }
290
+ else if (action.action === 'task.check_resource') {
291
+ const { action: readAction, args, expected } = action.args;
292
+ if (!this.deps.actionExecutor.readResource)
293
+ throw new Error('resource readback is unavailable');
294
+ if (Object.keys(action.args).length !== 3 || typeof readAction !== 'string'
295
+ || !args || typeof args !== 'object' || Array.isArray(args)
296
+ || !expected || typeof expected !== 'object' || Array.isArray(expected)) {
297
+ throw new Error('task.check_resource requires a read action, args and expected fields');
298
+ }
299
+ const fields = Object.entries(expected);
300
+ if (!fields.length || fields.length > 16 || JSON.stringify(expected).length > 16_384
301
+ || fields.some(([key]) => !key || key.length > 256 || ['__proto__', 'prototype', 'constructor'].includes(key))) {
302
+ throw new Error('expected must contain 1-16 resource fields within 16384 characters');
303
+ }
304
+ const resource = await this.deps.actionExecutor.readResource(work, { ...action, action: readAction, args: args });
305
+ const observed = resource && typeof resource === 'object' && !Array.isArray(resource)
306
+ && fields.every(([key]) => Object.hasOwn(resource, key))
307
+ ? Object.fromEntries(fields.map(([key]) => [key, resource[key]])) : undefined;
308
+ if (observed && JSON.stringify(observed).length > 65_536)
309
+ throw new Error('observed resource fields exceed the 65536 character limit');
310
+ result = { ok: true, value: { scope: 'observed_resource_fields', requestVersion, action: readAction, args, expected,
311
+ observedAt: new Date().toISOString(), status: observed === undefined ? 'not_observed'
312
+ : fields.every(([key, value]) => isDeepStrictEqual(observed[key], value)) ? 'pass' : 'fail',
313
+ ...(observed ? { observed } : {}),
314
+ limitation: 'Only these fields at observation time were checked. Replaying this receipt does not refresh it; absence does not prove deletion or overall goal completion.' } };
315
+ }
316
+ else if (action.action === 'task.ask') {
317
+ const question = action.args['question'];
318
+ if (Object.keys(action.args).length !== 1 || typeof question !== 'string' || !question.trim() || question.length > 4_000)
319
+ throw new Error('task.ask requires one non-empty question of at most 4000 characters');
320
+ result = { ok: true, value: { question }, directive: { type: 'defer', reason: 'user', data: { question } } };
321
+ }
322
+ else
323
+ result = await this.deps.actionExecutor.execute(work, action);
324
+ }
325
+ catch (error) {
326
+ result = { ok: false, ...(namespace === 'task' ? {} : { executionState: 'unknown' }), error: errorMessage(error) };
327
+ }
328
+ const recorded = await this.deps.actions.record(action.idempotencyKey, result);
329
+ this.deps.metrics?.counter('agentos_actions_executed_total', 'Host actions executed').inc({
330
+ namespace, ok: String(recorded.ok),
331
+ });
332
+ return recorded;
333
+ }
334
+ // -------------------------------------------------------------------------
335
+ // Run events
336
+ // -------------------------------------------------------------------------
337
+ async recordEvent(proof, event) {
338
+ const work = await this.requireLease(proof);
339
+ if (!event || typeof event !== 'object'
340
+ || event.runId !== work.id
341
+ || !Number.isSafeInteger(event.seq)
342
+ || typeof event.kind !== 'string' || !event.kind || event.kind.length > 160
343
+ || !RUN_STAGES.has(event.stage)
344
+ || (event.visibility !== 'user' && event.visibility !== 'internal')
345
+ || !event.data || typeof event.data !== 'object' || Array.isArray(event.data)) {
346
+ throw new ControlPlaneError(400, 'invalid run event envelope');
347
+ }
348
+ // Attempt-range fencing: an event must land inside its own attempt's
349
+ // sequence range so retried attempts can never shadow one another.
350
+ const rangeStart = Math.max(0, work.fence - 1) * RUN_SEQUENCE_SPAN;
351
+ const rangeEnd = work.fence * RUN_SEQUENCE_SPAN;
352
+ if (event.seq <= rangeStart || event.seq > rangeEnd) {
353
+ throw new ControlPlaneError(400, `event seq ${event.seq} is outside this attempt's range (${rangeStart}, ${rangeEnd}]`);
354
+ }
355
+ if (event.kind === 'ipython.completed') {
356
+ try {
357
+ snapshotArtifacts(event.data['artifacts']);
358
+ }
359
+ catch {
360
+ throw new ControlPlaneError(400, 'invalid kernel artifact event');
361
+ }
362
+ }
363
+ const inserted = await this.deps.events.append({
364
+ ...event,
365
+ tenantId: work.tenantId,
366
+ agentId: work.agentId,
367
+ recordedAt: new Date().toISOString(),
368
+ });
369
+ if (!inserted)
370
+ return; // duplicate delivery of an already-recorded event
371
+ this.deps.metrics?.counter('agentos_events_recorded_total', 'Run events recorded').inc({ kind: event.kind });
372
+ try {
373
+ await this.deps.delivery.onEvent(work, event);
374
+ }
375
+ catch (error) {
376
+ // Delivery is best-effort fan-out; the durable ledger already has the
377
+ // event, so a delivery hiccup must not fail the worker's call.
378
+ this.logger.warn('event delivery failed', { runId: event.runId, seq: event.seq, error: errorMessage(error) });
379
+ }
380
+ }
381
+ // -------------------------------------------------------------------------
382
+ // Final messages: stream-integrity verification
383
+ // -------------------------------------------------------------------------
384
+ async commitMessage(proof, message) {
385
+ const work = await this.requireLease(proof, { rejectCancelled: true });
386
+ if (!message || typeof message !== 'object'
387
+ || message.version !== 2
388
+ || 'data' in message
389
+ || message.runId !== work.id
390
+ || message.agentId !== work.agentId
391
+ || message.sessionId !== work.sessionId
392
+ || message.threadId !== work.threadId
393
+ || typeof message.body !== 'string' || !message.body.trim()) {
394
+ throw new ControlPlaneError(409, 'assistant message is missing its stream identity or body');
395
+ }
396
+ if (!message.envelope) {
397
+ throw new ControlPlaneError(409, 'response envelope is required');
398
+ }
399
+ const rangeStart = Math.max(0, work.fence - 1) * RUN_SEQUENCE_SPAN;
400
+ const rangeEnd = work.fence * RUN_SEQUENCE_SPAN;
401
+ const streamEvents = await this.deps.events.listRange(work.id, rangeStart, rangeEnd, ['model.delta', 'model.completed', 'ipython.completed', 'response.assessed']);
402
+ const streamed = streamEvents
403
+ .filter((event) => event.kind === 'model.delta'
404
+ && event.data['partType'] === 'text' && typeof event.data['delta'] === 'string')
405
+ .map((event) => String(event.data['delta']))
406
+ .join('')
407
+ .trim();
408
+ const completedTurns = streamEvents.filter((event) => event.kind === 'model.completed');
409
+ if (!streamed || completedTurns.length === 0 || streamed !== message.body.trim()) {
410
+ throw new ControlPlaneError(409, 'assistant final message does not match its durably streamed deltas');
411
+ }
412
+ if (message.envelope.goalOutcome?.verification === 'passed') {
413
+ throw new ControlPlaneError(409, 'goal verification requires authoritative acceptance evidence');
414
+ }
415
+ const session = await this.getSession(proof, sessionKeyOf(work));
416
+ if (!session?.request?.evidence)
417
+ throw new ControlPlaneError(409, 'response requires a saved request and evidence snapshot');
418
+ if (session.request.workId !== work.id)
419
+ throw new ControlPlaneError(409, 'response request belongs to another work item');
420
+ const evidence = session.request.evidence;
421
+ const artifacts = streamEvents.filter((event) => event.kind === 'ipython.completed')
422
+ .flatMap((event) => Array.isArray(event.data['artifacts']) ? event.data['artifacts'] : []);
423
+ if (message.envelope.requestVersion !== session.request.revisions.length + 1) {
424
+ throw new ControlPlaneError(409, 'response request version is stale');
425
+ }
426
+ if (message.envelope.goalOutcome.status === 'delegated'
427
+ && !await this.deps.work.hasPendingChild(work, message.envelope.goalOutcome.taskRef, message.envelope.requestVersion)) {
428
+ throw new ControlPlaneError(409, 'delegated task is not pending for this request');
429
+ }
430
+ await this.validateWait(work, message.envelope.goalOutcome);
431
+ let expected;
432
+ try {
433
+ const assessment = message.envelope.assessment;
434
+ if (assessment) {
435
+ parseFinalCandidate(JSON.stringify({ body: message.body, ...assessment }), session.request);
436
+ if (!streamEvents.some(event => event.kind === 'response.assessed' && event.data['body'] === message.body
437
+ && isDeepStrictEqual(event.data['assessment'], assessment)
438
+ && isDeepStrictEqual(event.data['goalOutcome'], message.envelope.goalOutcome)))
439
+ throw new Error('missing durable assessment');
440
+ }
441
+ if (message.envelope.goalOutcome.status === 'delegated' && (assessment?.status !== 'delegated'
442
+ || assessment.taskRef !== message.envelope.goalOutcome.taskRef))
443
+ throw new Error('missing delegated self-assessment');
444
+ if (message.envelope.goalOutcome.status === 'satisfied') {
445
+ if (assessment?.status !== 'satisfied')
446
+ throw new Error('missing satisfied self-assessment');
447
+ if ((await this.deps.actions.unsettled(work.id)).length)
448
+ throw new Error('business actions remain unresolved');
449
+ const latest = new Map();
450
+ for (const record of session.request.resourceChecks ?? []) {
451
+ const value = record.result.value;
452
+ if (value['requestVersion'] === message.envelope.requestVersion) {
453
+ latest.set(JSON.stringify([value['action'], value['args'], value['expected']]), value['status']);
454
+ }
455
+ }
456
+ if ([...latest.values()].some(status => status !== 'pass'))
457
+ throw new Error('known resource postconditions remain unresolved');
458
+ }
459
+ expected = createResponseEnvelope(message.body, message.envelope.goalOutcome, evidence, artifacts, session.request.contract, session.request.resourceChecks, assessment);
460
+ }
461
+ catch {
462
+ throw new ControlPlaneError(409, 'response does not match its evidence or artifact records');
463
+ }
464
+ if (!isDeepStrictEqual(expected, message.envelope)) {
465
+ throw new ControlPlaneError(409, 'response envelope is inconsistent with its durable records');
466
+ }
467
+ const current = await this.heartbeat(proof);
468
+ if (!current.ok || current.cancelRequested)
469
+ throw new ControlPlaneError(409, 'work lease lost or cancelled before delivery');
470
+ if (message.envelope.requestVersion !== (current.steer?.length ?? 0) + 1)
471
+ throw new ControlPlaneError(409, 'response request version is stale');
472
+ await this.deps.delivery.deliverMessage(work, message);
473
+ this.deps.metrics?.counter('agentos_messages_delivered_total', 'Final assistant messages delivered').inc();
474
+ }
475
+ // -------------------------------------------------------------------------
476
+ // Sessions
477
+ // -------------------------------------------------------------------------
478
+ async getSession(proof, key) {
479
+ const work = await this.requireLease(proof);
480
+ if (key !== sessionKeyOf(work))
481
+ throw new ControlPlaneError(403, 'session is outside this work lease');
482
+ const session = await this.deps.sessions.get(key);
483
+ if (session && (session.tenantId !== work.tenantId || session.agentId !== work.agentId
484
+ || session.sessionId !== work.sessionId || session.threadId !== work.threadId)) {
485
+ throw new ControlPlaneError(409, 'stored session identity mismatch');
486
+ }
487
+ return session;
488
+ }
489
+ async saveSession(proof, session) {
490
+ const work = await this.requireLease(proof);
491
+ const expectedKey = sessionKeyOf(work);
492
+ if (!session || typeof session !== 'object'
493
+ || session.key !== expectedKey
494
+ || session.tenantId !== work.tenantId || session.agentId !== work.agentId
495
+ || session.sessionId !== work.sessionId || session.threadId !== work.threadId
496
+ || !Array.isArray(session.history)
497
+ || !session.history.every(isModelItem)
498
+ || !Number.isSafeInteger(session.revision) || session.revision < 0
499
+ || !Number.isSafeInteger(session.compactionEpoch) || session.compactionEpoch < 0
500
+ || !Array.isArray(session.appliedWorkIds)
501
+ || !session.appliedWorkIds.every((id) => typeof id === 'string')
502
+ || (typeof work.meta?.['text'] === 'string' && !session.request)
503
+ || (session.request !== undefined && (!session.request || session.request.version !== 1 || session.request.workId !== work.id
504
+ || session.request.tenantId !== work.tenantId || session.request.sessionId !== work.sessionId
505
+ || typeof session.request.authorId !== 'string' || typeof session.request.sourceRef !== 'string'
506
+ || session.request.sourceRef !== work.triggerRef
507
+ || (work.principalId !== undefined && session.request.authorId !== work.principalId)
508
+ || (typeof work.meta?.['text'] === 'string' && session.request.originalText !== work.meta['text'])
509
+ || typeof session.request.originalText !== 'string' || !Array.isArray(session.request.revisions)
510
+ || !session.request.revisions.every((item) => item && typeof item.id === 'string'
511
+ && typeof item.text === 'string' && typeof item.createdAt === 'string')))) {
512
+ throw new ControlPlaneError(400, 'invalid session record for this work lease');
513
+ }
514
+ if (session.request?.contract) {
515
+ const contract = session.request.contract;
516
+ try {
517
+ const expected = createTaskContract(session.request.originalText, session.request.revisions.length + 1, {
518
+ deliverables: contract.deliverables, constraints: contract.constraints,
519
+ actions: contract.actions, acceptance: contract.acceptance,
520
+ });
521
+ if (!isDeepStrictEqual(contract, expected))
522
+ throw new Error('contract provenance mismatch');
523
+ }
524
+ catch {
525
+ throw new ControlPlaneError(400, 'invalid request task contract');
526
+ }
527
+ }
528
+ if (session.request) {
529
+ try {
530
+ if (!isDeepStrictEqual(snapshotAttachments(session.request.attachments), snapshotAttachments(work.meta?.['attachments'] ?? [])))
531
+ throw new Error('attachment mismatch');
532
+ const attachmentCount = session.request.revisions.reduce((count, revision) => count + snapshotAttachments(revision.attachments ?? []).length, session.request.attachments.length);
533
+ if (attachmentCount > 20)
534
+ throw new Error('request attachment limit reached');
535
+ }
536
+ catch {
537
+ throw new ControlPlaneError(400, 'invalid request attachments or mismatch with the original input');
538
+ }
539
+ }
540
+ if (session.request) {
541
+ try {
542
+ if (!session.request.evidence || session.request.evidence.version !== 1)
543
+ throw new Error('invalid evidence version');
544
+ snapshotEvidence(session.request.evidence.id, session.request.evidence.items);
545
+ }
546
+ catch {
547
+ throw new ControlPlaneError(400, 'invalid request evidence snapshot');
548
+ }
549
+ }
550
+ if (session.request?.revisions.length) {
551
+ const current = await this.deps.work.heartbeat(proof.id, proof.fence, hashToken(proof.leaseToken));
552
+ if (!current)
553
+ throw new ControlPlaneError(409, 'work lease lost before saving revisions');
554
+ if (!session.request.revisions.every((revision, index) => isDeepStrictEqual(revision, current.steer[index]))) {
555
+ throw new ControlPlaneError(400, 'request revisions do not match persisted human steering');
556
+ }
557
+ }
558
+ const previous = (await this.deps.sessions.get(expectedKey))?.request;
559
+ const previousChecks = previous?.workId === work.id ? previous.resourceChecks ?? [] : [];
560
+ const nextChecks = session.request?.resourceChecks ?? [];
561
+ if (!Array.isArray(nextChecks) || nextChecks.length > 64
562
+ || !isDeepStrictEqual(nextChecks.slice(0, previousChecks.length), previousChecks)) {
563
+ throw new ControlPlaneError(409, 'resource observations cannot be rewritten');
564
+ }
565
+ if (nextChecks.length > previousChecks.length) {
566
+ if (previous?.workId !== work.id)
567
+ throw new ControlPlaneError(409, 'resource observations require a saved request');
568
+ let expected = previousChecks;
569
+ for (const record of nextChecks.slice(previousChecks.length)) {
570
+ const key = record?.actionKey;
571
+ const intent = typeof key === 'string' ? await this.deps.actions.findIntent(key) : null;
572
+ if (!intent || intent.workId !== work.id || intent.tenantId !== work.tenantId
573
+ || intent.principalId !== (work.principalId ?? null) || intent.agentId !== work.agentId
574
+ || intent.sessionId !== work.sessionId || intent.threadId !== (work.threadId ?? null)
575
+ || intent.requestVersion !== session.request.revisions.length + 1 || intent.action.action !== 'task.check_resource') {
576
+ throw new ControlPlaneError(409, 'resource observation lacks a current scoped read intent');
577
+ }
578
+ const result = await this.deps.actions.find(key);
579
+ if (!result)
580
+ throw new ControlPlaneError(409, 'resource observation lacks a recorded result');
581
+ try {
582
+ expected = appendResourceCheck(expected, key, result, session.request.revisions.length + 1);
583
+ }
584
+ catch {
585
+ throw new ControlPlaneError(409, 'invalid resource observation');
586
+ }
587
+ }
588
+ if (!isDeepStrictEqual(expected, nextChecks))
589
+ throw new ControlPlaneError(409, 'resource observations do not match recorded reads');
590
+ }
591
+ if (previous?.workId === work.id) {
592
+ const next = session.request;
593
+ if (!next || next.originalText !== previous.originalText || next.authorId !== previous.authorId
594
+ || next.sourceRef !== previous.sourceRef
595
+ || !previous.revisions.every((revision, index) => isDeepStrictEqual(revision, next.revisions[index]))) {
596
+ throw new ControlPlaneError(409, 'acquired request and evidence snapshots cannot be rewritten');
597
+ }
598
+ }
599
+ if ((previous?.workId !== work.id || !previous.evidence) && session.request?.evidence?.items.some(item => item.actionKey !== undefined)) {
600
+ throw new ControlPlaneError(409, 'research evidence requires a saved initial snapshot');
601
+ }
602
+ if (previous?.workId === work.id && previous.evidence && !isDeepStrictEqual(previous.evidence, session.request?.evidence)) {
603
+ const next = session.request?.evidence;
604
+ if (!next || next.items.length <= previous.evidence.items.length
605
+ || !isDeepStrictEqual(next.items.slice(0, previous.evidence.items.length), previous.evidence.items))
606
+ throw new ControlPlaneError(409, 'acquired evidence cannot be rewritten');
607
+ let expected = previous.evidence;
608
+ for (const item of next.items.slice(previous.evidence.items.length)) {
609
+ const key = item.actionKey;
610
+ const intent = key ? await this.deps.actions.findIntent(key) : null;
611
+ if (!key || !intent || intent.workId !== work.id || intent.tenantId !== work.tenantId
612
+ || intent.principalId !== (work.principalId ?? null) || intent.agentId !== work.agentId
613
+ || intent.sessionId !== work.sessionId || intent.threadId !== (work.threadId ?? null)
614
+ || intent.requestVersion !== session.request.revisions.length + 1 || intent.action.action !== 'research.read')
615
+ throw new ControlPlaneError(409, 'evidence lacks a current research read intent');
616
+ const result = await this.deps.actions.find(key);
617
+ if (!result)
618
+ throw new ControlPlaneError(409, 'evidence lacks a recorded research read');
619
+ expected = appendResearchEvidence(expected, key, result);
620
+ }
621
+ if (!isDeepStrictEqual(expected, next))
622
+ throw new ControlPlaneError(409, 'evidence does not match recorded research reads');
623
+ }
624
+ const saved = await this.deps.sessions.save(session);
625
+ if (!saved.ok)
626
+ throw new ControlPlaneError(409, 'session revision conflict');
627
+ return { revision: saved.revision };
628
+ }
629
+ }
630
+ //# sourceMappingURL=service.js.map