@flyingrobots/graft 0.7.0 → 0.7.1

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 (283) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +0 -1
  3. package/bin/graft.js +26 -26
  4. package/dist/cli/init-client-config.d.ts.map +1 -1
  5. package/dist/cli/init-client-config.js +40 -4
  6. package/dist/cli/init-client-config.js.map +1 -1
  7. package/dist/cli/init-model.js +2 -2
  8. package/dist/cli/init-model.js.map +1 -1
  9. package/dist/cli/json-document.d.ts +1 -0
  10. package/dist/cli/json-document.d.ts.map +1 -1
  11. package/dist/cli/json-document.js +3 -0
  12. package/dist/cli/json-document.js.map +1 -1
  13. package/dist/hooks/posttooluse-read.js +1 -1
  14. package/dist/hooks/posttooluse-read.js.map +1 -1
  15. package/dist/hooks/pretooluse-read.js +1 -1
  16. package/dist/hooks/pretooluse-read.js.map +1 -1
  17. package/dist/mcp/daemon-worker-child-pool.d.ts +2 -0
  18. package/dist/mcp/daemon-worker-child-pool.d.ts.map +1 -1
  19. package/dist/mcp/daemon-worker-child-pool.js +10 -3
  20. package/dist/mcp/daemon-worker-child-pool.js.map +1 -1
  21. package/dist/mcp/daemon-worker-pool.d.ts +1 -1
  22. package/dist/mcp/daemon-worker-pool.d.ts.map +1 -1
  23. package/dist/mcp/daemon-worker-pool.js +1 -1
  24. package/dist/mcp/daemon-worker-pool.js.map +1 -1
  25. package/dist/version.d.ts.map +1 -1
  26. package/dist/version.js +3 -1
  27. package/dist/version.js.map +1 -1
  28. package/docs/SETUP.md +11 -7
  29. package/package.json +9 -4
  30. package/src/adapters/canonical-json.ts +0 -56
  31. package/src/adapters/json-text-decoder.ts +0 -11
  32. package/src/adapters/node-fs.ts +0 -43
  33. package/src/adapters/node-git.ts +0 -47
  34. package/src/adapters/node-paths.ts +0 -35
  35. package/src/adapters/node-process-runner.ts +0 -27
  36. package/src/adapters/repo-paths.ts +0 -63
  37. package/src/adapters/rotating-ndjson-log.ts +0 -66
  38. package/src/api/index.ts +0 -107
  39. package/src/api/repo-local-graft.ts +0 -21
  40. package/src/api/repo-workspace.ts +0 -34
  41. package/src/api/tool-bridge.ts +0 -22
  42. package/src/cli/activity-render.ts +0 -112
  43. package/src/cli/cli-error.ts +0 -192
  44. package/src/cli/command-parser.ts +0 -484
  45. package/src/cli/daemon-status-model.ts +0 -238
  46. package/src/cli/daemon-status-render.ts +0 -102
  47. package/src/cli/daemon-status.ts +0 -100
  48. package/src/cli/entrypoint.ts +0 -18
  49. package/src/cli/git-graft-enhance-model.ts +0 -131
  50. package/src/cli/git-graft-enhance-render.ts +0 -33
  51. package/src/cli/git-graft-enhance.ts +0 -54
  52. package/src/cli/index-cmd.ts +0 -90
  53. package/src/cli/index-model.ts +0 -75
  54. package/src/cli/init-bootstrap.ts +0 -47
  55. package/src/cli/init-client-config.ts +0 -428
  56. package/src/cli/init-model.ts +0 -284
  57. package/src/cli/init-render.ts +0 -54
  58. package/src/cli/init-target-hooks.ts +0 -93
  59. package/src/cli/init.ts +0 -124
  60. package/src/cli/json-document.ts +0 -155
  61. package/src/cli/local-history-dag-model.ts +0 -437
  62. package/src/cli/local-history-dag-render.ts +0 -133
  63. package/src/cli/local-history-dag.ts +0 -103
  64. package/src/cli/main.ts +0 -197
  65. package/src/cli/migrate-local-history.ts +0 -119
  66. package/src/cli/peer-command.ts +0 -60
  67. package/src/contracts/capabilities.ts +0 -540
  68. package/src/contracts/causal-ontology.ts +0 -627
  69. package/src/contracts/causal-surface-next-action.ts +0 -18
  70. package/src/contracts/json-object.ts +0 -18
  71. package/src/contracts/mcp-runtime.ts +0 -16
  72. package/src/contracts/output-schema-cli.ts +0 -103
  73. package/src/contracts/output-schema-fragments.ts +0 -881
  74. package/src/contracts/output-schema-mcp.ts +0 -501
  75. package/src/contracts/output-schema-meta.ts +0 -119
  76. package/src/contracts/output-schemas.ts +0 -1524
  77. package/src/git/diff.ts +0 -161
  78. package/src/git/target-git-hook-bootstrap.ts +0 -56
  79. package/src/guards/stream-boundary.ts +0 -110
  80. package/src/hooks/posttooluse-read.ts +0 -54
  81. package/src/hooks/pretooluse-read.ts +0 -52
  82. package/src/hooks/read-governor.ts +0 -95
  83. package/src/hooks/read-messages.ts +0 -53
  84. package/src/hooks/shared.ts +0 -168
  85. package/src/index.ts +0 -1
  86. package/src/mcp/burden.ts +0 -132
  87. package/src/mcp/cache.ts +0 -7
  88. package/src/mcp/cached-file.ts +0 -1
  89. package/src/mcp/context.ts +0 -150
  90. package/src/mcp/control-plane/authz-storage.ts +0 -129
  91. package/src/mcp/control-plane/session-registry.ts +0 -115
  92. package/src/mcp/control-plane/status-projection.ts +0 -76
  93. package/src/mcp/control-plane/types.ts +0 -144
  94. package/src/mcp/daemon-bootstrap.ts +0 -100
  95. package/src/mcp/daemon-control-plane.ts +0 -310
  96. package/src/mcp/daemon-job-scheduler.ts +0 -279
  97. package/src/mcp/daemon-repos.ts +0 -99
  98. package/src/mcp/daemon-server.ts +0 -172
  99. package/src/mcp/daemon-session-host.ts +0 -234
  100. package/src/mcp/daemon-stdio-bridge.ts +0 -262
  101. package/src/mcp/daemon-worker-child-pool.ts +0 -213
  102. package/src/mcp/daemon-worker-inline-pool.ts +0 -46
  103. package/src/mcp/daemon-worker-pool.ts +0 -7
  104. package/src/mcp/daemon-worker-process.ts +0 -52
  105. package/src/mcp/daemon-worker-types.ts +0 -66
  106. package/src/mcp/metrics.ts +0 -176
  107. package/src/mcp/monitor-health.ts +0 -83
  108. package/src/mcp/monitor-persistence.ts +0 -54
  109. package/src/mcp/monitor-tick-job.ts +0 -153
  110. package/src/mcp/monitor-types.ts +0 -103
  111. package/src/mcp/persisted-local-history-graph.ts +0 -592
  112. package/src/mcp/persisted-local-history-policy.ts +0 -501
  113. package/src/mcp/persisted-local-history-views.ts +0 -250
  114. package/src/mcp/persisted-local-history.ts +0 -1621
  115. package/src/mcp/persistent-monitor-runtime.ts +0 -482
  116. package/src/mcp/policy.ts +0 -75
  117. package/src/mcp/receipt.ts +0 -201
  118. package/src/mcp/repo-concurrency.ts +0 -392
  119. package/src/mcp/repo-overview/filter.ts +0 -44
  120. package/src/mcp/repo-overview/types.ts +0 -63
  121. package/src/mcp/repo-overview/view-projection.ts +0 -90
  122. package/src/mcp/repo-state-git.ts +0 -247
  123. package/src/mcp/repo-state-observation.ts +0 -203
  124. package/src/mcp/repo-state-transition.ts +0 -151
  125. package/src/mcp/repo-state-types.ts +0 -107
  126. package/src/mcp/repo-state.ts +0 -146
  127. package/src/mcp/repo-tool-job.ts +0 -102
  128. package/src/mcp/repo-tool-worker-context.ts +0 -215
  129. package/src/mcp/repo-workspace.ts +0 -21
  130. package/src/mcp/run-capture-config.ts +0 -33
  131. package/src/mcp/runtime-causal-context.ts +0 -72
  132. package/src/mcp/runtime-observability.ts +0 -217
  133. package/src/mcp/runtime-staged-target.ts +0 -161
  134. package/src/mcp/runtime-workspace-overlay.ts +0 -255
  135. package/src/mcp/secret-scrub.ts +0 -89
  136. package/src/mcp/semantic-transition-guidance.ts +0 -60
  137. package/src/mcp/semantic-transition-summary.ts +0 -130
  138. package/src/mcp/server-context.ts +0 -236
  139. package/src/mcp/server-invocation.ts +0 -388
  140. package/src/mcp/server-tool-access.ts +0 -148
  141. package/src/mcp/server.ts +0 -270
  142. package/src/mcp/stdio-server.ts +0 -26
  143. package/src/mcp/stdio.ts +0 -3
  144. package/src/mcp/tool-registry.ts +0 -98
  145. package/src/mcp/tools/activity-view.ts +0 -325
  146. package/src/mcp/tools/budget.ts +0 -23
  147. package/src/mcp/tools/causal-attach.ts +0 -67
  148. package/src/mcp/tools/causal-status.ts +0 -58
  149. package/src/mcp/tools/changed-since.ts +0 -71
  150. package/src/mcp/tools/code-find.ts +0 -192
  151. package/src/mcp/tools/code-refs.ts +0 -470
  152. package/src/mcp/tools/code-show.ts +0 -289
  153. package/src/mcp/tools/daemon-monitors.ts +0 -14
  154. package/src/mcp/tools/daemon-repos.ts +0 -22
  155. package/src/mcp/tools/daemon-sessions.ts +0 -14
  156. package/src/mcp/tools/daemon-status.ts +0 -12
  157. package/src/mcp/tools/diagnostic-models.ts +0 -97
  158. package/src/mcp/tools/doctor.ts +0 -83
  159. package/src/mcp/tools/explain.ts +0 -84
  160. package/src/mcp/tools/export-surface-diff.ts +0 -26
  161. package/src/mcp/tools/file-outline.ts +0 -67
  162. package/src/mcp/tools/git-files.ts +0 -73
  163. package/src/mcp/tools/graft-diff.ts +0 -35
  164. package/src/mcp/tools/graft-edit.ts +0 -276
  165. package/src/mcp/tools/knowledge-map.ts +0 -34
  166. package/src/mcp/tools/map-collector.ts +0 -338
  167. package/src/mcp/tools/map.ts +0 -185
  168. package/src/mcp/tools/monitor-nudge.ts +0 -19
  169. package/src/mcp/tools/monitor-pause.ts +0 -18
  170. package/src/mcp/tools/monitor-resume.ts +0 -18
  171. package/src/mcp/tools/monitor-start.ts +0 -20
  172. package/src/mcp/tools/monitor-stop.ts +0 -18
  173. package/src/mcp/tools/precision-live.ts +0 -158
  174. package/src/mcp/tools/precision-match.ts +0 -54
  175. package/src/mcp/tools/precision-paths.ts +0 -59
  176. package/src/mcp/tools/precision-query.ts +0 -127
  177. package/src/mcp/tools/precision-show.ts +0 -86
  178. package/src/mcp/tools/precision-visibility.ts +0 -44
  179. package/src/mcp/tools/precision-warp.ts +0 -101
  180. package/src/mcp/tools/precision.ts +0 -21
  181. package/src/mcp/tools/read-range.ts +0 -28
  182. package/src/mcp/tools/refactor-difficulty.ts +0 -42
  183. package/src/mcp/tools/run-capture.ts +0 -149
  184. package/src/mcp/tools/safe-read.ts +0 -148
  185. package/src/mcp/tools/since.ts +0 -53
  186. package/src/mcp/tools/state.ts +0 -39
  187. package/src/mcp/tools/stats.ts +0 -24
  188. package/src/mcp/tools/structural-blame.ts +0 -55
  189. package/src/mcp/tools/structural-churn.ts +0 -28
  190. package/src/mcp/tools/structural-log.ts +0 -41
  191. package/src/mcp/tools/structural-review.ts +0 -63
  192. package/src/mcp/tools/workspace-authorizations.ts +0 -14
  193. package/src/mcp/tools/workspace-authorize.ts +0 -20
  194. package/src/mcp/tools/workspace-bind.ts +0 -25
  195. package/src/mcp/tools/workspace-rebind.ts +0 -25
  196. package/src/mcp/tools/workspace-revoke.ts +0 -18
  197. package/src/mcp/tools/workspace-status.ts +0 -12
  198. package/src/mcp/warp-pool.ts +0 -36
  199. package/src/mcp/workspace-read-observation-model.ts +0 -48
  200. package/src/mcp/workspace-read-observation.ts +0 -103
  201. package/src/mcp/workspace-router-capability.ts +0 -32
  202. package/src/mcp/workspace-router-history.ts +0 -233
  203. package/src/mcp/workspace-router-model.ts +0 -131
  204. package/src/mcp/workspace-router-resolution.ts +0 -71
  205. package/src/mcp/workspace-router-runtime.ts +0 -246
  206. package/src/mcp/workspace-router.ts +0 -688
  207. package/src/metrics/logger.ts +0 -40
  208. package/src/metrics/types.ts +0 -40
  209. package/src/operations/adaptive-projection.ts +0 -54
  210. package/src/operations/agent-handoff.ts +0 -59
  211. package/src/operations/cached-file.ts +0 -40
  212. package/src/operations/capture-range.ts +0 -64
  213. package/src/operations/conversation-primer.ts +0 -82
  214. package/src/operations/cross-session-resume.ts +0 -104
  215. package/src/operations/deterministic-replay.ts +0 -143
  216. package/src/operations/diff-identity.ts +0 -127
  217. package/src/operations/export-surface-diff.ts +0 -355
  218. package/src/operations/file-outline.ts +0 -48
  219. package/src/operations/footprint-parallelism.ts +0 -98
  220. package/src/operations/graft-diff.ts +0 -169
  221. package/src/operations/horizon-of-readability.ts +0 -60
  222. package/src/operations/import-reference-impact.ts +0 -143
  223. package/src/operations/knowledge-map.ts +0 -96
  224. package/src/operations/observation-cache.ts +0 -165
  225. package/src/operations/projection-safety.ts +0 -111
  226. package/src/operations/read-range.ts +0 -64
  227. package/src/operations/repo-workspace.ts +0 -339
  228. package/src/operations/result-dto.ts +0 -23
  229. package/src/operations/safe-read.ts +0 -118
  230. package/src/operations/semantic-drift.ts +0 -70
  231. package/src/operations/session-filtration.ts +0 -76
  232. package/src/operations/session-replay.ts +0 -73
  233. package/src/operations/sludge-detector.ts +0 -356
  234. package/src/operations/state.ts +0 -66
  235. package/src/operations/structural-blame.ts +0 -197
  236. package/src/operations/structural-review.ts +0 -317
  237. package/src/operations/structured-buffer-compare.ts +0 -429
  238. package/src/operations/structured-buffer-model.ts +0 -456
  239. package/src/operations/structured-buffer-query.ts +0 -794
  240. package/src/operations/structured-buffer.ts +0 -181
  241. package/src/operations/teaching-hints.ts +0 -52
  242. package/src/parser/diff.ts +0 -320
  243. package/src/parser/lang.ts +0 -71
  244. package/src/parser/markdown.ts +0 -197
  245. package/src/parser/outline.ts +0 -316
  246. package/src/parser/runtime.ts +0 -71
  247. package/src/parser/types.ts +0 -75
  248. package/src/policy/evaluate.ts +0 -178
  249. package/src/policy/graftignore.ts +0 -6
  250. package/src/policy/types.ts +0 -87
  251. package/src/ports/codec.ts +0 -13
  252. package/src/ports/filesystem.ts +0 -18
  253. package/src/ports/git.ts +0 -16
  254. package/src/ports/guards.ts +0 -68
  255. package/src/ports/paths.ts +0 -22
  256. package/src/ports/process-runner.ts +0 -22
  257. package/src/release/security-gate.ts +0 -102
  258. package/src/session/tracker.ts +0 -145
  259. package/src/session/types.ts +0 -23
  260. package/src/version.ts +0 -3
  261. package/src/warp/ast-emitter.ts +0 -133
  262. package/src/warp/ast-import-resolver.ts +0 -333
  263. package/src/warp/commit-meta.ts +0 -19
  264. package/src/warp/context.ts +0 -48
  265. package/src/warp/dead-symbols.ts +0 -174
  266. package/src/warp/drift-sentinel.ts +0 -101
  267. package/src/warp/index-head.ts +0 -348
  268. package/src/warp/observers.ts +0 -101
  269. package/src/warp/open.ts +0 -67
  270. package/src/warp/outline-diff-trailer.ts +0 -100
  271. package/src/warp/plumbing.d.ts +0 -24
  272. package/src/warp/refactor-difficulty.ts +0 -257
  273. package/src/warp/references.ts +0 -77
  274. package/src/warp/stale-docs.ts +0 -210
  275. package/src/warp/structural-drift-detection.ts +0 -97
  276. package/src/warp/structural-queries.ts +0 -258
  277. package/src/warp/symbol-timeline.ts +0 -209
  278. package/src/warp/traverse-hydrate.ts +0 -61
  279. package/src/warp/warp-reference-count.ts +0 -47
  280. package/src/warp/warp-structural-blame.ts +0 -65
  281. package/src/warp/warp-structural-churn.ts +0 -309
  282. package/src/warp/warp-structural-log.ts +0 -149
  283. package/src/warp/writer-id.ts +0 -30
