@akagilnc/pi-workflow-roles 0.1.3783 → 0.1.3795
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 +3 -1
- package/README.zh-CN.md +3 -1
- package/dist/acp-host/production-host.js +681 -269
- package/dist/atomic-write.js +23 -0
- package/dist/collector-evidence.js +4 -1
- package/dist/collector-github.js +1 -0
- package/dist/collector-handbook.js +142 -0
- package/dist/collector-identity.js +11 -0
- package/dist/collector-ledger.js +109 -63
- package/dist/collector-role.js +182 -18
- package/dist/collector-tool-schemas.js +40 -3
- package/dist/headless-host/production-host.js +682 -270
- package/dist/pi/role-turn-host.js +1 -0
- package/dist/public-cli/invocation.js +15 -0
- package/dist/public-cli/main.js +45 -77
- package/dist/public-cli/option-definitions.js +16 -2
- package/dist/public-cli/run-lifecycle.js +8 -0
- package/dist/role-runtime.js +3 -0
- package/extensions/role-runtime.ts +2 -0
- package/package.json +1 -1
- package/resources/collector-bot-handbook.md +33 -0
- package/src/acp-host/production-host.ts +4 -0
- package/src/acp-host/role-envelope.ts +1 -0
- package/src/collector-evidence.ts +6 -1
- package/src/collector-github.ts +3 -0
- package/src/collector-handbook.ts +194 -0
- package/src/collector-identity.ts +14 -0
- package/src/collector-ledger.ts +132 -80
- package/src/collector-role.ts +198 -14
- package/src/collector-tool-schemas.ts +42 -3
- package/src/host-contracts.ts +2 -0
- package/src/pi/role-turn-host.ts +1 -0
- package/src/public-cli/collector-run.ts +2 -0
- package/src/public-cli/invocation.ts +21 -0
- package/src/public-cli/option-definitions.ts +16 -2
- package/src/public-cli/run-lifecycle.ts +10 -0
- package/src/role-runtime.ts +5 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-directory temp + rename atomic file replace.
|
|
3
|
+
* Shared primitive for ledger-adjacent typed pages (analyst metrics, etc.).
|
|
4
|
+
* Does not open/truncate an existing destination inode, so hard-linked twins
|
|
5
|
+
* keep prior bytes until the directory entry is swapped.
|
|
6
|
+
* Parent directory must already exist — callers that write under the package
|
|
7
|
+
* ledger home own confinement via ensureRealDirectoryTree (ADR 0038).
|
|
8
|
+
*/
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { rename, rm, writeFile } from "node:fs/promises";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
export async function writeFileAtomically(destination, contents) {
|
|
13
|
+
const parent = dirname(destination);
|
|
14
|
+
const temporary = join(parent, `.atomic-write-${randomUUID()}.tmp`);
|
|
15
|
+
try {
|
|
16
|
+
await writeFile(temporary, contents);
|
|
17
|
+
await rename(temporary, destination);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
|
|
2
|
+
/** Default collector wait window (D4): 10 minutes from the work-step open, not from session activation. */
|
|
3
|
+
export const COLLECTOR_DEFAULT_WAIT_WINDOW_MS = 10 * 60 * 1000;
|
|
3
4
|
export function createSystemCollectorClock() {
|
|
4
5
|
const start = process.hrtime.bigint();
|
|
5
6
|
return {
|
|
@@ -76,6 +77,7 @@ export function normalizePullRequestEvidence(pr, observedAt) {
|
|
|
76
77
|
number: pr.number,
|
|
77
78
|
state: pr.state,
|
|
78
79
|
headOid: pr.headOid,
|
|
80
|
+
createdAt: pr.createdAt ?? null,
|
|
79
81
|
updatedAt: pr.updatedAt ?? null,
|
|
80
82
|
htmlUrl: pr.url,
|
|
81
83
|
});
|
|
@@ -89,6 +91,7 @@ export function normalizePullRequestEvidence(pr, observedAt) {
|
|
|
89
91
|
state: pr.state,
|
|
90
92
|
commitOid: pr.headOid,
|
|
91
93
|
htmlUrl: pr.url,
|
|
94
|
+
// Version clock stays updatedAt. Create success for the wait window is snapshot.prCreatedAt only (#678).
|
|
92
95
|
authoritativeTime: pr.updatedAt ?? null,
|
|
93
96
|
firstObservedAt: observedAt,
|
|
94
97
|
raw: pr.raw,
|
package/dist/collector-github.js
CHANGED
|
@@ -289,6 +289,7 @@ export function normalizePullRequest(raw) {
|
|
|
289
289
|
number,
|
|
290
290
|
state,
|
|
291
291
|
headOid: head["sha"],
|
|
292
|
+
...(typeof raw["created_at"] === "string" ? { createdAt: raw["created_at"] } : {}),
|
|
292
293
|
...(typeof raw["updated_at"] === "string" ? { updatedAt: raw["updated_at"] } : {}),
|
|
293
294
|
url: htmlUrl,
|
|
294
295
|
raw,
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collector bot handbook — opaque working memory under the book topology.
|
|
3
|
+
* Runtime stores and delivers UTF-8 text only; never parses free text into
|
|
4
|
+
* code state rules (parent #673 D3 / #677).
|
|
5
|
+
*
|
|
6
|
+
* Placement reuses the machine ledger home (ADR 0048) when the admitted
|
|
7
|
+
* session already sits under books/<bookKey>/ — no parallel persistence frame.
|
|
8
|
+
*/
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
10
|
+
import { join, sep } from "node:path";
|
|
11
|
+
import { writeFileAtomically } from "./atomic-write.js";
|
|
12
|
+
import { activationBookDirectory, assertLedgerFileInsideHome, ensureRealDirectoryTree, resolveActivationLedgerHomeForPath, } from "./activation-ledger-topology.js";
|
|
13
|
+
import { COLLECTOR_HANDBOOK_MAX_BYTES } from "./collector-tool-schemas.js";
|
|
14
|
+
/**
|
|
15
|
+
* Derive handbook root from an admitted session/run path under
|
|
16
|
+
* `.ak-roles/books/<bookKey>/...`. Fails closed when topology is absent.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveCollectorHandbookRoot(sessionPath) {
|
|
19
|
+
if (typeof sessionPath !== "string" || sessionPath.trim().length === 0) {
|
|
20
|
+
throw new Error("通进司手册要求非空 session 路径,且位于 books/<bookKey>/");
|
|
21
|
+
}
|
|
22
|
+
// Platform path segments only — never rewrite non-separator characters (POSIX `\\` is literal).
|
|
23
|
+
const segments = sessionPath.split(sep);
|
|
24
|
+
let bookKey;
|
|
25
|
+
for (let i = 0; i + 3 < segments.length; i += 1) {
|
|
26
|
+
if (segments[i] === ".ak-roles" && segments[i + 1] === "books") {
|
|
27
|
+
const candidate = segments[i + 2];
|
|
28
|
+
// Require a following segment so bookKey is a true path component under books/.
|
|
29
|
+
if (candidate.length > 0) {
|
|
30
|
+
bookKey = candidate;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (bookKey === undefined) {
|
|
36
|
+
throw new Error(`通进司手册要求 session 位于 books/<bookKey>/;收到 ${sessionPath}`);
|
|
37
|
+
}
|
|
38
|
+
if (bookKey === "." || bookKey === "..") {
|
|
39
|
+
throw new Error(`通进司手册拒绝不安全 bookKey ${JSON.stringify(bookKey)}`);
|
|
40
|
+
}
|
|
41
|
+
const ledgerHome = resolveActivationLedgerHomeForPath(sessionPath);
|
|
42
|
+
const root = join(activationBookDirectory(ledgerHome, bookKey), "collector-handbook");
|
|
43
|
+
return { ledgerHome, bookKey, root };
|
|
44
|
+
}
|
|
45
|
+
/** Flat repo file name under handbook/repos/ — avoids nested owner/repo dirs. */
|
|
46
|
+
function collectorHandbookRepoFileName(repositoryCanonical) {
|
|
47
|
+
if (!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/.test(repositoryCanonical)) {
|
|
48
|
+
throw new Error(`通进司手册仓库文件要求规范 owner/repo,收到 ${JSON.stringify(repositoryCanonical)}`);
|
|
49
|
+
}
|
|
50
|
+
return `${repositoryCanonical.replaceAll("/", "__")}.md`;
|
|
51
|
+
}
|
|
52
|
+
export function createCollectorHandbookStore(input) {
|
|
53
|
+
const generalPath = join(input.handbookRoot, "general.md");
|
|
54
|
+
const repoDir = join(input.handbookRoot, "repos");
|
|
55
|
+
const repoPath = join(repoDir, collectorHandbookRepoFileName(input.repositoryCanonical));
|
|
56
|
+
/**
|
|
57
|
+
* ADR 0038: confine at the real read seam. Parent dirs must be physical under
|
|
58
|
+
* ledger home; the leaf must not be a pre-existing symlink (even in-home).
|
|
59
|
+
*/
|
|
60
|
+
/** Sole UTF-8 budget seam — write and read share COLLECTOR_HANDBOOK_MAX_BYTES. */
|
|
61
|
+
const assertHandbookBudget = (body, label) => {
|
|
62
|
+
const byteLength = Buffer.byteLength(body, "utf8");
|
|
63
|
+
if (byteLength > COLLECTOR_HANDBOOK_MAX_BYTES) {
|
|
64
|
+
throw new Error(`通进司手册${label} UTF-8 至多 ${COLLECTOR_HANDBOOK_MAX_BYTES} 字节,收到 ${byteLength}`);
|
|
65
|
+
}
|
|
66
|
+
return byteLength;
|
|
67
|
+
};
|
|
68
|
+
const readOptional = async (path, parentDir) => {
|
|
69
|
+
ensureRealDirectoryTree(input.ledgerHome, parentDir);
|
|
70
|
+
assertLedgerFileInsideHome(path, input.ledgerHome);
|
|
71
|
+
try {
|
|
72
|
+
const body = await readFile(path, "utf8");
|
|
73
|
+
assertHandbookBudget(body, "正文");
|
|
74
|
+
return body;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (isNotFound(error))
|
|
78
|
+
return undefined;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
root: input.handbookRoot,
|
|
84
|
+
repositoryCanonical: input.repositoryCanonical,
|
|
85
|
+
async read() {
|
|
86
|
+
const bookGeneral = await readOptional(generalPath, input.handbookRoot);
|
|
87
|
+
const bookRepo = await readOptional(repoPath, repoDir);
|
|
88
|
+
if (bookGeneral !== undefined) {
|
|
89
|
+
return {
|
|
90
|
+
general: bookGeneral,
|
|
91
|
+
repo: bookRepo ?? "",
|
|
92
|
+
generalSource: "book",
|
|
93
|
+
repoSource: bookRepo === undefined ? "empty" : "book",
|
|
94
|
+
generalPath,
|
|
95
|
+
repoPath,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const seed = input.seedGeneral;
|
|
99
|
+
if (typeof seed === "string" && seed.length > 0) {
|
|
100
|
+
assertHandbookBudget(seed, "正文");
|
|
101
|
+
return {
|
|
102
|
+
general: seed,
|
|
103
|
+
repo: bookRepo ?? "",
|
|
104
|
+
generalSource: "seed",
|
|
105
|
+
repoSource: bookRepo === undefined ? "empty" : "book",
|
|
106
|
+
generalPath,
|
|
107
|
+
repoPath,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
general: "",
|
|
112
|
+
repo: bookRepo ?? "",
|
|
113
|
+
generalSource: "empty",
|
|
114
|
+
repoSource: bookRepo === undefined ? "empty" : "book",
|
|
115
|
+
generalPath,
|
|
116
|
+
repoPath,
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
async write(scope, body) {
|
|
120
|
+
// body string shape = collectorHandbookWriteArgsSchema; byte budget is business (UTF-8).
|
|
121
|
+
const byteLength = assertHandbookBudget(body, "正文");
|
|
122
|
+
ensureRealDirectoryTree(input.ledgerHome, input.handbookRoot);
|
|
123
|
+
const path = scope === "general" ? generalPath : repoPath;
|
|
124
|
+
if (scope === "repo") {
|
|
125
|
+
ensureRealDirectoryTree(input.ledgerHome, repoDir);
|
|
126
|
+
}
|
|
127
|
+
assertLedgerFileInsideHome(path, input.ledgerHome);
|
|
128
|
+
await writeFileAtomically(path, body);
|
|
129
|
+
return {
|
|
130
|
+
scope,
|
|
131
|
+
path,
|
|
132
|
+
byteLength,
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function isNotFound(error) {
|
|
138
|
+
return typeof error === "object"
|
|
139
|
+
&& error !== null
|
|
140
|
+
&& "code" in error
|
|
141
|
+
&& error.code === "ENOENT";
|
|
142
|
+
}
|
|
@@ -90,6 +90,17 @@ export class CollectorNonOpenRequestError extends CorrectableSubmissionError {
|
|
|
90
90
|
this.name = "CollectorNonOpenRequestError";
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
+
/** #678: wait window not open or already ended — bounce without latching fatal so materials still seal. */
|
|
94
|
+
export class CollectorWaitWindowClosedError extends CorrectableSubmissionError {
|
|
95
|
+
constructor(action) {
|
|
96
|
+
super(action === "request"
|
|
97
|
+
? "通进司请求不在资格截止前"
|
|
98
|
+
: action === "wait-before-open"
|
|
99
|
+
? "通进司等待需要先在工作步骤开启等待窗(新建 PR 用创建成功时刻;已有 PR 在触发阶段结束后开启)"
|
|
100
|
+
: "通进司等待不在资格截止前");
|
|
101
|
+
this.name = "CollectorWaitWindowClosedError";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
93
104
|
function candidateRecord(candidate) {
|
|
94
105
|
if (candidate === undefined || candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
95
106
|
return undefined;
|
package/dist/collector-ledger.js
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { applyEvidenceVersionHistory, assignWindowRelations, COLLECTOR_ELIGIBILITY_MS, measureNormalizedBytes, normalizeAuthenticatedUserEvidence, normalizeIssueCommentEvidence, normalizePullRequestEvidence, normalizePullRequestReactionEvidence, normalizeReviewCommentEvidence, normalizeReviewEvidence, sha256Text, } from "./collector-evidence.js";
|
|
1
|
+
import { applyEvidenceVersionHistory, assignWindowRelations, COLLECTOR_DEFAULT_WAIT_WINDOW_MS, measureNormalizedBytes, normalizeAuthenticatedUserEvidence, normalizeIssueCommentEvidence, normalizePullRequestEvidence, normalizePullRequestReactionEvidence, normalizeReviewCommentEvidence, normalizeReviewEvidence, sha256Text, } from "./collector-evidence.js";
|
|
3
2
|
import { buildCollectorRequestBody, } from "./collector-github.js";
|
|
4
|
-
import { CollectorNonOpenRequestError } from "./collector-identity.js";
|
|
5
|
-
import { collectorObserveArgsSchema, collectorOutputArgsSchema, collectorReadArgsSchema, collectorRequestArgsSchema, collectorWaitArgsSchema, } from "./collector-tool-schemas.js";
|
|
3
|
+
import { CollectorNonOpenRequestError, CollectorWaitWindowClosedError, } from "./collector-identity.js";
|
|
6
4
|
import { COLLECTOR_OUTPUT_TOOL } from "./package-contracts/collector-output.js";
|
|
7
5
|
export const COLLECTOR_OBSERVE_TOOL = "ak_collector_observe";
|
|
8
6
|
export const COLLECTOR_READ_TOOL = "ak_collector_read";
|
|
9
7
|
export const COLLECTOR_REQUEST_TOOL = "ak_collector_request";
|
|
10
8
|
export const COLLECTOR_WAIT_TOOL = "ak_collector_wait";
|
|
9
|
+
/** #678 D4: open wait window at a work step — business tool, ledger-booked. */
|
|
10
|
+
export const COLLECTOR_OPEN_WAIT_WINDOW_TOOL = "ak_collector_open_wait_window";
|
|
11
11
|
/** #676 A: role-decided target bind — business tool, ledger-booked. */
|
|
12
12
|
export const COLLECTOR_BIND_TARGET_TOOL = "ak_collector_bind_target";
|
|
13
|
+
/** #677: opaque handbook write — business tool, ledger-booked. */
|
|
14
|
+
export const COLLECTOR_HANDBOOK_WRITE_TOOL = "ak_collector_handbook_write";
|
|
13
15
|
export { COLLECTOR_OUTPUT_TOOL };
|
|
14
16
|
export const COLLECTOR_OPERATIONAL_TOOLS = [
|
|
15
17
|
COLLECTOR_BIND_TARGET_TOOL,
|
|
16
18
|
COLLECTOR_OBSERVE_TOOL,
|
|
17
19
|
COLLECTOR_READ_TOOL,
|
|
18
20
|
COLLECTOR_REQUEST_TOOL,
|
|
21
|
+
COLLECTOR_OPEN_WAIT_WINDOW_TOOL,
|
|
19
22
|
COLLECTOR_WAIT_TOOL,
|
|
23
|
+
COLLECTOR_HANDBOOK_WRITE_TOOL,
|
|
20
24
|
];
|
|
21
25
|
/**
|
|
22
26
|
* #641 chain①: observe context projects at most this many bytes of each
|
|
@@ -24,7 +28,7 @@ export const COLLECTOR_OPERATIONAL_TOOLS = [
|
|
|
24
28
|
* (evidenceId + htmlUrl) and is preserved in the volume, never unconditionally
|
|
25
29
|
* transcribed into model context or receipt.
|
|
26
30
|
*/
|
|
27
|
-
|
|
31
|
+
const COLLECTOR_OBSERVE_BODY_HEAD_BYTES = 512;
|
|
28
32
|
function utf8SafePrefix(text, maxBytes) {
|
|
29
33
|
if (Buffer.byteLength(text, "utf8") <= maxBytes)
|
|
30
34
|
return text;
|
|
@@ -41,7 +45,7 @@ function utf8SafePrefix(text, maxBytes) {
|
|
|
41
45
|
* (collector-side; provider-visible observe details carry only this bounded
|
|
42
46
|
* projection), never unconditionally transcribed into model context.
|
|
43
47
|
*/
|
|
44
|
-
|
|
48
|
+
function projectObserveContextView(modelView) {
|
|
45
49
|
return {
|
|
46
50
|
...modelView,
|
|
47
51
|
evidence: modelView.evidence.map((entry) => entry.body === undefined
|
|
@@ -50,39 +54,34 @@ export function projectObserveContextView(modelView) {
|
|
|
50
54
|
};
|
|
51
55
|
}
|
|
52
56
|
export const COLLECTOR_ACTIVATION_ENTRY_TYPE = "ak-collector-activation";
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
const COLLECTOR_SNAPSHOT_ENTRY_TYPE = "ak-collector-snapshot";
|
|
58
|
+
const COLLECTOR_REQUEST_ENTRY_TYPE = "ak-collector-request";
|
|
59
|
+
const COLLECTOR_WAIT_ENTRY_TYPE = "ak-collector-wait";
|
|
56
60
|
function isOperationalTool(name) {
|
|
57
61
|
return COLLECTOR_OPERATIONAL_TOOLS.includes(name);
|
|
58
62
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
switch (name) {
|
|
65
|
-
case COLLECTOR_OBSERVE_TOOL:
|
|
66
|
-
return Value.Check(collectorObserveArgsSchema, args);
|
|
67
|
-
case COLLECTOR_READ_TOOL:
|
|
68
|
-
return Value.Check(collectorReadArgsSchema, args);
|
|
69
|
-
case COLLECTOR_REQUEST_TOOL:
|
|
70
|
-
return Value.Check(collectorRequestArgsSchema, args);
|
|
71
|
-
case COLLECTOR_WAIT_TOOL:
|
|
72
|
-
return Value.Check(collectorWaitArgsSchema, args);
|
|
73
|
-
case COLLECTOR_OUTPUT_TOOL:
|
|
74
|
-
return Value.Check(collectorOutputArgsSchema, args);
|
|
75
|
-
default:
|
|
76
|
-
return false;
|
|
63
|
+
function resolveWaitWindowMs(raw) {
|
|
64
|
+
if (raw === undefined)
|
|
65
|
+
return COLLECTOR_DEFAULT_WAIT_WINDOW_MS;
|
|
66
|
+
if (!Number.isSafeInteger(raw) || raw < 1) {
|
|
67
|
+
throw new Error("通进司 waitWindowMs 须为正安全整数毫秒");
|
|
77
68
|
}
|
|
69
|
+
return raw;
|
|
78
70
|
}
|
|
79
|
-
export function createCollectorLedger(
|
|
71
|
+
export function createCollectorLedger(input, options) {
|
|
72
|
+
const config = {
|
|
73
|
+
repository: input.repository,
|
|
74
|
+
prNumber: input.prNumber,
|
|
75
|
+
manifest: input.manifest,
|
|
76
|
+
waitWindowMs: resolveWaitWindowMs(input.waitWindowMs),
|
|
77
|
+
};
|
|
80
78
|
const clock = options?.clock;
|
|
81
79
|
const journal = options?.journal;
|
|
82
80
|
let fatal = false;
|
|
83
81
|
let fatalReason;
|
|
84
82
|
let outputCandidate = false;
|
|
85
83
|
let pendingOutputCallId;
|
|
84
|
+
let sessionReady = false;
|
|
86
85
|
let activationTime;
|
|
87
86
|
let deadlineTime;
|
|
88
87
|
let activationMono;
|
|
@@ -129,13 +128,13 @@ export function createCollectorLedger(config, options) {
|
|
|
129
128
|
};
|
|
130
129
|
const remainingMs = (clock) => {
|
|
131
130
|
if (deadlineMono === undefined)
|
|
132
|
-
return
|
|
131
|
+
return config.waitWindowMs;
|
|
133
132
|
return Math.max(0, deadlineMono - monoNowOrThrow(clock));
|
|
134
133
|
};
|
|
135
134
|
const prIdentity = (pr) => `${pr.state}|${pr.headOid}|${pr.updatedAt ?? ""}`;
|
|
136
135
|
const requireBoundPr = () => {
|
|
137
136
|
if (config.prNumber === undefined) {
|
|
138
|
-
throw new Error("
|
|
137
|
+
throw new Error("通进司 PR 目标未绑定");
|
|
139
138
|
}
|
|
140
139
|
return config.prNumber;
|
|
141
140
|
};
|
|
@@ -200,8 +199,10 @@ export function createCollectorLedger(config, options) {
|
|
|
200
199
|
return copy;
|
|
201
200
|
};
|
|
202
201
|
const commitActivationWindow = (nextActivation, nextDeadline) => {
|
|
202
|
+
// D6: once open, PR updates / second open must not reset the window.
|
|
203
203
|
if (activationTime !== undefined)
|
|
204
204
|
return;
|
|
205
|
+
sessionReady = true;
|
|
205
206
|
activationTime = nextActivation;
|
|
206
207
|
deadlineTime = nextDeadline;
|
|
207
208
|
bindDeadlineMonoFromWall();
|
|
@@ -329,6 +330,9 @@ export function createCollectorLedger(config, options) {
|
|
|
329
330
|
if (typeof data.activationTime === "string" && typeof data.deadlineTime === "string") {
|
|
330
331
|
commitActivationWindow(new Date(data.activationTime), new Date(data.deadlineTime));
|
|
331
332
|
}
|
|
333
|
+
else if (data.sessionReady === true) {
|
|
334
|
+
sessionReady = true;
|
|
335
|
+
}
|
|
332
336
|
continue;
|
|
333
337
|
}
|
|
334
338
|
if (entry.customType === COLLECTOR_SNAPSHOT_ENTRY_TYPE) {
|
|
@@ -389,7 +393,7 @@ export function createCollectorLedger(config, options) {
|
|
|
389
393
|
return outputCandidate || pendingOutputCallId !== undefined;
|
|
390
394
|
},
|
|
391
395
|
get activationRecorded() {
|
|
392
|
-
return
|
|
396
|
+
return sessionReady;
|
|
393
397
|
},
|
|
394
398
|
get activationTime() {
|
|
395
399
|
return activationTime;
|
|
@@ -426,17 +430,39 @@ export function createCollectorLedger(config, options) {
|
|
|
426
430
|
},
|
|
427
431
|
latchFatal,
|
|
428
432
|
assertNotFatal,
|
|
429
|
-
recordActivation(
|
|
433
|
+
recordActivation(_clock) {
|
|
430
434
|
assertNotFatal();
|
|
431
|
-
if (
|
|
435
|
+
if (sessionReady)
|
|
432
436
|
return;
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
+
sessionReady = true;
|
|
438
|
+
// Session ready only — wait window opens later at a work step (#678 D4).
|
|
439
|
+
appendJournal(COLLECTOR_ACTIVATION_ENTRY_TYPE, {
|
|
440
|
+
sessionReady: true,
|
|
441
|
+
});
|
|
442
|
+
},
|
|
443
|
+
openWaitWindow(clock, options) {
|
|
444
|
+
assertNotFatal();
|
|
445
|
+
if (!sessionReady) {
|
|
446
|
+
throw latchFatal("通进司开启等待窗需要先激活会话");
|
|
447
|
+
}
|
|
448
|
+
if (activationTime !== undefined)
|
|
449
|
+
return; // D6: do not reset
|
|
450
|
+
const startedAt = options?.startedAt ?? clock.wallNow();
|
|
451
|
+
if (!(startedAt instanceof Date) || Number.isNaN(startedAt.getTime())) {
|
|
452
|
+
throw new Error("通进司等待窗 startedAt 须为有效时间");
|
|
453
|
+
}
|
|
454
|
+
const nextDeadline = new Date(startedAt.getTime() + config.waitWindowMs);
|
|
455
|
+
// Bind mono relative to current clock so a past startedAt shortens remaining correctly.
|
|
456
|
+
activationTime = startedAt;
|
|
457
|
+
deadlineTime = nextDeadline;
|
|
458
|
+
const wallNow = clock.wallNow().getTime();
|
|
459
|
+
const monoNow = clock.monoNow();
|
|
460
|
+
activationMono = monoNow - (wallNow - startedAt.getTime());
|
|
461
|
+
deadlineMono = monoNow + (nextDeadline.getTime() - wallNow);
|
|
437
462
|
appendJournal(COLLECTOR_ACTIVATION_ENTRY_TYPE, {
|
|
438
463
|
activationTime: activationTime.toISOString(),
|
|
439
464
|
deadlineTime: deadlineTime.toISOString(),
|
|
465
|
+
waitWindowMs: config.waitWindowMs,
|
|
440
466
|
});
|
|
441
467
|
},
|
|
442
468
|
recordOutputCandidate() {
|
|
@@ -449,10 +475,10 @@ export function createCollectorLedger(config, options) {
|
|
|
449
475
|
throw new Error("通进司已产出输出候选,本局不再受理目标绑定");
|
|
450
476
|
}
|
|
451
477
|
if (!Number.isSafeInteger(prNumber) || prNumber < 1) {
|
|
452
|
-
throw new Error("
|
|
478
|
+
throw new Error("通进司绑定目标要求正安全整数 PR 号");
|
|
453
479
|
}
|
|
454
480
|
if (config.prNumber !== undefined && config.prNumber !== prNumber) {
|
|
455
|
-
throw new Error(
|
|
481
|
+
throw new Error(`通进司目标已绑定 PR ${config.prNumber},不可改绑为 ${prNumber}`);
|
|
456
482
|
}
|
|
457
483
|
config.prNumber = prNumber;
|
|
458
484
|
appendJournal("ak-collector-target-bound", {
|
|
@@ -521,7 +547,7 @@ export function createCollectorLedger(config, options) {
|
|
|
521
547
|
},
|
|
522
548
|
async observe(transport, clock, signal) {
|
|
523
549
|
assertNotFatal();
|
|
524
|
-
if (
|
|
550
|
+
if (!sessionReady) {
|
|
525
551
|
throw latchFatal("通进司观察需要激活");
|
|
526
552
|
}
|
|
527
553
|
if (signal?.aborted) {
|
|
@@ -611,6 +637,7 @@ export function createCollectorLedger(config, options) {
|
|
|
611
637
|
prNumber: requireBoundPr(),
|
|
612
638
|
prState: pr.state,
|
|
613
639
|
headOid: pr.headOid,
|
|
640
|
+
...(pr.createdAt === undefined ? {} : { prCreatedAt: pr.createdAt }),
|
|
614
641
|
complete: true,
|
|
615
642
|
evidenceIds: storedIds,
|
|
616
643
|
pageDiagnostics,
|
|
@@ -644,15 +671,29 @@ export function createCollectorLedger(config, options) {
|
|
|
644
671
|
},
|
|
645
672
|
async request(input, transport, clock, signal) {
|
|
646
673
|
assertNotFatal();
|
|
647
|
-
if (
|
|
674
|
+
if (!sessionReady) {
|
|
648
675
|
throw latchFatal("通进司请求需要激活");
|
|
649
676
|
}
|
|
650
677
|
if (ledger.unresolvedTransportFailure) {
|
|
651
678
|
throw latchFatal("通进司请求时存在未恢复的传输失败");
|
|
652
679
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
680
|
+
// Sole request identity seam: trim once, then use everywhere (lookup/marker/attemptKey).
|
|
681
|
+
// Schema pattern rejects leading/trailing whitespace at host; trim still collapses
|
|
682
|
+
// direct ledger callers and any residual padding into one stable id (#677 D2).
|
|
683
|
+
if (typeof input.requestId !== "string") {
|
|
684
|
+
throw new Error("通进司 requestId 须为字符串");
|
|
685
|
+
}
|
|
686
|
+
const requestId = input.requestId.trim();
|
|
687
|
+
if (requestId.length === 0) {
|
|
688
|
+
throw new Error("通进司 requestId 须为非空(首尾空白不构成身份)");
|
|
689
|
+
}
|
|
690
|
+
const configured = config.manifest.requests.find((item) => item.id === requestId);
|
|
691
|
+
const roleBody = typeof input.body === "string" ? input.body : undefined;
|
|
692
|
+
if (configured === undefined) {
|
|
693
|
+
// #677: role-decided trigger from handbook/field activity — body required when not in manifest.
|
|
694
|
+
if (roleBody === undefined || roleBody.trim().length === 0) {
|
|
695
|
+
throw new Error(`未知通进司 requestId "${requestId}" 且未提供 body;请提交角色判定的请求正文或使用 request-manifest`);
|
|
696
|
+
}
|
|
656
697
|
}
|
|
657
698
|
const snapshot = snapshots.find((item) => item.snapshotId === input.snapshotId);
|
|
658
699
|
if (snapshot === undefined) {
|
|
@@ -667,12 +708,15 @@ export function createCollectorLedger(config, options) {
|
|
|
667
708
|
}
|
|
668
709
|
if (pastCutoff(clock)) {
|
|
669
710
|
finalObservationRequired = true;
|
|
670
|
-
|
|
711
|
+
// #678: do not latch fatal — materials must still seal after the window ends.
|
|
712
|
+
throw new CollectorWaitWindowClosedError("request");
|
|
671
713
|
}
|
|
714
|
+
// Caller manifest body wins when requestId is configured; otherwise role body.
|
|
715
|
+
const configuredBody = configured?.requestBody ?? roleBody;
|
|
672
716
|
const { body, marker } = buildCollectorRequestBody({
|
|
673
|
-
configuredBody
|
|
717
|
+
configuredBody,
|
|
674
718
|
manifestDigest: config.manifest.digest,
|
|
675
|
-
requestId
|
|
719
|
+
requestId,
|
|
676
720
|
headOid: snapshot.headOid,
|
|
677
721
|
});
|
|
678
722
|
const existingMarker = snapshot.evidenceIds.some((id) => {
|
|
@@ -683,23 +727,23 @@ export function createCollectorLedger(config, options) {
|
|
|
683
727
|
record.body.includes(marker);
|
|
684
728
|
});
|
|
685
729
|
if (existingMarker) {
|
|
686
|
-
throw new Error(`通进司在此 HEAD 已有同 marker 的已认证请求 "${
|
|
730
|
+
throw new Error(`通进司在此 HEAD 已有同 marker 的已认证请求 "${requestId}"`);
|
|
687
731
|
}
|
|
688
732
|
const boundPr = requireBoundPr();
|
|
689
733
|
const attemptKey = [
|
|
690
734
|
config.repository.canonical,
|
|
691
735
|
String(boundPr),
|
|
692
736
|
snapshot.headOid,
|
|
693
|
-
|
|
737
|
+
requestId,
|
|
694
738
|
].join("|");
|
|
695
739
|
if (attemptKeys.has(attemptKey)) {
|
|
696
|
-
throw new Error(`通进司进程内请求 "${
|
|
740
|
+
throw new Error(`通进司进程内请求 "${requestId}" 在 HEAD ${snapshot.headOid} 的 attempt 已用`);
|
|
697
741
|
}
|
|
698
742
|
const startedAt = clock.wallNow().toISOString();
|
|
699
743
|
const attemptId = sha256Text(`${attemptKey}:${startedAt}`).slice(0, 16);
|
|
700
744
|
const attempt = {
|
|
701
745
|
attemptId,
|
|
702
|
-
requestId
|
|
746
|
+
requestId,
|
|
703
747
|
observedHead: snapshot.headOid,
|
|
704
748
|
snapshotId: snapshot.snapshotId,
|
|
705
749
|
marker,
|
|
@@ -741,7 +785,7 @@ export function createCollectorLedger(config, options) {
|
|
|
741
785
|
return {
|
|
742
786
|
status: "succeeded",
|
|
743
787
|
attemptId,
|
|
744
|
-
requestId
|
|
788
|
+
requestId,
|
|
745
789
|
observedHead: snapshot.headOid,
|
|
746
790
|
marker,
|
|
747
791
|
commentEvidenceId: record.evidenceId,
|
|
@@ -755,7 +799,7 @@ export function createCollectorLedger(config, options) {
|
|
|
755
799
|
return {
|
|
756
800
|
status: "ambiguous_loss",
|
|
757
801
|
attemptId,
|
|
758
|
-
requestId
|
|
802
|
+
requestId,
|
|
759
803
|
observedHead: snapshot.headOid,
|
|
760
804
|
marker,
|
|
761
805
|
diagnostics: result.diagnostics,
|
|
@@ -769,23 +813,22 @@ export function createCollectorLedger(config, options) {
|
|
|
769
813
|
},
|
|
770
814
|
async wait(input, clock, signal) {
|
|
771
815
|
assertNotFatal();
|
|
772
|
-
if (
|
|
816
|
+
if (!sessionReady) {
|
|
773
817
|
throw latchFatal("通进司等待需要激活");
|
|
774
818
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
throw new Error(`通进司等待 durationMs 至多为 ${COLLECTOR_ELIGIBILITY_MS}`);
|
|
819
|
+
// #678: wait never invents a window start. Work-step open (create-success / trigger-end)
|
|
820
|
+
// must be explicit — silent "now" would break the new-PR create-success clock.
|
|
821
|
+
if (activationTime === undefined || deadlineTime === undefined) {
|
|
822
|
+
throw new CollectorWaitWindowClosedError("wait-before-open");
|
|
780
823
|
}
|
|
824
|
+
// durationMs shape authority = collectorWaitArgsSchema (host parameters).
|
|
781
825
|
if (pastCutoff(clock)) {
|
|
782
826
|
finalObservationRequired = true;
|
|
783
|
-
throw
|
|
827
|
+
throw new CollectorWaitWindowClosedError("wait");
|
|
784
828
|
}
|
|
785
829
|
const remaining = remainingMs(clock);
|
|
786
|
-
//
|
|
787
|
-
const
|
|
788
|
-
const effectiveMs = Math.min(input.durationMs, remaining, COLLECTOR_SINGLE_WAIT_MAX_MS);
|
|
830
|
+
// Cap only by remaining window — no package-local single-wait ceiling (ADR 0035 / #678).
|
|
831
|
+
const effectiveMs = Math.min(input.durationMs, remaining);
|
|
789
832
|
const startedAt = clock.wallNow().toISOString();
|
|
790
833
|
const waitId = sha256Text(`wait:${startedAt}:${effectiveMs}`).slice(0, 16);
|
|
791
834
|
await clock.sleep(effectiveMs, signal);
|
|
@@ -868,6 +911,9 @@ function buildObserveModelView(input) {
|
|
|
868
911
|
completedAt: input.snapshot.completedAt,
|
|
869
912
|
prState: input.snapshot.prState,
|
|
870
913
|
headOid: input.snapshot.headOid,
|
|
914
|
+
...(input.snapshot.prCreatedAt === undefined
|
|
915
|
+
? {}
|
|
916
|
+
: { prCreatedAt: input.snapshot.prCreatedAt }),
|
|
871
917
|
complete: input.snapshot.complete,
|
|
872
918
|
evidence: relevant.map((record) => projectEvidenceEntryView(record)),
|
|
873
919
|
requestAttempts: input.attempts.map((attempt) => ({
|