@akagilnc/pi-workflow-roles 0.1.1857 → 0.1.1876
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/sitian-record-entry.js +34 -10
- package/package.json +1 -1
- package/src/role-runtime.ts +1 -0
- package/src/sitian-record-entry.ts +56 -24
- package/src/worker-role.ts +44 -3
- package/src/worker-submission-gates.ts +12 -24
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { dirname, resolve, join } from "node:path";
|
|
3
4
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import { resolveBookKeyFromGit } from "./activation-ledger-git.js";
|
|
@@ -12,31 +13,54 @@ import {
|
|
|
12
13
|
resolveActivationLedgerHome
|
|
13
14
|
} from "./activation-ledger-topology.js";
|
|
14
15
|
const { findMostRecentSession } = await import(new URL("./core/session-manager.js", import.meta.resolve("@earendil-works/pi-coding-agent")).href);
|
|
16
|
+
const WORKER_SUBMISSION_GATE_KIND = "worker-submission-gate";
|
|
15
17
|
function createRecordSession(options) {
|
|
16
18
|
const cwd = options.cwd;
|
|
17
19
|
const parentFile = options.parent?.getSessionFile();
|
|
18
20
|
const ledgerHome = resolveActivationLedgerHome();
|
|
21
|
+
let sessionDir;
|
|
22
|
+
let parentSession;
|
|
19
23
|
if (options.subject !== void 0) {
|
|
20
24
|
const digest = createHash("sha256").update(options.subject).digest("hex").slice(0, 32);
|
|
21
|
-
|
|
25
|
+
sessionDir = join(
|
|
22
26
|
activationBookDirectory(ledgerHome, resolveBookKeyFromGit(cwd)),
|
|
23
27
|
options.kind,
|
|
24
28
|
digest
|
|
25
29
|
);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (recentFile !== null) return SessionManager.open(recentFile, sessionDir2, cwd);
|
|
29
|
-
return SessionManager.create(cwd, sessionDir2, parentFile ? { parentSession: parentFile } : void 0);
|
|
30
|
-
}
|
|
31
|
-
if (parentFile === void 0 || parentFile.length === 0) {
|
|
30
|
+
parentSession = parentFile && parentFile.length > 0 ? parentFile : void 0;
|
|
31
|
+
} else if (parentFile === void 0 || parentFile.length === 0) {
|
|
32
32
|
return SessionManager.inMemory(cwd);
|
|
33
|
+
} else {
|
|
34
|
+
const parentResolved = resolve(parentFile);
|
|
35
|
+
sessionDir = physicallyContainedIn(ledgerHome, parentResolved) ? join(dirname(parentResolved), options.kind) : join(activationBookDirectory(ledgerHome, resolveBookKeyFromGit(cwd)), options.kind);
|
|
36
|
+
parentSession = parentFile;
|
|
33
37
|
}
|
|
34
|
-
const parentResolved = resolve(parentFile);
|
|
35
|
-
const sessionDir = physicallyContainedIn(ledgerHome, parentResolved) ? join(dirname(parentResolved), options.kind) : join(activationBookDirectory(ledgerHome, resolveBookKeyFromGit(cwd)), options.kind);
|
|
36
38
|
ensureRealDirectoryTree(ledgerHome, sessionDir);
|
|
37
|
-
|
|
39
|
+
const mayResumeSameNest = options.subject !== void 0 || options.kind === WORKER_SUBMISSION_GATE_KIND;
|
|
40
|
+
if (mayResumeSameNest) {
|
|
41
|
+
const recentFile = findMostRecentSession(sessionDir, cwd);
|
|
42
|
+
if (recentFile !== null) return SessionManager.open(recentFile, sessionDir, cwd);
|
|
43
|
+
}
|
|
44
|
+
const session = SessionManager.create(
|
|
45
|
+
cwd,
|
|
46
|
+
sessionDir,
|
|
47
|
+
parentSession === void 0 ? void 0 : { parentSession }
|
|
48
|
+
);
|
|
49
|
+
if (session.isPersisted()) {
|
|
50
|
+
const file = session.getSessionFile();
|
|
51
|
+
if (file !== void 0 && !existsSync(file)) {
|
|
52
|
+
const header = session.getHeader();
|
|
53
|
+
if (header !== null && header.type === "session") {
|
|
54
|
+
writeFileSync(file, `${JSON.stringify(header)}
|
|
55
|
+
`, { flag: "wx" });
|
|
56
|
+
session.setSessionFile(file);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return session;
|
|
38
61
|
}
|
|
39
62
|
export {
|
|
63
|
+
WORKER_SUBMISSION_GATE_KIND,
|
|
40
64
|
createRecordSession,
|
|
41
65
|
roleRunSessionCoordinates
|
|
42
66
|
};
|
package/package.json
CHANGED
package/src/role-runtime.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* 「谁调了谁」复用 Pi parentSession + ADR 0047 correlation,不新增 caller 字段。
|
|
5
5
|
*/
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
7
8
|
import { dirname, resolve, join } from "node:path";
|
|
8
9
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
9
10
|
|
|
@@ -42,48 +43,79 @@ export type CreateRecordSessionOptions = {
|
|
|
42
43
|
readonly subject?: string;
|
|
43
44
|
};
|
|
44
45
|
|
|
46
|
+
/** Authorized no-subject kind that may resume the most recent same-nest peer (ADR 0066). Sole string true source for gate resume identity. */
|
|
47
|
+
export const WORKER_SUBMISSION_GATE_KIND = "worker-submission-gate";
|
|
48
|
+
|
|
45
49
|
/**
|
|
46
50
|
* Sole package entry that constructs a durable Pi session record (ADR 0065).
|
|
47
51
|
* No destination/path parameters — location is computed from ledger topology only.
|
|
52
|
+
* Resume via Pi findMostRecentSession is limited to subject-keyed identity and the
|
|
53
|
+
* authorized worker-submission-gate durable path. Ordinary no-subject children
|
|
54
|
+
* (evidence-children, auditor-roles, …) always mint a fresh session — never reopen
|
|
55
|
+
* a sibling volume selected only by kind/cwd/mtime.
|
|
56
|
+
* New persisted principals materialize their deferred session header before return so
|
|
57
|
+
* custom-entry-only writers do not need a parallel delayed-header helper.
|
|
48
58
|
*/
|
|
49
59
|
export function createRecordSession(options: CreateRecordSessionOptions): SessionManager {
|
|
50
60
|
const cwd = options.cwd;
|
|
51
61
|
const parentFile = options.parent?.getSessionFile();
|
|
52
62
|
const ledgerHome = resolveActivationLedgerHome();
|
|
53
63
|
|
|
64
|
+
let sessionDir: string;
|
|
65
|
+
let parentSession: string | undefined;
|
|
66
|
+
|
|
54
67
|
if (options.subject !== undefined) {
|
|
55
68
|
const digest = createHash("sha256").update(options.subject).digest("hex").slice(0, 32);
|
|
56
|
-
|
|
69
|
+
sessionDir = join(
|
|
57
70
|
activationBookDirectory(ledgerHome, resolveBookKeyFromGit(cwd)),
|
|
58
71
|
options.kind,
|
|
59
72
|
digest,
|
|
60
73
|
);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// mtime ordering, valid-header scan, and cwd filtering semantics.
|
|
64
|
-
const recentFile = findMostRecentSession(sessionDir, cwd);
|
|
65
|
-
if (recentFile !== null) return SessionManager.open(recentFile, sessionDir, cwd);
|
|
66
|
-
return SessionManager.create(cwd, sessionDir, parentFile
|
|
67
|
-
? { parentSession: parentFile }
|
|
68
|
-
: undefined);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (parentFile === undefined || parentFile.length === 0) {
|
|
74
|
+
parentSession = parentFile && parentFile.length > 0 ? parentFile : undefined;
|
|
75
|
+
} else if (parentFile === undefined || parentFile.length === 0) {
|
|
72
76
|
// No durable parent principal — preserve prior in-memory child behavior.
|
|
73
77
|
return SessionManager.inMemory(cwd);
|
|
78
|
+
} else {
|
|
79
|
+
const parentResolved = resolve(parentFile);
|
|
80
|
+
// Nest under parent only when the parent record already lives under the package home.
|
|
81
|
+
// Nest base is dirname(parent file) — the durable principal's directory — never a
|
|
82
|
+
// separate getSessionDir() that can diverge (empty in-memory dir + durable file).
|
|
83
|
+
// Otherwise the book is resolved from cwd (ADR 0048) and the kind sits under that book —
|
|
84
|
+
// workspace / foreign parents cannot drag records out of home.
|
|
85
|
+
sessionDir = physicallyContainedIn(ledgerHome, parentResolved)
|
|
86
|
+
? join(dirname(parentResolved), options.kind)
|
|
87
|
+
: join(activationBookDirectory(ledgerHome, resolveBookKeyFromGit(cwd)), options.kind);
|
|
88
|
+
parentSession = parentFile;
|
|
74
89
|
}
|
|
75
90
|
|
|
76
|
-
const parentResolved = resolve(parentFile);
|
|
77
|
-
|
|
78
|
-
// Nest under parent only when the parent record already lives under the package home.
|
|
79
|
-
// Nest base is dirname(parent file) — the durable principal's directory — never a
|
|
80
|
-
// separate getSessionDir() that can diverge (empty in-memory dir + durable file).
|
|
81
|
-
// Otherwise the book is resolved from cwd (ADR 0048) and the kind sits under that book —
|
|
82
|
-
// workspace / foreign parents cannot drag records out of home.
|
|
83
|
-
const sessionDir = physicallyContainedIn(ledgerHome, parentResolved)
|
|
84
|
-
? join(dirname(parentResolved), options.kind)
|
|
85
|
-
: join(activationBookDirectory(ledgerHome, resolveBookKeyFromGit(cwd)), options.kind);
|
|
86
|
-
|
|
87
91
|
ensureRealDirectoryTree(ledgerHome, sessionDir);
|
|
88
|
-
|
|
92
|
+
// Subject-keyed nests continue by subject digest; gate durable resume is the only
|
|
93
|
+
// authorized no-subject same-nest continuation. All other kinds mint fresh.
|
|
94
|
+
const mayResumeSameNest =
|
|
95
|
+
options.subject !== undefined || options.kind === WORKER_SUBMISSION_GATE_KIND;
|
|
96
|
+
if (mayResumeSameNest) {
|
|
97
|
+
const recentFile = findMostRecentSession(sessionDir, cwd);
|
|
98
|
+
if (recentFile !== null) return SessionManager.open(recentFile, sessionDir, cwd);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const session = SessionManager.create(
|
|
102
|
+
cwd,
|
|
103
|
+
sessionDir,
|
|
104
|
+
parentSession === undefined ? undefined : { parentSession },
|
|
105
|
+
);
|
|
106
|
+
// Pi defers session-file create until the first assistant message. Custom-entry-only
|
|
107
|
+
// records never get that turn, so the sole record entry materializes the in-memory
|
|
108
|
+
// header onto the UUIDv7 path before returning. Existing path → early return.
|
|
109
|
+
if (session.isPersisted()) {
|
|
110
|
+
const file = session.getSessionFile();
|
|
111
|
+
if (file !== undefined && !existsSync(file)) {
|
|
112
|
+
const header = session.getHeader();
|
|
113
|
+
if (header !== null && header.type === "session") {
|
|
114
|
+
writeFileSync(file, `${JSON.stringify(header)}\n`, { flag: "wx" });
|
|
115
|
+
// Rebind so subsequent appendCustomEntry uses O_APPEND (flushed=true).
|
|
116
|
+
session.setSessionFile(file);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return session;
|
|
89
121
|
}
|
package/src/worker-role.ts
CHANGED
|
@@ -26,7 +26,11 @@ import {
|
|
|
26
26
|
parseFixerPrerequisites,
|
|
27
27
|
type FixerInvocationInput,
|
|
28
28
|
} from "./package-contracts/fixer-packet.ts";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
createWorkerSubmissionGate,
|
|
31
|
+
WorkerCommitReminderError,
|
|
32
|
+
WorkerUnfinishedReasonReminderError,
|
|
33
|
+
} from "./worker-submission-gates.ts";
|
|
30
34
|
|
|
31
35
|
export {
|
|
32
36
|
CODER_OUTPUT_TOOL_NAME,
|
|
@@ -171,9 +175,32 @@ function requireSingletonSubmissionCall(
|
|
|
171
175
|
}
|
|
172
176
|
}
|
|
173
177
|
|
|
178
|
+
/** Reminder bounces stay typed rejects; IO/infrastructure keep identity via host failInfrastructure. */
|
|
179
|
+
function assertAcceptableThroughHost(
|
|
180
|
+
submissionGate: { assertAcceptable(status: string, details?: unknown): void },
|
|
181
|
+
status: string,
|
|
182
|
+
details: unknown,
|
|
183
|
+
hostActions: WorkerRoleHostActions,
|
|
184
|
+
ctx: ExtensionContext,
|
|
185
|
+
toolCallId: string,
|
|
186
|
+
): void {
|
|
187
|
+
try {
|
|
188
|
+
submissionGate.assertAcceptable(status, details);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (
|
|
191
|
+
error instanceof WorkerCommitReminderError ||
|
|
192
|
+
error instanceof WorkerUnfinishedReasonReminderError
|
|
193
|
+
) {
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
hostActions.failInfrastructure(error, ctx, toolCallId);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
174
200
|
export function createFixerRoleRuntime(
|
|
175
201
|
pi: ExtensionAPI,
|
|
176
202
|
dependencies: FixerRoleDependencies,
|
|
203
|
+
hostActions: WorkerRoleHostActions,
|
|
177
204
|
): WorkerRoleRuntime {
|
|
178
205
|
let soul: string | undefined;
|
|
179
206
|
let packet: FixerInvocationInput | undefined;
|
|
@@ -251,7 +278,14 @@ export function createFixerRoleRuntime(
|
|
|
251
278
|
ctx,
|
|
252
279
|
);
|
|
253
280
|
const output = deepFreeze(validateFixerOutputForPacket(parameters, phase, packet));
|
|
254
|
-
|
|
281
|
+
assertAcceptableThroughHost(
|
|
282
|
+
submissionGate,
|
|
283
|
+
output.status,
|
|
284
|
+
output,
|
|
285
|
+
hostActions,
|
|
286
|
+
ctx,
|
|
287
|
+
toolCallId,
|
|
288
|
+
);
|
|
255
289
|
return {
|
|
256
290
|
content: [{ type: "text" as const, text: "Fixer report accepted" }],
|
|
257
291
|
details: output,
|
|
@@ -383,7 +417,14 @@ export function createCoderRoleRuntime(
|
|
|
383
417
|
"Coder completed requires the Matt tdd skill to be expanded through Pi /skill:tdd",
|
|
384
418
|
);
|
|
385
419
|
}
|
|
386
|
-
|
|
420
|
+
assertAcceptableThroughHost(
|
|
421
|
+
submissionGate,
|
|
422
|
+
output.status,
|
|
423
|
+
output,
|
|
424
|
+
hostActions,
|
|
425
|
+
ctx,
|
|
426
|
+
toolCallId,
|
|
427
|
+
);
|
|
387
428
|
return {
|
|
388
429
|
content: [{ type: "text" as const, text: "Coder report accepted" }],
|
|
389
430
|
details: output,
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
/** #242 worker gates ①②④. ① durability: ADR 0065/#216 createRecordSession only — no appendCustomEntry bypass / parallel ledger. */
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
chmodSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
4
10
|
import { resolve } from "node:path";
|
|
5
11
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
6
12
|
|
|
7
13
|
import {
|
|
8
14
|
createRecordSession,
|
|
9
15
|
type RecordSessionParent,
|
|
16
|
+
WORKER_SUBMISSION_GATE_KIND,
|
|
10
17
|
} from "./sitian-record-entry.ts";
|
|
11
18
|
|
|
12
19
|
export const WORKER_COMMIT_SUBJECT_PREFIX = "ak-roles:";
|
|
13
20
|
/** Sitian kind for gate ① durable baseline / bounce records (single path segment, not a destination). */
|
|
14
|
-
export const WORKER_SUBMISSION_GATE_RECORD_KIND =
|
|
21
|
+
export const WORKER_SUBMISSION_GATE_RECORD_KIND = WORKER_SUBMISSION_GATE_KIND;
|
|
15
22
|
export const WORKER_COMMIT_BASELINE_ENTRY_TYPE = "commit-baseline";
|
|
16
23
|
export const WORKER_COMMIT_REMINDER_BOUNCE_ENTRY_TYPE = "commit-reminder-bounce";
|
|
17
24
|
|
|
@@ -163,34 +170,15 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
163
170
|
}
|
|
164
171
|
|
|
165
172
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* activation-ledger-session materializeDeferredSessionFile — then custom entries append.
|
|
173
|
+
* Open gate ① record via sitian entry only — resume/lifecycle live in createRecordSession.
|
|
174
|
+
* Gate consumes the returned session; no nest scan, unlink, or peer reopen.
|
|
169
175
|
*/
|
|
170
|
-
function materializeSessionHeader(session: SessionManager): void {
|
|
171
|
-
if (!session.isPersisted()) return;
|
|
172
|
-
const file = session.getSessionFile();
|
|
173
|
-
if (file === undefined || existsSync(file)) return;
|
|
174
|
-
const header = session.getHeader();
|
|
175
|
-
if (header === null || header.type !== "session") return;
|
|
176
|
-
writeFileSync(file, `${JSON.stringify(header)}\n`, { flag: "wx" });
|
|
177
|
-
// Rebind so subsequent appendCustomEntry uses O_APPEND (flushed=true).
|
|
178
|
-
session.setSessionFile(file);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/** Open gate ① record via sitian entry; resume continues the nest the entry resolved (no self-computed destination). */
|
|
182
176
|
function openGateRecord(cwd: string, parent?: WorkerSubmissionGateParent): SessionManager {
|
|
183
|
-
|
|
177
|
+
return createRecordSession({
|
|
184
178
|
cwd,
|
|
185
179
|
kind: WORKER_SUBMISSION_GATE_RECORD_KIND,
|
|
186
180
|
...(parent === undefined ? {} : { parent }),
|
|
187
181
|
});
|
|
188
|
-
// No durable parent principal → in-memory only (same-process bounce still works).
|
|
189
|
-
if (!discovered.isPersisted()) return discovered;
|
|
190
|
-
// Destination came from createRecordSession; continueRecent only picks the latest file in that nest.
|
|
191
|
-
const session = SessionManager.continueRecent(cwd, discovered.getSessionDir());
|
|
192
|
-
materializeSessionHeader(session);
|
|
193
|
-
return session;
|
|
194
182
|
}
|
|
195
183
|
|
|
196
184
|
function readGateState(session: SessionManager): {
|