@flyingrobots/graft 0.13.0 → 0.14.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/CHANGELOG.md +40 -0
- package/dist/cli/daemon-status-model.d.ts +1 -0
- package/dist/cli/daemon-status-model.d.ts.map +1 -1
- package/dist/cli/daemon-status-model.js +1 -0
- package/dist/cli/daemon-status-model.js.map +1 -1
- package/dist/cli/daemon-status-render.d.ts.map +1 -1
- package/dist/cli/daemon-status-render.js +1 -0
- package/dist/cli/daemon-status-render.js.map +1 -1
- package/dist/contracts/output-schema-fragments.d.ts +2 -0
- package/dist/contracts/output-schema-fragments.d.ts.map +1 -1
- package/dist/contracts/output-schema-fragments.js +1 -0
- package/dist/contracts/output-schema-fragments.js.map +1 -1
- package/dist/contracts/output-schema-mcp.d.ts +1 -0
- package/dist/contracts/output-schema-mcp.d.ts.map +1 -1
- package/dist/contracts/output-schema-meta.js +1 -1
- package/dist/contracts/output-schema-meta.js.map +1 -1
- package/dist/contracts/output-schemas.d.ts.map +1 -1
- package/dist/contracts/output-schemas.js +1 -0
- package/dist/contracts/output-schemas.js.map +1 -1
- package/dist/mcp/control-plane/types.d.ts +1 -0
- package/dist/mcp/control-plane/types.d.ts.map +1 -1
- package/dist/mcp/daemon-server.d.ts +4 -0
- package/dist/mcp/daemon-server.d.ts.map +1 -1
- package/dist/mcp/daemon-server.js +37 -12
- package/dist/mcp/daemon-server.js.map +1 -1
- package/dist/mcp/daemon-session-host.d.ts +2 -2
- package/dist/mcp/daemon-session-host.d.ts.map +1 -1
- package/dist/mcp/daemon-session-host.js +130 -59
- package/dist/mcp/daemon-session-host.js.map +1 -1
- package/dist/mcp/persisted-local-history.d.ts +5 -0
- package/dist/mcp/persisted-local-history.d.ts.map +1 -1
- package/dist/mcp/persisted-local-history.js +5 -1
- package/dist/mcp/persisted-local-history.js.map +1 -1
- package/dist/mcp/server-context.d.ts.map +1 -1
- package/dist/mcp/server-context.js +28 -11
- package/dist/mcp/server-context.js.map +1 -1
- package/dist/mcp/server-invocation.d.ts.map +1 -1
- package/dist/mcp/server-invocation.js +15 -7
- package/dist/mcp/server-invocation.js.map +1 -1
- package/dist/mcp/server.d.ts +3 -2
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +22 -10
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/warp-pool.d.ts +45 -6
- package/dist/mcp/warp-pool.d.ts.map +1 -1
- package/dist/mcp/warp-pool.js +128 -19
- package/dist/mcp/warp-pool.js.map +1 -1
- package/dist/mcp/workspace-router-model.d.ts +4 -1
- package/dist/mcp/workspace-router-model.d.ts.map +1 -1
- package/dist/mcp/workspace-router-model.js.map +1 -1
- package/dist/mcp/workspace-router-runtime.d.ts +15 -5
- package/dist/mcp/workspace-router-runtime.d.ts.map +1 -1
- package/dist/mcp/workspace-router-runtime.js +47 -7
- package/dist/mcp/workspace-router-runtime.js.map +1 -1
- package/dist/mcp/workspace-router.d.ts +20 -10
- package/dist/mcp/workspace-router.d.ts.map +1 -1
- package/dist/mcp/workspace-router.js +254 -125
- package/dist/mcp/workspace-router.js.map +1 -1
- package/docs/MCP.md +54 -0
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@ import { DEFAULT_REPO_LOCAL_CAPABILITY_PROFILE, WorkspaceBindingRequiredError, W
|
|
|
11
11
|
import { resolveWorkspaceRequest, stableWorkspaceId } from "./workspace-router-resolution.js";
|
|
12
12
|
import { DEFAULT_WARP_WRITER_ID } from "../warp/writer-id.js";
|
|
13
13
|
import { GovernorTracker } from "../session/tracker.js";
|
|
14
|
-
import { boundWorkspaceStatus, buildPersistedLocalHistoryContext, buildPersistedLocalHistoryContextFromExecution, buildPersistedLocalHistoryGraphContext, buildWorkspaceCausalContext, createBoundWorkspace, createWorkspaceSlice, nextBindingSliceDir, resolveCheckoutBoundaryHookEvent, unboundWorkspaceStatus, } from "./workspace-router-runtime.js";
|
|
14
|
+
import { boundWorkspaceStatus, buildPersistedLocalHistoryContext, buildPersistedLocalHistoryContextFromExecution, buildPersistedLocalHistoryGraphContext, buildWorkspaceCausalContext, createBoundWorkspace, createWorkspaceWarpLease, createWorkspaceSlice, nextBindingSliceDir, resolveCheckoutBoundaryHookEvent, unboundWorkspaceStatus, } from "./workspace-router-runtime.js";
|
|
15
15
|
export { DEFAULT_DAEMON_CAPABILITY_PROFILE, DEFAULT_REPO_LOCAL_CAPABILITY_PROFILE, WorkspaceBindingRequiredError, WorkspaceCapabilityDeniedError, WorkspaceResolutionError, WorkspaceRouteUnauthorizedError, } from "./workspace-router-model.js";
|
|
16
16
|
export { resolveWorkspaceRequest } from "./workspace-router-resolution.js";
|
|
17
17
|
const MAX_ROUTED_BINDINGS = 8;
|
|
@@ -28,6 +28,7 @@ function workspaceCapabilityProfilesEqual(left, right) {
|
|
|
28
28
|
export class WorkspaceRouter {
|
|
29
29
|
options;
|
|
30
30
|
bindingCounter = 0;
|
|
31
|
+
executionCounter = 0;
|
|
31
32
|
sliceIdCounter = 0;
|
|
32
33
|
currentSlice;
|
|
33
34
|
currentBinding = null;
|
|
@@ -35,6 +36,8 @@ export class WorkspaceRouter {
|
|
|
35
36
|
openedWorkspaces = new Map();
|
|
36
37
|
routedBindings = new Map();
|
|
37
38
|
routedBindingInitializations = new Map();
|
|
39
|
+
bindingCommitTail = Promise.resolve();
|
|
40
|
+
warpLeaseRelease = null;
|
|
38
41
|
constructor(options) {
|
|
39
42
|
this.options = options;
|
|
40
43
|
const initialProjectRoot = options.mode === "repo_local" ? options.projectRoot : undefined;
|
|
@@ -78,14 +81,13 @@ export class WorkspaceRouter {
|
|
|
78
81
|
};
|
|
79
82
|
const currentBinding = await this.createBoundWorkspace(initialWorkspace, this.options.graftDir, DEFAULT_REPO_LOCAL_CAPABILITY_PROFILE, undefined, this.currentSlice);
|
|
80
83
|
const currentRepoState = currentBinding.slice.repoState;
|
|
81
|
-
if (currentRepoState === null)
|
|
84
|
+
if (currentRepoState === null)
|
|
82
85
|
throw new WorkspaceBindingRequiredError("workspace");
|
|
83
|
-
}
|
|
84
86
|
await currentRepoState.initialize();
|
|
85
|
-
await this.options.persistedLocalHistory.noteBinding({
|
|
87
|
+
await this.withBindingGraph(currentBinding, (currentGraph) => this.options.persistedLocalHistory.noteBinding({
|
|
86
88
|
current: this.buildPersistedLocalHistoryContext(currentBinding, currentRepoState.getState()),
|
|
87
|
-
currentGraph
|
|
88
|
-
});
|
|
89
|
+
currentGraph,
|
|
90
|
+
}));
|
|
89
91
|
this.currentBinding = currentBinding;
|
|
90
92
|
this.noteOpenedWorkspace(initialWorkspace, DEFAULT_REPO_LOCAL_CAPABILITY_PROFILE, "startup", true);
|
|
91
93
|
})();
|
|
@@ -118,25 +120,42 @@ export class WorkspaceRouter {
|
|
|
118
120
|
getPathResolver() {
|
|
119
121
|
return this.requireBinding().resolvePath;
|
|
120
122
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
async withWarp(operation) {
|
|
124
|
+
const execution = this.captureExecutionContext();
|
|
125
|
+
try {
|
|
126
|
+
return await operation(await execution.getWarp());
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
await execution.releaseWarpLease();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
releaseWarpLeases() {
|
|
133
|
+
if (this.warpLeaseRelease !== null)
|
|
134
|
+
return this.warpLeaseRelease;
|
|
135
|
+
// Bindings own metadata only. Already admitted operations release their
|
|
136
|
+
// own capabilities; retirement must not revoke those capabilities.
|
|
137
|
+
const pending = [this.initialization ?? Promise.resolve(), this.bindingCommitTail, ...this.routedBindingInitializations.values()];
|
|
138
|
+
this.warpLeaseRelease = Promise.allSettled(pending).then(() => undefined);
|
|
139
|
+
return this.warpLeaseRelease;
|
|
123
140
|
}
|
|
124
|
-
async observeRepoState() {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
141
|
+
async observeRepoState(execution = null) {
|
|
142
|
+
const scope = this.resolveWorkspaceHistoryScope(execution);
|
|
143
|
+
if (scope === null) {
|
|
144
|
+
throw new WorkspaceBindingRequiredError("workspace");
|
|
145
|
+
}
|
|
146
|
+
const previousObservation = scope.repoState.getState();
|
|
147
|
+
const nextObservation = await scope.repoState.observe();
|
|
129
148
|
const checkoutBoundaryHookEvent = previousObservation.checkoutEpoch !== nextObservation.checkoutEpoch
|
|
130
|
-
? await this.resolveCheckoutBoundaryHookEvent(
|
|
149
|
+
? await this.resolveCheckoutBoundaryHookEvent(scope, previousObservation.observedAt, nextObservation)
|
|
131
150
|
: null;
|
|
132
|
-
const previousContext =
|
|
133
|
-
const nextContext =
|
|
151
|
+
const previousContext = scope.buildHistoryContext(previousObservation);
|
|
152
|
+
const nextContext = scope.buildHistoryContext(nextObservation, checkoutBoundaryHookEvent);
|
|
134
153
|
if (previousContext.checkoutEpochId !== nextContext.checkoutEpochId) {
|
|
135
|
-
await this.options.persistedLocalHistory.noteCheckoutBoundary({
|
|
154
|
+
await scope.withGraph((graph) => this.options.persistedLocalHistory.noteCheckoutBoundary({
|
|
136
155
|
previous: previousContext,
|
|
137
156
|
current: nextContext,
|
|
138
|
-
graph
|
|
139
|
-
});
|
|
157
|
+
graph,
|
|
158
|
+
}));
|
|
140
159
|
}
|
|
141
160
|
}
|
|
142
161
|
getRepoState() {
|
|
@@ -231,9 +250,9 @@ export class WorkspaceRouter {
|
|
|
231
250
|
...(result.error !== undefined ? { error: result.error } : {}),
|
|
232
251
|
};
|
|
233
252
|
}
|
|
234
|
-
async getPersistedLocalHistorySummary() {
|
|
235
|
-
const
|
|
236
|
-
if (
|
|
253
|
+
async getPersistedLocalHistorySummary(execution = null) {
|
|
254
|
+
const scope = this.resolveWorkspaceHistoryScope(execution);
|
|
255
|
+
if (scope === null) {
|
|
237
256
|
return {
|
|
238
257
|
availability: "none",
|
|
239
258
|
persistence: "persisted_local_history",
|
|
@@ -281,43 +300,43 @@ export class WorkspaceRouter {
|
|
|
281
300
|
nextAction: "bind_workspace_to_begin_local_history",
|
|
282
301
|
};
|
|
283
302
|
}
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
303
|
+
const repoState = scope.repoState.getState();
|
|
304
|
+
const causalContext = scope.getCausalContext();
|
|
305
|
+
return scope.withGraph(async (graph) => {
|
|
306
|
+
let summary = await this.options.persistedLocalHistory.summarize(scope.status, causalContext, graph);
|
|
307
|
+
if (repoState.semanticTransition !== null) {
|
|
308
|
+
await this.options.persistedLocalHistory.noteSemanticTransitionObservation({
|
|
309
|
+
current: scope.buildHistoryContext(repoState),
|
|
310
|
+
semanticTransition: repoState.semanticTransition,
|
|
311
|
+
transition: repoState.lastTransition,
|
|
312
|
+
attribution: summary.attribution,
|
|
313
|
+
graph,
|
|
314
|
+
});
|
|
315
|
+
summary = await this.options.persistedLocalHistory.summarize(scope.status, causalContext, graph);
|
|
316
|
+
}
|
|
317
|
+
const stagedTarget = buildRuntimeStagedTarget(scope.status, causalContext, repoState, summary.attribution);
|
|
318
|
+
if (stagedTarget.availability === "full_file") {
|
|
319
|
+
await this.options.persistedLocalHistory.noteStageObservation({
|
|
320
|
+
current: scope.buildHistoryContext(repoState),
|
|
321
|
+
stagedTarget,
|
|
322
|
+
attribution: summary.attribution,
|
|
323
|
+
graph,
|
|
324
|
+
});
|
|
325
|
+
return this.options.persistedLocalHistory.summarize(scope.status, causalContext, graph);
|
|
326
|
+
}
|
|
327
|
+
return summary;
|
|
328
|
+
});
|
|
310
329
|
}
|
|
311
|
-
async getRepoConcurrencySummary() {
|
|
312
|
-
const
|
|
313
|
-
if (
|
|
330
|
+
async getRepoConcurrencySummary(execution = null) {
|
|
331
|
+
const scope = this.resolveWorkspaceHistoryScope(execution);
|
|
332
|
+
if (scope === null) {
|
|
314
333
|
return null;
|
|
315
334
|
}
|
|
316
|
-
return this.options.persistedLocalHistory.summarizeRepoConcurrency(
|
|
335
|
+
return scope.withGraph((graph) => this.options.persistedLocalHistory.summarizeRepoConcurrency(scope.status, graph));
|
|
317
336
|
}
|
|
318
|
-
async getPersistedLocalActivityWindow(limit) {
|
|
319
|
-
const
|
|
320
|
-
if (
|
|
337
|
+
async getPersistedLocalActivityWindow(limit, execution = null) {
|
|
338
|
+
const scope = this.resolveWorkspaceHistoryScope(execution);
|
|
339
|
+
if (scope === null) {
|
|
321
340
|
return {
|
|
322
341
|
historyPath: null,
|
|
323
342
|
limit,
|
|
@@ -326,36 +345,49 @@ export class WorkspaceRouter {
|
|
|
326
345
|
items: [],
|
|
327
346
|
};
|
|
328
347
|
}
|
|
329
|
-
await this.getPersistedLocalHistorySummary();
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
const causalContext = this.buildCausalContext(binding, repoState);
|
|
333
|
-
const graph = await this.buildPersistedLocalHistoryGraphContext(binding);
|
|
334
|
-
return this.options.persistedLocalHistory.listRecentActivity(status, causalContext, limit, graph);
|
|
348
|
+
await this.getPersistedLocalHistorySummary(execution);
|
|
349
|
+
const causalContext = scope.getCausalContext();
|
|
350
|
+
return scope.withGraph((graph) => this.options.persistedLocalHistory.listRecentActivity(scope.status, causalContext, limit, graph));
|
|
335
351
|
}
|
|
336
|
-
getWorkspaceOverlayFooting() {
|
|
337
|
-
const
|
|
338
|
-
if (
|
|
352
|
+
getWorkspaceOverlayFooting(execution = null) {
|
|
353
|
+
const scope = this.resolveWorkspaceHistoryScope(execution);
|
|
354
|
+
if (scope === null) {
|
|
339
355
|
return Promise.resolve(null);
|
|
340
356
|
}
|
|
341
|
-
return buildRuntimeWorkspaceOverlayFooting(this.options.fs, this.options.git,
|
|
357
|
+
return buildRuntimeWorkspaceOverlayFooting(this.options.fs, this.options.git, scope.worktreeRoot, scope.gitCommonDir, scope.repoState.getState());
|
|
342
358
|
}
|
|
343
359
|
async noteReadObservation(toolName, args, result, execution) {
|
|
344
|
-
const
|
|
360
|
+
const ownedExecution = execution === null || execution === undefined
|
|
361
|
+
? this.captureCurrentExecutionContext()
|
|
362
|
+
: null;
|
|
363
|
+
const active = execution ?? ownedExecution;
|
|
345
364
|
if (active === null) {
|
|
346
365
|
return;
|
|
347
366
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
367
|
+
try {
|
|
368
|
+
const readObservation = buildWorkspaceReadObservation(active, toolName, args, result);
|
|
369
|
+
if (readObservation === null) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const summary = await this.options.persistedLocalHistory.summarize(active.status, active.getCausalContext(), await this.buildPersistedLocalHistoryGraphContextFromExecution(active));
|
|
373
|
+
await this.options.persistedLocalHistory.noteReadObservation({
|
|
374
|
+
current: this.buildPersistedLocalHistoryContextFromExecution(active, active.repoState.getState()),
|
|
375
|
+
attribution: summary.attribution,
|
|
376
|
+
graph: await this.buildPersistedLocalHistoryGraphContextFromExecution(active),
|
|
377
|
+
...readObservation,
|
|
378
|
+
});
|
|
351
379
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
380
|
+
finally {
|
|
381
|
+
await ownedExecution?.releaseWarpLease();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
getRuntimeCausalContext() {
|
|
385
|
+
const binding = this.currentBinding;
|
|
386
|
+
const repoState = binding?.slice.repoState;
|
|
387
|
+
if (binding === null || repoState === null || repoState === undefined) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
return this.buildCausalContext(binding, repoState.getState());
|
|
359
391
|
}
|
|
360
392
|
captureExecutionContext() {
|
|
361
393
|
const binding = this.requireBinding();
|
|
@@ -377,8 +409,20 @@ export class WorkspaceRouter {
|
|
|
377
409
|
? DEFAULT_REPO_LOCAL_CAPABILITY_PROFILE
|
|
378
410
|
: (await this.options.authorizationPolicy?.getCapabilityProfile(resolved)) ?? null;
|
|
379
411
|
if (capabilityProfile === null) {
|
|
380
|
-
this.routedBindings.
|
|
381
|
-
|
|
412
|
+
const cached = this.routedBindings.get(resolved.worktreeId);
|
|
413
|
+
if (cached !== undefined) {
|
|
414
|
+
this.disposeRoutedBinding(cached);
|
|
415
|
+
}
|
|
416
|
+
const initializing = this.routedBindingInitializations.get(resolved.worktreeId);
|
|
417
|
+
if (initializing !== undefined) {
|
|
418
|
+
if (this.routedBindingInitializations.get(resolved.worktreeId) === initializing) {
|
|
419
|
+
this.routedBindingInitializations.delete(resolved.worktreeId);
|
|
420
|
+
}
|
|
421
|
+
const initialized = await initializing.catch(() => null);
|
|
422
|
+
if (initialized !== null && initialized !== cached) {
|
|
423
|
+
this.disposeRoutedBinding(initialized);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
382
426
|
throw new WorkspaceRouteUnauthorizedError(resolved.worktreeRoot);
|
|
383
427
|
}
|
|
384
428
|
if (this.currentBinding !== null
|
|
@@ -418,9 +462,8 @@ export class WorkspaceRouter {
|
|
|
418
462
|
await this.options.fs.mkdir(routeDir, { recursive: true });
|
|
419
463
|
const binding = await this.createBoundWorkspace(resolved, routeDir, capabilityProfile, undefined);
|
|
420
464
|
const repoState = binding.slice.repoState;
|
|
421
|
-
if (repoState === null)
|
|
465
|
+
if (repoState === null)
|
|
422
466
|
throw new WorkspaceBindingRequiredError("workspace");
|
|
423
|
-
}
|
|
424
467
|
await repoState.initialize();
|
|
425
468
|
return binding;
|
|
426
469
|
}
|
|
@@ -431,21 +474,50 @@ export class WorkspaceRouter {
|
|
|
431
474
|
&& workspaceCapabilityProfilesEqual(binding.capabilityProfile, capabilityProfile);
|
|
432
475
|
}
|
|
433
476
|
noteRoutedBinding(binding) {
|
|
477
|
+
const replaced = this.routedBindings.get(binding.worktreeId);
|
|
434
478
|
this.routedBindings.delete(binding.worktreeId);
|
|
435
479
|
this.routedBindings.set(binding.worktreeId, binding);
|
|
480
|
+
if (replaced !== undefined && replaced !== binding) {
|
|
481
|
+
this.disposeRoutedBinding(replaced);
|
|
482
|
+
}
|
|
436
483
|
while (this.routedBindings.size > MAX_ROUTED_BINDINGS) {
|
|
437
|
-
const
|
|
438
|
-
if (
|
|
484
|
+
const oldestKey = this.routedBindings.keys().next().value;
|
|
485
|
+
if (oldestKey === undefined) {
|
|
439
486
|
return;
|
|
440
487
|
}
|
|
441
|
-
this.routedBindings.
|
|
488
|
+
const evicted = this.routedBindings.get(oldestKey);
|
|
489
|
+
if (evicted !== undefined) {
|
|
490
|
+
this.disposeRoutedBinding(evicted);
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
this.routedBindings.delete(oldestKey);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
disposeRoutedBinding(binding) {
|
|
498
|
+
if (this.routedBindings.get(binding.worktreeId) === binding) {
|
|
499
|
+
this.routedBindings.delete(binding.worktreeId);
|
|
442
500
|
}
|
|
443
501
|
}
|
|
444
502
|
buildExecutionContext(binding, workspaceRoute = null) {
|
|
503
|
+
if (this.warpLeaseRelease !== null) {
|
|
504
|
+
throw new Error("workspace WARP leases have already been released");
|
|
505
|
+
}
|
|
445
506
|
const repoState = binding.slice.repoState;
|
|
446
507
|
if (repoState === null) {
|
|
447
508
|
throw new WorkspaceBindingRequiredError("workspace");
|
|
448
509
|
}
|
|
510
|
+
const warpLease = createWorkspaceWarpLease({
|
|
511
|
+
repoId: binding.repoId,
|
|
512
|
+
worktreeRoot: binding.worktreeRoot,
|
|
513
|
+
writerId: binding.warpWriterId,
|
|
514
|
+
ownerId: [
|
|
515
|
+
binding.transportSessionId,
|
|
516
|
+
"execution",
|
|
517
|
+
String(++this.executionCounter).padStart(6, "0"),
|
|
518
|
+
].join(":"),
|
|
519
|
+
warpPool: this.options.warpPool,
|
|
520
|
+
});
|
|
449
521
|
return {
|
|
450
522
|
sliceId: binding.slice.sliceId,
|
|
451
523
|
repoId: binding.repoId,
|
|
@@ -458,14 +530,17 @@ export class WorkspaceRouter {
|
|
|
458
530
|
resolvePath: binding.resolvePath,
|
|
459
531
|
capabilityProfile: binding.capabilityProfile,
|
|
460
532
|
warpWriterId: binding.warpWriterId,
|
|
461
|
-
getCausalContext: () =>
|
|
533
|
+
getCausalContext: (observation) => {
|
|
534
|
+
return this.buildCausalContext(binding, observation ?? repoState.getState());
|
|
535
|
+
},
|
|
462
536
|
status: boundWorkspaceStatus(this.options.mode, binding),
|
|
463
537
|
governor: binding.slice.governor,
|
|
464
538
|
cache: binding.slice.cache,
|
|
465
539
|
metrics: binding.slice.metrics,
|
|
466
540
|
graftDir: binding.slice.graftDir,
|
|
467
541
|
repoState,
|
|
468
|
-
getWarp:
|
|
542
|
+
getWarp: () => warpLease.getWarp(),
|
|
543
|
+
releaseWarpLease: () => warpLease.release(),
|
|
469
544
|
};
|
|
470
545
|
}
|
|
471
546
|
async bind(request, actionName) {
|
|
@@ -484,51 +559,51 @@ export class WorkspaceRouter {
|
|
|
484
559
|
}
|
|
485
560
|
return this.bindInternal("rebind", request, actionName);
|
|
486
561
|
}
|
|
487
|
-
async declareAttach(declaration) {
|
|
488
|
-
const
|
|
489
|
-
if (
|
|
562
|
+
async declareAttach(declaration, execution = null) {
|
|
563
|
+
const scope = this.resolveWorkspaceHistoryScope(execution);
|
|
564
|
+
if (scope === null) {
|
|
490
565
|
return {
|
|
491
566
|
ok: false,
|
|
492
567
|
action: "attach",
|
|
493
568
|
...this.getStatus(),
|
|
494
|
-
persistedLocalHistory: await this.getPersistedLocalHistorySummary(),
|
|
569
|
+
persistedLocalHistory: await this.getPersistedLocalHistorySummary(execution),
|
|
495
570
|
errorCode: "UNBOUND_SESSION",
|
|
496
571
|
error: "causal_attach requires an active workspace binding.",
|
|
497
572
|
};
|
|
498
573
|
}
|
|
499
574
|
try {
|
|
500
|
-
await this.options.persistedLocalHistory.declareAttach({
|
|
501
|
-
current:
|
|
575
|
+
await scope.withGraph((graph) => this.options.persistedLocalHistory.declareAttach({
|
|
576
|
+
current: scope.buildHistoryContext(scope.repoState.getState()),
|
|
502
577
|
declaration,
|
|
503
|
-
graph
|
|
504
|
-
});
|
|
578
|
+
graph,
|
|
579
|
+
}));
|
|
505
580
|
}
|
|
506
581
|
catch (error) {
|
|
507
582
|
if (error instanceof PersistedLocalHistoryAttachUnavailableError) {
|
|
508
583
|
const sharedAttachSource = this.options.sharedAttachPolicy?.resolveSharedAttachSource({
|
|
509
584
|
sessionId: this.options.transportSessionId,
|
|
510
|
-
repoId:
|
|
511
|
-
worktreeId:
|
|
585
|
+
repoId: scope.repoId,
|
|
586
|
+
worktreeId: scope.worktreeId,
|
|
512
587
|
}) ?? null;
|
|
513
588
|
if (sharedAttachSource !== null) {
|
|
514
|
-
await this.options.persistedLocalHistory.declareSharedAttach({
|
|
515
|
-
current:
|
|
589
|
+
await scope.withGraph((graph) => this.options.persistedLocalHistory.declareSharedAttach({
|
|
590
|
+
current: scope.buildHistoryContext(scope.repoState.getState()),
|
|
516
591
|
declaration,
|
|
517
592
|
source: sharedAttachSource,
|
|
518
|
-
graph
|
|
519
|
-
});
|
|
593
|
+
graph,
|
|
594
|
+
}));
|
|
520
595
|
return {
|
|
521
596
|
ok: true,
|
|
522
597
|
action: "attach",
|
|
523
|
-
...
|
|
524
|
-
persistedLocalHistory: await this.getPersistedLocalHistorySummary(),
|
|
598
|
+
...scope.status,
|
|
599
|
+
persistedLocalHistory: await this.getPersistedLocalHistorySummary(execution),
|
|
525
600
|
};
|
|
526
601
|
}
|
|
527
602
|
return {
|
|
528
603
|
ok: false,
|
|
529
604
|
action: "attach",
|
|
530
|
-
...
|
|
531
|
-
persistedLocalHistory: await this.getPersistedLocalHistorySummary(),
|
|
605
|
+
...scope.status,
|
|
606
|
+
persistedLocalHistory: await this.getPersistedLocalHistorySummary(execution),
|
|
532
607
|
errorCode: error.code,
|
|
533
608
|
error: error.message,
|
|
534
609
|
};
|
|
@@ -538,8 +613,8 @@ export class WorkspaceRouter {
|
|
|
538
613
|
return {
|
|
539
614
|
ok: true,
|
|
540
615
|
action: "attach",
|
|
541
|
-
...
|
|
542
|
-
persistedLocalHistory: await this.getPersistedLocalHistorySummary(),
|
|
616
|
+
...scope.status,
|
|
617
|
+
persistedLocalHistory: await this.getPersistedLocalHistorySummary(execution),
|
|
543
618
|
};
|
|
544
619
|
}
|
|
545
620
|
async bindInternal(action, request, actionName, options = {}) {
|
|
@@ -569,27 +644,31 @@ export class WorkspaceRouter {
|
|
|
569
644
|
error: `Workspace ${resolved.worktreeRoot} is not authorized for daemon binding. Call workspace_authorize first.`,
|
|
570
645
|
};
|
|
571
646
|
}
|
|
647
|
+
return this.enqueueBindingCommit(() => this.commitBinding(action, resolved, sliceDir, capabilityProfile, actionName, options));
|
|
648
|
+
}
|
|
649
|
+
enqueueBindingCommit(operation) {
|
|
650
|
+
const result = this.bindingCommitTail.then(operation);
|
|
651
|
+
this.bindingCommitTail = result.then(() => undefined, () => undefined);
|
|
652
|
+
return result;
|
|
653
|
+
}
|
|
654
|
+
async commitBinding(action, resolved, sliceDir, capabilityProfile, actionName, options) {
|
|
655
|
+
const previousBinding = this.currentBinding;
|
|
572
656
|
const nextBinding = await this.createBoundWorkspace(resolved, sliceDir, capabilityProfile, actionName);
|
|
573
657
|
const nextRepoState = nextBinding.slice.repoState;
|
|
574
|
-
if (nextRepoState === null)
|
|
658
|
+
if (nextRepoState === null)
|
|
575
659
|
throw new WorkspaceBindingRequiredError("workspace");
|
|
576
|
-
}
|
|
577
660
|
await nextRepoState.initialize();
|
|
578
|
-
const previousBinding = this.currentBinding;
|
|
579
661
|
const previousRepoState = previousBinding?.slice.repoState;
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
});
|
|
590
|
-
if (this.options.mode === "daemon") {
|
|
662
|
+
const current = this.buildPersistedLocalHistoryContext(nextBinding, nextRepoState.getState());
|
|
663
|
+
const previous = previousBinding === null || previousRepoState == null
|
|
664
|
+
? null
|
|
665
|
+
: this.buildPersistedLocalHistoryContext(previousBinding, previousRepoState.getState());
|
|
666
|
+
if (previous !== null && (previous.repoId !== current.repoId || previous.worktreeId !== current.worktreeId)) {
|
|
667
|
+
await this.withBindingGraph(previousBinding, (previousGraph) => this.options.persistedLocalHistory.noteBindingDeparture({ current, previous, previousGraph }));
|
|
668
|
+
}
|
|
669
|
+
await this.withBindingGraph(nextBinding, (currentGraph) => this.options.persistedLocalHistory.noteBinding({ current, previous, currentGraph }));
|
|
670
|
+
if (this.options.mode === "daemon")
|
|
591
671
|
await this.options.authorizationPolicy?.noteBound(resolved);
|
|
592
|
-
}
|
|
593
672
|
this.currentBinding = nextBinding;
|
|
594
673
|
this.currentSlice = nextBinding.slice;
|
|
595
674
|
this.noteOpenedWorkspace(resolved, capabilityProfile, options.openedSource ?? (this.options.mode === "daemon" ? "daemon_authorized" : "session_opened"), true);
|
|
@@ -608,6 +687,8 @@ export class WorkspaceRouter {
|
|
|
608
687
|
git: this.options.git,
|
|
609
688
|
nextSliceId: `slice-${String(++this.sliceIdCounter).padStart(4, "0")}`,
|
|
610
689
|
});
|
|
690
|
+
if (this.warpLeaseRelease !== null)
|
|
691
|
+
throw new Error("workspace graph admission has closed");
|
|
611
692
|
return createBoundWorkspace({
|
|
612
693
|
resolved,
|
|
613
694
|
graftDir,
|
|
@@ -617,7 +698,6 @@ export class WorkspaceRouter {
|
|
|
617
698
|
fs: this.options.fs,
|
|
618
699
|
transportSessionId: this.options.transportSessionId,
|
|
619
700
|
warpWriterId: this.options.warpWriterId ?? DEFAULT_WARP_WRITER_ID,
|
|
620
|
-
warpPool: this.options.warpPool,
|
|
621
701
|
});
|
|
622
702
|
}
|
|
623
703
|
requireBinding() {
|
|
@@ -693,11 +773,12 @@ export class WorkspaceRouter {
|
|
|
693
773
|
hookEvent,
|
|
694
774
|
});
|
|
695
775
|
}
|
|
696
|
-
buildPersistedLocalHistoryContextFromExecution(execution, observation) {
|
|
776
|
+
buildPersistedLocalHistoryContextFromExecution(execution, observation, hookEvent = null) {
|
|
697
777
|
return buildPersistedLocalHistoryContextFromExecution({
|
|
698
778
|
persistedLocalHistory: this.options.persistedLocalHistory,
|
|
699
779
|
execution,
|
|
700
780
|
observation,
|
|
781
|
+
hookEvent,
|
|
701
782
|
});
|
|
702
783
|
}
|
|
703
784
|
async resolveCheckoutBoundaryHookEvent(binding, previousObservedAt, observation) {
|
|
@@ -715,11 +796,59 @@ export class WorkspaceRouter {
|
|
|
715
796
|
}
|
|
716
797
|
return this.captureExecutionContext();
|
|
717
798
|
}
|
|
718
|
-
|
|
719
|
-
if (
|
|
799
|
+
resolveWorkspaceHistoryScope(execution) {
|
|
800
|
+
if (execution !== null) {
|
|
801
|
+
return {
|
|
802
|
+
status: execution.status,
|
|
803
|
+
repoId: execution.repoId,
|
|
804
|
+
worktreeId: execution.worktreeId,
|
|
805
|
+
worktreeRoot: execution.worktreeRoot,
|
|
806
|
+
gitCommonDir: execution.gitCommonDir,
|
|
807
|
+
repoState: execution.repoState,
|
|
808
|
+
getCausalContext: () => execution.getCausalContext(),
|
|
809
|
+
buildHistoryContext: (observation, hookEvent) => {
|
|
810
|
+
return this.buildPersistedLocalHistoryContextFromExecution(execution, observation, hookEvent);
|
|
811
|
+
},
|
|
812
|
+
withGraph: async (operation) => operation(await this.buildPersistedLocalHistoryGraphContextFromExecution(execution)),
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
const binding = this.currentBinding;
|
|
816
|
+
const repoState = binding?.slice.repoState;
|
|
817
|
+
if (binding === null || repoState === null || repoState === undefined) {
|
|
720
818
|
return null;
|
|
721
819
|
}
|
|
722
|
-
return
|
|
820
|
+
return {
|
|
821
|
+
status: boundWorkspaceStatus(this.options.mode, binding),
|
|
822
|
+
repoId: binding.repoId,
|
|
823
|
+
worktreeId: binding.worktreeId,
|
|
824
|
+
worktreeRoot: binding.worktreeRoot,
|
|
825
|
+
gitCommonDir: binding.gitCommonDir,
|
|
826
|
+
repoState,
|
|
827
|
+
getCausalContext: () => this.buildCausalContext(binding, repoState.getState()),
|
|
828
|
+
buildHistoryContext: (observation, hookEvent) => {
|
|
829
|
+
return this.buildPersistedLocalHistoryContext(binding, observation, hookEvent);
|
|
830
|
+
},
|
|
831
|
+
withGraph: (operation) => this.withBindingGraph(binding, operation),
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
async withBindingGraph(binding, operation) {
|
|
835
|
+
if (this.warpLeaseRelease !== null)
|
|
836
|
+
throw new Error("workspace graph admission has closed");
|
|
837
|
+
if (binding === null || this.options.persistedLocalHistoryGraph === false)
|
|
838
|
+
return operation(null);
|
|
839
|
+
const lease = createWorkspaceWarpLease({
|
|
840
|
+
repoId: binding.repoId,
|
|
841
|
+
worktreeRoot: binding.worktreeRoot,
|
|
842
|
+
writerId: binding.warpWriterId,
|
|
843
|
+
ownerId: binding.transportSessionId + ":history:" + String(++this.executionCounter),
|
|
844
|
+
warpPool: this.options.warpPool,
|
|
845
|
+
});
|
|
846
|
+
try {
|
|
847
|
+
return await operation(await buildPersistedLocalHistoryGraphContext(binding.worktreeRoot, () => lease.getWarp()));
|
|
848
|
+
}
|
|
849
|
+
finally {
|
|
850
|
+
await lease.release();
|
|
851
|
+
}
|
|
723
852
|
}
|
|
724
853
|
async buildPersistedLocalHistoryGraphContextFromExecution(execution) {
|
|
725
854
|
if (this.options.persistedLocalHistoryGraph === false) {
|