devcontour-harness 0.1.0-alpha.5 → 0.1.0-alpha.7

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 (263) hide show
  1. package/README.md +42 -2
  2. package/docs/compatibility-versions.md +56 -0
  3. package/docs/compatibility.md +2 -2
  4. package/docs/connectors.md +323 -0
  5. package/docs/contracts.md +33 -5
  6. package/docs/integrations/hindsight.md +148 -0
  7. package/docs/quickstart.md +1 -1
  8. package/docs/release-notes/0.1.0-alpha.6.md +238 -0
  9. package/docs/release-notes/0.1.0-alpha.7.md +71 -0
  10. package/docs/review-probes.md +199 -0
  11. package/docs/review-protocol.md +251 -0
  12. package/docs/toolchains.md +167 -0
  13. package/docs/turbo-starter.md +269 -0
  14. package/docs/upgrade.md +77 -24
  15. package/examples/README.md +1 -0
  16. package/examples/toolchains/README.md +32 -0
  17. package/examples/toolchains/go/bad/label/label.go +9 -0
  18. package/examples/toolchains/go/good/label/label.go +9 -0
  19. package/examples/toolchains/go/policy.json +79 -0
  20. package/examples/toolchains/go/repo/REQUIREMENTS.md +9 -0
  21. package/examples/toolchains/go/repo/_gitignore +1 -0
  22. package/examples/toolchains/go/repo/go.mod +5 -0
  23. package/examples/toolchains/go/repo/go.sum +2 -0
  24. package/examples/toolchains/go/repo/label/label.go +9 -0
  25. package/examples/toolchains/go/repo/label/label_test.go +27 -0
  26. package/examples/toolchains/go/repo/label/version.go +9 -0
  27. package/examples/toolchains/go/task.json +25 -0
  28. package/examples/toolchains/mixed-go/bad/backend/label/label.go +9 -0
  29. package/examples/toolchains/mixed-go/bad/frontend/src/label.js +4 -0
  30. package/examples/toolchains/mixed-go/good/backend/label/label.go +9 -0
  31. package/examples/toolchains/mixed-go/good/frontend/src/label.js +4 -0
  32. package/examples/toolchains/mixed-go/policy.json +126 -0
  33. package/examples/toolchains/mixed-go/repo/REQUIREMENTS.md +9 -0
  34. package/examples/toolchains/mixed-go/repo/_gitignore +6 -0
  35. package/examples/toolchains/mixed-go/repo/backend/go.mod +3 -0
  36. package/examples/toolchains/mixed-go/repo/backend/label/label.go +9 -0
  37. package/examples/toolchains/mixed-go/repo/backend/label/label_test.go +30 -0
  38. package/examples/toolchains/mixed-go/repo/contract/check.mjs +17 -0
  39. package/examples/toolchains/mixed-go/repo/contract/label.json +9 -0
  40. package/examples/toolchains/mixed-go/repo/frontend/package.json +15 -0
  41. package/examples/toolchains/mixed-go/repo/frontend/pnpm-lock.yaml +22 -0
  42. package/examples/toolchains/mixed-go/repo/frontend/src/label.js +4 -0
  43. package/examples/toolchains/mixed-go/repo/frontend/test/label.test.js +11 -0
  44. package/examples/toolchains/mixed-go/task.json +25 -0
  45. package/examples/toolchains/mixed-python/bad/backend/src/fx_backend/__init__.py +6 -0
  46. package/examples/toolchains/mixed-python/bad/frontend/src/label.js +4 -0
  47. package/examples/toolchains/mixed-python/good/backend/src/fx_backend/__init__.py +6 -0
  48. package/examples/toolchains/mixed-python/good/frontend/src/label.js +4 -0
  49. package/examples/toolchains/mixed-python/policy.json +121 -0
  50. package/examples/toolchains/mixed-python/repo/REQUIREMENTS.md +9 -0
  51. package/examples/toolchains/mixed-python/repo/_gitignore +6 -0
  52. package/examples/toolchains/mixed-python/repo/backend/.python-version +1 -0
  53. package/examples/toolchains/mixed-python/repo/backend/pyproject.toml +19 -0
  54. package/examples/toolchains/mixed-python/repo/backend/src/fx_backend/__init__.py +6 -0
  55. package/examples/toolchains/mixed-python/repo/backend/tests/test_label.py +11 -0
  56. package/examples/toolchains/mixed-python/repo/backend/uv.lock +108 -0
  57. package/examples/toolchains/mixed-python/repo/contract/check.mjs +17 -0
  58. package/examples/toolchains/mixed-python/repo/contract/label.json +9 -0
  59. package/examples/toolchains/mixed-python/repo/frontend/package.json +15 -0
  60. package/examples/toolchains/mixed-python/repo/frontend/pnpm-lock.yaml +22 -0
  61. package/examples/toolchains/mixed-python/repo/frontend/src/label.js +4 -0
  62. package/examples/toolchains/mixed-python/repo/frontend/test/label.test.js +11 -0
  63. package/examples/toolchains/mixed-python/task.json +25 -0
  64. package/examples/toolchains/npm/bad/src/label.js +4 -0
  65. package/examples/toolchains/npm/good/src/label.js +4 -0
  66. package/examples/toolchains/npm/policy.json +82 -0
  67. package/examples/toolchains/npm/repo/REQUIREMENTS.md +9 -0
  68. package/examples/toolchains/npm/repo/_gitignore +4 -0
  69. package/examples/toolchains/npm/repo/package-lock.json +22 -0
  70. package/examples/toolchains/npm/repo/package.json +13 -0
  71. package/examples/toolchains/npm/repo/src/label.js +4 -0
  72. package/examples/toolchains/npm/repo/test/label.test.js +13 -0
  73. package/examples/toolchains/npm/task.json +25 -0
  74. package/examples/toolchains/pnpm/bad/packages/label/src/label.js +4 -0
  75. package/examples/toolchains/pnpm/good/packages/label/src/label.js +4 -0
  76. package/examples/toolchains/pnpm/policy.json +85 -0
  77. package/examples/toolchains/pnpm/repo/REQUIREMENTS.md +9 -0
  78. package/examples/toolchains/pnpm/repo/_gitignore +4 -0
  79. package/examples/toolchains/pnpm/repo/package.json +10 -0
  80. package/examples/toolchains/pnpm/repo/packages/checker/check.mjs +5 -0
  81. package/examples/toolchains/pnpm/repo/packages/checker/package.json +9 -0
  82. package/examples/toolchains/pnpm/repo/packages/label/package.json +14 -0
  83. package/examples/toolchains/pnpm/repo/packages/label/src/label.js +4 -0
  84. package/examples/toolchains/pnpm/repo/packages/label/test/label.test.js +13 -0
  85. package/examples/toolchains/pnpm/repo/pnpm-lock.yaml +29 -0
  86. package/examples/toolchains/pnpm/repo/pnpm-workspace.yaml +2 -0
  87. package/examples/toolchains/pnpm/task.json +25 -0
  88. package/examples/toolchains/python/bad/src/fx_label/__init__.py +6 -0
  89. package/examples/toolchains/python/good/src/fx_label/__init__.py +6 -0
  90. package/examples/toolchains/python/policy.json +80 -0
  91. package/examples/toolchains/python/repo/.python-version +1 -0
  92. package/examples/toolchains/python/repo/REQUIREMENTS.md +9 -0
  93. package/examples/toolchains/python/repo/_gitignore +5 -0
  94. package/examples/toolchains/python/repo/pyproject.toml +19 -0
  95. package/examples/toolchains/python/repo/src/fx_label/__init__.py +6 -0
  96. package/examples/toolchains/python/repo/tests/test_label.py +13 -0
  97. package/examples/toolchains/python/repo/uv.lock +108 -0
  98. package/examples/toolchains/python/task.json +25 -0
  99. package/examples/toolchains/turbo/bad/packages/label/src/label.js +4 -0
  100. package/examples/toolchains/turbo/good/packages/label/src/label.js +4 -0
  101. package/examples/toolchains/turbo/policy.json +110 -0
  102. package/examples/toolchains/turbo/repo/REQUIREMENTS.md +9 -0
  103. package/examples/toolchains/turbo/repo/_gitignore +4 -0
  104. package/examples/toolchains/turbo/repo/package.json +13 -0
  105. package/examples/toolchains/turbo/repo/packages/checker/check.mjs +5 -0
  106. package/examples/toolchains/turbo/repo/packages/checker/package.json +9 -0
  107. package/examples/toolchains/turbo/repo/packages/label/package.json +14 -0
  108. package/examples/toolchains/turbo/repo/packages/label/src/label.js +4 -0
  109. package/examples/toolchains/turbo/repo/packages/label/test/label.test.js +13 -0
  110. package/examples/toolchains/turbo/repo/pnpm-lock.yaml +94 -0
  111. package/examples/toolchains/turbo/repo/pnpm-workspace.yaml +2 -0
  112. package/examples/toolchains/turbo/repo/turbo.json +7 -0
  113. package/examples/toolchains/turbo/task.json +25 -0
  114. package/examples/turbo-starter/README.md +16 -0
  115. package/examples/turbo-starter/changes/issued-at-consumer-only.patch +12 -0
  116. package/examples/turbo-starter/changes/issued-at.patch +130 -0
  117. package/examples/turbo-starter/cleanup.mjs +58 -0
  118. package/examples/turbo-starter/graph.json +32 -0
  119. package/examples/turbo-starter/harness/acceptance/db-isolation.test.mjs +97 -0
  120. package/examples/turbo-starter/harness/acceptance/env-probe.test.mjs +75 -0
  121. package/examples/turbo-starter/harness/acceptance/generation.test.mjs +54 -0
  122. package/examples/turbo-starter/harness/acceptance/issued-at.test.mjs +43 -0
  123. package/examples/turbo-starter/harness/acceptance/lifecycle-probe.test.mjs +47 -0
  124. package/examples/turbo-starter/harness/compose-bridge.mjs +593 -0
  125. package/examples/turbo-starter/harness/compose.acceptance.yml +23 -0
  126. package/examples/turbo-starter/harness/env/backend.env.template +7 -0
  127. package/examples/turbo-starter/harness/env/compose.env.template +6 -0
  128. package/examples/turbo-starter/policy.json +109 -0
  129. package/examples/turbo-starter/policy.lifecycle.json +114 -0
  130. package/examples/turbo-starter/setup.mjs +352 -0
  131. package/lib/agents/cli-adapter.js +10 -1
  132. package/lib/agents/inspect.d.ts +56 -2
  133. package/lib/agents/inspect.js +264 -14
  134. package/lib/agents/review-probe.d.ts +229 -7
  135. package/lib/agents/review-probe.js +830 -72
  136. package/lib/agents/schemas.d.ts +3 -1
  137. package/lib/agents/schemas.js +7 -3
  138. package/lib/cli.js +167 -7
  139. package/lib/contracts/cli.d.ts +820 -11
  140. package/lib/contracts/cli.js +243 -4
  141. package/lib/contracts/errors.d.ts +3 -1
  142. package/lib/contracts/errors.js +2 -0
  143. package/lib/contracts/events.d.ts +37 -5
  144. package/lib/contracts/events.js +9 -0
  145. package/lib/contracts/execution.d.ts +389 -15
  146. package/lib/contracts/execution.js +76 -4
  147. package/lib/contracts/failure.js +5 -0
  148. package/lib/contracts/index.d.ts +1 -0
  149. package/lib/contracts/index.js +1 -0
  150. package/lib/contracts/policy.d.ts +15 -0
  151. package/lib/contracts/policy.js +10 -0
  152. package/lib/contracts/ports.d.ts +50 -0
  153. package/lib/contracts/profiles.d.ts +14 -0
  154. package/lib/contracts/profiles.js +4 -0
  155. package/lib/contracts/published.d.ts +279 -8
  156. package/lib/contracts/result.d.ts +171 -0
  157. package/lib/contracts/result.js +39 -1
  158. package/lib/contracts/review.d.ts +231 -0
  159. package/lib/contracts/review.js +326 -2
  160. package/lib/contracts/toolchain.d.ts +96 -0
  161. package/lib/contracts/toolchain.js +89 -0
  162. package/lib/control/doctor.d.ts +16 -1
  163. package/lib/control/doctor.js +45 -3
  164. package/lib/control/harness.d.ts +52 -6
  165. package/lib/control/harness.js +28 -1
  166. package/lib/control/recovery.d.ts +3 -1
  167. package/lib/control/review.js +23 -11
  168. package/lib/engine/budget.d.ts +3 -3
  169. package/lib/engine/classify.js +1 -0
  170. package/lib/engine/engine.d.ts +67 -17
  171. package/lib/engine/prompt.d.ts +29 -1
  172. package/lib/engine/prompt.js +57 -0
  173. package/lib/engine/property-recovery.js +2 -0
  174. package/lib/engine/result.js +52 -0
  175. package/lib/engine/reuse.js +6 -1
  176. package/lib/engine/runner.d.ts +25 -0
  177. package/lib/engine/runner.js +500 -123
  178. package/lib/environment/lifecycle.js +6 -0
  179. package/lib/environment/live-calls.js +3 -2
  180. package/lib/environment/os-tools.d.ts +16 -0
  181. package/lib/environment/os-tools.js +35 -0
  182. package/lib/environment/process.d.ts +8 -0
  183. package/lib/environment/process.js +13 -6
  184. package/lib/integrations/connector/commands.d.ts +358 -0
  185. package/lib/integrations/connector/commands.js +428 -0
  186. package/lib/integrations/connector/config.d.ts +215 -0
  187. package/lib/integrations/connector/config.js +214 -0
  188. package/lib/integrations/connector/delivery.d.ts +107 -0
  189. package/lib/integrations/connector/delivery.js +591 -0
  190. package/lib/integrations/connector/fake.d.ts +291 -0
  191. package/lib/integrations/connector/fake.js +786 -0
  192. package/lib/integrations/connector/history.d.ts +124 -0
  193. package/lib/integrations/connector/history.js +497 -0
  194. package/lib/integrations/connector/index.d.ts +16 -0
  195. package/lib/integrations/connector/index.js +16 -0
  196. package/lib/integrations/connector/redact.d.ts +19 -0
  197. package/lib/integrations/connector/redact.js +91 -0
  198. package/lib/integrations/connector/registry.d.ts +15 -0
  199. package/lib/integrations/connector/registry.js +43 -0
  200. package/lib/integrations/connector/snapshot.d.ts +131 -0
  201. package/lib/integrations/connector/snapshot.js +422 -0
  202. package/lib/integrations/connector/source.d.ts +37 -0
  203. package/lib/integrations/connector/source.js +73 -0
  204. package/lib/integrations/connector/steps.d.ts +71 -0
  205. package/lib/integrations/connector/steps.js +188 -0
  206. package/lib/integrations/connector/store.d.ts +140 -0
  207. package/lib/integrations/connector/store.js +277 -0
  208. package/lib/integrations/connector/summary.d.ts +38 -0
  209. package/lib/integrations/connector/summary.js +59 -0
  210. package/lib/integrations/connector/testing-worker.d.ts +1 -0
  211. package/lib/integrations/connector/testing-worker.js +81 -0
  212. package/lib/integrations/connector/testing.d.ts +205 -0
  213. package/lib/integrations/connector/testing.js +1615 -0
  214. package/lib/integrations/connector/transport.d.ts +240 -0
  215. package/lib/integrations/connector/transport.js +28 -0
  216. package/lib/integrations/memory/context.d.ts +120 -0
  217. package/lib/integrations/memory/context.js +239 -0
  218. package/lib/integrations/memory/index.d.ts +8 -0
  219. package/lib/integrations/memory/index.js +8 -0
  220. package/lib/integrations/memory/provider.d.ts +79 -0
  221. package/lib/integrations/memory/provider.js +22 -0
  222. package/lib/integrations/memory/sink.d.ts +56 -0
  223. package/lib/integrations/memory/sink.js +116 -0
  224. package/lib/integrations/memory/testing.d.ts +44 -0
  225. package/lib/integrations/memory/testing.js +61 -0
  226. package/lib/integrations/relay-v1/index.d.ts +122 -0
  227. package/lib/integrations/relay-v1/index.js +860 -0
  228. package/lib/integrations/relay-v2/index.d.ts +145 -0
  229. package/lib/integrations/relay-v2/index.js +752 -0
  230. package/lib/integrations/relay-v2/process.d.ts +32 -0
  231. package/lib/integrations/relay-v2/process.js +112 -0
  232. package/lib/integrations/relay-v2/protocol.d.ts +172 -0
  233. package/lib/integrations/relay-v2/protocol.js +142 -0
  234. package/lib/profiles/resolve.d.ts +5 -1
  235. package/lib/profiles/resolve.js +31 -0
  236. package/lib/storage/document.d.ts +116 -16
  237. package/lib/toolchains/index.d.ts +9 -0
  238. package/lib/toolchains/index.js +9 -0
  239. package/lib/toolchains/inspect.d.ts +105 -0
  240. package/lib/toolchains/inspect.js +831 -0
  241. package/lib/toolchains/manifests.d.ts +67 -0
  242. package/lib/toolchains/manifests.js +238 -0
  243. package/lib/toolchains/probe.d.ts +28 -0
  244. package/lib/toolchains/probe.js +54 -0
  245. package/lib/toolchains/recipes.d.ts +33 -0
  246. package/lib/toolchains/recipes.js +51 -0
  247. package/lib/verification/acceptance.d.ts +16 -0
  248. package/lib/verification/acceptance.js +16 -0
  249. package/lib/verification/gates.d.ts +38 -0
  250. package/lib/verification/gates.js +108 -26
  251. package/package.json +22 -1
  252. package/profiles/go-gotestsum.json +21 -0
  253. package/profiles/npm-ci.json +22 -0
  254. package/profiles/pnpm-frozen.json +24 -0
  255. package/profiles/python-uv.json +26 -0
  256. package/schemas/execution-document.schema.json +345 -4
  257. package/schemas/execution-policy.schema.json +48 -0
  258. package/schemas/result-envelope.schema.json +213 -1
  259. package/schemas/run-event.schema.json +93 -0
  260. package/schemas/run-result.schema.json +207 -0
  261. package/schemas/stack-profile.schema.json +43 -0
  262. package/templates/controller-mode/AGENTS.controller.md +58 -0
  263. package/templates/controller-mode/README.md +44 -0
