gsd-pi 2.70.1-dev.6504106 → 2.70.1-dev.7d1d9d3
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/dist/resources/extensions/gsd/custom-workflow-engine.js +16 -12
- package/dist/resources/extensions/gsd/file-lock.js +60 -0
- package/dist/resources/extensions/gsd/workflow-events.js +25 -13
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +78 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +13 -3
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +98 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +12 -3
- package/src/resources/extensions/gsd/custom-workflow-engine.ts +19 -14
- package/src/resources/extensions/gsd/file-lock.ts +59 -0
- package/src/resources/extensions/gsd/tests/file-lock.test.ts +103 -0
- package/src/resources/extensions/gsd/workflow-events.ts +34 -25
- /package/dist/web/standalone/.next/static/{T3hWbQ-WQBDEIGoaOOfEo → 52NuiWbmUzXpzxaTEDopT}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{T3hWbQ-WQBDEIGoaOOfEo → 52NuiWbmUzXpzxaTEDopT}/_ssgManifest.js +0 -0
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { injectContext } from "./context-injector.js";
|
|
35
35
|
import type { WorkflowDefinition, StepDefinition } from "./definition-loader.js";
|
|
36
36
|
import { parseUnitId } from "./unit-id.js";
|
|
37
|
+
import { withFileLock } from "./file-lock.js";
|
|
37
38
|
|
|
38
39
|
/** Read and parse the frozen DEFINITION.yaml from a run directory. */
|
|
39
40
|
export function readFrozenDefinition(runDir: string): WorkflowDefinition {
|
|
@@ -179,24 +180,28 @@ export class CustomWorkflowEngine implements WorkflowEngine {
|
|
|
179
180
|
state: EngineState,
|
|
180
181
|
completedStep: CompletedStep,
|
|
181
182
|
): Promise<ReconcileResult> {
|
|
182
|
-
|
|
183
|
-
// workflow edits with a stale in-memory snapshot from deriveState().
|
|
184
|
-
const graph = readGraph(this.runDir);
|
|
183
|
+
const graphPath = join(this.runDir, "GRAPH.yaml");
|
|
185
184
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
185
|
+
return await withFileLock(graphPath, () => {
|
|
186
|
+
// Re-read the graph from disk so we do not overwrite concurrent
|
|
187
|
+
// workflow edits with a stale in-memory snapshot from deriveState().
|
|
188
|
+
const graph = readGraph(this.runDir);
|
|
189
189
|
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
// Extract stepId from "<workflowName>/<stepId>"
|
|
191
|
+
const { milestone, slice, task } = parseUnitId(completedStep.unitId);
|
|
192
|
+
const stepId = task ?? slice ?? milestone;
|
|
192
193
|
|
|
193
|
-
|
|
194
|
-
(
|
|
195
|
-
);
|
|
194
|
+
const updatedGraph = markStepComplete(graph, stepId);
|
|
195
|
+
writeGraph(this.runDir, updatedGraph);
|
|
196
196
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
const allDone = updatedGraph.steps.every(
|
|
198
|
+
(s) => s.status === "complete" || s.status === "expanded",
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
outcome: allDone ? "milestone-complete" : "continue",
|
|
203
|
+
};
|
|
204
|
+
});
|
|
200
205
|
}
|
|
201
206
|
|
|
202
207
|
/**
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
function _require(name: string) {
|
|
4
|
+
try {
|
|
5
|
+
return require(name);
|
|
6
|
+
} catch {
|
|
7
|
+
try {
|
|
8
|
+
const gsdPiRequire = require("module").createRequire(
|
|
9
|
+
require("path").join(process.cwd(), "node_modules", "gsd-pi", "index.js")
|
|
10
|
+
);
|
|
11
|
+
return gsdPiRequire(name);
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function withFileLockSync<T>(filePath: string, fn: () => T): T {
|
|
19
|
+
const lockfile = _require("proper-lockfile");
|
|
20
|
+
if (!lockfile) return fn();
|
|
21
|
+
|
|
22
|
+
if (!existsSync(filePath)) return fn();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const release = lockfile.lockSync(filePath, { retries: 5, stale: 10000 });
|
|
26
|
+
try {
|
|
27
|
+
return fn();
|
|
28
|
+
} finally {
|
|
29
|
+
release();
|
|
30
|
+
}
|
|
31
|
+
} catch (err: any) {
|
|
32
|
+
if (err.code === "ELOCKED") {
|
|
33
|
+
// Could not get lock after retries, let's fallback to un-locked instead of crashing the whole state machine
|
|
34
|
+
return fn();
|
|
35
|
+
}
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function withFileLock<T>(filePath: string, fn: () => Promise<T> | T): Promise<T> {
|
|
41
|
+
const lockfile = _require("proper-lockfile");
|
|
42
|
+
if (!lockfile) return await fn();
|
|
43
|
+
|
|
44
|
+
if (!existsSync(filePath)) return await fn();
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const release = await lockfile.lock(filePath, { retries: 5, stale: 10000 });
|
|
48
|
+
try {
|
|
49
|
+
return await fn();
|
|
50
|
+
} finally {
|
|
51
|
+
await release();
|
|
52
|
+
}
|
|
53
|
+
} catch (err: any) {
|
|
54
|
+
if (err.code === "ELOCKED") {
|
|
55
|
+
return await fn();
|
|
56
|
+
}
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
|
|
8
|
+
import { withFileLock, withFileLockSync } from "../file-lock.ts";
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
|
|
12
|
+
function hasProperLockfile(): boolean {
|
|
13
|
+
try {
|
|
14
|
+
require("proper-lockfile");
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("withFileLockSync: executes callback when file does not exist", () => {
|
|
22
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-file-lock-test-"));
|
|
23
|
+
try {
|
|
24
|
+
const missingPath = join(dir, "missing.txt");
|
|
25
|
+
let called = 0;
|
|
26
|
+
const result = withFileLockSync(missingPath, () => {
|
|
27
|
+
called++;
|
|
28
|
+
return "ok";
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
assert.equal(result, "ok");
|
|
32
|
+
assert.equal(called, 1, "callback should execute exactly once");
|
|
33
|
+
} finally {
|
|
34
|
+
rmSync(dir, { recursive: true, force: true });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("withFileLock: executes callback when file does not exist", async () => {
|
|
39
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-file-lock-test-"));
|
|
40
|
+
try {
|
|
41
|
+
const missingPath = join(dir, "missing.txt");
|
|
42
|
+
let called = 0;
|
|
43
|
+
const result = await withFileLock(missingPath, async () => {
|
|
44
|
+
called++;
|
|
45
|
+
return "ok";
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
assert.equal(result, "ok");
|
|
49
|
+
assert.equal(called, 1, "callback should execute exactly once");
|
|
50
|
+
} finally {
|
|
51
|
+
rmSync(dir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("withFileLockSync: falls back to unlocked callback on ELOCKED", () => {
|
|
56
|
+
if (!hasProperLockfile() || process.platform === "win32") {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const lockfile = require("proper-lockfile");
|
|
61
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-file-lock-test-"));
|
|
62
|
+
const filePath = join(dir, "locked.jsonl");
|
|
63
|
+
writeFileSync(filePath, "{}\n", "utf-8");
|
|
64
|
+
|
|
65
|
+
const release = lockfile.lockSync(filePath, { retries: 0, stale: 10000 });
|
|
66
|
+
try {
|
|
67
|
+
let called = 0;
|
|
68
|
+
const result = withFileLockSync(filePath, () => {
|
|
69
|
+
called++;
|
|
70
|
+
return "fallback-ok";
|
|
71
|
+
});
|
|
72
|
+
assert.equal(result, "fallback-ok");
|
|
73
|
+
assert.equal(called, 1, "callback should run even when lock acquisition fails");
|
|
74
|
+
} finally {
|
|
75
|
+
release();
|
|
76
|
+
rmSync(dir, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("withFileLock: falls back to unlocked callback on ELOCKED", async () => {
|
|
81
|
+
if (!hasProperLockfile() || process.platform === "win32") {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const lockfile = require("proper-lockfile");
|
|
86
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-file-lock-test-"));
|
|
87
|
+
const filePath = join(dir, "locked.jsonl");
|
|
88
|
+
writeFileSync(filePath, "{}\n", "utf-8");
|
|
89
|
+
|
|
90
|
+
const release = await lockfile.lock(filePath, { retries: 0, stale: 10000 });
|
|
91
|
+
try {
|
|
92
|
+
let called = 0;
|
|
93
|
+
const result = await withFileLock(filePath, async () => {
|
|
94
|
+
called++;
|
|
95
|
+
return "fallback-ok";
|
|
96
|
+
});
|
|
97
|
+
assert.equal(result, "fallback-ok");
|
|
98
|
+
assert.equal(called, 1, "callback should run even when lock acquisition fails");
|
|
99
|
+
} finally {
|
|
100
|
+
await release();
|
|
101
|
+
rmSync(dir, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { appendFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
5
|
+
import { withFileLockSync } from "./file-lock.js";
|
|
5
6
|
import { logWarning } from "./workflow-logger.js";
|
|
6
7
|
|
|
7
8
|
// ─── Session ID ───────────────────────────────────────────────────────────
|
|
@@ -127,31 +128,39 @@ export function compactMilestoneEvents(
|
|
|
127
128
|
const logPath = join(basePath, ".gsd", "event-log.jsonl");
|
|
128
129
|
const archivePath = join(basePath, ".gsd", `event-log-${milestoneId}.jsonl.archived`);
|
|
129
130
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
return withFileLockSync(logPath, () => {
|
|
132
|
+
const allEvents = readEvents(logPath);
|
|
133
|
+
|
|
134
|
+
// Single-pass partition to halve the work (per reviewer agent)
|
|
135
|
+
const toArchive: WorkflowEvent[] = [];
|
|
136
|
+
const remaining: WorkflowEvent[] = [];
|
|
137
|
+
|
|
138
|
+
for (const e of allEvents) {
|
|
139
|
+
if ((e.params as { milestoneId?: string }).milestoneId === milestoneId) {
|
|
140
|
+
toArchive.push(e);
|
|
141
|
+
} else {
|
|
142
|
+
remaining.push(e);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
137
145
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
146
|
+
if (toArchive.length === 0) {
|
|
147
|
+
return { archived: 0 };
|
|
148
|
+
}
|
|
141
149
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
150
|
+
// Write archived events to .jsonl.archived file (crash-safe)
|
|
151
|
+
atomicWriteSync(
|
|
152
|
+
archivePath,
|
|
153
|
+
toArchive.map((e) => JSON.stringify(e)).join("\n") + "\n",
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Truncate active log to remaining events only
|
|
157
|
+
atomicWriteSync(
|
|
158
|
+
logPath,
|
|
159
|
+
remaining.length > 0
|
|
160
|
+
? remaining.map((e) => JSON.stringify(e)).join("\n") + "\n"
|
|
161
|
+
: "",
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return { archived: toArchive.length };
|
|
165
|
+
});
|
|
157
166
|
}
|
|
File without changes
|
|
File without changes
|