@flyingrobots/graft 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +386 -0
- package/CHANGELOG.md +47 -0
- package/CODE_OF_CONDUCT.md +65 -0
- package/README.md +153 -17
- package/bin/graft.js +4 -14
- package/docs/ADVANCED_GUIDE.md +49 -0
- package/docs/CLI.md +43 -0
- package/docs/GUIDE.md +321 -32
- package/docs/MCP.md +44 -0
- package/package.json +15 -4
- package/src/adapters/node-fs.ts +4 -0
- package/src/adapters/node-git.ts +47 -0
- package/src/adapters/node-process-runner.ts +27 -0
- package/src/cli/index-cmd.ts +75 -11
- package/src/cli/init.ts +808 -57
- package/src/cli/main.ts +437 -0
- package/src/contracts/capabilities.ts +341 -0
- package/src/contracts/causal-ontology.ts +622 -0
- package/src/contracts/causal-surface-next-action.ts +18 -0
- package/src/contracts/output-schemas.ts +1169 -0
- package/src/git/diff.ts +25 -21
- package/src/git/target-git-hook-bootstrap.ts +56 -0
- package/src/hooks/posttooluse-read.ts +21 -74
- package/src/hooks/pretooluse-read.ts +20 -56
- package/src/hooks/read-governor.ts +95 -0
- package/src/hooks/read-messages.ts +53 -0
- package/src/mcp/burden.ts +123 -0
- package/src/mcp/cache.ts +51 -0
- package/src/mcp/cached-file.ts +10 -8
- package/src/mcp/context.ts +65 -2
- package/src/mcp/daemon-control-plane.ts +554 -0
- package/src/mcp/daemon-job-scheduler.ts +279 -0
- package/src/mcp/daemon-repos.ts +216 -0
- package/src/mcp/daemon-server.ts +396 -0
- package/src/mcp/daemon-worker-pool.ts +310 -0
- package/src/mcp/daemon-worker-process.ts +52 -0
- package/src/mcp/metrics.ts +108 -1
- package/src/mcp/monitor-tick-job.ts +99 -0
- package/src/mcp/persisted-local-history.ts +1246 -0
- package/src/mcp/persistent-monitor-runtime.ts +549 -0
- package/src/mcp/policy.ts +84 -0
- package/src/mcp/receipt.ts +82 -12
- package/src/mcp/repo-concurrency.ts +318 -0
- package/src/mcp/repo-state.ts +777 -0
- package/src/mcp/repo-tool-job.ts +302 -0
- package/src/mcp/run-capture-config.ts +33 -0
- package/src/mcp/runtime-causal-context.ts +72 -0
- package/src/mcp/runtime-observability.ts +219 -0
- package/src/mcp/runtime-staged-target.ts +161 -0
- package/src/mcp/runtime-workspace-overlay.ts +255 -0
- package/src/mcp/semantic-transition-guidance.ts +60 -0
- package/src/mcp/semantic-transition-summary.ts +130 -0
- package/src/mcp/server.ts +696 -55
- package/src/mcp/stdio-server.ts +12 -0
- package/src/mcp/stdio.ts +2 -5
- package/src/mcp/tools/activity-view.ts +325 -0
- package/src/mcp/tools/causal-attach.ts +67 -0
- package/src/mcp/tools/causal-status.ts +58 -0
- package/src/mcp/tools/changed-since.ts +13 -11
- package/src/mcp/tools/code-find.ts +164 -0
- package/src/mcp/tools/code-refs.ts +466 -0
- package/src/mcp/tools/code-show.ts +252 -0
- package/src/mcp/tools/daemon-monitors.ts +14 -0
- package/src/mcp/tools/daemon-repos.ts +22 -0
- package/src/mcp/tools/daemon-sessions.ts +14 -0
- package/src/mcp/tools/daemon-status.ts +12 -0
- package/src/mcp/tools/doctor.ts +45 -2
- package/src/mcp/tools/explain.ts +4 -0
- package/src/mcp/tools/file-outline.ts +7 -3
- package/src/mcp/tools/git-files.ts +73 -0
- package/src/mcp/tools/graft-diff.ts +12 -4
- package/src/mcp/tools/map.ts +92 -38
- package/src/mcp/tools/monitor-pause.ts +18 -0
- package/src/mcp/tools/monitor-resume.ts +18 -0
- package/src/mcp/tools/monitor-start.ts +20 -0
- package/src/mcp/tools/monitor-stop.ts +18 -0
- package/src/mcp/tools/precision-match.ts +51 -0
- package/src/mcp/tools/precision-query.ts +127 -0
- package/src/mcp/tools/precision.ts +312 -0
- package/src/mcp/tools/run-capture.ts +126 -44
- package/src/mcp/tools/safe-read.ts +14 -12
- package/src/mcp/tools/since.ts +7 -2
- package/src/mcp/tools/state.ts +11 -3
- package/src/mcp/tools/stats.ts +5 -1
- package/src/mcp/tools/workspace-authorizations.ts +14 -0
- package/src/mcp/tools/workspace-authorize.ts +20 -0
- package/src/mcp/tools/workspace-bind.ts +25 -0
- package/src/mcp/tools/workspace-rebind.ts +25 -0
- package/src/mcp/tools/workspace-revoke.ts +18 -0
- package/src/mcp/tools/workspace-status.ts +12 -0
- package/src/mcp/warp-pool.ts +36 -0
- package/src/mcp/workspace-router.ts +984 -0
- package/src/operations/file-outline.ts +12 -2
- package/src/operations/graft-diff.ts +56 -10
- package/src/operations/safe-read.ts +27 -4
- package/src/operations/state.ts +6 -9
- package/src/parser/lang.ts +19 -3
- package/src/parser/outline.ts +191 -2
- package/src/parser/types.ts +9 -1
- package/src/policy/types.ts +4 -3
- package/src/ports/filesystem.ts +1 -0
- package/src/ports/git.ts +16 -0
- package/src/ports/process-runner.ts +22 -0
- package/src/release/security-gate.ts +102 -0
- package/src/session/tracker.ts +31 -0
- package/src/version.ts +3 -0
- package/src/warp/indexer.ts +171 -56
- package/src/warp/observers.ts +1 -1
- package/src/warp/open.ts +4 -3
- package/src/warp/plumbing.d.ts +5 -1
- package/src/warp/writer-id.ts +30 -0
|
@@ -0,0 +1,1246 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import {
|
|
5
|
+
attributionSummarySchema,
|
|
6
|
+
attributionConfidenceSchema,
|
|
7
|
+
readEventSchema,
|
|
8
|
+
stageEventSchema,
|
|
9
|
+
transitionEventSchema,
|
|
10
|
+
evidenceSchema,
|
|
11
|
+
getMaximumConfidenceForEvidence,
|
|
12
|
+
localHistoryContinuityOperationSchema,
|
|
13
|
+
type AttributionSummary,
|
|
14
|
+
type AttributionConfidence,
|
|
15
|
+
type CausalFootprint,
|
|
16
|
+
type CausalEvent,
|
|
17
|
+
type Evidence,
|
|
18
|
+
type LocalHistoryContinuityOperation,
|
|
19
|
+
type SourceLayer,
|
|
20
|
+
} from "../contracts/causal-ontology.js";
|
|
21
|
+
import type { JsonCodec } from "../ports/codec.js";
|
|
22
|
+
import type { FileSystem } from "../ports/filesystem.js";
|
|
23
|
+
import type { RepoObservation } from "./repo-state.js";
|
|
24
|
+
import {
|
|
25
|
+
buildRepoConcurrencyContributorKey,
|
|
26
|
+
buildRepoConcurrencyTouches,
|
|
27
|
+
deriveRepoConcurrencySummary,
|
|
28
|
+
type RepoConcurrencySummary,
|
|
29
|
+
type RepoConcurrencyWorktreeHistory,
|
|
30
|
+
} from "./repo-concurrency.js";
|
|
31
|
+
import type { GitTransitionHookEvent } from "./runtime-workspace-overlay.js";
|
|
32
|
+
import type { RuntimeCausalContext } from "./runtime-causal-context.js";
|
|
33
|
+
import type { RuntimeStagedTargetFullFile } from "./runtime-staged-target.js";
|
|
34
|
+
import type { WorkspaceStatus } from "./workspace-router.js";
|
|
35
|
+
|
|
36
|
+
const PERSISTED_LOCAL_HISTORY_PRESERVES = Object.freeze([
|
|
37
|
+
"continuity_operations",
|
|
38
|
+
"read_events",
|
|
39
|
+
"stage_events",
|
|
40
|
+
"transition_events",
|
|
41
|
+
"runtime_context_ids",
|
|
42
|
+
"workspace_overlay_snapshots",
|
|
43
|
+
] as const);
|
|
44
|
+
|
|
45
|
+
const PERSISTED_LOCAL_HISTORY_EXCLUDES = Object.freeze([
|
|
46
|
+
"raw_chat_transcripts",
|
|
47
|
+
"queue_bookkeeping",
|
|
48
|
+
"canonical_provenance",
|
|
49
|
+
"canonical_structural_truth",
|
|
50
|
+
] as const);
|
|
51
|
+
|
|
52
|
+
const MAX_CONTINUITY_RECORDS = 128;
|
|
53
|
+
const MAX_READ_EVENTS = 256;
|
|
54
|
+
const MAX_STAGE_EVENTS = 128;
|
|
55
|
+
const MAX_TRANSITION_EVENTS = 128;
|
|
56
|
+
|
|
57
|
+
const continuityRecordSchema = z.object({
|
|
58
|
+
recordId: z.string().min(1),
|
|
59
|
+
continuityKey: z.string().min(1),
|
|
60
|
+
operation: localHistoryContinuityOperationSchema,
|
|
61
|
+
repoId: z.string().min(1),
|
|
62
|
+
worktreeId: z.string().min(1),
|
|
63
|
+
transportSessionId: z.string().min(1),
|
|
64
|
+
workspaceSliceId: z.string().min(1),
|
|
65
|
+
causalSessionId: z.string().min(1),
|
|
66
|
+
strandId: z.string().min(1),
|
|
67
|
+
checkoutEpochId: z.string().min(1),
|
|
68
|
+
workspaceOverlayId: z.string().min(1).nullable(),
|
|
69
|
+
occurredAt: z.string().min(1),
|
|
70
|
+
continuedFromRecordId: z.string().min(1).nullable(),
|
|
71
|
+
continuedFromCausalSessionId: z.string().min(1).nullable(),
|
|
72
|
+
continuedFromStrandId: z.string().min(1).nullable(),
|
|
73
|
+
continuityConfidence: attributionConfidenceSchema,
|
|
74
|
+
continuityEvidence: z.array(evidenceSchema),
|
|
75
|
+
attribution: attributionSummarySchema,
|
|
76
|
+
}).strict();
|
|
77
|
+
|
|
78
|
+
type ContinuityRecord = z.infer<typeof continuityRecordSchema>;
|
|
79
|
+
|
|
80
|
+
const continuityStateSchema = z.object({
|
|
81
|
+
continuityKey: z.string().min(1),
|
|
82
|
+
repoId: z.string().min(1),
|
|
83
|
+
worktreeId: z.string().min(1),
|
|
84
|
+
activeRecordId: z.string().min(1).nullable(),
|
|
85
|
+
records: z.array(continuityRecordSchema),
|
|
86
|
+
readEvents: z.array(readEventSchema),
|
|
87
|
+
stageEvents: z.array(stageEventSchema),
|
|
88
|
+
transitionEvents: z.array(transitionEventSchema),
|
|
89
|
+
}).strict();
|
|
90
|
+
|
|
91
|
+
type ContinuityState = z.infer<typeof continuityStateSchema>;
|
|
92
|
+
|
|
93
|
+
export interface PersistedLocalHistoryContext {
|
|
94
|
+
readonly repoId: string;
|
|
95
|
+
readonly worktreeId: string;
|
|
96
|
+
readonly transportSessionId: string;
|
|
97
|
+
readonly workspaceSliceId: string;
|
|
98
|
+
readonly causalSessionId: string;
|
|
99
|
+
readonly strandId: string;
|
|
100
|
+
readonly checkoutEpochId: string;
|
|
101
|
+
readonly workspaceOverlayId: string | null;
|
|
102
|
+
readonly observedAt: string;
|
|
103
|
+
readonly warpWriterId: string;
|
|
104
|
+
readonly transitionKind: RepoObservation["lastTransition"] extends infer T
|
|
105
|
+
? T extends { kind: infer K } | null
|
|
106
|
+
? K | null
|
|
107
|
+
: null
|
|
108
|
+
: null;
|
|
109
|
+
readonly transitionReflogSubject: string | null;
|
|
110
|
+
readonly hookTransitionName: GitTransitionHookEvent["hookName"] | null;
|
|
111
|
+
readonly hookTransitionArgs: readonly string[] | null;
|
|
112
|
+
readonly hookTransitionObservedAt: string | null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface PersistedLocalHistoryAttachDeclaration {
|
|
116
|
+
readonly actorKind: "human" | "agent";
|
|
117
|
+
readonly actorId?: string | undefined;
|
|
118
|
+
readonly fromActorId?: string | undefined;
|
|
119
|
+
readonly note?: string | undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface PersistedLocalHistorySharedAttachSource {
|
|
123
|
+
readonly sourceSessionId: string;
|
|
124
|
+
readonly causalSessionId: string;
|
|
125
|
+
readonly strandId: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface PersistedLocalHistorySummaryNone {
|
|
129
|
+
readonly availability: "none";
|
|
130
|
+
readonly persistence: "persisted_local_history";
|
|
131
|
+
readonly historyPath: null;
|
|
132
|
+
readonly totalContinuityRecords: 0;
|
|
133
|
+
readonly active: false;
|
|
134
|
+
readonly lastOperation: null;
|
|
135
|
+
readonly lastObservedAt: null;
|
|
136
|
+
readonly continuityKey: null;
|
|
137
|
+
readonly causalSessionId: null;
|
|
138
|
+
readonly strandId: null;
|
|
139
|
+
readonly checkoutEpochId: null;
|
|
140
|
+
readonly continuedFromCausalSessionId: null;
|
|
141
|
+
readonly continuityConfidence: AttributionConfidence;
|
|
142
|
+
readonly continuityEvidence: readonly Evidence[];
|
|
143
|
+
readonly attribution: AttributionSummary;
|
|
144
|
+
readonly latestReadEvent: null;
|
|
145
|
+
readonly latestStageEvent: null;
|
|
146
|
+
readonly latestTransitionEvent: null;
|
|
147
|
+
readonly preserves: readonly string[];
|
|
148
|
+
readonly excludes: readonly string[];
|
|
149
|
+
readonly nextAction: "bind_workspace_to_begin_local_history";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface PersistedLocalHistorySummaryPresent {
|
|
153
|
+
readonly availability: "present";
|
|
154
|
+
readonly persistence: "persisted_local_history";
|
|
155
|
+
readonly historyPath: string;
|
|
156
|
+
readonly totalContinuityRecords: number;
|
|
157
|
+
readonly active: boolean;
|
|
158
|
+
readonly lastOperation: LocalHistoryContinuityOperation;
|
|
159
|
+
readonly lastObservedAt: string;
|
|
160
|
+
readonly continuityKey: string;
|
|
161
|
+
readonly causalSessionId: string;
|
|
162
|
+
readonly strandId: string;
|
|
163
|
+
readonly checkoutEpochId: string;
|
|
164
|
+
readonly continuedFromCausalSessionId: string | null;
|
|
165
|
+
readonly continuityConfidence: AttributionConfidence;
|
|
166
|
+
readonly continuityEvidence: readonly Evidence[];
|
|
167
|
+
readonly attribution: AttributionSummary;
|
|
168
|
+
readonly latestReadEvent: Extract<CausalEvent, { eventKind: "read" }> | null;
|
|
169
|
+
readonly latestStageEvent: Extract<CausalEvent, { eventKind: "stage" }> | null;
|
|
170
|
+
readonly latestTransitionEvent: Extract<CausalEvent, { eventKind: "transition" }> | null;
|
|
171
|
+
readonly preserves: readonly string[];
|
|
172
|
+
readonly excludes: readonly string[];
|
|
173
|
+
readonly nextAction:
|
|
174
|
+
| "continue_active_causal_workspace"
|
|
175
|
+
| "review_transition_boundary_before_continuing"
|
|
176
|
+
| "inspect_or_resume_local_history";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export type PersistedLocalHistorySummary =
|
|
180
|
+
| PersistedLocalHistorySummaryNone
|
|
181
|
+
| PersistedLocalHistorySummaryPresent;
|
|
182
|
+
|
|
183
|
+
export interface PersistedLocalActivityContinuityItem {
|
|
184
|
+
readonly itemKind: "continuity";
|
|
185
|
+
readonly recordId: string;
|
|
186
|
+
readonly operation: LocalHistoryContinuityOperation;
|
|
187
|
+
readonly occurredAt: string;
|
|
188
|
+
readonly causalSessionId: string;
|
|
189
|
+
readonly strandId: string;
|
|
190
|
+
readonly attribution: AttributionSummary;
|
|
191
|
+
readonly continuedFromCausalSessionId: string | null;
|
|
192
|
+
readonly continuedFromStrandId: string | null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export type PersistedLocalActivityItem =
|
|
196
|
+
| PersistedLocalActivityContinuityItem
|
|
197
|
+
| Extract<CausalEvent, { eventKind: "read" | "stage" | "transition" }>;
|
|
198
|
+
|
|
199
|
+
export interface PersistedLocalActivityWindow {
|
|
200
|
+
readonly historyPath: string | null;
|
|
201
|
+
readonly limit: number;
|
|
202
|
+
readonly totalMatchingItems: number;
|
|
203
|
+
readonly truncated: boolean;
|
|
204
|
+
readonly items: readonly PersistedLocalActivityItem[];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export type { RepoConcurrencySummary } from "./repo-concurrency.js";
|
|
208
|
+
|
|
209
|
+
export class PersistedLocalHistoryAttachUnavailableError extends Error {
|
|
210
|
+
readonly code = "NO_ATTACHABLE_HISTORY";
|
|
211
|
+
|
|
212
|
+
constructor(message = "No attachable persisted local-history lineage is available for the current footing.") {
|
|
213
|
+
super(message);
|
|
214
|
+
this.name = "PersistedLocalHistoryAttachUnavailableError";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function stableId(prefix: string, input: string): string {
|
|
219
|
+
return `${prefix}:${crypto.createHash("sha256").update(input).digest("hex").slice(0, 16)}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function buildContinuityKey(repoId: string, worktreeId: string): string {
|
|
223
|
+
return stableId("continuity", `${repoId}:${worktreeId}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function buildRecordId(
|
|
227
|
+
continuityKey: string,
|
|
228
|
+
operation: LocalHistoryContinuityOperation,
|
|
229
|
+
context: PersistedLocalHistoryContext,
|
|
230
|
+
previousRecordId: string | null,
|
|
231
|
+
additionalEvidenceIds: readonly string[],
|
|
232
|
+
): string {
|
|
233
|
+
return stableId(
|
|
234
|
+
"history",
|
|
235
|
+
JSON.stringify({
|
|
236
|
+
continuityKey,
|
|
237
|
+
operation,
|
|
238
|
+
occurredAt: context.observedAt,
|
|
239
|
+
causalSessionId: context.causalSessionId,
|
|
240
|
+
strandId: context.strandId,
|
|
241
|
+
workspaceSliceId: context.workspaceSliceId,
|
|
242
|
+
previousRecordId,
|
|
243
|
+
additionalEvidenceIds,
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function buildEvidenceId(
|
|
249
|
+
operation: LocalHistoryContinuityOperation,
|
|
250
|
+
kind: Evidence["evidenceKind"],
|
|
251
|
+
context: PersistedLocalHistoryContext,
|
|
252
|
+
index: number,
|
|
253
|
+
): string {
|
|
254
|
+
return stableId(
|
|
255
|
+
"evidence",
|
|
256
|
+
JSON.stringify({
|
|
257
|
+
operation,
|
|
258
|
+
kind,
|
|
259
|
+
index,
|
|
260
|
+
occurredAt: context.observedAt,
|
|
261
|
+
transportSessionId: context.transportSessionId,
|
|
262
|
+
causalSessionId: context.causalSessionId,
|
|
263
|
+
strandId: context.strandId,
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function createEmptyState(continuityKey: string, repoId: string, worktreeId: string): ContinuityState {
|
|
269
|
+
return {
|
|
270
|
+
continuityKey,
|
|
271
|
+
repoId,
|
|
272
|
+
worktreeId,
|
|
273
|
+
activeRecordId: null,
|
|
274
|
+
records: [],
|
|
275
|
+
readEvents: [],
|
|
276
|
+
stageEvents: [],
|
|
277
|
+
transitionEvents: [],
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function findRecord(state: ContinuityState, recordId: string | null): ContinuityRecord | null {
|
|
282
|
+
if (recordId === null) return null;
|
|
283
|
+
for (let index = state.records.length - 1; index >= 0; index--) {
|
|
284
|
+
const record = state.records[index];
|
|
285
|
+
if (record?.recordId === recordId) {
|
|
286
|
+
return record;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function appendRecord(state: ContinuityState, record: ContinuityRecord, activeAfter: boolean): void {
|
|
293
|
+
state.records = [...state.records, record].slice(-MAX_CONTINUITY_RECORDS);
|
|
294
|
+
state.activeRecordId = activeAfter ? record.recordId : null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function keepRecent<T>(items: readonly T[], limit: number): T[] {
|
|
298
|
+
return items.slice(-limit);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function buildUnknownAttribution(): AttributionSummary {
|
|
302
|
+
return {
|
|
303
|
+
actor: {
|
|
304
|
+
actorId: "unknown",
|
|
305
|
+
actorKind: "unknown",
|
|
306
|
+
displayName: "Unknown",
|
|
307
|
+
source: "persisted_local_history.fallback",
|
|
308
|
+
authorityScope: "inferred",
|
|
309
|
+
},
|
|
310
|
+
confidence: "unknown",
|
|
311
|
+
basis: "unknown_fallback",
|
|
312
|
+
evidence: [],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export class PersistedLocalHistoryStore {
|
|
317
|
+
constructor(
|
|
318
|
+
private readonly deps: {
|
|
319
|
+
readonly fs: FileSystem;
|
|
320
|
+
readonly codec: JsonCodec;
|
|
321
|
+
readonly graftDir: string;
|
|
322
|
+
},
|
|
323
|
+
) {}
|
|
324
|
+
|
|
325
|
+
async noteBinding(input: {
|
|
326
|
+
readonly current: PersistedLocalHistoryContext;
|
|
327
|
+
readonly previous?: PersistedLocalHistoryContext | null;
|
|
328
|
+
}): Promise<void> {
|
|
329
|
+
const previous = input.previous ?? null;
|
|
330
|
+
const currentKey = buildContinuityKey(input.current.repoId, input.current.worktreeId);
|
|
331
|
+
|
|
332
|
+
if (previous !== null) {
|
|
333
|
+
const previousKey = buildContinuityKey(previous.repoId, previous.worktreeId);
|
|
334
|
+
if (previousKey !== currentKey) {
|
|
335
|
+
const previousState = await this.loadState(previousKey, previous.repoId, previous.worktreeId);
|
|
336
|
+
const previousActive = findRecord(previousState, previousState.activeRecordId);
|
|
337
|
+
if (previousActive !== null) {
|
|
338
|
+
appendRecord(
|
|
339
|
+
previousState,
|
|
340
|
+
this.createRecord("park", previousKey, previous, previousActive),
|
|
341
|
+
false,
|
|
342
|
+
);
|
|
343
|
+
await this.saveState(previousState);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const currentState = await this.loadState(currentKey, input.current.repoId, input.current.worktreeId);
|
|
349
|
+
const previousCurrentActive = findRecord(currentState, currentState.activeRecordId);
|
|
350
|
+
if (previousCurrentActive !== null) {
|
|
351
|
+
appendRecord(
|
|
352
|
+
currentState,
|
|
353
|
+
this.createRecord("park", currentKey, input.current, previousCurrentActive),
|
|
354
|
+
false,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const operation = this.classifyOperation(previousCurrentActive, input.current);
|
|
359
|
+
appendRecord(
|
|
360
|
+
currentState,
|
|
361
|
+
this.createRecord(operation, currentKey, input.current, previousCurrentActive),
|
|
362
|
+
true,
|
|
363
|
+
);
|
|
364
|
+
await this.saveState(currentState);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async noteCheckoutBoundary(input: {
|
|
368
|
+
readonly previous: PersistedLocalHistoryContext;
|
|
369
|
+
readonly current: PersistedLocalHistoryContext;
|
|
370
|
+
}): Promise<void> {
|
|
371
|
+
const continuityKey = buildContinuityKey(input.current.repoId, input.current.worktreeId);
|
|
372
|
+
const state = await this.loadState(continuityKey, input.current.repoId, input.current.worktreeId);
|
|
373
|
+
const activeRecord = findRecord(state, state.activeRecordId);
|
|
374
|
+
const baseRecord = activeRecord ?? state.records.at(-1) ?? null;
|
|
375
|
+
if (baseRecord === null) {
|
|
376
|
+
appendRecord(
|
|
377
|
+
state,
|
|
378
|
+
this.createRecord("start", continuityKey, input.current, null),
|
|
379
|
+
true,
|
|
380
|
+
);
|
|
381
|
+
await this.saveState(state);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (baseRecord.checkoutEpochId === input.current.checkoutEpochId) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const transitionEvidence = this.buildGitTransitionEvidence("fork", input.current);
|
|
389
|
+
appendRecord(
|
|
390
|
+
state,
|
|
391
|
+
this.createRecord(
|
|
392
|
+
"park",
|
|
393
|
+
continuityKey,
|
|
394
|
+
{ ...input.previous, observedAt: input.current.observedAt },
|
|
395
|
+
baseRecord,
|
|
396
|
+
transitionEvidence,
|
|
397
|
+
),
|
|
398
|
+
false,
|
|
399
|
+
);
|
|
400
|
+
appendRecord(
|
|
401
|
+
state,
|
|
402
|
+
this.createRecord("fork", continuityKey, input.current, baseRecord, transitionEvidence),
|
|
403
|
+
true,
|
|
404
|
+
);
|
|
405
|
+
await this.saveState(state);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async summarize(status: WorkspaceStatus, causalContext: RuntimeCausalContext): Promise<PersistedLocalHistorySummary> {
|
|
409
|
+
if (status.repoId === null || status.worktreeId === null) {
|
|
410
|
+
return this.emptySummary();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const continuityKey = buildContinuityKey(status.repoId, status.worktreeId);
|
|
414
|
+
const state = await this.loadState(continuityKey, status.repoId, status.worktreeId);
|
|
415
|
+
const activeRecord = findRecord(state, state.activeRecordId);
|
|
416
|
+
const lastRecord = state.records.at(-1) ?? null;
|
|
417
|
+
if (lastRecord === null || status.graftDir === null) {
|
|
418
|
+
return this.emptySummary();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const effectiveRecord = activeRecord ?? lastRecord;
|
|
422
|
+
return {
|
|
423
|
+
availability: "present",
|
|
424
|
+
persistence: "persisted_local_history",
|
|
425
|
+
historyPath: this.historyPathFor(continuityKey),
|
|
426
|
+
totalContinuityRecords: state.records.length,
|
|
427
|
+
active: activeRecord !== null,
|
|
428
|
+
lastOperation: lastRecord.operation,
|
|
429
|
+
lastObservedAt: lastRecord.occurredAt,
|
|
430
|
+
continuityKey,
|
|
431
|
+
causalSessionId: effectiveRecord.causalSessionId,
|
|
432
|
+
strandId: effectiveRecord.strandId,
|
|
433
|
+
checkoutEpochId: effectiveRecord.checkoutEpochId,
|
|
434
|
+
continuedFromCausalSessionId: lastRecord.continuedFromCausalSessionId,
|
|
435
|
+
continuityConfidence: lastRecord.continuityConfidence,
|
|
436
|
+
continuityEvidence: lastRecord.continuityEvidence,
|
|
437
|
+
attribution: effectiveRecord.attribution,
|
|
438
|
+
latestReadEvent: state.readEvents.at(-1) ?? null,
|
|
439
|
+
latestStageEvent: state.stageEvents.at(-1) ?? null,
|
|
440
|
+
latestTransitionEvent: state.transitionEvents.at(-1) ?? null,
|
|
441
|
+
preserves: PERSISTED_LOCAL_HISTORY_PRESERVES,
|
|
442
|
+
excludes: PERSISTED_LOCAL_HISTORY_EXCLUDES,
|
|
443
|
+
nextAction: activeRecord?.causalSessionId === causalContext.causalSessionId
|
|
444
|
+
? (lastRecord.operation === "fork"
|
|
445
|
+
? "review_transition_boundary_before_continuing"
|
|
446
|
+
: "continue_active_causal_workspace")
|
|
447
|
+
: "inspect_or_resume_local_history",
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async summarizeRepoConcurrency(status: WorkspaceStatus): Promise<RepoConcurrencySummary | null> {
|
|
452
|
+
if (status.repoId === null || status.worktreeId === null) {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const continuityKey = buildContinuityKey(status.repoId, status.worktreeId);
|
|
457
|
+
const currentState = await this.loadState(continuityKey, status.repoId, status.worktreeId);
|
|
458
|
+
const repoStates = await this.loadStatesForRepo(status.repoId);
|
|
459
|
+
|
|
460
|
+
return deriveRepoConcurrencySummary({
|
|
461
|
+
currentWorktreeId: status.worktreeId,
|
|
462
|
+
histories: [
|
|
463
|
+
this.toWorktreeHistory(currentState),
|
|
464
|
+
...repoStates
|
|
465
|
+
.filter((state) => state.continuityKey !== currentState.continuityKey)
|
|
466
|
+
.map((state) => this.toWorktreeHistory(state)),
|
|
467
|
+
],
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async listRecentActivity(
|
|
472
|
+
status: WorkspaceStatus,
|
|
473
|
+
causalContext: RuntimeCausalContext,
|
|
474
|
+
limit: number,
|
|
475
|
+
): Promise<PersistedLocalActivityWindow> {
|
|
476
|
+
if (status.repoId === null || status.worktreeId === null || status.graftDir === null) {
|
|
477
|
+
return {
|
|
478
|
+
historyPath: null,
|
|
479
|
+
limit,
|
|
480
|
+
totalMatchingItems: 0,
|
|
481
|
+
truncated: false,
|
|
482
|
+
items: [],
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const continuityKey = buildContinuityKey(status.repoId, status.worktreeId);
|
|
487
|
+
const state = await this.loadState(continuityKey, status.repoId, status.worktreeId);
|
|
488
|
+
const activeRecord = findRecord(state, state.activeRecordId);
|
|
489
|
+
const effectiveCheckoutEpochId =
|
|
490
|
+
activeRecord?.checkoutEpochId
|
|
491
|
+
?? state.records.at(-1)?.checkoutEpochId
|
|
492
|
+
?? causalContext.checkoutEpochId;
|
|
493
|
+
|
|
494
|
+
const continuityItems: PersistedLocalActivityContinuityItem[] = state.records
|
|
495
|
+
.filter((record) => record.checkoutEpochId === effectiveCheckoutEpochId)
|
|
496
|
+
.map((record) => ({
|
|
497
|
+
itemKind: "continuity",
|
|
498
|
+
recordId: record.recordId,
|
|
499
|
+
operation: record.operation,
|
|
500
|
+
occurredAt: record.occurredAt,
|
|
501
|
+
causalSessionId: record.causalSessionId,
|
|
502
|
+
strandId: record.strandId,
|
|
503
|
+
attribution: record.attribution,
|
|
504
|
+
continuedFromCausalSessionId: record.continuedFromCausalSessionId,
|
|
505
|
+
continuedFromStrandId: record.continuedFromStrandId,
|
|
506
|
+
}));
|
|
507
|
+
|
|
508
|
+
const eventItems: PersistedLocalActivityItem[] = [
|
|
509
|
+
...state.readEvents,
|
|
510
|
+
...state.stageEvents,
|
|
511
|
+
...state.transitionEvents,
|
|
512
|
+
].filter((event) => event.checkoutEpochId === effectiveCheckoutEpochId);
|
|
513
|
+
|
|
514
|
+
const allItems = [...continuityItems, ...eventItems].sort((left, right) => {
|
|
515
|
+
const byTime = right.occurredAt.localeCompare(left.occurredAt);
|
|
516
|
+
if (byTime !== 0) {
|
|
517
|
+
return byTime;
|
|
518
|
+
}
|
|
519
|
+
const leftId = "recordId" in left ? left.recordId : left.eventId;
|
|
520
|
+
const rightId = "recordId" in right ? right.recordId : right.eventId;
|
|
521
|
+
return rightId.localeCompare(leftId);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
historyPath: this.historyPathFor(continuityKey),
|
|
526
|
+
limit,
|
|
527
|
+
totalMatchingItems: allItems.length,
|
|
528
|
+
truncated: allItems.length > limit,
|
|
529
|
+
items: allItems.slice(0, limit),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async declareAttach(input: {
|
|
534
|
+
readonly current: PersistedLocalHistoryContext;
|
|
535
|
+
readonly declaration: PersistedLocalHistoryAttachDeclaration;
|
|
536
|
+
}): Promise<void> {
|
|
537
|
+
const continuityKey = buildContinuityKey(input.current.repoId, input.current.worktreeId);
|
|
538
|
+
const state = await this.loadState(continuityKey, input.current.repoId, input.current.worktreeId);
|
|
539
|
+
const activeRecord = findRecord(state, state.activeRecordId);
|
|
540
|
+
const baseRecord = activeRecord ?? state.records.at(-1) ?? null;
|
|
541
|
+
if (baseRecord?.continuedFromRecordId === null || baseRecord === null) {
|
|
542
|
+
throw new PersistedLocalHistoryAttachUnavailableError();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
appendRecord(
|
|
546
|
+
state,
|
|
547
|
+
this.createRecord(
|
|
548
|
+
"attach",
|
|
549
|
+
continuityKey,
|
|
550
|
+
input.current,
|
|
551
|
+
baseRecord,
|
|
552
|
+
this.buildAttachDeclarationEvidence(input.current, input.declaration),
|
|
553
|
+
),
|
|
554
|
+
true,
|
|
555
|
+
);
|
|
556
|
+
await this.saveState(state);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async declareSharedAttach(input: {
|
|
560
|
+
readonly current: PersistedLocalHistoryContext;
|
|
561
|
+
readonly declaration: PersistedLocalHistoryAttachDeclaration;
|
|
562
|
+
readonly source: PersistedLocalHistorySharedAttachSource;
|
|
563
|
+
}): Promise<void> {
|
|
564
|
+
const continuityKey = buildContinuityKey(input.current.repoId, input.current.worktreeId);
|
|
565
|
+
const state = await this.loadState(continuityKey, input.current.repoId, input.current.worktreeId);
|
|
566
|
+
const baseRecord = findRecord(state, state.activeRecordId) ?? state.records.at(-1) ?? null;
|
|
567
|
+
if (baseRecord === null) {
|
|
568
|
+
throw new PersistedLocalHistoryAttachUnavailableError();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
appendRecord(
|
|
572
|
+
state,
|
|
573
|
+
this.createRecord(
|
|
574
|
+
"attach",
|
|
575
|
+
continuityKey,
|
|
576
|
+
input.current,
|
|
577
|
+
baseRecord,
|
|
578
|
+
[
|
|
579
|
+
...this.buildAttachDeclarationEvidence(input.current, input.declaration),
|
|
580
|
+
{
|
|
581
|
+
evidenceId: buildEvidenceId("attach", "daemon_session_observation", input.current, 400),
|
|
582
|
+
evidenceKind: "daemon_session_observation",
|
|
583
|
+
source: "daemon_control_plane.shared_attach",
|
|
584
|
+
capturedAt: input.current.observedAt,
|
|
585
|
+
strength: "strong",
|
|
586
|
+
details: {
|
|
587
|
+
sourceSessionId: input.source.sourceSessionId,
|
|
588
|
+
sourceCausalSessionId: input.source.causalSessionId,
|
|
589
|
+
sourceStrandId: input.source.strandId,
|
|
590
|
+
handoffKind: "cross_session_same_worktree",
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
],
|
|
594
|
+
{
|
|
595
|
+
continuedFromCausalSessionId: input.source.causalSessionId,
|
|
596
|
+
continuedFromStrandId: input.source.strandId,
|
|
597
|
+
},
|
|
598
|
+
),
|
|
599
|
+
true,
|
|
600
|
+
);
|
|
601
|
+
await this.saveState(state);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async noteStageObservation(input: {
|
|
605
|
+
readonly current: PersistedLocalHistoryContext;
|
|
606
|
+
readonly stagedTarget: RuntimeStagedTargetFullFile;
|
|
607
|
+
readonly attribution: AttributionSummary;
|
|
608
|
+
}): Promise<void> {
|
|
609
|
+
const continuityKey = buildContinuityKey(input.current.repoId, input.current.worktreeId);
|
|
610
|
+
const state = await this.loadState(continuityKey, input.current.repoId, input.current.worktreeId);
|
|
611
|
+
const event = this.createStageEvent(input.current, input.stagedTarget, input.attribution);
|
|
612
|
+
if (state.stageEvents.at(-1)?.eventId === event.eventId) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
state.stageEvents = keepRecent([...state.stageEvents, event], MAX_STAGE_EVENTS);
|
|
616
|
+
await this.saveState(state);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async noteReadObservation(input: {
|
|
620
|
+
readonly current: PersistedLocalHistoryContext;
|
|
621
|
+
readonly attribution: AttributionSummary;
|
|
622
|
+
readonly surface: string;
|
|
623
|
+
readonly projection: string;
|
|
624
|
+
readonly sourceLayer: SourceLayer;
|
|
625
|
+
readonly reason: string;
|
|
626
|
+
readonly footprint: CausalFootprint;
|
|
627
|
+
}): Promise<void> {
|
|
628
|
+
const continuityKey = buildContinuityKey(input.current.repoId, input.current.worktreeId);
|
|
629
|
+
const state = await this.loadState(continuityKey, input.current.repoId, input.current.worktreeId);
|
|
630
|
+
state.readEvents = keepRecent([...state.readEvents, this.createReadEvent(input)], MAX_READ_EVENTS);
|
|
631
|
+
await this.saveState(state);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async noteSemanticTransitionObservation(input: {
|
|
635
|
+
readonly current: PersistedLocalHistoryContext;
|
|
636
|
+
readonly semanticTransition: NonNullable<RepoObservation["semanticTransition"]>;
|
|
637
|
+
readonly transition: RepoObservation["lastTransition"];
|
|
638
|
+
readonly attribution: AttributionSummary;
|
|
639
|
+
}): Promise<void> {
|
|
640
|
+
const continuityKey = buildContinuityKey(input.current.repoId, input.current.worktreeId);
|
|
641
|
+
const state = await this.loadState(continuityKey, input.current.repoId, input.current.worktreeId);
|
|
642
|
+
const event = this.createTransitionEvent(input);
|
|
643
|
+
if (state.transitionEvents.at(-1)?.eventId === event.eventId) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
state.transitionEvents = keepRecent([...state.transitionEvents, event], MAX_TRANSITION_EVENTS);
|
|
647
|
+
await this.saveState(state);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
buildContext(
|
|
651
|
+
status: WorkspaceStatus,
|
|
652
|
+
causalContext: RuntimeCausalContext,
|
|
653
|
+
repoState: RepoObservation,
|
|
654
|
+
hookEvent: GitTransitionHookEvent | null = null,
|
|
655
|
+
): PersistedLocalHistoryContext | null {
|
|
656
|
+
if (status.repoId === null || status.worktreeId === null) {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
repoId: status.repoId,
|
|
661
|
+
worktreeId: status.worktreeId,
|
|
662
|
+
transportSessionId: causalContext.transportSessionId,
|
|
663
|
+
workspaceSliceId: causalContext.workspaceSliceId,
|
|
664
|
+
causalSessionId: causalContext.causalSessionId,
|
|
665
|
+
strandId: causalContext.strandId,
|
|
666
|
+
checkoutEpochId: causalContext.checkoutEpochId,
|
|
667
|
+
workspaceOverlayId: repoState.workspaceOverlayId,
|
|
668
|
+
observedAt: repoState.observedAt,
|
|
669
|
+
warpWriterId: causalContext.warpWriterId,
|
|
670
|
+
transitionKind: repoState.lastTransition?.kind ?? null,
|
|
671
|
+
transitionReflogSubject: repoState.lastTransition?.evidence.reflogSubject ?? null,
|
|
672
|
+
hookTransitionName: hookEvent?.hookName ?? null,
|
|
673
|
+
hookTransitionArgs: hookEvent?.hookArgs ?? null,
|
|
674
|
+
hookTransitionObservedAt: hookEvent?.observedAt ?? null,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
private classifyOperation(
|
|
679
|
+
previous: ContinuityRecord | null,
|
|
680
|
+
current: PersistedLocalHistoryContext,
|
|
681
|
+
): LocalHistoryContinuityOperation {
|
|
682
|
+
if (previous === null) {
|
|
683
|
+
return "start";
|
|
684
|
+
}
|
|
685
|
+
if (previous.transportSessionId === current.transportSessionId) {
|
|
686
|
+
return "resume";
|
|
687
|
+
}
|
|
688
|
+
if (previous.checkoutEpochId === current.checkoutEpochId) {
|
|
689
|
+
return "attach";
|
|
690
|
+
}
|
|
691
|
+
return "fork";
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private createRecord(
|
|
695
|
+
operation: LocalHistoryContinuityOperation,
|
|
696
|
+
continuityKey: string,
|
|
697
|
+
context: PersistedLocalHistoryContext,
|
|
698
|
+
previous: ContinuityRecord | null,
|
|
699
|
+
additionalEvidence: readonly Evidence[] = [],
|
|
700
|
+
overrides: {
|
|
701
|
+
readonly continuedFromCausalSessionId?: string | null;
|
|
702
|
+
readonly continuedFromStrandId?: string | null;
|
|
703
|
+
} = {},
|
|
704
|
+
): ContinuityRecord {
|
|
705
|
+
const continuityEvidence = [
|
|
706
|
+
...this.buildContinuityEvidence(operation, context, previous),
|
|
707
|
+
...additionalEvidence,
|
|
708
|
+
];
|
|
709
|
+
return {
|
|
710
|
+
recordId: buildRecordId(
|
|
711
|
+
continuityKey,
|
|
712
|
+
operation,
|
|
713
|
+
context,
|
|
714
|
+
previous?.recordId ?? null,
|
|
715
|
+
additionalEvidence.map((evidence) => evidence.evidenceId),
|
|
716
|
+
),
|
|
717
|
+
continuityKey,
|
|
718
|
+
operation,
|
|
719
|
+
repoId: context.repoId,
|
|
720
|
+
worktreeId: context.worktreeId,
|
|
721
|
+
transportSessionId: context.transportSessionId,
|
|
722
|
+
workspaceSliceId: context.workspaceSliceId,
|
|
723
|
+
causalSessionId: context.causalSessionId,
|
|
724
|
+
strandId: context.strandId,
|
|
725
|
+
checkoutEpochId: context.checkoutEpochId,
|
|
726
|
+
workspaceOverlayId: context.workspaceOverlayId,
|
|
727
|
+
occurredAt: context.observedAt,
|
|
728
|
+
continuedFromRecordId: previous?.recordId ?? null,
|
|
729
|
+
continuedFromCausalSessionId: overrides.continuedFromCausalSessionId ?? previous?.causalSessionId ?? null,
|
|
730
|
+
continuedFromStrandId: overrides.continuedFromStrandId ?? previous?.strandId ?? null,
|
|
731
|
+
continuityConfidence: getMaximumConfidenceForEvidence(
|
|
732
|
+
continuityEvidence.map((evidence) => evidence.strength),
|
|
733
|
+
),
|
|
734
|
+
continuityEvidence,
|
|
735
|
+
attribution: this.deriveAttribution(operation, continuityEvidence),
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
private buildContinuityEvidence(
|
|
740
|
+
operation: LocalHistoryContinuityOperation,
|
|
741
|
+
context: PersistedLocalHistoryContext,
|
|
742
|
+
previous: ContinuityRecord | null,
|
|
743
|
+
): Evidence[] {
|
|
744
|
+
const evidence: Evidence[] = [];
|
|
745
|
+
|
|
746
|
+
evidence.push({
|
|
747
|
+
evidenceId: buildEvidenceId(operation, "mcp_transport_binding", context, evidence.length),
|
|
748
|
+
evidenceKind: "mcp_transport_binding",
|
|
749
|
+
source: "persisted_local_history.transport_binding",
|
|
750
|
+
capturedAt: context.observedAt,
|
|
751
|
+
strength: operation === "start" || operation === "resume" ? "direct" : "strong",
|
|
752
|
+
details: {
|
|
753
|
+
operation,
|
|
754
|
+
transportSessionId: context.transportSessionId,
|
|
755
|
+
workspaceSliceId: context.workspaceSliceId,
|
|
756
|
+
},
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
evidence.push({
|
|
760
|
+
evidenceId: buildEvidenceId(operation, "worktree_fs_observation", context, evidence.length),
|
|
761
|
+
evidenceKind: "worktree_fs_observation",
|
|
762
|
+
source: "persisted_local_history.workspace_footing",
|
|
763
|
+
capturedAt: context.observedAt,
|
|
764
|
+
strength: "strong",
|
|
765
|
+
details: {
|
|
766
|
+
repoId: context.repoId,
|
|
767
|
+
worktreeId: context.worktreeId,
|
|
768
|
+
checkoutEpochId: context.checkoutEpochId,
|
|
769
|
+
workspaceOverlayId: context.workspaceOverlayId,
|
|
770
|
+
previousCheckoutEpochId: previous?.checkoutEpochId ?? null,
|
|
771
|
+
previousCausalSessionId: previous?.causalSessionId ?? null,
|
|
772
|
+
},
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
evidence.push({
|
|
776
|
+
evidenceId: buildEvidenceId(operation, "writer_lane_identity", context, evidence.length),
|
|
777
|
+
evidenceKind: "writer_lane_identity",
|
|
778
|
+
source: "persisted_local_history.writer_lane",
|
|
779
|
+
capturedAt: context.observedAt,
|
|
780
|
+
strength: "strong",
|
|
781
|
+
details: {
|
|
782
|
+
warpWriterId: context.warpWriterId,
|
|
783
|
+
causalSessionId: context.causalSessionId,
|
|
784
|
+
strandId: context.strandId,
|
|
785
|
+
},
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
return evidence;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
private buildGitTransitionEvidence(
|
|
792
|
+
operation: LocalHistoryContinuityOperation,
|
|
793
|
+
context: PersistedLocalHistoryContext,
|
|
794
|
+
): Evidence[] {
|
|
795
|
+
const evidence: Evidence[] = [];
|
|
796
|
+
|
|
797
|
+
if (context.transitionKind !== null) {
|
|
798
|
+
evidence.push({
|
|
799
|
+
evidenceId: buildEvidenceId(operation, "git_transition_observation", context, 500),
|
|
800
|
+
evidenceKind: "git_transition_observation",
|
|
801
|
+
source: "persisted_local_history.checkout_transition",
|
|
802
|
+
capturedAt: context.observedAt,
|
|
803
|
+
strength: "strong",
|
|
804
|
+
details: {
|
|
805
|
+
operation,
|
|
806
|
+
transitionKind: context.transitionKind,
|
|
807
|
+
reflogSubject: context.transitionReflogSubject,
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (context.hookTransitionName !== null) {
|
|
813
|
+
evidence.push({
|
|
814
|
+
evidenceId: buildEvidenceId(operation, "git_hook_transition", context, 501),
|
|
815
|
+
evidenceKind: "git_hook_transition",
|
|
816
|
+
source: "persisted_local_history.checkout_transition_hook",
|
|
817
|
+
capturedAt: context.hookTransitionObservedAt ?? context.observedAt,
|
|
818
|
+
strength: "direct",
|
|
819
|
+
details: {
|
|
820
|
+
operation,
|
|
821
|
+
hookName: context.hookTransitionName,
|
|
822
|
+
hookArgs: context.hookTransitionArgs,
|
|
823
|
+
},
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return evidence;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
private buildAttachDeclarationEvidence(
|
|
831
|
+
context: PersistedLocalHistoryContext,
|
|
832
|
+
declaration: PersistedLocalHistoryAttachDeclaration,
|
|
833
|
+
): Evidence[] {
|
|
834
|
+
const evidence: Evidence[] = [];
|
|
835
|
+
const declarationKind = declaration.actorKind === "human"
|
|
836
|
+
? "explicit_user_declaration"
|
|
837
|
+
: "explicit_agent_declaration";
|
|
838
|
+
|
|
839
|
+
evidence.push({
|
|
840
|
+
evidenceId: buildEvidenceId("attach", declarationKind, context, evidence.length + 100),
|
|
841
|
+
evidenceKind: declarationKind,
|
|
842
|
+
source: "causal_attach.declaration",
|
|
843
|
+
capturedAt: context.observedAt,
|
|
844
|
+
strength: "direct",
|
|
845
|
+
details: {
|
|
846
|
+
actorKind: declaration.actorKind,
|
|
847
|
+
actorId: declaration.actorId ?? null,
|
|
848
|
+
note: declaration.note ?? null,
|
|
849
|
+
},
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
if (declaration.fromActorId !== undefined) {
|
|
853
|
+
evidence.push({
|
|
854
|
+
evidenceId: buildEvidenceId("attach", "explicit_handoff", context, evidence.length + 100),
|
|
855
|
+
evidenceKind: "explicit_handoff",
|
|
856
|
+
source: "causal_attach.handoff",
|
|
857
|
+
capturedAt: context.observedAt,
|
|
858
|
+
strength: "direct",
|
|
859
|
+
details: {
|
|
860
|
+
actorKind: declaration.actorKind,
|
|
861
|
+
actorId: declaration.actorId ?? null,
|
|
862
|
+
fromActorId: declaration.fromActorId,
|
|
863
|
+
note: declaration.note ?? null,
|
|
864
|
+
},
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
return evidence;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
private deriveAttribution(
|
|
872
|
+
operation: LocalHistoryContinuityOperation,
|
|
873
|
+
continuityEvidence: readonly Evidence[],
|
|
874
|
+
): AttributionSummary {
|
|
875
|
+
const explicitUserEvidence = continuityEvidence.filter((evidence) => evidence.evidenceKind === "explicit_user_declaration");
|
|
876
|
+
const explicitAgentEvidence = continuityEvidence.filter((evidence) => evidence.evidenceKind === "explicit_agent_declaration");
|
|
877
|
+
|
|
878
|
+
if (explicitUserEvidence.length > 0 && explicitAgentEvidence.length > 0) {
|
|
879
|
+
return {
|
|
880
|
+
actor: {
|
|
881
|
+
actorId: "unknown",
|
|
882
|
+
actorKind: "unknown",
|
|
883
|
+
displayName: "Conflicting actor signals",
|
|
884
|
+
source: "persisted_local_history.conflict",
|
|
885
|
+
authorityScope: "mixed",
|
|
886
|
+
},
|
|
887
|
+
confidence: "unknown",
|
|
888
|
+
basis: "conflicting_signals",
|
|
889
|
+
evidence: [...continuityEvidence],
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (explicitUserEvidence.length > 0 || explicitAgentEvidence.length > 0) {
|
|
894
|
+
const declarationEvidence = explicitUserEvidence.length > 0 ? explicitUserEvidence : explicitAgentEvidence;
|
|
895
|
+
const primary = declarationEvidence[0];
|
|
896
|
+
const actorKind = explicitUserEvidence.length > 0 ? "human" : "agent";
|
|
897
|
+
const actorId = typeof primary?.details["actorId"] === "string" && primary.details["actorId"].length > 0
|
|
898
|
+
? primary.details["actorId"]
|
|
899
|
+
: `${actorKind}:declared`;
|
|
900
|
+
return {
|
|
901
|
+
actor: {
|
|
902
|
+
actorId,
|
|
903
|
+
actorKind,
|
|
904
|
+
displayName: actorKind === "human" ? "Declared human actor" : "Declared agent actor",
|
|
905
|
+
source: "causal_attach.declaration",
|
|
906
|
+
authorityScope: "declared",
|
|
907
|
+
},
|
|
908
|
+
confidence: getMaximumConfidenceForEvidence(
|
|
909
|
+
declarationEvidence.map((evidence) => evidence.strength),
|
|
910
|
+
),
|
|
911
|
+
basis: "explicit_declaration",
|
|
912
|
+
evidence: continuityEvidence.filter((evidence) =>
|
|
913
|
+
evidence.evidenceKind === "explicit_user_declaration" ||
|
|
914
|
+
evidence.evidenceKind === "explicit_agent_declaration" ||
|
|
915
|
+
evidence.evidenceKind === "explicit_handoff"
|
|
916
|
+
),
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const gitEvidence = continuityEvidence.filter((evidence) =>
|
|
921
|
+
evidence.evidenceKind === "git_transition_observation" ||
|
|
922
|
+
evidence.evidenceKind === "git_hook_transition"
|
|
923
|
+
);
|
|
924
|
+
if (gitEvidence.length > 0 && (operation === "fork" || operation === "park")) {
|
|
925
|
+
return {
|
|
926
|
+
actor: {
|
|
927
|
+
actorId: "git:transition",
|
|
928
|
+
actorKind: "git",
|
|
929
|
+
displayName: "Git transition",
|
|
930
|
+
source: "persisted_local_history.checkout_transition",
|
|
931
|
+
authorityScope: "inferred",
|
|
932
|
+
},
|
|
933
|
+
confidence: getMaximumConfidenceForEvidence(
|
|
934
|
+
gitEvidence.map((evidence) => evidence.strength),
|
|
935
|
+
),
|
|
936
|
+
basis: "git_transition",
|
|
937
|
+
evidence: gitEvidence,
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
return buildUnknownAttribution();
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
private emptySummary(): PersistedLocalHistorySummaryNone {
|
|
945
|
+
return {
|
|
946
|
+
availability: "none",
|
|
947
|
+
persistence: "persisted_local_history",
|
|
948
|
+
historyPath: null,
|
|
949
|
+
totalContinuityRecords: 0,
|
|
950
|
+
active: false,
|
|
951
|
+
lastOperation: null,
|
|
952
|
+
lastObservedAt: null,
|
|
953
|
+
continuityKey: null,
|
|
954
|
+
causalSessionId: null,
|
|
955
|
+
strandId: null,
|
|
956
|
+
checkoutEpochId: null,
|
|
957
|
+
continuedFromCausalSessionId: null,
|
|
958
|
+
continuityConfidence: "unknown",
|
|
959
|
+
continuityEvidence: [],
|
|
960
|
+
attribution: buildUnknownAttribution(),
|
|
961
|
+
latestReadEvent: null,
|
|
962
|
+
latestStageEvent: null,
|
|
963
|
+
latestTransitionEvent: null,
|
|
964
|
+
preserves: PERSISTED_LOCAL_HISTORY_PRESERVES,
|
|
965
|
+
excludes: PERSISTED_LOCAL_HISTORY_EXCLUDES,
|
|
966
|
+
nextAction: "bind_workspace_to_begin_local_history",
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
private createStageEvent(
|
|
971
|
+
context: PersistedLocalHistoryContext,
|
|
972
|
+
stagedTarget: RuntimeStagedTargetFullFile,
|
|
973
|
+
attribution: AttributionSummary,
|
|
974
|
+
): Extract<CausalEvent, { eventKind: "stage" }> {
|
|
975
|
+
const footprint = {
|
|
976
|
+
paths: stagedTarget.target.selectionEntries.map((entry) => entry.path),
|
|
977
|
+
symbols: stagedTarget.target.selectionEntries.flatMap((entry) => entry.symbols),
|
|
978
|
+
regions: stagedTarget.target.selectionEntries.flatMap((entry) => entry.regions),
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
return {
|
|
982
|
+
eventId: stableId(
|
|
983
|
+
"event",
|
|
984
|
+
JSON.stringify({
|
|
985
|
+
eventKind: "stage",
|
|
986
|
+
targetId: stagedTarget.target.targetId,
|
|
987
|
+
actorId: attribution.actor.actorId,
|
|
988
|
+
confidence: attribution.confidence,
|
|
989
|
+
evidenceIds: attribution.evidence.map((evidence) => evidence.evidenceId),
|
|
990
|
+
}),
|
|
991
|
+
),
|
|
992
|
+
eventKind: "stage",
|
|
993
|
+
repoId: context.repoId,
|
|
994
|
+
worktreeId: context.worktreeId,
|
|
995
|
+
checkoutEpochId: context.checkoutEpochId,
|
|
996
|
+
workspaceOverlayId: context.workspaceOverlayId,
|
|
997
|
+
transportSessionId: context.transportSessionId,
|
|
998
|
+
workspaceSliceId: context.workspaceSliceId,
|
|
999
|
+
causalSessionId: context.causalSessionId,
|
|
1000
|
+
strandId: context.strandId,
|
|
1001
|
+
actorId: attribution.actor.actorId,
|
|
1002
|
+
confidence: attribution.confidence,
|
|
1003
|
+
evidenceIds: attribution.evidence.map((evidence) => evidence.evidenceId),
|
|
1004
|
+
attribution,
|
|
1005
|
+
footprint,
|
|
1006
|
+
occurredAt: context.observedAt,
|
|
1007
|
+
payload: {
|
|
1008
|
+
targetId: stagedTarget.target.targetId,
|
|
1009
|
+
footprint,
|
|
1010
|
+
selectionKind: stagedTarget.target.selectionKind,
|
|
1011
|
+
},
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
private createReadEvent(input: {
|
|
1016
|
+
readonly current: PersistedLocalHistoryContext;
|
|
1017
|
+
readonly attribution: AttributionSummary;
|
|
1018
|
+
readonly surface: string;
|
|
1019
|
+
readonly projection: string;
|
|
1020
|
+
readonly sourceLayer: SourceLayer;
|
|
1021
|
+
readonly reason: string;
|
|
1022
|
+
readonly footprint: CausalFootprint;
|
|
1023
|
+
}): Extract<CausalEvent, { eventKind: "read" }> {
|
|
1024
|
+
return {
|
|
1025
|
+
eventId: stableId(
|
|
1026
|
+
"event",
|
|
1027
|
+
JSON.stringify({
|
|
1028
|
+
eventKind: "read",
|
|
1029
|
+
occurredAt: input.current.observedAt,
|
|
1030
|
+
surface: input.surface,
|
|
1031
|
+
projection: input.projection,
|
|
1032
|
+
sourceLayer: input.sourceLayer,
|
|
1033
|
+
reason: input.reason,
|
|
1034
|
+
footprint: input.footprint,
|
|
1035
|
+
actorId: input.attribution.actor.actorId,
|
|
1036
|
+
confidence: input.attribution.confidence,
|
|
1037
|
+
evidenceIds: input.attribution.evidence.map((evidence) => evidence.evidenceId),
|
|
1038
|
+
}),
|
|
1039
|
+
),
|
|
1040
|
+
eventKind: "read",
|
|
1041
|
+
repoId: input.current.repoId,
|
|
1042
|
+
worktreeId: input.current.worktreeId,
|
|
1043
|
+
checkoutEpochId: input.current.checkoutEpochId,
|
|
1044
|
+
workspaceOverlayId: input.current.workspaceOverlayId,
|
|
1045
|
+
transportSessionId: input.current.transportSessionId,
|
|
1046
|
+
workspaceSliceId: input.current.workspaceSliceId,
|
|
1047
|
+
causalSessionId: input.current.causalSessionId,
|
|
1048
|
+
strandId: input.current.strandId,
|
|
1049
|
+
actorId: input.attribution.actor.actorId,
|
|
1050
|
+
confidence: input.attribution.confidence,
|
|
1051
|
+
evidenceIds: input.attribution.evidence.map((evidence) => evidence.evidenceId),
|
|
1052
|
+
attribution: input.attribution,
|
|
1053
|
+
footprint: input.footprint,
|
|
1054
|
+
occurredAt: input.current.observedAt,
|
|
1055
|
+
payload: {
|
|
1056
|
+
surface: input.surface,
|
|
1057
|
+
projection: input.projection,
|
|
1058
|
+
sourceLayer: input.sourceLayer,
|
|
1059
|
+
reason: input.reason,
|
|
1060
|
+
},
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
private createTransitionEvent(input: {
|
|
1065
|
+
readonly current: PersistedLocalHistoryContext;
|
|
1066
|
+
readonly semanticTransition: NonNullable<RepoObservation["semanticTransition"]>;
|
|
1067
|
+
readonly transition: RepoObservation["lastTransition"];
|
|
1068
|
+
readonly attribution: AttributionSummary;
|
|
1069
|
+
}): Extract<CausalEvent, { eventKind: "transition" }> {
|
|
1070
|
+
const transition = input.transition;
|
|
1071
|
+
const semanticTransition = input.semanticTransition;
|
|
1072
|
+
const footprint = {
|
|
1073
|
+
paths: [],
|
|
1074
|
+
symbols: [],
|
|
1075
|
+
regions: [],
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
return {
|
|
1079
|
+
eventId: stableId(
|
|
1080
|
+
"event",
|
|
1081
|
+
JSON.stringify({
|
|
1082
|
+
eventKind: "transition",
|
|
1083
|
+
checkoutEpochId: input.current.checkoutEpochId,
|
|
1084
|
+
workspaceOverlayId: input.current.workspaceOverlayId,
|
|
1085
|
+
semanticKind: semanticTransition.kind,
|
|
1086
|
+
authority: semanticTransition.authority,
|
|
1087
|
+
phase: semanticTransition.phase,
|
|
1088
|
+
summary: semanticTransition.summary,
|
|
1089
|
+
transitionKind: transition?.kind ?? null,
|
|
1090
|
+
fromRef: transition?.fromRef ?? null,
|
|
1091
|
+
toRef: transition?.toRef ?? null,
|
|
1092
|
+
actorId: input.attribution.actor.actorId,
|
|
1093
|
+
confidence: input.attribution.confidence,
|
|
1094
|
+
evidenceIds: input.attribution.evidence.map((evidence) => evidence.evidenceId),
|
|
1095
|
+
}),
|
|
1096
|
+
),
|
|
1097
|
+
eventKind: "transition",
|
|
1098
|
+
repoId: input.current.repoId,
|
|
1099
|
+
worktreeId: input.current.worktreeId,
|
|
1100
|
+
checkoutEpochId: input.current.checkoutEpochId,
|
|
1101
|
+
workspaceOverlayId: input.current.workspaceOverlayId,
|
|
1102
|
+
transportSessionId: input.current.transportSessionId,
|
|
1103
|
+
workspaceSliceId: input.current.workspaceSliceId,
|
|
1104
|
+
causalSessionId: input.current.causalSessionId,
|
|
1105
|
+
strandId: input.current.strandId,
|
|
1106
|
+
actorId: input.attribution.actor.actorId,
|
|
1107
|
+
confidence: input.attribution.confidence,
|
|
1108
|
+
evidenceIds: input.attribution.evidence.map((evidence) => evidence.evidenceId),
|
|
1109
|
+
attribution: input.attribution,
|
|
1110
|
+
footprint,
|
|
1111
|
+
occurredAt: input.current.observedAt,
|
|
1112
|
+
payload: {
|
|
1113
|
+
semanticKind: semanticTransition.kind,
|
|
1114
|
+
authority: semanticTransition.authority,
|
|
1115
|
+
phase: semanticTransition.phase,
|
|
1116
|
+
summary: semanticTransition.summary,
|
|
1117
|
+
transitionKind: transition?.kind ?? null,
|
|
1118
|
+
fromRef: transition?.fromRef ?? null,
|
|
1119
|
+
toRef: transition?.toRef ?? null,
|
|
1120
|
+
createdCheckoutEpochId: transition !== null ? input.current.checkoutEpochId : null,
|
|
1121
|
+
},
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
private historyPathFor(continuityKey: string): string {
|
|
1126
|
+
return path.join(this.deps.graftDir, "local-history", `${continuityKey}.json`);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
private async loadStatesForRepo(repoId: string): Promise<ContinuityState[]> {
|
|
1130
|
+
const historyDir = path.join(this.deps.graftDir, "local-history");
|
|
1131
|
+
let filenames: string[];
|
|
1132
|
+
try {
|
|
1133
|
+
filenames = await this.deps.fs.readdir(historyDir);
|
|
1134
|
+
} catch {
|
|
1135
|
+
return [];
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const states: ContinuityState[] = [];
|
|
1139
|
+
for (const filename of filenames) {
|
|
1140
|
+
if (!filename.endsWith(".json")) {
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
try {
|
|
1144
|
+
const raw = await this.deps.fs.readFile(path.join(historyDir, filename), "utf-8");
|
|
1145
|
+
const state = continuityStateSchema.parse(this.deps.codec.decode(raw));
|
|
1146
|
+
if (state.repoId === repoId) {
|
|
1147
|
+
states.push(state);
|
|
1148
|
+
}
|
|
1149
|
+
} catch {
|
|
1150
|
+
// Ignore malformed local-history artifacts.
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
return states;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
private toWorktreeHistory(state: ContinuityState): RepoConcurrencyWorktreeHistory {
|
|
1158
|
+
const effectiveRecord = findRecord(state, state.activeRecordId) ?? state.records.at(-1) ?? null;
|
|
1159
|
+
const checkoutEpochId = effectiveRecord?.checkoutEpochId ?? null;
|
|
1160
|
+
const recordsForEpoch = checkoutEpochId === null
|
|
1161
|
+
? []
|
|
1162
|
+
: state.records.filter((record) => record.checkoutEpochId === checkoutEpochId);
|
|
1163
|
+
const eventsForEpoch = checkoutEpochId === null
|
|
1164
|
+
? []
|
|
1165
|
+
: [...state.readEvents, ...state.stageEvents, ...state.transitionEvents]
|
|
1166
|
+
.filter((event) => event.checkoutEpochId === checkoutEpochId);
|
|
1167
|
+
|
|
1168
|
+
const contributorKeys = new Set<string>();
|
|
1169
|
+
const actorIds = new Set<string>();
|
|
1170
|
+
const causalSessionIds = new Set<string>();
|
|
1171
|
+
|
|
1172
|
+
for (const record of recordsForEpoch) {
|
|
1173
|
+
causalSessionIds.add(record.causalSessionId);
|
|
1174
|
+
if (record.continuedFromCausalSessionId !== null) {
|
|
1175
|
+
causalSessionIds.add(record.continuedFromCausalSessionId);
|
|
1176
|
+
}
|
|
1177
|
+
const contributorKey = buildRepoConcurrencyContributorKey({
|
|
1178
|
+
actorId: record.attribution.actor.actorId,
|
|
1179
|
+
attribution: record.attribution,
|
|
1180
|
+
causalSessionId: record.causalSessionId,
|
|
1181
|
+
transportSessionId: record.transportSessionId,
|
|
1182
|
+
});
|
|
1183
|
+
if (contributorKey !== null) {
|
|
1184
|
+
contributorKeys.add(contributorKey);
|
|
1185
|
+
}
|
|
1186
|
+
if (record.continuedFromCausalSessionId !== null) {
|
|
1187
|
+
contributorKeys.add(`causal:${record.continuedFromCausalSessionId}`);
|
|
1188
|
+
}
|
|
1189
|
+
if (record.attribution.actor.actorKind === "human" || record.attribution.actor.actorKind === "agent") {
|
|
1190
|
+
actorIds.add(record.attribution.actor.actorId);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
for (const event of eventsForEpoch) {
|
|
1195
|
+
if (event.causalSessionId !== null) {
|
|
1196
|
+
causalSessionIds.add(event.causalSessionId);
|
|
1197
|
+
}
|
|
1198
|
+
const contributorKey = buildRepoConcurrencyContributorKey({
|
|
1199
|
+
actorId: event.actorId,
|
|
1200
|
+
attribution: event.attribution,
|
|
1201
|
+
causalSessionId: event.causalSessionId,
|
|
1202
|
+
transportSessionId: event.transportSessionId,
|
|
1203
|
+
});
|
|
1204
|
+
if (contributorKey !== null) {
|
|
1205
|
+
contributorKeys.add(contributorKey);
|
|
1206
|
+
}
|
|
1207
|
+
if (event.attribution.actor.actorKind === "human" || event.attribution.actor.actorKind === "agent") {
|
|
1208
|
+
actorIds.add(event.attribution.actor.actorId);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
return {
|
|
1213
|
+
worktreeId: state.worktreeId,
|
|
1214
|
+
active: state.activeRecordId !== null,
|
|
1215
|
+
checkoutEpochId,
|
|
1216
|
+
causalSessionIds: [...causalSessionIds],
|
|
1217
|
+
actorIds: [...actorIds],
|
|
1218
|
+
contributorKeys: [...contributorKeys],
|
|
1219
|
+
explicitHandoff: recordsForEpoch.some((record) =>
|
|
1220
|
+
record.continuityEvidence.some((evidence) =>
|
|
1221
|
+
evidence.evidenceKind === "explicit_handoff"
|
|
1222
|
+
)
|
|
1223
|
+
),
|
|
1224
|
+
touches: buildRepoConcurrencyTouches(eventsForEpoch),
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
private async loadState(
|
|
1229
|
+
continuityKey: string,
|
|
1230
|
+
repoId: string,
|
|
1231
|
+
worktreeId: string,
|
|
1232
|
+
): Promise<ContinuityState> {
|
|
1233
|
+
try {
|
|
1234
|
+
const raw = await this.deps.fs.readFile(this.historyPathFor(continuityKey), "utf-8");
|
|
1235
|
+
return continuityStateSchema.parse(this.deps.codec.decode(raw));
|
|
1236
|
+
} catch {
|
|
1237
|
+
return createEmptyState(continuityKey, repoId, worktreeId);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
private async saveState(state: ContinuityState): Promise<void> {
|
|
1242
|
+
const filePath = this.historyPathFor(state.continuityKey);
|
|
1243
|
+
await this.deps.fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
1244
|
+
await this.deps.fs.writeFile(filePath, this.deps.codec.encode(state), "utf-8");
|
|
1245
|
+
}
|
|
1246
|
+
}
|