@@ -0,0 +1,860 @@
1
+ /**
2
+ * Opt-in entrypoint `devcontour-harness/integrations/relay-v1`: транспорт
3
+ * публичного REST `/api/v1` Relay v1 (сервер `@oim-dev/relay-server` 0.13.x).
4
+ *
5
+ * Зависит только от wire-контракта: `fetch` из Node, без `@relay/rest-sdk` и
6
+ * любых upstream-модулей. Все вызовы проекта идут через project-scoped routes
7
+ * `/api/v1/projects/{projectId}/...`; корневые — только `GET /health`.
8
+ * Каждый ответ проверяется по envelope `{ok,data}` / `{ok:false,error}` и по
9
+ * схеме данных операции; несовместимый ответ — `protocol`, а не пустой успех.
10
+ * `requestId` уникален для каждого вызова и служит только корреляции:
11
+ * сервер не делает dedup, поэтому мутация выполняется ровно один раз, а
12
+ * повтор/read-back — решение ядра коннектора.
13
+ */
14
+ import { createHash, randomUUID } from 'node:crypto';
15
+ import { z } from 'zod';
16
+ import { HarnessError } from "../../contracts/index.js";
17
+ import { TransportError, } from "../connector/transport.js";
18
+ export const RELAY_V1_PROVIDER = 'relay-v1-http';
19
+ /** Capability истории работы; без неё методы истории не вызывают routes. */
20
+ export const WORK_HISTORY_CAPABILITY = 'relay-work-history-v1';
21
+ /* ------------------------------------------------------------------ */
22
+ /* Настройки */
23
+ /* ------------------------------------------------------------------ */
24
+ export const relayV1SettingsSchema = z
25
+ .object({
26
+ /** Origin сервера (`http://127.0.0.1:4700`); суффикс `/api/v1` допустим и отбрасывается. */
27
+ endpoint: z
28
+ .string()
29
+ .url()
30
+ .refine((v) => /^https?:\/\//.test(v), 'endpoint должен быть http(s) URL'),
31
+ /** Постоянный ID проекта Relay (`GET /context` → `projectId`), не имя и не `local`. */
32
+ projectId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/),
33
+ /** Автор мутаций (`actor` в теле запроса). */
34
+ actor: z.string().min(1).max(256),
35
+ timeoutMs: z.number().int().positive().max(600_000).default(15_000),
36
+ maxResponseBytes: z
37
+ .number()
38
+ .int()
39
+ .positive()
40
+ .max(64 * 1024 * 1024)
41
+ .default(4 * 1024 * 1024),
42
+ })
43
+ .strict();
44
+ /* ------------------------------------------------------------------ */
45
+ /* Схемы ответов (только поля, которые использует адаптер) */
46
+ /* ------------------------------------------------------------------ */
47
+ const envelopeSchema = z.union([
48
+ z.object({ ok: z.literal(true), data: z.unknown() }),
49
+ z.object({
50
+ ok: z.literal(false),
51
+ error: z.object({
52
+ code: z.string().min(1).max(128),
53
+ message: z.string(),
54
+ exitCode: z.number().optional(),
55
+ details: z.unknown().optional(),
56
+ }),
57
+ }),
58
+ ]);
59
+ const columnSchema = z.enum(['inbox', 'ready', 'in-progress', 'review', 'done', 'cancelled']);
60
+ const revisionSchema = z.number().int().nonnegative();
61
+ const idString = z.string().min(1);
62
+ const taskSummarySchema = z.object({
63
+ id: idString,
64
+ key: z.string(),
65
+ title: z.string(),
66
+ column: columnSchema,
67
+ revision: revisionSchema,
68
+ parentId: z.string().nullable(),
69
+ dependencies: z.array(z.string()),
70
+ canComplete: z.boolean().optional(),
71
+ });
72
+ const taskViewSchema = taskSummarySchema.extend({ description: z.string() });
73
+ const offsetPage = (item) => z.object({
74
+ items: z.array(item),
75
+ total: z.number().int().nonnegative(),
76
+ nextOffset: z.number().int().nonnegative().nullable(),
77
+ version: z.string().min(1),
78
+ });
79
+ const criterionSummarySchema = z.object({
80
+ id: idString,
81
+ title: z.string(),
82
+ completed: z.boolean(),
83
+ });
84
+ const criterionViewSchema = z.object({
85
+ criterion: z.object({
86
+ id: idString,
87
+ title: z.string(),
88
+ summary: z.string(),
89
+ description: z.string(),
90
+ completed: z.boolean(),
91
+ }),
92
+ revision: revisionSchema,
93
+ });
94
+ const taskSavedSchema = z.object({ id: idString, revision: revisionSchema, action: z.string() });
95
+ const commentSummarySchema = z.object({
96
+ id: idString,
97
+ sequence: z.number().int().positive(),
98
+ at: z.string(),
99
+ actor: z.string(),
100
+ title: z.string(),
101
+ });
102
+ const commentSchema = commentSummarySchema.extend({ description: z.string() });
103
+ const commentsPageSchema = z.object({
104
+ items: z.array(commentSummarySchema),
105
+ nextCursor: z.string().nullable(),
106
+ snapshot: z.number().int().nonnegative(),
107
+ });
108
+ const commentSavedSchema = z.object({
109
+ id: idString,
110
+ commentId: idString,
111
+ action: z.literal('comment-publish'),
112
+ });
113
+ const entitySummarySchema = z.object({
114
+ ref: z.object({ kind: z.literal('document'), id: idString }),
115
+ key: z.string(),
116
+ revision: revisionSchema,
117
+ });
118
+ const documentDetailSchema = entitySummarySchema.extend({
119
+ data: z.object({ kind: z.literal('document'), name: z.string(), body: z.string() }),
120
+ });
121
+ const healthSchema = z.object({ status: z.string(), contractVersion: z.number().int() });
122
+ const contextSchema = z.object({
123
+ projectId: z.string().min(1),
124
+ capabilities: z.array(z.string()).optional(),
125
+ });
126
+ const sessionSchema = z.object({
127
+ id: idString,
128
+ key: z.string(),
129
+ title: z.string(),
130
+ description: z.string(),
131
+ parentSessionId: z.string().nullable(),
132
+ status: z.enum(['pending', 'open', 'paused', 'completed', 'cancelled']),
133
+ currentRunId: z.string().nullable(),
134
+ result: z.string(),
135
+ revision: revisionSchema,
136
+ });
137
+ const sessionOverviewSchema = z.object({ id: idString, title: z.string() });
138
+ const checkpointSchema = z.object({
139
+ id: idString,
140
+ key: z.string(),
141
+ sessionId: idString,
142
+ runId: idString,
143
+ kind: z.enum(['intent', 'progress', 'result', 'cancel']),
144
+ intentId: z.string().nullable(),
145
+ title: z.string(),
146
+ body: z.string(),
147
+ });
148
+ const mutationSchema = z.object({
149
+ session: sessionSchema,
150
+ run: z.object({ id: idString }).nullable(),
151
+ checkpoint: checkpointSchema.nullable(),
152
+ childSession: sessionSchema.nullable(),
153
+ });
154
+ const sessionDetailSchema = z.object({ session: sessionSchema });
155
+ const historyItemSchema = z.object({ kind: z.string(), checkpoint: checkpointSchema.optional() });
156
+ /* ------------------------------------------------------------------ */
157
+ /* Классификация отказов */
158
+ /* ------------------------------------------------------------------ */
159
+ /** Коды, по которым мутация точно не применена из-за CAS/состояния. */
160
+ const CONFLICT_CODES = new Set([
161
+ 'REVISION_CONFLICT',
162
+ 'WORK_RUN_NOT_CURRENT',
163
+ 'VERSION_CONFLICT',
164
+ 'BOARD_CHANGED',
165
+ 'ENTITIES_CHANGED',
166
+ 'CONFLICT',
167
+ ]);
168
+ /** Доменные отказы backend: мутация не применена. */
169
+ const REFUSED_CODES = new Set([
170
+ 'TASK_ACCEPTANCE_INCOMPLETE',
171
+ 'TASK_BLOCKED',
172
+ 'TASK_ACCEPTANCE_LOCKED',
173
+ 'VALIDATION_ERROR',
174
+ 'INVALID_ARGUMENT',
175
+ 'INVALID_ID',
176
+ 'INVALID_CURSOR',
177
+ 'INVALID_REFERENCE',
178
+ 'DEPENDENCY_CYCLE',
179
+ 'UNKNOWN_STATUS',
180
+ 'TASK_TOO_LARGE',
181
+ 'RESPONSE_TOO_LARGE',
182
+ 'PAYLOAD_TOO_LARGE',
183
+ 'UNSUPPORTED_MEDIA_TYPE',
184
+ 'FORBIDDEN_ORIGIN',
185
+ 'ENTITY_KEY_CONFLICT',
186
+ 'STORAGE_MIGRATION_REQUIRED',
187
+ 'STORAGE_INDEX_STALE',
188
+ ]);
189
+ const UNAVAILABLE_CODES = new Set(['SYNC_NETWORK_UNAVAILABLE']);
190
+ const UNSUPPORTED_CODES = new Set(['UNSUPPORTED', 'SERVER_INCOMPATIBLE']);
191
+ /** Коды, при которых постраничное чтение начинается заново. */
192
+ const PAGE_RESTART_CODES = new Set(['BOARD_CHANGED', 'VERSION_CONFLICT', 'ENTITIES_CHANGED']);
193
+ /** Сетевые ошибки, при которых запрос точно не отправлен. */
194
+ const PRE_SEND_CODES = new Set([
195
+ 'ECONNREFUSED',
196
+ 'ENOTFOUND',
197
+ 'EAI_AGAIN',
198
+ 'EHOSTUNREACH',
199
+ 'ENETUNREACH',
200
+ 'EADDRNOTAVAIL',
201
+ 'UND_ERR_CONNECT_TIMEOUT',
202
+ 'ERR_INVALID_URL',
203
+ ]);
204
+ export function classifyCode(code, status, mutation) {
205
+ if (CONFLICT_CODES.has(code) || code.startsWith('WORK_'))
206
+ return 'conflict';
207
+ if (REFUSED_CODES.has(code))
208
+ return 'refused';
209
+ if (code === 'NOT_FOUND' || code.endsWith('_NOT_FOUND') || code === 'ENTITY_RELOCATED')
210
+ return 'not-found';
211
+ if (UNAVAILABLE_CODES.has(code))
212
+ return 'unavailable';
213
+ if (UNSUPPORTED_CODES.has(code))
214
+ return 'unsupported';
215
+ if (status >= 400 && status < 500)
216
+ return 'refused';
217
+ /* 5xx с неизвестным кодом: для мутации исход не доказан. */
218
+ return mutation ? 'uncertain' : 'unavailable';
219
+ }
220
+ /** Безопасный фрагмент текста backend: одна строка, ограниченная длина. */
221
+ function safe(text, max = 200) {
222
+ const line = text.replace(/[\r\n\t]+/g, ' ').trim();
223
+ return line.length > max ? `${line.slice(0, max)}…` : line;
224
+ }
225
+ function errorCode(e) {
226
+ let cur = e;
227
+ for (let i = 0; i < 5 && cur && typeof cur === 'object'; i++) {
228
+ const code = cur.code;
229
+ if (typeof code === 'string')
230
+ return code;
231
+ cur = cur.cause;
232
+ }
233
+ return undefined;
234
+ }
235
+ class PageRestart extends Error {
236
+ }
237
+ export class RelayV1Transport {
238
+ provider = RELAY_V1_PROVIDER;
239
+ settings;
240
+ base;
241
+ fetchImpl;
242
+ nextRequestId;
243
+ maxPageRestarts;
244
+ maxReadRetries;
245
+ actorRole;
246
+ capabilities;
247
+ constructor(settings, options = {}) {
248
+ this.settings = Object.freeze({ ...settings });
249
+ this.base = settings.endpoint.replace(/\/+$/, '').replace(/\/api\/v1$/, '');
250
+ this.fetchImpl = options.fetch ?? fetch;
251
+ this.nextRequestId = options.requestId ?? (() => `dc-${randomUUID()}`);
252
+ this.maxPageRestarts = options.maxPageRestarts ?? 3;
253
+ this.maxReadRetries = options.maxReadRetries ?? 3;
254
+ this.actorRole = options.actorRole ?? 'worker';
255
+ }
256
+ fail(kind, operation, reason, extra = {}) {
257
+ return new TransportError(kind, operation, `relay-v1 ${operation} (project ${this.settings.projectId}): ${reason}`, { provider: this.provider, projectId: this.settings.projectId, ...extra });
258
+ }
259
+ url(o) {
260
+ const prefix = o.root ? '' : `/api/v1/projects/${encodeURIComponent(this.settings.projectId)}`;
261
+ const u = new URL(`${this.base}${prefix}${o.path}`);
262
+ for (const [k, v] of Object.entries(o.query ?? {}))
263
+ if (v !== undefined)
264
+ u.searchParams.set(k, String(v));
265
+ return u.toString();
266
+ }
267
+ /** Один HTTP-вызов без повторов: envelope, классификация, схема данных. */
268
+ async call(operation, schema, o) {
269
+ const mutation = o.method === 'POST';
270
+ if (o.signal?.aborted)
271
+ throw this.fail('unavailable', operation, 'aborted before send', { mutation: false });
272
+ const controller = new AbortController();
273
+ let timedOut = false;
274
+ const timer = setTimeout(() => {
275
+ timedOut = true;
276
+ controller.abort();
277
+ }, this.settings.timeoutMs);
278
+ const onAbort = () => controller.abort();
279
+ o.signal?.addEventListener('abort', onAbort, { once: true });
280
+ const body = mutation
281
+ ? JSON.stringify({ ...o.body, requestId: this.nextRequestId(), actor: this.settings.actor })
282
+ : undefined;
283
+ let status;
284
+ let text;
285
+ try {
286
+ let res;
287
+ try {
288
+ res = await this.fetchImpl(this.url(o), {
289
+ method: o.method,
290
+ headers: {
291
+ accept: 'application/json',
292
+ ...(body ? { 'content-type': 'application/json' } : {}),
293
+ },
294
+ body,
295
+ signal: controller.signal,
296
+ redirect: 'error',
297
+ });
298
+ }
299
+ catch (e) {
300
+ throw this.transportFailure(operation, e, mutation, timedOut, 'request');
301
+ }
302
+ status = res.status;
303
+ try {
304
+ text = await this.readLimited(res, operation, mutation);
305
+ }
306
+ catch (e) {
307
+ if (e instanceof TransportError)
308
+ throw e;
309
+ throw this.transportFailure(operation, e, mutation, timedOut, 'response');
310
+ }
311
+ }
312
+ finally {
313
+ clearTimeout(timer);
314
+ o.signal?.removeEventListener('abort', onAbort);
315
+ }
316
+ let json;
317
+ try {
318
+ json = JSON.parse(text);
319
+ }
320
+ catch {
321
+ throw this.fail('protocol', operation, `invalid JSON response (HTTP ${status})`, {
322
+ mutation,
323
+ });
324
+ }
325
+ const env = envelopeSchema.safeParse(json);
326
+ if (!env.success) {
327
+ throw this.fail('protocol', operation, `response is not a Relay envelope (HTTP ${status})`, {
328
+ mutation,
329
+ });
330
+ }
331
+ if (!env.data.ok) {
332
+ const { code, message, details } = env.data.error;
333
+ if (status < 400) {
334
+ throw this.fail('protocol', operation, `error envelope with HTTP ${status}`, {
335
+ code,
336
+ mutation,
337
+ });
338
+ }
339
+ const kind = classifyCode(code, status, mutation);
340
+ const actual = details?.actual;
341
+ throw this.fail(kind, operation, `${code}: ${safe(message)}`, {
342
+ code,
343
+ mutation,
344
+ ...(typeof actual === 'number' ? { actualRevision: actual } : {}),
345
+ });
346
+ }
347
+ if (status < 200 || status >= 300) {
348
+ throw this.fail('protocol', operation, `success envelope with HTTP ${status}`, { mutation });
349
+ }
350
+ const data = schema.safeParse(env.data.data);
351
+ if (!data.success) {
352
+ const issue = data.error.issues[0];
353
+ throw this.fail('protocol', operation, `incompatible response data at ${issue?.path.join('.') || '<root>'}: ${safe(issue?.message ?? 'invalid')}`, { mutation });
354
+ }
355
+ return data.data;
356
+ }
357
+ transportFailure(operation, e, mutation, timedOut, phase) {
358
+ const code = errorCode(e);
359
+ if (phase === 'request' && code && PRE_SEND_CODES.has(code)) {
360
+ return this.fail('unavailable', operation, `server unreachable (${code})`, {
361
+ mutation: false,
362
+ });
363
+ }
364
+ const reason = timedOut
365
+ ? `timeout after ${this.settings.timeoutMs}ms`
366
+ : `connection failed (${code ?? 'aborted'})`;
367
+ if (mutation) {
368
+ return this.fail('uncertain', operation, `${reason} after send; outcome unknown, read back before retry`, {
369
+ mutation: true,
370
+ });
371
+ }
372
+ return this.fail('unavailable', operation, `${reason} during ${phase}`, { mutation: false });
373
+ }
374
+ async readLimited(res, operation, mutation) {
375
+ const max = this.settings.maxResponseBytes;
376
+ const declared = Number(res.headers.get('content-length') ?? NaN);
377
+ const tooLarge = () => this.fail('protocol', operation, `response exceeds ${max} bytes (HTTP ${res.status})`, {
378
+ mutation,
379
+ });
380
+ if (Number.isFinite(declared) && declared > max) {
381
+ await res.body?.cancel().catch(() => undefined);
382
+ throw tooLarge();
383
+ }
384
+ if (!res.body)
385
+ return '';
386
+ const reader = res.body.getReader();
387
+ const chunks = [];
388
+ let size = 0;
389
+ for (;;) {
390
+ const { done, value } = await reader.read();
391
+ if (done)
392
+ break;
393
+ size += value.byteLength;
394
+ if (size > max) {
395
+ await reader.cancel().catch(() => undefined);
396
+ throw tooLarge();
397
+ }
398
+ chunks.push(value);
399
+ }
400
+ return Buffer.concat(chunks).toString('utf8');
401
+ }
402
+ /** Полное постраничное чтение offset/limit/version; частичный список не возвращается. */
403
+ async pages(operation, item, path, query, signal) {
404
+ const schema = offsetPage(item);
405
+ for (let attempt = 0; attempt <= this.maxPageRestarts; attempt++) {
406
+ try {
407
+ return (await this.pagesOnce(operation, schema, path, query, signal));
408
+ }
409
+ catch (e) {
410
+ if (e instanceof PageRestart)
411
+ continue;
412
+ throw e;
413
+ }
414
+ }
415
+ throw this.fail('conflict', operation, `list kept changing after ${this.maxPageRestarts} restarts`);
416
+ }
417
+ async pagesOnce(operation, schema, path, query, signal) {
418
+ const items = [];
419
+ let offset = 0;
420
+ let version;
421
+ let total;
422
+ for (let page = 0;; page++) {
423
+ if (page > 10_000)
424
+ throw this.fail('protocol', operation, 'pagination does not terminate');
425
+ let data;
426
+ try {
427
+ data = await this.call(operation, schema, {
428
+ method: 'GET',
429
+ path,
430
+ query: { ...query, offset, limit: 100, version },
431
+ signal,
432
+ });
433
+ }
434
+ catch (e) {
435
+ if (e instanceof TransportError &&
436
+ page > 0 &&
437
+ e.details.code &&
438
+ PAGE_RESTART_CODES.has(e.details.code)) {
439
+ throw new PageRestart();
440
+ }
441
+ throw e;
442
+ }
443
+ if (version !== undefined && data.version !== version)
444
+ throw new PageRestart();
445
+ if (total !== undefined && data.total !== total)
446
+ throw new PageRestart();
447
+ version = data.version;
448
+ total = data.total;
449
+ items.push(...data.items);
450
+ if (data.nextOffset === null)
451
+ break;
452
+ if (data.nextOffset <= offset || data.items.length === 0) {
453
+ throw this.fail('protocol', operation, 'pagination offset does not advance');
454
+ }
455
+ offset = data.nextOffset;
456
+ }
457
+ if (items.length !== total) {
458
+ throw this.fail('protocol', operation, `list incomplete: ${items.length} of ${total}`);
459
+ }
460
+ return { items, version: version ?? '' };
461
+ }
462
+ enc(ref) {
463
+ if (!ref)
464
+ throw this.fail('refused', 'ref', 'empty reference');
465
+ return encodeURIComponent(ref);
466
+ }
467
+ /* ---------------- Подключение ---------------- */
468
+ async identity(signal) {
469
+ const health = await this.call('identity.health', healthSchema, {
470
+ method: 'GET',
471
+ path: '/api/v1/health',
472
+ root: true,
473
+ signal,
474
+ });
475
+ const ctx = await this.call('identity.context', contextSchema, {
476
+ method: 'GET',
477
+ path: '/context',
478
+ signal,
479
+ });
480
+ const capabilities = [...(ctx.capabilities ?? [])].sort();
481
+ this.capabilities = Promise.resolve(capabilities);
482
+ return {
483
+ provider: this.provider,
484
+ projectId: ctx.projectId,
485
+ capabilities,
486
+ build: { api: 'v1', contractVersion: String(health.contractVersion), health: health.status },
487
+ };
488
+ }
489
+ /** Capability истории (кэш на транспорт); без неё route не вызывается. */
490
+ async requireHistory(operation, signal) {
491
+ if (!this.capabilities) {
492
+ const p = this.call(`${operation}.capabilities`, contextSchema, {
493
+ method: 'GET',
494
+ path: '/context',
495
+ signal,
496
+ }).then((ctx) => ctx.capabilities ?? []);
497
+ this.capabilities = p;
498
+ p.catch(() => {
499
+ if (this.capabilities === p)
500
+ this.capabilities = undefined;
501
+ });
502
+ }
503
+ const caps = await this.capabilities;
504
+ if (!caps.includes(WORK_HISTORY_CAPABILITY)) {
505
+ throw this.fail('unsupported', operation, `server does not declare ${WORK_HISTORY_CAPABILITY}`, {
506
+ code: 'UNSUPPORTED',
507
+ });
508
+ }
509
+ }
510
+ /* ---------------- Задачи ---------------- */
511
+ summary(t) {
512
+ return {
513
+ id: t.id,
514
+ key: t.key,
515
+ title: t.title,
516
+ column: t.column,
517
+ revision: t.revision,
518
+ finished: t.column === 'done' && t.canComplete === true,
519
+ };
520
+ }
521
+ async readTaskOnce(ref, signal) {
522
+ const op = 'task.read';
523
+ const r = this.enc(ref);
524
+ const view = await this.call(op, taskViewSchema, {
525
+ method: 'GET',
526
+ path: `/board-tasks/${r}`,
527
+ signal,
528
+ });
529
+ const id = this.enc(view.id);
530
+ const crit = await this.pages(op, criterionSummarySchema, `/board-tasks/${id}/criteria`, {}, signal);
531
+ const criteria = [];
532
+ for (const c of crit.items) {
533
+ const full = await this.call(op, criterionViewSchema, {
534
+ method: 'GET',
535
+ path: `/board-tasks/${id}/criteria/${this.enc(c.id)}`,
536
+ signal,
537
+ });
538
+ const { id: cid, title, summary, description, completed } = full.criterion;
539
+ criteria.push({ id: cid, title, summary, description, completed });
540
+ }
541
+ const children = await this.pages(op, taskSummarySchema, '/board-tasks', { parentId: view.id }, signal);
542
+ const dependencies = [];
543
+ for (const dep of view.dependencies) {
544
+ const d = await this.call(op, taskSummarySchema, {
545
+ method: 'GET',
546
+ path: `/board-tasks/${this.enc(dep)}`,
547
+ signal,
548
+ });
549
+ dependencies.push(this.summary(d));
550
+ }
551
+ const docs = await this.pages(op, entitySummarySchema, '/entities', { kind: 'document', target: view.id }, signal);
552
+ const documents = [];
553
+ for (const d of docs.items) {
554
+ const full = await this.call(op, documentDetailSchema, {
555
+ method: 'GET',
556
+ path: '/entities/get',
557
+ query: { ref: d.ref.id, kind: 'document' },
558
+ signal,
559
+ });
560
+ documents.push({
561
+ id: full.ref.id,
562
+ key: full.key,
563
+ name: full.data.name,
564
+ body: full.data.body,
565
+ revision: full.revision,
566
+ });
567
+ }
568
+ const read = {
569
+ task: {
570
+ id: view.id,
571
+ key: view.key,
572
+ title: view.title,
573
+ description: view.description,
574
+ column: view.column,
575
+ revision: view.revision,
576
+ parentId: view.parentId,
577
+ dependencies: [...view.dependencies],
578
+ criteria,
579
+ },
580
+ children: children.items.map((c) => this.summary(c)),
581
+ dependencies,
582
+ documents,
583
+ };
584
+ const token = digest([
585
+ view.revision,
586
+ crit.version,
587
+ children.version,
588
+ docs.version,
589
+ dependencies.map((d) => [d.id, d.revision, d.finished]),
590
+ documents.map((d) => [d.id, d.revision]),
591
+ ]);
592
+ return { read, token, id: view.id };
593
+ }
594
+ /** Токен текущей редакции без полного чтения тел: ревизии и версии списков. */
595
+ async probe(id, signal) {
596
+ const op = 'task.read';
597
+ const r = this.enc(id);
598
+ const view = await this.call(op, taskViewSchema, {
599
+ method: 'GET',
600
+ path: `/board-tasks/${r}`,
601
+ signal,
602
+ });
603
+ const crit = await this.pages(op, criterionSummarySchema, `/board-tasks/${r}/criteria`, {}, signal);
604
+ const children = await this.pages(op, taskSummarySchema, '/board-tasks', { parentId: id }, signal);
605
+ const deps = [];
606
+ for (const dep of view.dependencies) {
607
+ const d = await this.call(op, taskSummarySchema, {
608
+ method: 'GET',
609
+ path: `/board-tasks/${this.enc(dep)}`,
610
+ signal,
611
+ });
612
+ const s = this.summary(d);
613
+ deps.push([s.id, s.revision, s.finished]);
614
+ }
615
+ const docs = await this.pages(op, entitySummarySchema, '/entities', { kind: 'document', target: id }, signal);
616
+ return digest([
617
+ view.revision,
618
+ crit.version,
619
+ children.version,
620
+ docs.version,
621
+ deps,
622
+ docs.items.map((d) => [d.ref.id, d.revision]),
623
+ ]);
624
+ }
625
+ async readTask(ref, signal) {
626
+ let last;
627
+ for (let attempt = 0; attempt <= this.maxReadRetries; attempt++) {
628
+ const { read, token, id } = await this.readTaskOnce(ref, signal);
629
+ const after = await this.probe(id, signal);
630
+ last = { ...read, consistency: { before: token, after } };
631
+ if (token === after)
632
+ return last;
633
+ }
634
+ /* Не согласовалось за отведённые повторы: ядро увидит before ≠ after. */
635
+ return last;
636
+ }
637
+ async listComments(ref, signal) {
638
+ const op = 'comment.list';
639
+ const r = this.enc(ref);
640
+ const seen = new Map();
641
+ let cursor;
642
+ let snapshot;
643
+ for (let page = 0;; page++) {
644
+ if (page > 10_000)
645
+ throw this.fail('protocol', op, 'comment pagination does not terminate');
646
+ const data = await this.call(op, commentsPageSchema, {
647
+ method: 'GET',
648
+ path: `/board-tasks/${r}/comments`,
649
+ query: { limit: 100, cursor },
650
+ signal,
651
+ });
652
+ if (snapshot !== undefined && data.snapshot !== snapshot) {
653
+ throw this.fail('protocol', op, 'comment snapshot changed during cursor pagination');
654
+ }
655
+ snapshot = data.snapshot;
656
+ for (const c of data.items)
657
+ seen.set(c.sequence, c);
658
+ if (data.nextCursor === null)
659
+ break;
660
+ if (data.nextCursor === cursor || data.items.length === 0) {
661
+ throw this.fail('protocol', op, 'comment cursor does not advance');
662
+ }
663
+ cursor = data.nextCursor;
664
+ }
665
+ const ordered = [...seen.values()].sort((a, b) => a.sequence - b.sequence);
666
+ const out = [];
667
+ for (const c of ordered) {
668
+ const full = await this.call(op, commentSchema, {
669
+ method: 'GET',
670
+ path: `/board-tasks/${r}/comments/${this.enc(c.id)}`,
671
+ signal,
672
+ });
673
+ out.push({
674
+ id: full.id,
675
+ title: full.title,
676
+ body: full.description,
677
+ actor: full.actor,
678
+ createdAt: full.at,
679
+ });
680
+ }
681
+ return out;
682
+ }
683
+ async addComment(ref, input, signal) {
684
+ const saved = await this.call('comment.add', commentSavedSchema, {
685
+ method: 'POST',
686
+ path: `/board-tasks/${this.enc(ref)}/comments`,
687
+ body: { title: input.title, description: input.body, actorRole: this.actorRole },
688
+ signal,
689
+ });
690
+ return { commentId: saved.commentId };
691
+ }
692
+ async setCriterion(ref, input, signal) {
693
+ const saved = await this.call('task.criterion', taskSavedSchema, {
694
+ method: 'POST',
695
+ path: `/board-tasks/${this.enc(ref)}/criteria`,
696
+ body: {
697
+ action: 'complete',
698
+ criterionId: input.criterionId,
699
+ completed: input.completed,
700
+ ifRevision: input.ifRevision,
701
+ },
702
+ signal,
703
+ });
704
+ return { revision: saved.revision };
705
+ }
706
+ async move(ref, input, signal) {
707
+ const saved = await this.call('task.move', taskSavedSchema, {
708
+ method: 'POST',
709
+ path: `/board-tasks/${this.enc(ref)}/move`,
710
+ body: { column: input.column, ifRevision: input.ifRevision },
711
+ signal,
712
+ });
713
+ return { revision: saved.revision };
714
+ }
715
+ /* ---------------- История работы ---------------- */
716
+ session(s) {
717
+ return {
718
+ id: s.id,
719
+ key: s.key,
720
+ status: s.status,
721
+ revision: s.revision,
722
+ currentRunId: s.currentRunId,
723
+ parentSessionId: s.parentSessionId,
724
+ title: s.title,
725
+ description: s.description,
726
+ result: s.result,
727
+ };
728
+ }
729
+ checkpointRecord(c) {
730
+ return {
731
+ id: c.id,
732
+ key: c.key,
733
+ sessionId: c.sessionId,
734
+ runId: c.runId,
735
+ kind: c.kind,
736
+ intentId: c.intentId,
737
+ title: c.title,
738
+ body: c.body,
739
+ };
740
+ }
741
+ async historyPost(operation, path, body, signal) {
742
+ await this.requireHistory(operation, signal);
743
+ const m = await this.call(operation, mutationSchema, {
744
+ method: 'POST',
745
+ path: `/work-history${path}`,
746
+ body,
747
+ signal,
748
+ });
749
+ const out = { session: this.session(m.session) };
750
+ if (m.run)
751
+ out.runId = m.run.id;
752
+ if (m.checkpoint)
753
+ out.checkpoint = this.checkpointRecord(m.checkpoint);
754
+ if (m.childSession)
755
+ out.childSession = this.session(m.childSession);
756
+ return out;
757
+ }
758
+ startSession(input, signal) {
759
+ return this.historyPost('history.start', '/sessions/start', {
760
+ title: input.title,
761
+ description: input.description,
762
+ client: input.client,
763
+ ...(input.sourceSessionId ? { sourceSessionId: input.sourceSessionId } : {}),
764
+ }, signal);
765
+ }
766
+ async getSession(ref, signal) {
767
+ await this.requireHistory('history.get', signal);
768
+ const d = await this.call('history.get', sessionDetailSchema, {
769
+ method: 'GET',
770
+ path: `/work-history/sessions/${this.enc(ref)}`,
771
+ signal,
772
+ });
773
+ return this.session(d.session);
774
+ }
775
+ async findSessions(marker, signal) {
776
+ const op = 'history.find';
777
+ await this.requireHistory(op, signal);
778
+ if (!marker)
779
+ throw this.fail('refused', op, 'empty marker');
780
+ const list = await this.pages(op, sessionOverviewSchema, '/work-history/sessions', { q: marker }, signal);
781
+ const out = [];
782
+ for (const s of list.items) {
783
+ const full = await this.getSession(s.id, signal);
784
+ if (full.title.includes(marker) || full.description.includes(marker))
785
+ out.push(full);
786
+ }
787
+ return out;
788
+ }
789
+ async checkpoints(sessionRef, signal) {
790
+ const op = 'history.checkpoints';
791
+ await this.requireHistory(op, signal);
792
+ const page = await this.pages(op, historyItemSchema, `/work-history/sessions/${this.enc(sessionRef)}/history`, { kind: 'checkpoint', order: 'asc' }, signal);
793
+ return page.items.map((i) => {
794
+ if (i.kind !== 'checkpoint' || !i.checkpoint) {
795
+ throw this.fail('protocol', op, `unexpected history item kind ${safe(i.kind, 40)}`);
796
+ }
797
+ return this.checkpointRecord(i.checkpoint);
798
+ });
799
+ }
800
+ checkpointBody(input, accepted) {
801
+ return {
802
+ runId: input.runId,
803
+ ifRevision: input.ifRevision,
804
+ title: input.title,
805
+ body: input.body,
806
+ ...(input.summary !== undefined ? { summary: input.summary } : {}),
807
+ ...(input.checks !== undefined ? { checks: input.checks } : {}),
808
+ ...(input.references ? { references: input.references } : {}),
809
+ ...(accepted && input.acceptedSessionIds
810
+ ? { acceptedSessionIds: input.acceptedSessionIds }
811
+ : {}),
812
+ };
813
+ }
814
+ openIntent(sessionRef, input, signal) {
815
+ if (input.acceptedSessionIds?.length) {
816
+ return Promise.reject(this.fail('refused', 'history.intent', 'intent cannot accept child sessions'));
817
+ }
818
+ return this.historyPost('history.intent', '/checkpoints/open', { ref: sessionRef, ...this.checkpointBody(input, false) }, signal);
819
+ }
820
+ checkpoint(intentRef, kind, input, signal) {
821
+ const route = { progress: 'save', result: 'complete', cancel: 'cancel' }[kind];
822
+ return this.historyPost(`history.checkpoint.${kind}`, `/checkpoints/${route}`, { ref: intentRef, ...this.checkpointBody(input, true) }, signal);
823
+ }
824
+ fork(intentRef, input, signal) {
825
+ return this.historyPost('history.fork', '/sessions/fork', { ref: intentRef, ...input }, signal);
826
+ }
827
+ attach(sessionRef, input, signal) {
828
+ return this.historyPost('history.attach', '/sessions/attach', {
829
+ ref: sessionRef,
830
+ ifRevision: input.ifRevision,
831
+ client: input.client,
832
+ ...(input.sourceSessionId ? { sourceSessionId: input.sourceSessionId } : {}),
833
+ }, signal);
834
+ }
835
+ takeover(sessionRef, input, signal) {
836
+ return this.historyPost('history.takeover', '/sessions/takeover', { ref: sessionRef, ifRevision: input.ifRevision, reason: input.reason, client: input.client }, signal);
837
+ }
838
+ finishSession(sessionRef, outcome, input, signal) {
839
+ return this.historyPost(`history.${outcome}`, `/sessions/${outcome}`, {
840
+ ref: sessionRef,
841
+ ifRevision: input.ifRevision,
842
+ result: input.result,
843
+ ...(input.runId ? { runId: input.runId } : {}),
844
+ }, signal);
845
+ }
846
+ }
847
+ function digest(value) {
848
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex');
849
+ }
850
+ /** Создаёт транспорт Relay v1 по проверенным настройкам; неверные — `INVALID_INPUT`. */
851
+ export function createTransport(settings, options = {}) {
852
+ const parsed = relayV1SettingsSchema.safeParse(settings);
853
+ if (!parsed.success) {
854
+ throw new HarnessError('INVALID_INPUT', 'relay-v1 settings are invalid', parsed.error.issues.map((i) => ({
855
+ path: i.path.filter((p) => typeof p !== 'symbol'),
856
+ message: i.message,
857
+ })));
858
+ }
859
+ return new RelayV1Transport(parsed.data, options);
860
+ }