@@ -1,1621 +0,0 @@
1
- import * as crypto from "node:crypto";
2
- import * as path from "node:path";
3
- import { z } from "zod";
4
- import {
5
- actorSchema,
6
- attributionSummarySchema,
7
- attributionConfidenceSchema,
8
- causalFootprintSchema,
9
- causalRegionSchema,
10
- readEventSchema,
11
- stageEventSchema,
12
- transitionEventSchema,
13
- evidenceSchema,
14
- localHistoryContinuityOperationSchema,
15
- type AttributionSummary,
16
- type AttributionConfidence,
17
- type Actor,
18
- type CausalEvent,
19
- type CausalFootprint,
20
- type Evidence,
21
- type LocalHistoryContinuityOperation,
22
- type SourceLayer,
23
- } from "../contracts/causal-ontology.js";
24
- import type { JsonCodec } from "../ports/codec.js";
25
- import type { FileSystem } from "../ports/filesystem.js";
26
- import type { RepoObservation } from "./repo-state.js";
27
- import {
28
- type PersistedLocalHistoryGraphContext,
29
- writeCausalEventToGraph,
30
- writeContinuityRecordToGraph,
31
- } from "./persisted-local-history-graph.js";
32
- import {
33
- deriveRepoConcurrencySummary,
34
- type RepoConcurrencySummary,
35
- } from "./repo-concurrency.js";
36
- import {
37
- buildPersistedLocalActivityWindow,
38
- emptyPersistedLocalActivityWindow,
39
- emptyPersistedLocalHistorySummary,
40
- findRecord,
41
- summarizeContinuityState,
42
- toRepoConcurrencyWorktreeHistory,
43
- } from "./persisted-local-history-views.js";
44
- import {
45
- buildAttachDeclarationEvidence,
46
- buildGitTransitionEvidence,
47
- buildUnknownAttributionForActorId,
48
- classifyContinuityOperation,
49
- createContinuityRecord,
50
- createReadEvent,
51
- createStageEvent,
52
- createTransitionEvent,
53
- deriveContinuityAttribution,
54
- } from "./persisted-local-history-policy.js";
55
- import type { GitTransitionHookEvent } from "./runtime-workspace-overlay.js";
56
- import type { RuntimeCausalContext } from "./runtime-causal-context.js";
57
- import type { RuntimeStagedTargetFullFile } from "./runtime-staged-target.js";
58
- import type { WorkspaceStatus } from "./workspace-router.js";
59
-
60
- const PERSISTED_LOCAL_HISTORY_PRESERVES = Object.freeze([
61
- "continuity_operations",
62
- "read_events",
63
- "stage_events",
64
- "transition_events",
65
- "runtime_context_ids",
66
- "workspace_overlay_snapshots",
67
- ] as const);
68
-
69
- const PERSISTED_LOCAL_HISTORY_EXCLUDES = Object.freeze([
70
- "raw_chat_transcripts",
71
- "queue_bookkeeping",
72
- "canonical_provenance",
73
- "canonical_structural_truth",
74
- ] as const);
75
-
76
- const MAX_CONTINUITY_RECORDS = 128;
77
- const MAX_READ_EVENTS = 256;
78
- const MAX_STAGE_EVENTS = 128;
79
- const MAX_TRANSITION_EVENTS = 128;
80
-
81
- const continuityRecordSchema = z.object({
82
- recordId: z.string().min(1),
83
- continuityKey: z.string().min(1),
84
- operation: localHistoryContinuityOperationSchema,
85
- repoId: z.string().min(1),
86
- worktreeId: z.string().min(1),
87
- transportSessionId: z.string().min(1),
88
- workspaceSliceId: z.string().min(1),
89
- causalSessionId: z.string().min(1),
90
- strandId: z.string().min(1),
91
- checkoutEpochId: z.string().min(1),
92
- workspaceOverlayId: z.string().min(1).nullable(),
93
- occurredAt: z.string().min(1),
94
- continuedFromRecordId: z.string().min(1).nullable(),
95
- continuedFromCausalSessionId: z.string().min(1).nullable(),
96
- continuedFromStrandId: z.string().min(1).nullable(),
97
- continuityConfidence: attributionConfidenceSchema,
98
- continuityEvidence: z.array(evidenceSchema),
99
- attribution: attributionSummarySchema,
100
- }).strict();
101
-
102
- export type ContinuityRecord = z.infer<typeof continuityRecordSchema>;
103
-
104
- const continuityStateSchema = z.object({
105
- continuityKey: z.string().min(1),
106
- repoId: z.string().min(1),
107
- worktreeId: z.string().min(1),
108
- activeRecordId: z.string().min(1).nullable(),
109
- records: z.array(continuityRecordSchema),
110
- readEvents: z.array(readEventSchema),
111
- stageEvents: z.array(stageEventSchema),
112
- transitionEvents: z.array(transitionEventSchema),
113
- }).strict();
114
-
115
- export type ContinuityState = z.infer<typeof continuityStateSchema>;
116
-
117
- export interface PersistedLocalHistoryContext {
118
- readonly repoId: string;
119
- readonly worktreeId: string;
120
- readonly transportSessionId: string;
121
- readonly workspaceSliceId: string;
122
- readonly causalSessionId: string;
123
- readonly strandId: string;
124
- readonly checkoutEpochId: string;
125
- readonly workspaceOverlayId: string | null;
126
- readonly observedAt: string;
127
- readonly warpWriterId: string;
128
- readonly transitionKind: RepoObservation["lastTransition"] extends infer T
129
- ? T extends { kind: infer K } | null
130
- ? K | null
131
- : null
132
- : null;
133
- readonly transitionReflogSubject: string | null;
134
- readonly hookTransitionName: GitTransitionHookEvent["hookName"] | null;
135
- readonly hookTransitionArgs: readonly string[] | null;
136
- readonly hookTransitionObservedAt: string | null;
137
- }
138
-
139
- export interface PersistedLocalHistoryAttachDeclaration {
140
- readonly actorKind: "human" | "agent";
141
- readonly actorId?: string | undefined;
142
- readonly fromActorId?: string | undefined;
143
- readonly note?: string | undefined;
144
- }
145
-
146
- export interface PersistedLocalHistorySharedAttachSource {
147
- readonly sourceSessionId: string;
148
- readonly causalSessionId: string;
149
- readonly strandId: string;
150
- }
151
-
152
- export interface PersistedLocalHistorySummaryNone {
153
- readonly availability: "none";
154
- readonly persistence: "persisted_local_history";
155
- readonly historyPath: null;
156
- readonly totalContinuityRecords: 0;
157
- readonly active: false;
158
- readonly lastOperation: null;
159
- readonly lastObservedAt: null;
160
- readonly continuityKey: null;
161
- readonly causalSessionId: null;
162
- readonly strandId: null;
163
- readonly checkoutEpochId: null;
164
- readonly continuedFromCausalSessionId: null;
165
- readonly continuityConfidence: AttributionConfidence;
166
- readonly continuityEvidence: readonly Evidence[];
167
- readonly attribution: AttributionSummary;
168
- readonly latestReadEvent: null;
169
- readonly latestStageEvent: null;
170
- readonly latestTransitionEvent: null;
171
- readonly preserves: readonly string[];
172
- readonly excludes: readonly string[];
173
- readonly nextAction: "bind_workspace_to_begin_local_history";
174
- }
175
-
176
- export interface PersistedLocalHistorySummaryPresent {
177
- readonly availability: "present";
178
- readonly persistence: "persisted_local_history";
179
- readonly historyPath: string | null;
180
- readonly totalContinuityRecords: number;
181
- readonly active: boolean;
182
- readonly lastOperation: LocalHistoryContinuityOperation;
183
- readonly lastObservedAt: string;
184
- readonly continuityKey: string;
185
- readonly causalSessionId: string;
186
- readonly strandId: string;
187
- readonly checkoutEpochId: string;
188
- readonly continuedFromCausalSessionId: string | null;
189
- readonly continuityConfidence: AttributionConfidence;
190
- readonly continuityEvidence: readonly Evidence[];
191
- readonly attribution: AttributionSummary;
192
- readonly latestReadEvent: Extract<CausalEvent, { eventKind: "read" }> | null;
193
- readonly latestStageEvent: Extract<CausalEvent, { eventKind: "stage" }> | null;
194
- readonly latestTransitionEvent: Extract<CausalEvent, { eventKind: "transition" }> | null;
195
- readonly preserves: readonly string[];
196
- readonly excludes: readonly string[];
197
- readonly nextAction:
198
- | "continue_active_causal_workspace"
199
- | "review_transition_boundary_before_continuing"
200
- | "inspect_or_resume_local_history";
201
- }
202
-
203
- export type PersistedLocalHistorySummary =
204
- | PersistedLocalHistorySummaryNone
205
- | PersistedLocalHistorySummaryPresent;
206
-
207
- export interface PersistedLocalActivityContinuityItem {
208
- readonly itemKind: "continuity";
209
- readonly recordId: string;
210
- readonly operation: LocalHistoryContinuityOperation;
211
- readonly occurredAt: string;
212
- readonly causalSessionId: string;
213
- readonly strandId: string;
214
- readonly attribution: AttributionSummary;
215
- readonly continuedFromCausalSessionId: string | null;
216
- readonly continuedFromStrandId: string | null;
217
- }
218
-
219
- export type PersistedLocalActivityItem =
220
- | PersistedLocalActivityContinuityItem
221
- | Extract<CausalEvent, { eventKind: "read" | "stage" | "transition" }>;
222
-
223
- export interface PersistedLocalActivityWindow {
224
- readonly historyPath: string | null;
225
- readonly limit: number;
226
- readonly totalMatchingItems: number;
227
- readonly truncated: boolean;
228
- readonly items: readonly PersistedLocalActivityItem[];
229
- }
230
-
231
- export type { RepoConcurrencySummary } from "./repo-concurrency.js";
232
-
233
- export class PersistedLocalHistoryAttachUnavailableError extends Error {
234
- readonly code = "NO_ATTACHABLE_HISTORY";
235
-
236
- constructor(message = "No attachable persisted local-history lineage is available for the current footing.") {
237
- super(message);
238
- this.name = "PersistedLocalHistoryAttachUnavailableError";
239
- }
240
- }
241
-
242
- function stableId(prefix: string, input: string): string {
243
- return `${prefix}:${crypto.createHash("sha256").update(input).digest("hex").slice(0, 16)}`;
244
- }
245
-
246
- export function buildContinuityKey(repoId: string, worktreeId: string): string {
247
- return stableId("continuity", `${repoId}:${worktreeId}`);
248
- }
249
-
250
- function buildEvidenceId(
251
- operation: LocalHistoryContinuityOperation,
252
- kind: Evidence["evidenceKind"],
253
- context: PersistedLocalHistoryContext,
254
- index: number,
255
- ): string {
256
- return stableId(
257
- "evidence",
258
- JSON.stringify({
259
- operation,
260
- kind,
261
- index,
262
- occurredAt: context.observedAt,
263
- transportSessionId: context.transportSessionId,
264
- causalSessionId: context.causalSessionId,
265
- strandId: context.strandId,
266
- }),
267
- );
268
- }
269
-
270
- function createEmptyState(continuityKey: string, repoId: string, worktreeId: string): ContinuityState {
271
- return {
272
- continuityKey,
273
- repoId,
274
- worktreeId,
275
- activeRecordId: null,
276
- records: [],
277
- readEvents: [],
278
- stageEvents: [],
279
- transitionEvents: [],
280
- };
281
- }
282
-
283
- function appendRecord(state: ContinuityState, record: ContinuityRecord, activeAfter: boolean): void {
284
- if (state.records.at(-1)?.recordId === record.recordId) {
285
- state.activeRecordId = activeAfter ? record.recordId : null;
286
- return;
287
- }
288
- state.records = [...state.records, record].slice(-MAX_CONTINUITY_RECORDS);
289
- state.activeRecordId = activeAfter ? record.recordId : null;
290
- }
291
-
292
- function keepRecent<T>(items: readonly T[], limit: number): T[] {
293
- return items.slice(-limit);
294
- }
295
-
296
- function compareByOccurredAtThenId(
297
- left: { readonly occurredAt: string; readonly id: string },
298
- right: { readonly occurredAt: string; readonly id: string },
299
- ): number {
300
- const byTime = left.occurredAt.localeCompare(right.occurredAt);
301
- if (byTime !== 0) {
302
- return byTime;
303
- }
304
- return left.id.localeCompare(right.id);
305
- }
306
-
307
- function asString(value: unknown): string | undefined {
308
- return typeof value === "string" && value.length > 0 ? value : undefined;
309
- }
310
-
311
- function asNullableString(value: unknown): string | null | undefined {
312
- if (value === null) {
313
- return null;
314
- }
315
- return asString(value);
316
- }
317
-
318
- function asStringArray(value: unknown): string[] | undefined {
319
- if (!Array.isArray(value)) {
320
- return undefined;
321
- }
322
- const strings: string[] = [];
323
- for (const entry of value) {
324
- if (typeof entry !== "string" || entry.length === 0) {
325
- return undefined;
326
- }
327
- strings.push(entry);
328
- }
329
- return strings;
330
- }
331
-
332
- function asUnknownRecord(value: unknown): Record<string, unknown> | undefined {
333
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
334
- return undefined;
335
- }
336
- return value as Record<string, unknown>;
337
- }
338
-
339
- interface ObservedLocalHistoryNode {
340
- readonly id: string;
341
- readonly props: Record<string, unknown>;
342
- }
343
-
344
- interface ObservedLocalHistoryEdge {
345
- readonly from: string;
346
- readonly to: string;
347
- readonly label: string;
348
- }
349
-
350
- interface ObservedLocalHistoryGraphSnapshot {
351
- readonly nodesById: ReadonlyMap<string, ObservedLocalHistoryNode>;
352
- readonly outgoingEdges: ReadonlyMap<string, readonly ObservedLocalHistoryEdge[]>;
353
- }
354
-
355
- const LOCAL_HISTORY_GRAPH_MATCH = ["lh:*"] as const;
356
- const LOCAL_HISTORY_GRAPH_EXPOSE = [
357
- "actorId",
358
- "actorKind",
359
- "authority",
360
- "authorityScope",
361
- "attributionBasis",
362
- "attributionConfidence",
363
- "capturedAt",
364
- "causalSessionId",
365
- "checkoutEpochId",
366
- "continuityKey",
367
- "continuityOperation",
368
- "continuedFromCausalSessionId",
369
- "continuedFromEventId",
370
- "continuedFromStrandId",
371
- "details",
372
- "displayName",
373
- "entityKind",
374
- "eventId",
375
- "eventKind",
376
- "evidenceId",
377
- "evidenceKind",
378
- "fromRef",
379
- "occurredAt",
380
- "path",
381
- "paths",
382
- "phase",
383
- "projection",
384
- "reason",
385
- "repoId",
386
- "selectionKind",
387
- "semanticKind",
388
- "source",
389
- "sourceLayer",
390
- "startLine",
391
- "endLine",
392
- "strandId",
393
- "strength",
394
- "summary",
395
- "surface",
396
- "symbols",
397
- "targetId",
398
- "toRef",
399
- "transitionKind",
400
- "transportSessionId",
401
- "workspaceOverlayId",
402
- "workspaceSliceId",
403
- "worktreeId",
404
- "createdCheckoutEpochId",
405
- ] as const;
406
-
407
- function sortEvidence(evidence: readonly Evidence[]): Evidence[] {
408
- return [...evidence].sort((left, right) => {
409
- const byCapturedAt = left.capturedAt.localeCompare(right.capturedAt);
410
- if (byCapturedAt !== 0) {
411
- return byCapturedAt;
412
- }
413
- return left.evidenceId.localeCompare(right.evidenceId);
414
- });
415
- }
416
-
417
- function sortEvents<T extends { readonly occurredAt: string; readonly eventId: string }>(events: readonly T[]): T[] {
418
- return [...events].sort((left, right) => compareByOccurredAtThenId(
419
- { occurredAt: left.occurredAt, id: left.eventId },
420
- { occurredAt: right.occurredAt, id: right.eventId },
421
- ));
422
- }
423
-
424
- function sortContinuityRecords(records: readonly ContinuityRecord[]): ContinuityRecord[] {
425
- return [...records].sort((left, right) => {
426
- const byTime = left.occurredAt.localeCompare(right.occurredAt);
427
- if (byTime !== 0) {
428
- return byTime;
429
- }
430
- if (left.continuedFromRecordId === right.recordId) {
431
- return 1;
432
- }
433
- if (right.continuedFromRecordId === left.recordId) {
434
- return -1;
435
- }
436
- if (left.operation !== right.operation) {
437
- if (left.operation === "park") {
438
- return -1;
439
- }
440
- if (right.operation === "park") {
441
- return 1;
442
- }
443
- }
444
- return left.recordId.localeCompare(right.recordId);
445
- });
446
- }
447
-
448
- function outgoingTargets(
449
- snapshot: ObservedLocalHistoryGraphSnapshot,
450
- nodeId: string,
451
- label: string,
452
- ): string[] {
453
- return (snapshot.outgoingEdges.get(nodeId) ?? [])
454
- .filter((edge) => edge.label === label)
455
- .map((edge) => edge.to);
456
- }
457
-
458
- async function loadObservedLocalHistoryGraph(
459
- graph: PersistedLocalHistoryGraphContext | null | undefined,
460
- ): Promise<ObservedLocalHistoryGraphSnapshot | null> {
461
- if (graph === null || graph === undefined) {
462
- return null;
463
- }
464
-
465
- const observer = await graph.warp.app.observer({
466
- match: [...LOCAL_HISTORY_GRAPH_MATCH],
467
- expose: [...LOCAL_HISTORY_GRAPH_EXPOSE],
468
- });
469
- const nodeIds = await observer.getNodes();
470
- if (nodeIds.length === 0) {
471
- return null;
472
- }
473
-
474
- const nodesById = new Map<string, ObservedLocalHistoryNode>();
475
- for (const nodeId of nodeIds) {
476
- const props = await observer.getNodeProps(nodeId);
477
- const record = asUnknownRecord(props);
478
- if (record !== undefined) {
479
- nodesById.set(nodeId, { id: nodeId, props: record });
480
- }
481
- }
482
-
483
- if (nodesById.size === 0) {
484
- return null;
485
- }
486
-
487
- const outgoing = new Map<string, ObservedLocalHistoryEdge[]>();
488
- for (const edge of await observer.getEdges()) {
489
- if (!nodesById.has(edge.from) || !nodesById.has(edge.to)) {
490
- continue;
491
- }
492
- const bucket = outgoing.get(edge.from) ?? [];
493
- bucket.push(edge);
494
- outgoing.set(edge.from, bucket);
495
- }
496
-
497
- return {
498
- nodesById,
499
- outgoingEdges: outgoing,
500
- };
501
- }
502
-
503
- function readEvidenceNode(
504
- snapshot: ObservedLocalHistoryGraphSnapshot,
505
- nodeId: string,
506
- ): Evidence | null {
507
- const node = snapshot.nodesById.get(nodeId);
508
- if (node === undefined || asString(node.props["entityKind"]) !== "evidence") {
509
- return null;
510
- }
511
- const parsed = evidenceSchema.safeParse({
512
- evidenceId: asString(node.props["evidenceId"]),
513
- evidenceKind: asString(node.props["evidenceKind"]),
514
- source: asString(node.props["source"]),
515
- capturedAt: asString(node.props["capturedAt"]),
516
- strength: asString(node.props["strength"]),
517
- details: asUnknownRecord(node.props["details"]) ?? {},
518
- });
519
- return parsed.success ? parsed.data : null;
520
- }
521
-
522
- function readActorNode(
523
- snapshot: ObservedLocalHistoryGraphSnapshot,
524
- nodeId: string,
525
- ): Actor | null {
526
- const node = snapshot.nodesById.get(nodeId);
527
- if (node === undefined || asString(node.props["entityKind"]) !== "actor") {
528
- return null;
529
- }
530
- const parsed = actorSchema.safeParse({
531
- actorId: asString(node.props["actorId"]),
532
- actorKind: asString(node.props["actorKind"]),
533
- displayName: asString(node.props["displayName"]),
534
- source: asString(node.props["source"]),
535
- authorityScope: asString(node.props["authorityScope"]),
536
- });
537
- return parsed.success ? parsed.data : null;
538
- }
539
-
540
- function readFootprint(
541
- snapshot: ObservedLocalHistoryGraphSnapshot,
542
- eventNodeId: string,
543
- ): CausalFootprint {
544
- const footprintNodeId = outgoingTargets(snapshot, eventNodeId, "has_footprint")[0];
545
- const footprintNode = footprintNodeId === undefined ? undefined : snapshot.nodesById.get(footprintNodeId);
546
- const regions = (footprintNodeId === undefined ? [] : outgoingTargets(snapshot, footprintNodeId, "has_region"))
547
- .map((regionNodeId) => {
548
- const regionNode = snapshot.nodesById.get(regionNodeId);
549
- if (regionNode === undefined) {
550
- return null;
551
- }
552
- const parsed = causalRegionSchema.safeParse({
553
- path: asString(regionNode.props["path"]),
554
- startLine: regionNode.props["startLine"],
555
- endLine: regionNode.props["endLine"],
556
- });
557
- return parsed.success ? parsed.data : null;
558
- })
559
- .filter((region): region is z.infer<typeof causalRegionSchema> => region !== null);
560
-
561
- const parsed = causalFootprintSchema.safeParse({
562
- paths: footprintNode === undefined ? [] : asStringArray(footprintNode.props["paths"]) ?? [],
563
- symbols: footprintNode === undefined ? [] : asStringArray(footprintNode.props["symbols"]) ?? [],
564
- regions,
565
- });
566
- if (parsed.success) {
567
- return parsed.data;
568
- }
569
- return {
570
- paths: [],
571
- symbols: [],
572
- regions: [],
573
- };
574
- }
575
-
576
- function readSupportedEvidence(
577
- snapshot: ObservedLocalHistoryGraphSnapshot,
578
- eventNodeId: string,
579
- ): Evidence[] {
580
- return sortEvidence(
581
- outgoingTargets(snapshot, eventNodeId, "supported_by")
582
- .map((nodeId) => readEvidenceNode(snapshot, nodeId))
583
- .filter((evidence): evidence is Evidence => evidence !== null),
584
- );
585
- }
586
-
587
- function readEventAttribution(
588
- snapshot: ObservedLocalHistoryGraphSnapshot,
589
- eventNode: ObservedLocalHistoryNode,
590
- input: {
591
- readonly continuityOperation?: LocalHistoryContinuityOperation | undefined;
592
- readonly evidence: readonly Evidence[];
593
- },
594
- ): AttributionSummary {
595
- if (input.continuityOperation !== undefined) {
596
- return deriveContinuityAttribution(input.continuityOperation, input.evidence);
597
- }
598
-
599
- const actorNodeId = outgoingTargets(snapshot, eventNode.id, "attributed_to")[0];
600
- const actor = actorNodeId === undefined ? null : readActorNode(snapshot, actorNodeId);
601
- const parsed = attributionSummarySchema.safeParse({
602
- actor: actor ?? buildUnknownAttributionForActorId(asString(eventNode.props["actorId"])).actor,
603
- confidence: asString(eventNode.props["attributionConfidence"]) ?? "unknown",
604
- basis: asString(eventNode.props["attributionBasis"]) ?? "unknown_fallback",
605
- evidence: [...input.evidence],
606
- });
607
- return parsed.success ? parsed.data : buildUnknownAttributionForActorId(asString(eventNode.props["actorId"]));
608
- }
609
-
610
- function readContinuityRecordFromGraph(
611
- snapshot: ObservedLocalHistoryGraphSnapshot,
612
- node: ObservedLocalHistoryNode,
613
- ): ContinuityRecord | null {
614
- if (
615
- asString(node.props["entityKind"]) !== "local_history_event" ||
616
- asString(node.props["eventKind"]) !== "continuity"
617
- ) {
618
- return null;
619
- }
620
- const operation = localHistoryContinuityOperationSchema.safeParse(asString(node.props["continuityOperation"]));
621
- const repoId = asString(node.props["repoId"]);
622
- const worktreeId = asString(node.props["worktreeId"]);
623
- const recordId = asString(node.props["eventId"]);
624
- const occurredAt = asString(node.props["occurredAt"]);
625
- const continuityEvidence = readSupportedEvidence(snapshot, node.id);
626
- if (!operation.success || repoId === undefined || worktreeId === undefined || recordId === undefined || occurredAt === undefined) {
627
- return null;
628
- }
629
- const parsed = continuityRecordSchema.safeParse({
630
- recordId,
631
- continuityKey: asString(node.props["continuityKey"]) ?? buildContinuityKey(repoId, worktreeId),
632
- operation: operation.data,
633
- repoId,
634
- worktreeId,
635
- transportSessionId: asString(node.props["transportSessionId"]),
636
- workspaceSliceId: asString(node.props["workspaceSliceId"]),
637
- causalSessionId: asString(node.props["causalSessionId"]),
638
- strandId: asString(node.props["strandId"]),
639
- checkoutEpochId: asString(node.props["checkoutEpochId"]),
640
- workspaceOverlayId: asNullableString(node.props["workspaceOverlayId"]) ?? null,
641
- occurredAt,
642
- continuedFromRecordId: asNullableString(node.props["continuedFromEventId"]) ?? null,
643
- continuedFromCausalSessionId: asNullableString(node.props["continuedFromCausalSessionId"]) ?? null,
644
- continuedFromStrandId: asNullableString(node.props["continuedFromStrandId"]) ?? null,
645
- continuityConfidence: asString(node.props["attributionConfidence"]) ?? "unknown",
646
- continuityEvidence,
647
- attribution: readEventAttribution(snapshot, node, {
648
- continuityOperation: operation.data,
649
- evidence: continuityEvidence,
650
- }),
651
- });
652
- return parsed.success ? parsed.data : null;
653
- }
654
-
655
- function readReadEventFromGraph(
656
- snapshot: ObservedLocalHistoryGraphSnapshot,
657
- node: ObservedLocalHistoryNode,
658
- ): Extract<CausalEvent, { eventKind: "read" }> | null {
659
- if (asString(node.props["entityKind"]) !== "local_history_event" || asString(node.props["eventKind"]) !== "read") {
660
- return null;
661
- }
662
- const evidence = readSupportedEvidence(snapshot, node.id);
663
- const parsed = readEventSchema.safeParse({
664
- eventId: asString(node.props["eventId"]),
665
- eventKind: "read",
666
- repoId: asString(node.props["repoId"]),
667
- worktreeId: asString(node.props["worktreeId"]),
668
- checkoutEpochId: asString(node.props["checkoutEpochId"]),
669
- workspaceOverlayId: asNullableString(node.props["workspaceOverlayId"]) ?? null,
670
- transportSessionId: asString(node.props["transportSessionId"]),
671
- workspaceSliceId: asString(node.props["workspaceSliceId"]),
672
- causalSessionId: asString(node.props["causalSessionId"]),
673
- strandId: asString(node.props["strandId"]),
674
- actorId: asString(node.props["actorId"]) ?? "unknown",
675
- confidence: asString(node.props["attributionConfidence"]) ?? "unknown",
676
- evidenceIds: evidence.map((item) => item.evidenceId),
677
- attribution: readEventAttribution(snapshot, node, { evidence }),
678
- footprint: readFootprint(snapshot, node.id),
679
- occurredAt: asString(node.props["occurredAt"]),
680
- payload: {
681
- surface: asString(node.props["surface"]),
682
- projection: asString(node.props["projection"]),
683
- sourceLayer: asString(node.props["sourceLayer"]),
684
- reason: asString(node.props["reason"]),
685
- },
686
- });
687
- return parsed.success ? parsed.data : null;
688
- }
689
-
690
- function readStageEventFromGraph(
691
- snapshot: ObservedLocalHistoryGraphSnapshot,
692
- node: ObservedLocalHistoryNode,
693
- ): Extract<CausalEvent, { eventKind: "stage" }> | null {
694
- if (asString(node.props["entityKind"]) !== "local_history_event" || asString(node.props["eventKind"]) !== "stage") {
695
- return null;
696
- }
697
- const evidence = readSupportedEvidence(snapshot, node.id);
698
- const footprint = readFootprint(snapshot, node.id);
699
- const parsed = stageEventSchema.safeParse({
700
- eventId: asString(node.props["eventId"]),
701
- eventKind: "stage",
702
- repoId: asString(node.props["repoId"]),
703
- worktreeId: asString(node.props["worktreeId"]),
704
- checkoutEpochId: asString(node.props["checkoutEpochId"]),
705
- workspaceOverlayId: asNullableString(node.props["workspaceOverlayId"]) ?? null,
706
- transportSessionId: asString(node.props["transportSessionId"]),
707
- workspaceSliceId: asString(node.props["workspaceSliceId"]),
708
- causalSessionId: asString(node.props["causalSessionId"]),
709
- strandId: asString(node.props["strandId"]),
710
- actorId: asString(node.props["actorId"]) ?? "unknown",
711
- confidence: asString(node.props["attributionConfidence"]) ?? "unknown",
712
- evidenceIds: evidence.map((item) => item.evidenceId),
713
- attribution: readEventAttribution(snapshot, node, { evidence }),
714
- footprint,
715
- occurredAt: asString(node.props["occurredAt"]),
716
- payload: {
717
- targetId: asString(node.props["targetId"]),
718
- footprint,
719
- selectionKind: asString(node.props["selectionKind"]),
720
- },
721
- });
722
- return parsed.success ? parsed.data : null;
723
- }
724
-
725
- function readTransitionEventFromGraph(
726
- snapshot: ObservedLocalHistoryGraphSnapshot,
727
- node: ObservedLocalHistoryNode,
728
- ): Extract<CausalEvent, { eventKind: "transition" }> | null {
729
- if (
730
- asString(node.props["entityKind"]) !== "local_history_event" ||
731
- asString(node.props["eventKind"]) !== "transition"
732
- ) {
733
- return null;
734
- }
735
- const evidence = readSupportedEvidence(snapshot, node.id);
736
- const parsed = transitionEventSchema.safeParse({
737
- eventId: asString(node.props["eventId"]),
738
- eventKind: "transition",
739
- repoId: asString(node.props["repoId"]),
740
- worktreeId: asString(node.props["worktreeId"]),
741
- checkoutEpochId: asString(node.props["checkoutEpochId"]),
742
- workspaceOverlayId: asNullableString(node.props["workspaceOverlayId"]) ?? null,
743
- transportSessionId: asString(node.props["transportSessionId"]),
744
- workspaceSliceId: asString(node.props["workspaceSliceId"]),
745
- causalSessionId: asString(node.props["causalSessionId"]),
746
- strandId: asString(node.props["strandId"]),
747
- actorId: asString(node.props["actorId"]) ?? "unknown",
748
- confidence: asString(node.props["attributionConfidence"]) ?? "unknown",
749
- evidenceIds: evidence.map((item) => item.evidenceId),
750
- attribution: readEventAttribution(snapshot, node, { evidence }),
751
- footprint: readFootprint(snapshot, node.id),
752
- occurredAt: asString(node.props["occurredAt"]),
753
- payload: {
754
- semanticKind: asString(node.props["semanticKind"]),
755
- authority: asString(node.props["authority"]),
756
- phase: asNullableString(node.props["phase"]) ?? null,
757
- summary: asString(node.props["summary"]),
758
- transitionKind: asNullableString(node.props["transitionKind"]) ?? null,
759
- fromRef: asNullableString(node.props["fromRef"]) ?? null,
760
- toRef: asNullableString(node.props["toRef"]) ?? null,
761
- createdCheckoutEpochId: asNullableString(node.props["createdCheckoutEpochId"]) ?? null,
762
- },
763
- });
764
- return parsed.success ? parsed.data : null;
765
- }
766
-
767
- function loadStateFromGraphSnapshot(
768
- snapshot: ObservedLocalHistoryGraphSnapshot,
769
- repoId: string,
770
- worktreeId: string,
771
- ): ContinuityState | null {
772
- const continuityRecords = sortContinuityRecords(
773
- [...snapshot.nodesById.values()]
774
- .map((node) => readContinuityRecordFromGraph(snapshot, node))
775
- .filter((record): record is ContinuityRecord =>
776
- record !== null && record.repoId === repoId && record.worktreeId === worktreeId
777
- ),
778
- );
779
-
780
- const readEvents = sortEvents(
781
- [...snapshot.nodesById.values()]
782
- .map((node) => readReadEventFromGraph(snapshot, node))
783
- .filter((event): event is Extract<CausalEvent, { eventKind: "read" }> =>
784
- event !== null && event.repoId === repoId && event.worktreeId === worktreeId
785
- ),
786
- );
787
- const stageEvents = sortEvents(
788
- [...snapshot.nodesById.values()]
789
- .map((node) => readStageEventFromGraph(snapshot, node))
790
- .filter((event): event is Extract<CausalEvent, { eventKind: "stage" }> =>
791
- event !== null && event.repoId === repoId && event.worktreeId === worktreeId
792
- ),
793
- );
794
- const transitionEvents = sortEvents(
795
- [...snapshot.nodesById.values()]
796
- .map((node) => readTransitionEventFromGraph(snapshot, node))
797
- .filter((event): event is Extract<CausalEvent, { eventKind: "transition" }> =>
798
- event !== null && event.repoId === repoId && event.worktreeId === worktreeId
799
- ),
800
- );
801
-
802
- if (
803
- continuityRecords.length === 0 &&
804
- readEvents.length === 0 &&
805
- stageEvents.length === 0 &&
806
- transitionEvents.length === 0
807
- ) {
808
- return null;
809
- }
810
-
811
- const lastRecord = continuityRecords.at(-1) ?? null;
812
- return {
813
- continuityKey: lastRecord?.continuityKey ?? buildContinuityKey(repoId, worktreeId),
814
- repoId,
815
- worktreeId,
816
- activeRecordId: lastRecord === null || lastRecord.operation === "park" ? null : lastRecord.recordId,
817
- records: continuityRecords,
818
- readEvents,
819
- stageEvents,
820
- transitionEvents,
821
- };
822
- }
823
-
824
- async function loadStateFromGraph(
825
- graph: PersistedLocalHistoryGraphContext | null | undefined,
826
- repoId: string,
827
- worktreeId: string,
828
- ): Promise<ContinuityState | null> {
829
- const snapshot = await loadObservedLocalHistoryGraph(graph);
830
- if (snapshot === null) {
831
- return null;
832
- }
833
- return loadStateFromGraphSnapshot(snapshot, repoId, worktreeId);
834
- }
835
-
836
- async function loadStatesForRepoFromGraph(
837
- graph: PersistedLocalHistoryGraphContext | null | undefined,
838
- repoId: string,
839
- ): Promise<ContinuityState[] | null> {
840
- const snapshot = await loadObservedLocalHistoryGraph(graph);
841
- if (snapshot === null) {
842
- return null;
843
- }
844
-
845
- const worktreeIds = new Set(
846
- [...snapshot.nodesById.values()]
847
- .filter((node) => asString(node.props["repoId"]) === repoId)
848
- .map((node) => asString(node.props["worktreeId"]))
849
- .filter((value): value is string => value !== undefined),
850
- );
851
-
852
- const states = [...worktreeIds]
853
- .map((worktreeId) => loadStateFromGraphSnapshot(snapshot, repoId, worktreeId))
854
- .filter((state): state is ContinuityState => state !== null);
855
-
856
- return states.length > 0 ? states : null;
857
- }
858
-
859
- interface LegacyPersistedLocalHistoryArtifact {
860
- readonly filePath: string;
861
- readonly state: ContinuityState;
862
- }
863
-
864
- export interface PersistedLocalHistoryMigrationResult {
865
- graftDir: string;
866
- discoveredArtifacts: number;
867
- migratedArtifacts: number;
868
- malformedArtifacts: number;
869
- importedContinuityRecords: number;
870
- importedReadEvents: number;
871
- importedStageEvents: number;
872
- importedTransitionEvents: number;
873
- skippedContinuityRecords: number;
874
- skippedReadEvents: number;
875
- skippedStageEvents: number;
876
- skippedTransitionEvents: number;
877
- }
878
-
879
- async function loadLegacyPersistedLocalHistoryArtifacts(input: {
880
- readonly fs: FileSystem;
881
- readonly codec: JsonCodec;
882
- readonly graftDir: string;
883
- }): Promise<{
884
- readonly artifacts: readonly LegacyPersistedLocalHistoryArtifact[];
885
- readonly malformedArtifacts: number;
886
- }> {
887
- const historyDir = path.join(input.graftDir, "local-history");
888
- let filenames: string[];
889
- try {
890
- filenames = await input.fs.readdir(historyDir);
891
- } catch {
892
- return {
893
- artifacts: [],
894
- malformedArtifacts: 0,
895
- };
896
- }
897
-
898
- const artifacts: LegacyPersistedLocalHistoryArtifact[] = [];
899
- let malformedArtifacts = 0;
900
-
901
- for (const filename of filenames) {
902
- if (!filename.endsWith(".json")) {
903
- continue;
904
- }
905
- const filePath = path.join(historyDir, filename);
906
- try {
907
- const raw = await input.fs.readFile(filePath, "utf-8");
908
- const state = continuityStateSchema.parse(input.codec.decode(raw));
909
- artifacts.push({ filePath, state });
910
- } catch {
911
- malformedArtifacts += 1;
912
- }
913
- }
914
-
915
- return {
916
- artifacts,
917
- malformedArtifacts,
918
- };
919
- }
920
-
921
- function buildLegacyMigrationSequence(state: ContinuityState): {
922
- readonly kind: "continuity" | "read" | "stage" | "transition";
923
- readonly occurredAt: string;
924
- readonly sequence: number;
925
- readonly id: string;
926
- readonly record?: ContinuityRecord;
927
- readonly event?: Extract<CausalEvent, { eventKind: "read" | "stage" | "transition" }>;
928
- }[] {
929
- const sequence = [
930
- ...state.records.map((record, sequenceIndex) => ({
931
- kind: "continuity" as const,
932
- occurredAt: record.occurredAt,
933
- sequence: sequenceIndex,
934
- id: record.recordId,
935
- record,
936
- })),
937
- ...state.readEvents.map((event, sequenceIndex) => ({
938
- kind: "read" as const,
939
- occurredAt: event.occurredAt,
940
- sequence: sequenceIndex,
941
- id: event.eventId,
942
- event,
943
- })),
944
- ...state.stageEvents.map((event, sequenceIndex) => ({
945
- kind: "stage" as const,
946
- occurredAt: event.occurredAt,
947
- sequence: sequenceIndex,
948
- id: event.eventId,
949
- event,
950
- })),
951
- ...state.transitionEvents.map((event, sequenceIndex) => ({
952
- kind: "transition" as const,
953
- occurredAt: event.occurredAt,
954
- sequence: sequenceIndex,
955
- id: event.eventId,
956
- event,
957
- })),
958
- ];
959
-
960
- const priority = { continuity: 0, read: 1, stage: 2, transition: 3 } as const;
961
-
962
- return sequence.sort((left, right) => {
963
- const byTime = left.occurredAt.localeCompare(right.occurredAt);
964
- if (byTime !== 0) {
965
- return byTime;
966
- }
967
- const byPriority = priority[left.kind] - priority[right.kind];
968
- if (byPriority !== 0) {
969
- return byPriority;
970
- }
971
- const bySequence = left.sequence - right.sequence;
972
- if (bySequence !== 0) {
973
- return bySequence;
974
- }
975
- return left.id.localeCompare(right.id);
976
- });
977
- }
978
-
979
- async function hasLocalHistoryEventNode(
980
- graph: PersistedLocalHistoryGraphContext,
981
- eventId: string,
982
- ): Promise<boolean> {
983
- return graph.warp.app.core().hasNode(`lh:event:${eventId}`);
984
- }
985
-
986
- export async function migrateLegacyPersistedLocalHistoryToGraph(input: {
987
- readonly fs: FileSystem;
988
- readonly codec: JsonCodec;
989
- readonly graftDir: string;
990
- readonly graph: PersistedLocalHistoryGraphContext;
991
- }): Promise<PersistedLocalHistoryMigrationResult> {
992
- const { artifacts, malformedArtifacts } = await loadLegacyPersistedLocalHistoryArtifacts(input);
993
- const result: PersistedLocalHistoryMigrationResult = {
994
- graftDir: input.graftDir,
995
- discoveredArtifacts: artifacts.length,
996
- migratedArtifacts: 0,
997
- malformedArtifacts,
998
- importedContinuityRecords: 0,
999
- importedReadEvents: 0,
1000
- importedStageEvents: 0,
1001
- importedTransitionEvents: 0,
1002
- skippedContinuityRecords: 0,
1003
- skippedReadEvents: 0,
1004
- skippedStageEvents: 0,
1005
- skippedTransitionEvents: 0,
1006
- };
1007
-
1008
- for (const artifact of artifacts) {
1009
- const importedState = createEmptyState(
1010
- artifact.state.continuityKey,
1011
- artifact.state.repoId,
1012
- artifact.state.worktreeId,
1013
- );
1014
-
1015
- for (const item of buildLegacyMigrationSequence(artifact.state)) {
1016
- if (item.kind === "continuity" && item.record !== undefined) {
1017
- const previousEventId = findLatestEventIdForStrand(importedState, item.record.strandId);
1018
- if (await hasLocalHistoryEventNode(input.graph, item.record.recordId)) {
1019
- result.skippedContinuityRecords += 1;
1020
- } else {
1021
- await writeContinuityRecordToGraph(input.graph, {
1022
- record: item.record,
1023
- previousEventId,
1024
- });
1025
- result.importedContinuityRecords += 1;
1026
- }
1027
- importedState.records.push(item.record);
1028
- if (artifact.state.activeRecordId === item.record.recordId) {
1029
- importedState.activeRecordId = item.record.recordId;
1030
- }
1031
- continue;
1032
- }
1033
-
1034
- if (item.event === undefined) {
1035
- continue;
1036
- }
1037
-
1038
- const previousEventId = item.event.strandId === null
1039
- ? null
1040
- : findLatestEventIdForStrand(importedState, item.event.strandId);
1041
- if (await hasLocalHistoryEventNode(input.graph, item.event.eventId)) {
1042
- if (item.kind === "read") result.skippedReadEvents += 1;
1043
- if (item.kind === "stage") result.skippedStageEvents += 1;
1044
- if (item.kind === "transition") result.skippedTransitionEvents += 1;
1045
- } else {
1046
- await writeCausalEventToGraph(input.graph, {
1047
- event: item.event,
1048
- previousEventId,
1049
- });
1050
- if (item.kind === "read") result.importedReadEvents += 1;
1051
- if (item.kind === "stage") result.importedStageEvents += 1;
1052
- if (item.kind === "transition") result.importedTransitionEvents += 1;
1053
- }
1054
-
1055
- if (item.kind === "read") {
1056
- importedState.readEvents.push(item.event as Extract<CausalEvent, { eventKind: "read" }>);
1057
- } else if (item.kind === "stage") {
1058
- importedState.stageEvents.push(item.event as Extract<CausalEvent, { eventKind: "stage" }>);
1059
- } else {
1060
- importedState.transitionEvents.push(item.event as Extract<CausalEvent, { eventKind: "transition" }>);
1061
- }
1062
- }
1063
-
1064
- result.migratedArtifacts += 1;
1065
- }
1066
-
1067
- return result;
1068
- }
1069
-
1070
- export class PersistedLocalHistoryStore {
1071
- constructor(
1072
- private readonly deps: {
1073
- readonly fs: FileSystem;
1074
- readonly codec: JsonCodec;
1075
- readonly graftDir: string;
1076
- },
1077
- ) {}
1078
-
1079
- async noteBinding(input: {
1080
- readonly current: PersistedLocalHistoryContext;
1081
- readonly previous?: PersistedLocalHistoryContext | null;
1082
- readonly currentGraph?: PersistedLocalHistoryGraphContext | null;
1083
- readonly previousGraph?: PersistedLocalHistoryGraphContext | null;
1084
- }): Promise<void> {
1085
- const previous = input.previous ?? null;
1086
- const currentKey = buildContinuityKey(input.current.repoId, input.current.worktreeId);
1087
-
1088
- if (previous !== null && input.previousGraph !== null && input.previousGraph !== undefined) {
1089
- const previousKey = buildContinuityKey(previous.repoId, previous.worktreeId);
1090
- if (previousKey !== currentKey) {
1091
- const previousState = await this.loadWritableState(
1092
- input.previousGraph,
1093
- previousKey,
1094
- previous.repoId,
1095
- previous.worktreeId,
1096
- );
1097
- const previousActive = findRecord(previousState, previousState.activeRecordId);
1098
- if (previousActive !== null) {
1099
- const parkRecord = createContinuityRecord({
1100
- operation: "park",
1101
- continuityKey: previousKey,
1102
- context: previous,
1103
- previous: previousActive,
1104
- });
1105
- const previousEventId = findLatestEventIdForStrand(previousState, previous.strandId);
1106
- appendRecord(
1107
- previousState,
1108
- parkRecord,
1109
- false,
1110
- );
1111
- await writeContinuityRecordToGraph(input.previousGraph, {
1112
- record: parkRecord,
1113
- previousEventId,
1114
- });
1115
- }
1116
- }
1117
- }
1118
-
1119
- const currentState = await this.loadWritableState(
1120
- input.currentGraph,
1121
- currentKey,
1122
- input.current.repoId,
1123
- input.current.worktreeId,
1124
- );
1125
- const previousCurrentActive = findRecord(currentState, currentState.activeRecordId);
1126
- let latestCurrentEventId = findLatestEventIdForStrand(currentState, input.current.strandId);
1127
- if (previousCurrentActive !== null) {
1128
- const parkRecord = createContinuityRecord({
1129
- operation: "park",
1130
- continuityKey: currentKey,
1131
- context: input.current,
1132
- previous: previousCurrentActive,
1133
- });
1134
- const parkPreviousEventId = latestCurrentEventId;
1135
- appendRecord(
1136
- currentState,
1137
- parkRecord,
1138
- false,
1139
- );
1140
- if (parkRecord.strandId === input.current.strandId) {
1141
- latestCurrentEventId = parkRecord.recordId;
1142
- }
1143
- if (input.currentGraph !== null && input.currentGraph !== undefined) {
1144
- await writeContinuityRecordToGraph(input.currentGraph, {
1145
- record: parkRecord,
1146
- previousEventId: parkPreviousEventId,
1147
- });
1148
- }
1149
- }
1150
-
1151
- const operation = classifyContinuityOperation(previousCurrentActive, input.current);
1152
- const currentRecord = createContinuityRecord({
1153
- operation,
1154
- continuityKey: currentKey,
1155
- context: input.current,
1156
- previous: previousCurrentActive,
1157
- });
1158
- appendRecord(
1159
- currentState,
1160
- currentRecord,
1161
- true,
1162
- );
1163
- if (input.currentGraph !== null && input.currentGraph !== undefined) {
1164
- await writeContinuityRecordToGraph(input.currentGraph, {
1165
- record: currentRecord,
1166
- previousEventId: latestCurrentEventId,
1167
- });
1168
- }
1169
- }
1170
-
1171
- async noteCheckoutBoundary(input: {
1172
- readonly previous: PersistedLocalHistoryContext;
1173
- readonly current: PersistedLocalHistoryContext;
1174
- readonly graph?: PersistedLocalHistoryGraphContext | null;
1175
- }): Promise<void> {
1176
- const continuityKey = buildContinuityKey(input.current.repoId, input.current.worktreeId);
1177
- const state = await this.loadWritableState(
1178
- input.graph,
1179
- continuityKey,
1180
- input.current.repoId,
1181
- input.current.worktreeId,
1182
- );
1183
- const activeRecord = findRecord(state, state.activeRecordId);
1184
- const baseRecord = activeRecord ?? state.records.at(-1) ?? null;
1185
- if (baseRecord === null) {
1186
- const startRecord = createContinuityRecord({
1187
- operation: "start",
1188
- continuityKey,
1189
- context: input.current,
1190
- previous: null,
1191
- });
1192
- appendRecord(
1193
- state,
1194
- startRecord,
1195
- true,
1196
- );
1197
- if (input.graph !== null && input.graph !== undefined) {
1198
- await writeContinuityRecordToGraph(input.graph, {
1199
- record: startRecord,
1200
- previousEventId: null,
1201
- });
1202
- }
1203
- return;
1204
- }
1205
- if (baseRecord.checkoutEpochId === input.current.checkoutEpochId) {
1206
- return;
1207
- }
1208
-
1209
- const transitionEvidence = buildGitTransitionEvidence("fork", input.current);
1210
- const parkPreviousEventId = findLatestEventIdForStrand(state, input.previous.strandId);
1211
- const parkRecord = createContinuityRecord({
1212
- operation: "park",
1213
- continuityKey,
1214
- context: { ...input.previous, observedAt: input.current.observedAt },
1215
- previous: baseRecord,
1216
- additionalEvidence: transitionEvidence,
1217
- });
1218
- appendRecord(
1219
- state,
1220
- parkRecord,
1221
- false,
1222
- );
1223
- const forkRecord = createContinuityRecord({
1224
- operation: "fork",
1225
- continuityKey,
1226
- context: input.current,
1227
- previous: baseRecord,
1228
- additionalEvidence: transitionEvidence,
1229
- });
1230
- appendRecord(
1231
- state,
1232
- forkRecord,
1233
- true,
1234
- );
1235
- if (input.graph !== null && input.graph !== undefined) {
1236
- await writeContinuityRecordToGraph(input.graph, {
1237
- record: parkRecord,
1238
- previousEventId: parkPreviousEventId,
1239
- });
1240
- await writeContinuityRecordToGraph(input.graph, {
1241
- record: forkRecord,
1242
- previousEventId: null,
1243
- });
1244
- }
1245
- }
1246
-
1247
- async summarize(
1248
- status: WorkspaceStatus,
1249
- causalContext: RuntimeCausalContext,
1250
- graph?: PersistedLocalHistoryGraphContext | null,
1251
- ): Promise<PersistedLocalHistorySummary> {
1252
- if (status.repoId === null || status.worktreeId === null) {
1253
- return emptyPersistedLocalHistorySummary({
1254
- preserves: PERSISTED_LOCAL_HISTORY_PRESERVES,
1255
- excludes: PERSISTED_LOCAL_HISTORY_EXCLUDES,
1256
- });
1257
- }
1258
-
1259
- const graphState = await loadStateFromGraph(graph, status.repoId, status.worktreeId);
1260
- if (graphState === null) {
1261
- return emptyPersistedLocalHistorySummary({
1262
- preserves: PERSISTED_LOCAL_HISTORY_PRESERVES,
1263
- excludes: PERSISTED_LOCAL_HISTORY_EXCLUDES,
1264
- });
1265
- }
1266
- return summarizeContinuityState({
1267
- state: graphState,
1268
- causalContext,
1269
- preserves: PERSISTED_LOCAL_HISTORY_PRESERVES,
1270
- excludes: PERSISTED_LOCAL_HISTORY_EXCLUDES,
1271
- });
1272
- }
1273
-
1274
- async summarizeRepoConcurrency(
1275
- status: WorkspaceStatus,
1276
- graph?: PersistedLocalHistoryGraphContext | null,
1277
- ): Promise<RepoConcurrencySummary | null> {
1278
- if (status.repoId === null || status.worktreeId === null) {
1279
- return null;
1280
- }
1281
-
1282
- const currentState = await loadStateFromGraph(graph, status.repoId, status.worktreeId);
1283
- if (currentState === null) {
1284
- return null;
1285
- }
1286
- const graphStates = await loadStatesForRepoFromGraph(graph, status.repoId);
1287
- const repoStates = graphStates ?? [currentState];
1288
-
1289
- return deriveRepoConcurrencySummary({
1290
- currentWorktreeId: status.worktreeId,
1291
- histories: [
1292
- toRepoConcurrencyWorktreeHistory(currentState),
1293
- ...repoStates
1294
- .filter((state) => state.continuityKey !== currentState.continuityKey)
1295
- .map((state) => toRepoConcurrencyWorktreeHistory(state)),
1296
- ],
1297
- });
1298
- }
1299
-
1300
- async listRecentActivity(
1301
- status: WorkspaceStatus,
1302
- causalContext: RuntimeCausalContext,
1303
- limit: number,
1304
- graph?: PersistedLocalHistoryGraphContext | null,
1305
- ): Promise<PersistedLocalActivityWindow> {
1306
- if (status.repoId === null || status.worktreeId === null) {
1307
- return emptyPersistedLocalActivityWindow(limit);
1308
- }
1309
-
1310
- const graphState = await loadStateFromGraph(graph, status.repoId, status.worktreeId);
1311
- if (graphState === null) {
1312
- return emptyPersistedLocalActivityWindow(limit);
1313
- }
1314
- return buildPersistedLocalActivityWindow({
1315
- state: graphState,
1316
- causalContext,
1317
- limit,
1318
- });
1319
- }
1320
-
1321
- async declareAttach(input: {
1322
- readonly current: PersistedLocalHistoryContext;
1323
- readonly declaration: PersistedLocalHistoryAttachDeclaration;
1324
- readonly graph?: PersistedLocalHistoryGraphContext | null;
1325
- }): Promise<void> {
1326
- const continuityKey = buildContinuityKey(input.current.repoId, input.current.worktreeId);
1327
- const state = await this.loadWritableState(
1328
- input.graph,
1329
- continuityKey,
1330
- input.current.repoId,
1331
- input.current.worktreeId,
1332
- );
1333
- const activeRecord = findRecord(state, state.activeRecordId);
1334
- const baseRecord = activeRecord ?? state.records.at(-1) ?? null;
1335
- if (baseRecord?.continuedFromRecordId === null || baseRecord === null) {
1336
- throw new PersistedLocalHistoryAttachUnavailableError();
1337
- }
1338
-
1339
- const attachRecord = createContinuityRecord({
1340
- operation: "attach",
1341
- continuityKey,
1342
- context: input.current,
1343
- previous: baseRecord,
1344
- additionalEvidence: buildAttachDeclarationEvidence(input.current, input.declaration),
1345
- });
1346
- appendRecord(
1347
- state,
1348
- attachRecord,
1349
- true,
1350
- );
1351
- if (input.graph !== null && input.graph !== undefined) {
1352
- const previousEventId = findLatestEventIdForStrand(
1353
- {
1354
- ...state,
1355
- records: state.records.slice(0, -1),
1356
- },
1357
- input.current.strandId,
1358
- );
1359
- await writeContinuityRecordToGraph(input.graph, {
1360
- record: attachRecord,
1361
- previousEventId,
1362
- });
1363
- }
1364
- }
1365
-
1366
- async declareSharedAttach(input: {
1367
- readonly current: PersistedLocalHistoryContext;
1368
- readonly declaration: PersistedLocalHistoryAttachDeclaration;
1369
- readonly source: PersistedLocalHistorySharedAttachSource;
1370
- readonly graph?: PersistedLocalHistoryGraphContext | null;
1371
- }): Promise<void> {
1372
- const continuityKey = buildContinuityKey(input.current.repoId, input.current.worktreeId);
1373
- const state = await this.loadWritableState(
1374
- input.graph,
1375
- continuityKey,
1376
- input.current.repoId,
1377
- input.current.worktreeId,
1378
- );
1379
- const baseRecord = findRecord(state, state.activeRecordId) ?? state.records.at(-1) ?? null;
1380
- if (baseRecord === null) {
1381
- throw new PersistedLocalHistoryAttachUnavailableError();
1382
- }
1383
-
1384
- const attachRecord = createContinuityRecord({
1385
- operation: "attach",
1386
- continuityKey,
1387
- context: input.current,
1388
- previous: baseRecord,
1389
- additionalEvidence: [
1390
- ...buildAttachDeclarationEvidence(input.current, input.declaration),
1391
- {
1392
- evidenceId: buildEvidenceId("attach", "daemon_session_observation", input.current, 400),
1393
- evidenceKind: "daemon_session_observation",
1394
- source: "daemon_control_plane.shared_attach",
1395
- capturedAt: input.current.observedAt,
1396
- strength: "strong",
1397
- details: {
1398
- sourceSessionId: input.source.sourceSessionId,
1399
- sourceCausalSessionId: input.source.causalSessionId,
1400
- sourceStrandId: input.source.strandId,
1401
- handoffKind: "cross_session_same_worktree",
1402
- },
1403
- },
1404
- ],
1405
- overrides: {
1406
- continuedFromCausalSessionId: input.source.causalSessionId,
1407
- continuedFromStrandId: input.source.strandId,
1408
- },
1409
- });
1410
- appendRecord(
1411
- state,
1412
- attachRecord,
1413
- true,
1414
- );
1415
- if (input.graph !== null && input.graph !== undefined) {
1416
- const previousEventId = findLatestEventIdForStrand(
1417
- {
1418
- ...state,
1419
- records: state.records.slice(0, -1),
1420
- },
1421
- input.current.strandId,
1422
- );
1423
- await writeContinuityRecordToGraph(input.graph, {
1424
- record: attachRecord,
1425
- previousEventId,
1426
- });
1427
- }
1428
- }
1429
-
1430
- async noteStageObservation(input: {
1431
- readonly current: PersistedLocalHistoryContext;
1432
- readonly stagedTarget: RuntimeStagedTargetFullFile;
1433
- readonly attribution: AttributionSummary;
1434
- readonly graph?: PersistedLocalHistoryGraphContext | null;
1435
- }): Promise<void> {
1436
- const continuityKey = buildContinuityKey(input.current.repoId, input.current.worktreeId);
1437
- const state = await this.loadWritableState(
1438
- input.graph,
1439
- continuityKey,
1440
- input.current.repoId,
1441
- input.current.worktreeId,
1442
- );
1443
- const event = createStageEvent(input.current, input.stagedTarget, input.attribution);
1444
- if (state.stageEvents.at(-1)?.eventId === event.eventId) {
1445
- return;
1446
- }
1447
- const previousEventId = findLatestEventIdForStrand(state, input.current.strandId);
1448
- state.stageEvents = keepRecent([...state.stageEvents, event], MAX_STAGE_EVENTS);
1449
- if (input.graph !== null && input.graph !== undefined) {
1450
- await writeCausalEventToGraph(input.graph, {
1451
- event,
1452
- previousEventId,
1453
- stagedTarget: input.stagedTarget,
1454
- });
1455
- }
1456
- }
1457
-
1458
- async noteReadObservation(input: {
1459
- readonly current: PersistedLocalHistoryContext;
1460
- readonly attribution: AttributionSummary;
1461
- readonly surface: string;
1462
- readonly projection: string;
1463
- readonly sourceLayer: SourceLayer;
1464
- readonly reason: string;
1465
- readonly footprint: CausalFootprint;
1466
- readonly graph?: PersistedLocalHistoryGraphContext | null;
1467
- }): Promise<void> {
1468
- const continuityKey = buildContinuityKey(input.current.repoId, input.current.worktreeId);
1469
- const state = await this.loadWritableState(
1470
- input.graph,
1471
- continuityKey,
1472
- input.current.repoId,
1473
- input.current.worktreeId,
1474
- );
1475
- const event = createReadEvent(input);
1476
- if (state.readEvents.at(-1)?.eventId === event.eventId) {
1477
- return;
1478
- }
1479
- const previousEventId = findLatestEventIdForStrand(state, input.current.strandId);
1480
- state.readEvents = keepRecent([...state.readEvents, event], MAX_READ_EVENTS);
1481
- if (input.graph !== null && input.graph !== undefined) {
1482
- await writeCausalEventToGraph(input.graph, {
1483
- event,
1484
- previousEventId,
1485
- });
1486
- }
1487
- }
1488
-
1489
- async noteSemanticTransitionObservation(input: {
1490
- readonly current: PersistedLocalHistoryContext;
1491
- readonly semanticTransition: NonNullable<RepoObservation["semanticTransition"]>;
1492
- readonly transition: RepoObservation["lastTransition"];
1493
- readonly attribution: AttributionSummary;
1494
- readonly graph?: PersistedLocalHistoryGraphContext | null;
1495
- }): Promise<void> {
1496
- const continuityKey = buildContinuityKey(input.current.repoId, input.current.worktreeId);
1497
- const state = await this.loadWritableState(
1498
- input.graph,
1499
- continuityKey,
1500
- input.current.repoId,
1501
- input.current.worktreeId,
1502
- );
1503
- const event = createTransitionEvent(input);
1504
- if (state.transitionEvents.at(-1)?.eventId === event.eventId) {
1505
- return;
1506
- }
1507
- const previousEventId = findLatestEventIdForStrand(state, input.current.strandId);
1508
- state.transitionEvents = keepRecent([...state.transitionEvents, event], MAX_TRANSITION_EVENTS);
1509
- if (input.graph !== null && input.graph !== undefined) {
1510
- await writeCausalEventToGraph(input.graph, {
1511
- event,
1512
- previousEventId,
1513
- });
1514
- }
1515
- }
1516
-
1517
- buildContext(
1518
- status: WorkspaceStatus,
1519
- causalContext: RuntimeCausalContext,
1520
- repoState: RepoObservation,
1521
- hookEvent: GitTransitionHookEvent | null = null,
1522
- ): PersistedLocalHistoryContext | null {
1523
- if (status.repoId === null || status.worktreeId === null) {
1524
- return null;
1525
- }
1526
- return {
1527
- repoId: status.repoId,
1528
- worktreeId: status.worktreeId,
1529
- transportSessionId: causalContext.transportSessionId,
1530
- workspaceSliceId: causalContext.workspaceSliceId,
1531
- causalSessionId: causalContext.causalSessionId,
1532
- strandId: causalContext.strandId,
1533
- checkoutEpochId: causalContext.checkoutEpochId,
1534
- workspaceOverlayId: repoState.workspaceOverlayId,
1535
- observedAt: repoState.observedAt,
1536
- warpWriterId: causalContext.warpWriterId,
1537
- transitionKind: repoState.lastTransition?.kind ?? null,
1538
- transitionReflogSubject: repoState.lastTransition?.evidence.reflogSubject ?? null,
1539
- hookTransitionName: hookEvent?.hookName ?? null,
1540
- hookTransitionArgs: hookEvent?.hookArgs ?? null,
1541
- hookTransitionObservedAt: hookEvent?.observedAt ?? null,
1542
- };
1543
- }
1544
-
1545
- private async loadWritableState(
1546
- graph: PersistedLocalHistoryGraphContext | null | undefined,
1547
- continuityKey: string,
1548
- repoId: string,
1549
- worktreeId: string,
1550
- ): Promise<ContinuityState> {
1551
- return await loadStateFromGraph(graph, repoId, worktreeId) ?? createEmptyState(continuityKey, repoId, worktreeId);
1552
- }
1553
- }
1554
-
1555
- function findLatestEventIdForStrand(state: ContinuityState, strandId: string): string | null {
1556
- const items: {
1557
- readonly id: string;
1558
- readonly occurredAt: string;
1559
- readonly priority: number;
1560
- readonly sequence: number;
1561
- }[] = [];
1562
-
1563
- for (const [index, record] of state.records.entries()) {
1564
- if (record.strandId === strandId) {
1565
- items.push({
1566
- id: record.recordId,
1567
- occurredAt: record.occurredAt,
1568
- priority: 0,
1569
- sequence: index,
1570
- });
1571
- }
1572
- }
1573
- for (const [index, event] of state.readEvents.entries()) {
1574
- if (event.strandId === strandId) {
1575
- items.push({
1576
- id: event.eventId,
1577
- occurredAt: event.occurredAt,
1578
- priority: 1,
1579
- sequence: index,
1580
- });
1581
- }
1582
- }
1583
- for (const [index, event] of state.stageEvents.entries()) {
1584
- if (event.strandId === strandId) {
1585
- items.push({
1586
- id: event.eventId,
1587
- occurredAt: event.occurredAt,
1588
- priority: 2,
1589
- sequence: index,
1590
- });
1591
- }
1592
- }
1593
- for (const [index, event] of state.transitionEvents.entries()) {
1594
- if (event.strandId === strandId) {
1595
- items.push({
1596
- id: event.eventId,
1597
- occurredAt: event.occurredAt,
1598
- priority: 3,
1599
- sequence: index,
1600
- });
1601
- }
1602
- }
1603
-
1604
- items.sort((left, right) => {
1605
- const byTime = left.occurredAt.localeCompare(right.occurredAt);
1606
- if (byTime !== 0) {
1607
- return byTime;
1608
- }
1609
- const byPriority = left.priority - right.priority;
1610
- if (byPriority !== 0) {
1611
- return byPriority;
1612
- }
1613
- const bySequence = left.sequence - right.sequence;
1614
- if (bySequence !== 0) {
1615
- return bySequence;
1616
- }
1617
- return left.id.localeCompare(right.id);
1618
- });
1619
-
1620
- return items.at(-1)?.id ?? null;
1621
- }