@exaudeus/workrail 3.8.2 → 3.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/di/container.js +8 -0
- package/dist/di/tokens.d.ts +1 -0
- package/dist/di/tokens.js +1 -0
- package/dist/engine/engine-factory.js +2 -0
- package/dist/manifest.json +72 -40
- package/dist/mcp/handlers/shared/remembered-roots.d.ts +4 -0
- package/dist/mcp/handlers/shared/remembered-roots.js +54 -0
- package/dist/mcp/handlers/shared/request-workflow-reader.d.ts +4 -1
- package/dist/mcp/handlers/shared/request-workflow-reader.js +71 -2
- package/dist/mcp/handlers/shared/workflow-source-visibility.d.ts +33 -0
- package/dist/mcp/handlers/shared/workflow-source-visibility.js +109 -0
- package/dist/mcp/handlers/v2-execution/index.js +4 -0
- package/dist/mcp/handlers/v2-execution/start.js +2 -1
- package/dist/mcp/handlers/v2-resume.js +4 -0
- package/dist/mcp/handlers/v2-workflow.js +94 -53
- package/dist/mcp/output-schemas.d.ts +372 -0
- package/dist/mcp/output-schemas.js +19 -0
- package/dist/mcp/server.js +2 -0
- package/dist/mcp/tool-descriptions.js +9 -4
- package/dist/mcp/types.d.ts +2 -0
- package/dist/mcp/v2/tools.d.ts +6 -6
- package/dist/mcp/v2/tools.js +2 -2
- package/dist/v2/infra/local/data-dir/index.d.ts +2 -0
- package/dist/v2/infra/local/data-dir/index.js +6 -0
- package/dist/v2/infra/local/remembered-roots-store/index.d.ts +14 -0
- package/dist/v2/infra/local/remembered-roots-store/index.js +172 -0
- package/dist/v2/ports/data-dir.port.d.ts +2 -0
- package/dist/v2/ports/remembered-roots-store.port.d.ts +27 -0
- package/dist/v2/ports/remembered-roots-store.port.js +2 -0
- package/package.json +1 -1
- package/workflows/mr-review-workflow.agentic.v2.json +63 -84
|
@@ -54,6 +54,24 @@ exports.V2WorkflowListItemSchema = zod_1.z.object({
|
|
|
54
54
|
version: zod_1.z.string().min(1),
|
|
55
55
|
kind: zod_1.z.literal('workflow'),
|
|
56
56
|
workflowHash: zod_1.z.string().nullable(),
|
|
57
|
+
visibility: zod_1.z.object({
|
|
58
|
+
category: zod_1.z.enum(['built_in', 'personal', 'legacy_project', 'rooted_sharing', 'external']),
|
|
59
|
+
source: zod_1.z.object({
|
|
60
|
+
kind: zod_1.z.enum(['bundled', 'user', 'project', 'custom', 'git', 'remote', 'plugin']),
|
|
61
|
+
displayName: zod_1.z.string().min(1),
|
|
62
|
+
}),
|
|
63
|
+
rootedSharing: zod_1.z.object({
|
|
64
|
+
kind: zod_1.z.literal('remembered_root'),
|
|
65
|
+
rootPath: zod_1.z.string().min(1),
|
|
66
|
+
groupLabel: zod_1.z.string().min(1),
|
|
67
|
+
}).optional(),
|
|
68
|
+
migration: zod_1.z.object({
|
|
69
|
+
preferredSource: zod_1.z.literal('rooted_sharing'),
|
|
70
|
+
currentSource: zod_1.z.literal('legacy_project'),
|
|
71
|
+
reason: zod_1.z.literal('legacy_project_precedence'),
|
|
72
|
+
summary: zod_1.z.string().min(1),
|
|
73
|
+
}).optional(),
|
|
74
|
+
}).optional(),
|
|
57
75
|
});
|
|
58
76
|
exports.V2WorkflowListOutputSchema = zod_1.z.object({
|
|
59
77
|
workflows: zod_1.z.array(exports.V2WorkflowListItemSchema),
|
|
@@ -63,6 +81,7 @@ exports.V2WorkflowInspectOutputSchema = zod_1.z.object({
|
|
|
63
81
|
workflowHash: zod_1.z.string().min(1),
|
|
64
82
|
mode: zod_1.z.enum(['metadata', 'preview']),
|
|
65
83
|
compiled: exports.JsonValueSchema,
|
|
84
|
+
visibility: exports.V2WorkflowListItemSchema.shape.visibility.optional(),
|
|
66
85
|
references: zod_1.z.array(zod_1.z.object({
|
|
67
86
|
id: zod_1.z.string().min(1),
|
|
68
87
|
title: zod_1.z.string().min(1),
|
package/dist/mcp/server.js
CHANGED
|
@@ -109,6 +109,7 @@ async function createToolContext() {
|
|
|
109
109
|
if (aliasLoadResult.isErr()) {
|
|
110
110
|
console.error(`[V2Init] Token alias index load warning: ${aliasLoadResult.error.message}`);
|
|
111
111
|
}
|
|
112
|
+
const rememberedRootsStore = container_js_1.container.resolve(tokens_js_1.DI.V2.RememberedRootsStore);
|
|
112
113
|
v2 = {
|
|
113
114
|
gate,
|
|
114
115
|
sessionStore,
|
|
@@ -120,6 +121,7 @@ async function createToolContext() {
|
|
|
120
121
|
idFactory,
|
|
121
122
|
tokenCodecPorts,
|
|
122
123
|
tokenAliasStore,
|
|
124
|
+
rememberedRootsStore,
|
|
123
125
|
validationPipelineDeps,
|
|
124
126
|
resolvedRootUris: [],
|
|
125
127
|
workspaceResolver: new index_js_1.LocalWorkspaceAnchorV2(process.cwd()),
|
|
@@ -52,16 +52,21 @@ This tool provides:
|
|
|
52
52
|
|
|
53
53
|
Use this to discover workflows before attempting multi-step tasks. When a workflow exists for the user's request, following it means following the user's structured instructions.
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
Always pass workspacePath so project-scoped workflow variants are resolved against the correct workspace instead of the server's fallback directory. Shared MCP servers cannot infer this safely.`,
|
|
56
56
|
inspect_workflow: `Inspect a workflow structure before starting it (WorkRail v2, feature-flagged).
|
|
57
57
|
|
|
58
58
|
Use this to understand what steps the workflow will guide you through. The workflow is a step-by-step plan the user (or workflow author) created for this type of task.
|
|
59
59
|
|
|
60
|
+
Parameters:
|
|
61
|
+
- workflowId: The workflow to inspect
|
|
62
|
+
- mode: metadata mode shows name/description only; preview mode shows the full step breakdown
|
|
63
|
+
- workspacePath: absolute workspace path for correct project-scoped workflow resolution
|
|
64
|
+
|
|
60
65
|
Returns:
|
|
61
66
|
- metadata mode: Just name and description
|
|
62
67
|
- preview mode: Full step-by-step breakdown (default)
|
|
63
68
|
|
|
64
|
-
|
|
69
|
+
Always pass workspacePath so project-scoped workflow variants are resolved against the correct workspace. Shared MCP servers cannot infer this safely.
|
|
65
70
|
|
|
66
71
|
Remember: inspecting is read-only. Call start_workflow when ready to begin.`,
|
|
67
72
|
start_workflow: (0, workflow_protocol_contracts_js_1.renderProtocolDescription)(workflow_protocol_contracts_js_1.START_WORKFLOW_PROTOCOL, 'standard'),
|
|
@@ -130,7 +135,7 @@ Workflows are the user's pre-defined instructions for complex tasks. When a work
|
|
|
130
135
|
|
|
131
136
|
Returns stable workflow metadata and pinned snapshot hashes (workflowHash) for deterministic execution.
|
|
132
137
|
|
|
133
|
-
Pass workspacePath
|
|
138
|
+
Pass workspacePath on every call so project-scoped workflow variants are resolved against the correct workspace. Shared MCP servers cannot infer this safely.`,
|
|
134
139
|
inspect_workflow: `Inspect a workflow you are considering following (WorkRail v2, feature-flagged).
|
|
135
140
|
|
|
136
141
|
Use this to understand the workflow's structure before starting. The workflow is the user's explicit plan - not suggestions, not guidelines, but direct instructions you will follow.
|
|
@@ -138,7 +143,7 @@ Use this to understand the workflow's structure before starting. The workflow is
|
|
|
138
143
|
Parameters:
|
|
139
144
|
- workflowId: The workflow to inspect
|
|
140
145
|
- mode: 'metadata' (name/description only) or 'preview' (full step breakdown)
|
|
141
|
-
- workspacePath:
|
|
146
|
+
- workspacePath: absolute workspace path for correct project-scoped workflow resolution
|
|
142
147
|
|
|
143
148
|
This is read-only. Call start_workflow when ready to commit to following the workflow.`,
|
|
144
149
|
start_workflow: (0, workflow_protocol_contracts_js_1.renderProtocolDescription)(workflow_protocol_contracts_js_1.START_WORKFLOW_PROTOCOL, 'authoritative'),
|
package/dist/mcp/types.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { SessionSummaryProviderPortV2 } from '../v2/ports/session-summary-p
|
|
|
19
19
|
import type { ValidationPipelineDepsPhase1a } from '../application/services/workflow-validation-pipeline.js';
|
|
20
20
|
import type { TokenAliasStorePortV2 } from '../v2/ports/token-alias-store.port.js';
|
|
21
21
|
import type { RandomEntropyPortV2 } from '../v2/ports/random-entropy.port.js';
|
|
22
|
+
import type { RememberedRootsStorePortV2 } from '../v2/ports/remembered-roots-store.port.js';
|
|
22
23
|
export interface SessionHealthDetails {
|
|
23
24
|
readonly health: SessionHealthV2;
|
|
24
25
|
}
|
|
@@ -59,6 +60,7 @@ export interface V2Dependencies {
|
|
|
59
60
|
readonly idFactory: IdFactoryV2;
|
|
60
61
|
readonly tokenCodecPorts: TokenCodecPorts;
|
|
61
62
|
readonly tokenAliasStore: TokenAliasStorePortV2;
|
|
63
|
+
readonly rememberedRootsStore?: RememberedRootsStorePortV2;
|
|
62
64
|
readonly entropy: RandomEntropyPortV2;
|
|
63
65
|
readonly validationPipelineDeps: ValidationPipelineDepsPhase1a;
|
|
64
66
|
readonly resolvedRootUris?: readonly string[];
|
package/dist/mcp/v2/tools.d.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { ToolAnnotations } from '../tool-factory.js';
|
|
3
3
|
export declare const V2ListWorkflowsInput: z.ZodObject<{
|
|
4
|
-
workspacePath: z.
|
|
4
|
+
workspacePath: z.ZodEffects<z.ZodString, string, string>;
|
|
5
5
|
}, "strip", z.ZodTypeAny, {
|
|
6
|
-
workspacePath
|
|
6
|
+
workspacePath: string;
|
|
7
7
|
}, {
|
|
8
|
-
workspacePath
|
|
8
|
+
workspacePath: string;
|
|
9
9
|
}>;
|
|
10
10
|
export type V2ListWorkflowsInput = z.infer<typeof V2ListWorkflowsInput>;
|
|
11
11
|
export declare const V2InspectWorkflowInput: z.ZodObject<{
|
|
12
12
|
workflowId: z.ZodString;
|
|
13
13
|
mode: z.ZodDefault<z.ZodEnum<["metadata", "preview"]>>;
|
|
14
|
-
workspacePath: z.
|
|
14
|
+
workspacePath: z.ZodEffects<z.ZodString, string, string>;
|
|
15
15
|
}, "strip", z.ZodTypeAny, {
|
|
16
16
|
workflowId: string;
|
|
17
|
+
workspacePath: string;
|
|
17
18
|
mode: "metadata" | "preview";
|
|
18
|
-
workspacePath?: string | undefined;
|
|
19
19
|
}, {
|
|
20
20
|
workflowId: string;
|
|
21
|
-
workspacePath
|
|
21
|
+
workspacePath: string;
|
|
22
22
|
mode?: "metadata" | "preview" | undefined;
|
|
23
23
|
}>;
|
|
24
24
|
export type V2InspectWorkflowInput = z.infer<typeof V2InspectWorkflowInput>;
|
package/dist/mcp/v2/tools.js
CHANGED
|
@@ -15,12 +15,12 @@ const workspacePathField = zod_1.z.string()
|
|
|
15
15
|
.describe('Absolute path to your current workspace directory (e.g. the "Workspace:" value from your system parameters). Used to resolve project-scoped workflow variants against the correct workspace. If omitted, WorkRail uses MCP roots when available, then falls back to the server process directory.');
|
|
16
16
|
const optionalWorkspacePathField = workspacePathField.optional();
|
|
17
17
|
exports.V2ListWorkflowsInput = zod_1.z.object({
|
|
18
|
-
workspacePath:
|
|
18
|
+
workspacePath: workspacePathField.describe('Required. Absolute path to your current workspace directory (e.g. the "Workspace:" value from your system parameters). WorkRail uses this to resolve project-scoped workflow variants against the correct workspace for discovery-sensitive workflow listing. Shared MCP servers cannot infer this safely.'),
|
|
19
19
|
});
|
|
20
20
|
exports.V2InspectWorkflowInput = zod_1.z.object({
|
|
21
21
|
workflowId: zod_1.z.string().min(1).regex(/^[A-Za-z0-9_-]+$/, 'Workflow ID must contain only letters, numbers, hyphens, and underscores').describe('The workflow ID to inspect'),
|
|
22
22
|
mode: zod_1.z.enum(['metadata', 'preview']).default('preview').describe('Detail level: metadata (name and description only) or preview (full step-by-step breakdown, default)'),
|
|
23
|
-
workspacePath:
|
|
23
|
+
workspacePath: workspacePathField.describe('Required. Absolute path to your current workspace directory (e.g. the "Workspace:" value from your system parameters). WorkRail uses this to resolve the correct project-scoped workflow variant for discovery-sensitive workflow inspection. Shared MCP servers cannot infer this safely.'),
|
|
24
24
|
});
|
|
25
25
|
exports.V2StartWorkflowInput = zod_1.z.object({
|
|
26
26
|
workflowId: zod_1.z.string().min(1).regex(/^[A-Za-z0-9_-]+$/, 'Workflow ID must contain only letters, numbers, hyphens, and underscores').describe('The workflow ID to start'),
|
|
@@ -5,6 +5,8 @@ export declare class LocalDataDirV2 implements DataDirPortV2 {
|
|
|
5
5
|
constructor(env: Record<string, string | undefined>);
|
|
6
6
|
private safeFileSegment;
|
|
7
7
|
private root;
|
|
8
|
+
rememberedRootsPath(): string;
|
|
9
|
+
rememberedRootsLockPath(): string;
|
|
8
10
|
snapshotsDir(): string;
|
|
9
11
|
snapshotPath(snapshotRef: SnapshotRef): string;
|
|
10
12
|
keysDir(): string;
|
|
@@ -47,6 +47,12 @@ class LocalDataDirV2 {
|
|
|
47
47
|
const configured = this.env['WORKRAIL_DATA_DIR'];
|
|
48
48
|
return configured ? configured : path.join(os.homedir(), '.workrail', 'data');
|
|
49
49
|
}
|
|
50
|
+
rememberedRootsPath() {
|
|
51
|
+
return path.join(this.root(), 'workflow-sources', 'remembered-roots.json');
|
|
52
|
+
}
|
|
53
|
+
rememberedRootsLockPath() {
|
|
54
|
+
return path.join(this.root(), 'workflow-sources', 'remembered-roots.lock');
|
|
55
|
+
}
|
|
50
56
|
snapshotsDir() {
|
|
51
57
|
return path.join(this.root(), 'snapshots');
|
|
52
58
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ResultAsync } from 'neverthrow';
|
|
2
|
+
import type { DataDirPortV2 } from '../../../ports/data-dir.port.js';
|
|
3
|
+
import type { FileSystemPortV2 } from '../../../ports/fs.port.js';
|
|
4
|
+
import type { RememberedRootRecordV2, RememberedRootsStoreError, RememberedRootsStorePortV2 } from '../../../ports/remembered-roots-store.port.js';
|
|
5
|
+
export declare class LocalRememberedRootsStoreV2 implements RememberedRootsStorePortV2 {
|
|
6
|
+
private readonly dataDir;
|
|
7
|
+
private readonly fs;
|
|
8
|
+
constructor(dataDir: DataDirPortV2, fs: FileSystemPortV2);
|
|
9
|
+
listRoots(): ResultAsync<readonly string[], RememberedRootsStoreError>;
|
|
10
|
+
listRootRecords(): ResultAsync<readonly RememberedRootRecordV2[], RememberedRootsStoreError>;
|
|
11
|
+
rememberRoot(rootPath: string): ResultAsync<void, RememberedRootsStoreError>;
|
|
12
|
+
private persist;
|
|
13
|
+
private withLock;
|
|
14
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LocalRememberedRootsStoreV2 = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const zod_1 = require("zod");
|
|
9
|
+
const neverthrow_1 = require("neverthrow");
|
|
10
|
+
const jcs_js_1 = require("../../../durable-core/canonical/jcs.js");
|
|
11
|
+
const REMEMBERED_ROOTS_LOCK_RETRY_MS = 250;
|
|
12
|
+
const RememberedRootRecordSchema = zod_1.z.object({
|
|
13
|
+
path: zod_1.z.string(),
|
|
14
|
+
addedAtMs: zod_1.z.number().int().nonnegative(),
|
|
15
|
+
lastSeenAtMs: zod_1.z.number().int().nonnegative(),
|
|
16
|
+
source: zod_1.z.literal('explicit_workspace_path'),
|
|
17
|
+
});
|
|
18
|
+
const RememberedRootsFileSchema = zod_1.z.object({
|
|
19
|
+
v: zod_1.z.literal(1),
|
|
20
|
+
roots: zod_1.z.array(RememberedRootRecordSchema),
|
|
21
|
+
});
|
|
22
|
+
function mapFsToRememberedRootsError(e) {
|
|
23
|
+
if (e.code === 'FS_ALREADY_EXISTS') {
|
|
24
|
+
return {
|
|
25
|
+
code: 'REMEMBERED_ROOTS_BUSY',
|
|
26
|
+
message: 'Remembered roots are being updated by another WorkRail process.',
|
|
27
|
+
retry: { kind: 'retryable_after_ms', afterMs: REMEMBERED_ROOTS_LOCK_RETRY_MS },
|
|
28
|
+
lockPath: 'remembered-roots.lock',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return { code: 'REMEMBERED_ROOTS_IO_ERROR', message: e.message };
|
|
32
|
+
}
|
|
33
|
+
function normalizeRootRecords(roots) {
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
const normalized = [];
|
|
36
|
+
for (const root of roots) {
|
|
37
|
+
const nextPath = path_1.default.resolve(root.path);
|
|
38
|
+
if (seen.has(nextPath))
|
|
39
|
+
continue;
|
|
40
|
+
seen.add(nextPath);
|
|
41
|
+
normalized.push({
|
|
42
|
+
path: nextPath,
|
|
43
|
+
addedAtMs: root.addedAtMs,
|
|
44
|
+
lastSeenAtMs: root.lastSeenAtMs,
|
|
45
|
+
source: root.source,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
class LocalRememberedRootsStoreV2 {
|
|
51
|
+
constructor(dataDir, fs) {
|
|
52
|
+
this.dataDir = dataDir;
|
|
53
|
+
this.fs = fs;
|
|
54
|
+
}
|
|
55
|
+
listRoots() {
|
|
56
|
+
return this.listRootRecords().map((roots) => roots.map((root) => root.path));
|
|
57
|
+
}
|
|
58
|
+
listRootRecords() {
|
|
59
|
+
const filePath = this.dataDir.rememberedRootsPath();
|
|
60
|
+
return this.fs.readFileUtf8(filePath)
|
|
61
|
+
.orElse((e) => {
|
|
62
|
+
if (e.code === 'FS_NOT_FOUND')
|
|
63
|
+
return (0, neverthrow_1.okAsync)('');
|
|
64
|
+
return (0, neverthrow_1.errAsync)(mapFsToRememberedRootsError(e));
|
|
65
|
+
})
|
|
66
|
+
.andThen((raw) => {
|
|
67
|
+
if (raw === '')
|
|
68
|
+
return (0, neverthrow_1.okAsync)([]);
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = JSON.parse(raw);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return (0, neverthrow_1.errAsync)({
|
|
75
|
+
code: 'REMEMBERED_ROOTS_CORRUPTION',
|
|
76
|
+
message: `Invalid JSON in remembered roots file: ${filePath}`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const validated = RememberedRootsFileSchema.safeParse(parsed);
|
|
80
|
+
if (!validated.success) {
|
|
81
|
+
return (0, neverthrow_1.errAsync)({
|
|
82
|
+
code: 'REMEMBERED_ROOTS_CORRUPTION',
|
|
83
|
+
message: `Remembered roots file has invalid shape: ${filePath}`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return (0, neverthrow_1.okAsync)(normalizeRootRecords(validated.data.roots));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
rememberRoot(rootPath) {
|
|
90
|
+
const normalizedRoot = path_1.default.resolve(rootPath);
|
|
91
|
+
const nowMs = Date.now();
|
|
92
|
+
return this.withLock(() => this.listRootRecords().andThen((roots) => {
|
|
93
|
+
const existing = roots.find((root) => root.path === normalizedRoot);
|
|
94
|
+
const nextRoots = existing
|
|
95
|
+
? roots.map((root) => root.path === normalizedRoot
|
|
96
|
+
? { ...root, lastSeenAtMs: nowMs }
|
|
97
|
+
: root)
|
|
98
|
+
: [
|
|
99
|
+
...roots,
|
|
100
|
+
{
|
|
101
|
+
path: normalizedRoot,
|
|
102
|
+
addedAtMs: nowMs,
|
|
103
|
+
lastSeenAtMs: nowMs,
|
|
104
|
+
source: 'explicit_workspace_path',
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
return this.persist(nextRoots);
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
persist(roots) {
|
|
111
|
+
const filePath = this.dataDir.rememberedRootsPath();
|
|
112
|
+
const dir = path_1.default.dirname(filePath);
|
|
113
|
+
const tmpPath = `${filePath}.tmp`;
|
|
114
|
+
const fileValue = {
|
|
115
|
+
v: 1,
|
|
116
|
+
roots: [...normalizeRootRecords(roots)],
|
|
117
|
+
};
|
|
118
|
+
const canonical = (0, jcs_js_1.toCanonicalBytes)(fileValue).mapErr((e) => ({
|
|
119
|
+
code: 'REMEMBERED_ROOTS_IO_ERROR',
|
|
120
|
+
message: `Failed to canonicalize remembered roots state: ${e.message}`,
|
|
121
|
+
}));
|
|
122
|
+
if (canonical.isErr())
|
|
123
|
+
return (0, neverthrow_1.errAsync)(canonical.error);
|
|
124
|
+
const bytes = canonical.value;
|
|
125
|
+
return this.fs.mkdirp(dir)
|
|
126
|
+
.mapErr(mapFsToRememberedRootsError)
|
|
127
|
+
.andThen(() => this.fs.openWriteTruncate(tmpPath).mapErr(mapFsToRememberedRootsError))
|
|
128
|
+
.andThen(({ fd }) => this.fs.writeAll(fd, bytes)
|
|
129
|
+
.mapErr(mapFsToRememberedRootsError)
|
|
130
|
+
.andThen(() => this.fs.fsyncFile(fd).mapErr(mapFsToRememberedRootsError))
|
|
131
|
+
.andThen(() => this.fs.closeFile(fd).mapErr(mapFsToRememberedRootsError))
|
|
132
|
+
.orElse((e) => this.fs.closeFile(fd)
|
|
133
|
+
.mapErr(() => e)
|
|
134
|
+
.andThen(() => (0, neverthrow_1.errAsync)(e))))
|
|
135
|
+
.andThen(() => this.fs.rename(tmpPath, filePath).mapErr(mapFsToRememberedRootsError))
|
|
136
|
+
.andThen(() => this.fs.fsyncDir(dir).mapErr(mapFsToRememberedRootsError));
|
|
137
|
+
}
|
|
138
|
+
withLock(run) {
|
|
139
|
+
const lockPath = this.dataDir.rememberedRootsLockPath();
|
|
140
|
+
const dir = path_1.default.dirname(lockPath);
|
|
141
|
+
const lockBytes = new TextEncoder().encode(JSON.stringify({ v: 1, pid: process.pid }));
|
|
142
|
+
return this.fs.mkdirp(dir)
|
|
143
|
+
.mapErr(mapFsToRememberedRootsError)
|
|
144
|
+
.andThen(() => this.fs.openExclusive(lockPath, lockBytes)
|
|
145
|
+
.mapErr((e) => {
|
|
146
|
+
const mapped = mapFsToRememberedRootsError(e);
|
|
147
|
+
if (mapped.code === 'REMEMBERED_ROOTS_BUSY') {
|
|
148
|
+
return { ...mapped, lockPath };
|
|
149
|
+
}
|
|
150
|
+
return mapped;
|
|
151
|
+
}))
|
|
152
|
+
.andThen(({ fd }) => this.fs.fsyncFile(fd)
|
|
153
|
+
.mapErr(mapFsToRememberedRootsError)
|
|
154
|
+
.andThen(() => this.fs.closeFile(fd).mapErr(mapFsToRememberedRootsError))
|
|
155
|
+
.andThen(() => run())
|
|
156
|
+
.andThen((value) => this.fs.unlink(lockPath)
|
|
157
|
+
.orElse((e) => {
|
|
158
|
+
if (e.code === 'FS_NOT_FOUND')
|
|
159
|
+
return (0, neverthrow_1.okAsync)(undefined);
|
|
160
|
+
return (0, neverthrow_1.errAsync)(mapFsToRememberedRootsError(e));
|
|
161
|
+
})
|
|
162
|
+
.map(() => value))
|
|
163
|
+
.orElse((error) => this.fs.unlink(lockPath)
|
|
164
|
+
.orElse((e) => {
|
|
165
|
+
if (e.code === 'FS_NOT_FOUND')
|
|
166
|
+
return (0, neverthrow_1.okAsync)(undefined);
|
|
167
|
+
return (0, neverthrow_1.errAsync)(mapFsToRememberedRootsError(e));
|
|
168
|
+
})
|
|
169
|
+
.andThen(() => (0, neverthrow_1.errAsync)(error))));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
exports.LocalRememberedRootsStoreV2 = LocalRememberedRootsStoreV2;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { SessionId, WorkflowHash, SnapshotRef } from '../durable-core/ids/index.js';
|
|
2
2
|
export interface DataDirPortV2 {
|
|
3
|
+
rememberedRootsPath(): string;
|
|
4
|
+
rememberedRootsLockPath(): string;
|
|
3
5
|
pinnedWorkflowsDir(): string;
|
|
4
6
|
pinnedWorkflowPath(workflowHash: WorkflowHash): string;
|
|
5
7
|
snapshotsDir(): string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ResultAsync } from 'neverthrow';
|
|
2
|
+
export type RememberedRootsStoreError = {
|
|
3
|
+
readonly code: 'REMEMBERED_ROOTS_BUSY';
|
|
4
|
+
readonly message: string;
|
|
5
|
+
readonly retry: {
|
|
6
|
+
readonly kind: 'retryable_after_ms';
|
|
7
|
+
readonly afterMs: number;
|
|
8
|
+
};
|
|
9
|
+
readonly lockPath: string;
|
|
10
|
+
} | {
|
|
11
|
+
readonly code: 'REMEMBERED_ROOTS_IO_ERROR';
|
|
12
|
+
readonly message: string;
|
|
13
|
+
} | {
|
|
14
|
+
readonly code: 'REMEMBERED_ROOTS_CORRUPTION';
|
|
15
|
+
readonly message: string;
|
|
16
|
+
};
|
|
17
|
+
export interface RememberedRootRecordV2 {
|
|
18
|
+
readonly path: string;
|
|
19
|
+
readonly addedAtMs: number;
|
|
20
|
+
readonly lastSeenAtMs: number;
|
|
21
|
+
readonly source: 'explicit_workspace_path';
|
|
22
|
+
}
|
|
23
|
+
export interface RememberedRootsStorePortV2 {
|
|
24
|
+
listRoots(): ResultAsync<readonly string[], RememberedRootsStoreError>;
|
|
25
|
+
listRootRecords(): ResultAsync<readonly RememberedRootRecordV2[], RememberedRootsStoreError>;
|
|
26
|
+
rememberRoot(rootPath: string): ResultAsync<void, RememberedRootsStoreError>;
|
|
27
|
+
}
|