@flyingrobots/graft 0.12.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 +105 -0
- package/dist/adapters/fake-echo-kernel-transport.d.ts.map +1 -1
- package/dist/adapters/fake-echo-kernel-transport.js +12 -0
- package/dist/adapters/fake-echo-kernel-transport.js.map +1 -1
- 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/capabilities.d.ts +7 -0
- package/dist/contracts/capabilities.d.ts.map +1 -1
- package/dist/contracts/capabilities.js +28 -0
- package/dist/contracts/capabilities.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.d.ts +12 -1
- package/dist/contracts/output-schema-meta.d.ts.map +1 -1
- package/dist/contracts/output-schema-meta.js +33 -9
- 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 +14 -4
- package/dist/contracts/output-schemas.js.map +1 -1
- package/dist/echo/structural-history-client.d.ts.map +1 -1
- package/dist/echo/structural-history-client.js +12 -0
- package/dist/echo/structural-history-client.js.map +1 -1
- package/dist/echo/structural-reading-adapter.d.ts.map +1 -1
- package/dist/echo/structural-reading-adapter.js +13 -2
- package/dist/echo/structural-reading-adapter.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-control-plane.d.ts.map +1 -1
- package/dist/mcp/daemon-control-plane.js +6 -1
- package/dist/mcp/daemon-control-plane.js.map +1 -1
- package/dist/mcp/daemon-scheduler-config.d.ts +53 -0
- package/dist/mcp/daemon-scheduler-config.d.ts.map +1 -0
- package/dist/mcp/daemon-scheduler-config.js +33 -0
- package/dist/mcp/daemon-scheduler-config.js.map +1 -0
- 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 +39 -13
- 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/receipt.d.ts +3 -0
- package/dist/mcp/receipt.d.ts.map +1 -1
- package/dist/mcp/receipt.js +4 -0
- package/dist/mcp/receipt.js.map +1 -1
- package/dist/mcp/repo-tool-job.d.ts +2 -1
- package/dist/mcp/repo-tool-job.d.ts.map +1 -1
- package/dist/mcp/repo-tool-job.js.map +1 -1
- package/dist/mcp/repo-tool-worker-context.d.ts.map +1 -1
- package/dist/mcp/repo-tool-worker-context.js +1 -0
- package/dist/mcp/repo-tool-worker-context.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 +21 -7
- package/dist/mcp/server-invocation.js.map +1 -1
- package/dist/mcp/server-tool-access.d.ts.map +1 -1
- package/dist/mcp/server-tool-access.js +2 -12
- package/dist/mcp/server-tool-access.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 +25 -11
- 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 +16 -1
- package/dist/mcp/workspace-router-model.d.ts.map +1 -1
- package/dist/mcp/workspace-router-model.js +8 -0
- package/dist/mcp/workspace-router-model.js.map +1 -1
- package/dist/mcp/workspace-router-resolution.d.ts.map +1 -1
- package/dist/mcp/workspace-router-resolution.js +19 -1
- package/dist/mcp/workspace-router-resolution.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 +21 -11
- package/dist/mcp/workspace-router.d.ts.map +1 -1
- package/dist/mcp/workspace-router.js +273 -136
- package/dist/mcp/workspace-router.js.map +1 -1
- package/dist/ports/echo-kernel-transport.d.ts.map +1 -1
- package/dist/ports/echo-kernel-transport.js +12 -0
- package/dist/ports/echo-kernel-transport.js.map +1 -1
- package/docs/MCP.md +68 -0
- package/docs/SETUP.md +27 -0
- package/package.json +5 -2
|
@@ -7,12 +7,12 @@ import { RepoStateTracker } from "./repo-state.js";
|
|
|
7
7
|
import { buildRuntimeStagedTarget } from "./runtime-staged-target.js";
|
|
8
8
|
import { buildRuntimeWorkspaceOverlayFooting, } from "./runtime-workspace-overlay.js";
|
|
9
9
|
import { buildWorkspaceReadObservation } from "./workspace-read-observation.js";
|
|
10
|
-
import { DEFAULT_REPO_LOCAL_CAPABILITY_PROFILE, WorkspaceBindingRequiredError, WorkspaceRouteUnauthorizedError, } from "./workspace-router-model.js";
|
|
10
|
+
import { DEFAULT_REPO_LOCAL_CAPABILITY_PROFILE, WorkspaceBindingRequiredError, WorkspaceResolutionError, WorkspaceRouteUnauthorizedError, } from "./workspace-router-model.js";
|
|
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";
|
|
15
|
-
export { DEFAULT_DAEMON_CAPABILITY_PROFILE, DEFAULT_REPO_LOCAL_CAPABILITY_PROFILE, WorkspaceBindingRequiredError, WorkspaceCapabilityDeniedError, WorkspaceRouteUnauthorizedError, } from "./workspace-router-model.js";
|
|
14
|
+
import { boundWorkspaceStatus, buildPersistedLocalHistoryContext, buildPersistedLocalHistoryContextFromExecution, buildPersistedLocalHistoryGraphContext, buildWorkspaceCausalContext, createBoundWorkspace, createWorkspaceWarpLease, createWorkspaceSlice, nextBindingSliceDir, resolveCheckoutBoundaryHookEvent, unboundWorkspaceStatus, } from "./workspace-router-runtime.js";
|
|
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;
|
|
18
18
|
function workspaceCapabilityProfilesEqual(left, right) {
|
|
@@ -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
|
+
}
|
|
123
131
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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;
|
|
140
|
+
}
|
|
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
|
+
});
|
|
379
|
+
}
|
|
380
|
+
finally {
|
|
381
|
+
await ownedExecution?.releaseWarpLease();
|
|
351
382
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
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();
|
|
@@ -364,31 +396,50 @@ export class WorkspaceRouter {
|
|
|
364
396
|
async captureExecutionContextForWorkspace(request) {
|
|
365
397
|
const resolved = await resolveWorkspaceRequest(this.options.git, request);
|
|
366
398
|
if ("code" in resolved) {
|
|
367
|
-
throw new
|
|
368
|
-
}
|
|
399
|
+
throw new WorkspaceResolutionError(resolved.code, resolved.message);
|
|
400
|
+
}
|
|
401
|
+
const workspaceRoute = Object.freeze({
|
|
402
|
+
route: "explicit_cwd",
|
|
403
|
+
requestedRoot: path.resolve(request.cwd),
|
|
404
|
+
resolvedRoot: resolved.worktreeRoot,
|
|
405
|
+
repoId: resolved.repoId,
|
|
406
|
+
worktreeId: resolved.worktreeId,
|
|
407
|
+
});
|
|
369
408
|
const capabilityProfile = this.options.mode === "repo_local"
|
|
370
409
|
? DEFAULT_REPO_LOCAL_CAPABILITY_PROFILE
|
|
371
410
|
: (await this.options.authorizationPolicy?.getCapabilityProfile(resolved)) ?? null;
|
|
372
411
|
if (capabilityProfile === null) {
|
|
373
|
-
this.routedBindings.
|
|
374
|
-
|
|
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
|
+
}
|
|
375
426
|
throw new WorkspaceRouteUnauthorizedError(resolved.worktreeRoot);
|
|
376
427
|
}
|
|
377
|
-
if (this.currentBinding
|
|
378
|
-
&&
|
|
379
|
-
return this.
|
|
428
|
+
if (this.currentBinding !== null
|
|
429
|
+
&& this.routedBindingMatches(this.currentBinding, resolved, capabilityProfile)) {
|
|
430
|
+
return this.buildExecutionContext(this.currentBinding, workspaceRoute);
|
|
380
431
|
}
|
|
381
432
|
const existing = this.routedBindings.get(resolved.worktreeId);
|
|
382
433
|
if (existing !== undefined && this.routedBindingMatches(existing, resolved, capabilityProfile)) {
|
|
383
434
|
this.noteRoutedBinding(existing);
|
|
384
|
-
return this.buildExecutionContext(existing);
|
|
435
|
+
return this.buildExecutionContext(existing, workspaceRoute);
|
|
385
436
|
}
|
|
386
437
|
const initializing = this.routedBindingInitializations.get(resolved.worktreeId);
|
|
387
438
|
if (initializing !== undefined) {
|
|
388
439
|
const binding = await initializing;
|
|
389
440
|
if (this.routedBindingMatches(binding, resolved, capabilityProfile)) {
|
|
390
441
|
this.noteRoutedBinding(binding);
|
|
391
|
-
return this.buildExecutionContext(binding);
|
|
442
|
+
return this.buildExecutionContext(binding, workspaceRoute);
|
|
392
443
|
}
|
|
393
444
|
}
|
|
394
445
|
const bindingInitialization = this.createRoutedBinding(resolved, capabilityProfile);
|
|
@@ -404,16 +455,15 @@ export class WorkspaceRouter {
|
|
|
404
455
|
}
|
|
405
456
|
this.noteRoutedBinding(binding);
|
|
406
457
|
this.noteOpenedWorkspace(resolved, capabilityProfile, this.options.mode === "daemon" ? "daemon_authorized" : "session_opened", false);
|
|
407
|
-
return this.buildExecutionContext(binding);
|
|
458
|
+
return this.buildExecutionContext(binding, workspaceRoute);
|
|
408
459
|
}
|
|
409
460
|
async createRoutedBinding(resolved, capabilityProfile) {
|
|
410
461
|
const routeDir = path.join(this.options.graftDir, "routes", `slice-${String(++this.bindingCounter).padStart(4, "0")}`);
|
|
411
462
|
await this.options.fs.mkdir(routeDir, { recursive: true });
|
|
412
463
|
const binding = await this.createBoundWorkspace(resolved, routeDir, capabilityProfile, undefined);
|
|
413
464
|
const repoState = binding.slice.repoState;
|
|
414
|
-
if (repoState === null)
|
|
465
|
+
if (repoState === null)
|
|
415
466
|
throw new WorkspaceBindingRequiredError("workspace");
|
|
416
|
-
}
|
|
417
467
|
await repoState.initialize();
|
|
418
468
|
return binding;
|
|
419
469
|
}
|
|
@@ -424,21 +474,50 @@ export class WorkspaceRouter {
|
|
|
424
474
|
&& workspaceCapabilityProfilesEqual(binding.capabilityProfile, capabilityProfile);
|
|
425
475
|
}
|
|
426
476
|
noteRoutedBinding(binding) {
|
|
477
|
+
const replaced = this.routedBindings.get(binding.worktreeId);
|
|
427
478
|
this.routedBindings.delete(binding.worktreeId);
|
|
428
479
|
this.routedBindings.set(binding.worktreeId, binding);
|
|
480
|
+
if (replaced !== undefined && replaced !== binding) {
|
|
481
|
+
this.disposeRoutedBinding(replaced);
|
|
482
|
+
}
|
|
429
483
|
while (this.routedBindings.size > MAX_ROUTED_BINDINGS) {
|
|
430
|
-
const
|
|
431
|
-
if (
|
|
484
|
+
const oldestKey = this.routedBindings.keys().next().value;
|
|
485
|
+
if (oldestKey === undefined) {
|
|
432
486
|
return;
|
|
433
487
|
}
|
|
434
|
-
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);
|
|
435
500
|
}
|
|
436
501
|
}
|
|
437
|
-
buildExecutionContext(binding) {
|
|
502
|
+
buildExecutionContext(binding, workspaceRoute = null) {
|
|
503
|
+
if (this.warpLeaseRelease !== null) {
|
|
504
|
+
throw new Error("workspace WARP leases have already been released");
|
|
505
|
+
}
|
|
438
506
|
const repoState = binding.slice.repoState;
|
|
439
507
|
if (repoState === null) {
|
|
440
508
|
throw new WorkspaceBindingRequiredError("workspace");
|
|
441
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
|
+
});
|
|
442
521
|
return {
|
|
443
522
|
sliceId: binding.slice.sliceId,
|
|
444
523
|
repoId: binding.repoId,
|
|
@@ -446,18 +525,22 @@ export class WorkspaceRouter {
|
|
|
446
525
|
projectRoot: binding.worktreeRoot,
|
|
447
526
|
worktreeRoot: binding.worktreeRoot,
|
|
448
527
|
gitCommonDir: binding.gitCommonDir,
|
|
528
|
+
workspaceRoute,
|
|
449
529
|
graftignorePatterns: binding.graftignorePatterns,
|
|
450
530
|
resolvePath: binding.resolvePath,
|
|
451
531
|
capabilityProfile: binding.capabilityProfile,
|
|
452
532
|
warpWriterId: binding.warpWriterId,
|
|
453
|
-
getCausalContext: () =>
|
|
533
|
+
getCausalContext: (observation) => {
|
|
534
|
+
return this.buildCausalContext(binding, observation ?? repoState.getState());
|
|
535
|
+
},
|
|
454
536
|
status: boundWorkspaceStatus(this.options.mode, binding),
|
|
455
537
|
governor: binding.slice.governor,
|
|
456
538
|
cache: binding.slice.cache,
|
|
457
539
|
metrics: binding.slice.metrics,
|
|
458
540
|
graftDir: binding.slice.graftDir,
|
|
459
541
|
repoState,
|
|
460
|
-
getWarp:
|
|
542
|
+
getWarp: () => warpLease.getWarp(),
|
|
543
|
+
releaseWarpLease: () => warpLease.release(),
|
|
461
544
|
};
|
|
462
545
|
}
|
|
463
546
|
async bind(request, actionName) {
|
|
@@ -476,51 +559,51 @@ export class WorkspaceRouter {
|
|
|
476
559
|
}
|
|
477
560
|
return this.bindInternal("rebind", request, actionName);
|
|
478
561
|
}
|
|
479
|
-
async declareAttach(declaration) {
|
|
480
|
-
const
|
|
481
|
-
if (
|
|
562
|
+
async declareAttach(declaration, execution = null) {
|
|
563
|
+
const scope = this.resolveWorkspaceHistoryScope(execution);
|
|
564
|
+
if (scope === null) {
|
|
482
565
|
return {
|
|
483
566
|
ok: false,
|
|
484
567
|
action: "attach",
|
|
485
568
|
...this.getStatus(),
|
|
486
|
-
persistedLocalHistory: await this.getPersistedLocalHistorySummary(),
|
|
569
|
+
persistedLocalHistory: await this.getPersistedLocalHistorySummary(execution),
|
|
487
570
|
errorCode: "UNBOUND_SESSION",
|
|
488
571
|
error: "causal_attach requires an active workspace binding.",
|
|
489
572
|
};
|
|
490
573
|
}
|
|
491
574
|
try {
|
|
492
|
-
await this.options.persistedLocalHistory.declareAttach({
|
|
493
|
-
current:
|
|
575
|
+
await scope.withGraph((graph) => this.options.persistedLocalHistory.declareAttach({
|
|
576
|
+
current: scope.buildHistoryContext(scope.repoState.getState()),
|
|
494
577
|
declaration,
|
|
495
|
-
graph
|
|
496
|
-
});
|
|
578
|
+
graph,
|
|
579
|
+
}));
|
|
497
580
|
}
|
|
498
581
|
catch (error) {
|
|
499
582
|
if (error instanceof PersistedLocalHistoryAttachUnavailableError) {
|
|
500
583
|
const sharedAttachSource = this.options.sharedAttachPolicy?.resolveSharedAttachSource({
|
|
501
584
|
sessionId: this.options.transportSessionId,
|
|
502
|
-
repoId:
|
|
503
|
-
worktreeId:
|
|
585
|
+
repoId: scope.repoId,
|
|
586
|
+
worktreeId: scope.worktreeId,
|
|
504
587
|
}) ?? null;
|
|
505
588
|
if (sharedAttachSource !== null) {
|
|
506
|
-
await this.options.persistedLocalHistory.declareSharedAttach({
|
|
507
|
-
current:
|
|
589
|
+
await scope.withGraph((graph) => this.options.persistedLocalHistory.declareSharedAttach({
|
|
590
|
+
current: scope.buildHistoryContext(scope.repoState.getState()),
|
|
508
591
|
declaration,
|
|
509
592
|
source: sharedAttachSource,
|
|
510
|
-
graph
|
|
511
|
-
});
|
|
593
|
+
graph,
|
|
594
|
+
}));
|
|
512
595
|
return {
|
|
513
596
|
ok: true,
|
|
514
597
|
action: "attach",
|
|
515
|
-
...
|
|
516
|
-
persistedLocalHistory: await this.getPersistedLocalHistorySummary(),
|
|
598
|
+
...scope.status,
|
|
599
|
+
persistedLocalHistory: await this.getPersistedLocalHistorySummary(execution),
|
|
517
600
|
};
|
|
518
601
|
}
|
|
519
602
|
return {
|
|
520
603
|
ok: false,
|
|
521
604
|
action: "attach",
|
|
522
|
-
...
|
|
523
|
-
persistedLocalHistory: await this.getPersistedLocalHistorySummary(),
|
|
605
|
+
...scope.status,
|
|
606
|
+
persistedLocalHistory: await this.getPersistedLocalHistorySummary(execution),
|
|
524
607
|
errorCode: error.code,
|
|
525
608
|
error: error.message,
|
|
526
609
|
};
|
|
@@ -530,8 +613,8 @@ export class WorkspaceRouter {
|
|
|
530
613
|
return {
|
|
531
614
|
ok: true,
|
|
532
615
|
action: "attach",
|
|
533
|
-
...
|
|
534
|
-
persistedLocalHistory: await this.getPersistedLocalHistorySummary(),
|
|
616
|
+
...scope.status,
|
|
617
|
+
persistedLocalHistory: await this.getPersistedLocalHistorySummary(execution),
|
|
535
618
|
};
|
|
536
619
|
}
|
|
537
620
|
async bindInternal(action, request, actionName, options = {}) {
|
|
@@ -561,27 +644,31 @@ export class WorkspaceRouter {
|
|
|
561
644
|
error: `Workspace ${resolved.worktreeRoot} is not authorized for daemon binding. Call workspace_authorize first.`,
|
|
562
645
|
};
|
|
563
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;
|
|
564
656
|
const nextBinding = await this.createBoundWorkspace(resolved, sliceDir, capabilityProfile, actionName);
|
|
565
657
|
const nextRepoState = nextBinding.slice.repoState;
|
|
566
|
-
if (nextRepoState === null)
|
|
658
|
+
if (nextRepoState === null)
|
|
567
659
|
throw new WorkspaceBindingRequiredError("workspace");
|
|
568
|
-
}
|
|
569
660
|
await nextRepoState.initialize();
|
|
570
|
-
const previousBinding = this.currentBinding;
|
|
571
661
|
const previousRepoState = previousBinding?.slice.repoState;
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
});
|
|
582
|
-
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")
|
|
583
671
|
await this.options.authorizationPolicy?.noteBound(resolved);
|
|
584
|
-
}
|
|
585
672
|
this.currentBinding = nextBinding;
|
|
586
673
|
this.currentSlice = nextBinding.slice;
|
|
587
674
|
this.noteOpenedWorkspace(resolved, capabilityProfile, options.openedSource ?? (this.options.mode === "daemon" ? "daemon_authorized" : "session_opened"), true);
|
|
@@ -600,6 +687,8 @@ export class WorkspaceRouter {
|
|
|
600
687
|
git: this.options.git,
|
|
601
688
|
nextSliceId: `slice-${String(++this.sliceIdCounter).padStart(4, "0")}`,
|
|
602
689
|
});
|
|
690
|
+
if (this.warpLeaseRelease !== null)
|
|
691
|
+
throw new Error("workspace graph admission has closed");
|
|
603
692
|
return createBoundWorkspace({
|
|
604
693
|
resolved,
|
|
605
694
|
graftDir,
|
|
@@ -609,7 +698,6 @@ export class WorkspaceRouter {
|
|
|
609
698
|
fs: this.options.fs,
|
|
610
699
|
transportSessionId: this.options.transportSessionId,
|
|
611
700
|
warpWriterId: this.options.warpWriterId ?? DEFAULT_WARP_WRITER_ID,
|
|
612
|
-
warpPool: this.options.warpPool,
|
|
613
701
|
});
|
|
614
702
|
}
|
|
615
703
|
requireBinding() {
|
|
@@ -685,11 +773,12 @@ export class WorkspaceRouter {
|
|
|
685
773
|
hookEvent,
|
|
686
774
|
});
|
|
687
775
|
}
|
|
688
|
-
buildPersistedLocalHistoryContextFromExecution(execution, observation) {
|
|
776
|
+
buildPersistedLocalHistoryContextFromExecution(execution, observation, hookEvent = null) {
|
|
689
777
|
return buildPersistedLocalHistoryContextFromExecution({
|
|
690
778
|
persistedLocalHistory: this.options.persistedLocalHistory,
|
|
691
779
|
execution,
|
|
692
780
|
observation,
|
|
781
|
+
hookEvent,
|
|
693
782
|
});
|
|
694
783
|
}
|
|
695
784
|
async resolveCheckoutBoundaryHookEvent(binding, previousObservedAt, observation) {
|
|
@@ -707,11 +796,59 @@ export class WorkspaceRouter {
|
|
|
707
796
|
}
|
|
708
797
|
return this.captureExecutionContext();
|
|
709
798
|
}
|
|
710
|
-
|
|
711
|
-
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) {
|
|
712
818
|
return null;
|
|
713
819
|
}
|
|
714
|
-
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
|
+
}
|
|
715
852
|
}
|
|
716
853
|
async buildPersistedLocalHistoryGraphContextFromExecution(execution) {
|
|
717
854
|
if (this.options.persistedLocalHistoryGraph === false) {
|